Merge branch 'master' into drupal-new
Conflicts: lms/envs/aws.py lms/envs/common.py lms/static/sass/base/_base.scss lms/templates/forgot_password_modal.html
This commit is contained in:
148
README.md
Normal file
148
README.md
Normal file
@@ -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.
|
||||
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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'})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -10,7 +10,7 @@ var $newComponentTypePicker;
|
||||
var $newComponentTemplatePickers;
|
||||
var $newComponentButton;
|
||||
|
||||
$(document).ready(function () {
|
||||
$(document).ready(function() {
|
||||
$body = $('body');
|
||||
$modal = $('.history-modal');
|
||||
$modalCover = $('<div class="modal-cover">');
|
||||
@@ -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('<strong>Will Release:</strong> %(date)s at $(time)s UTC');
|
||||
var willReleaseAt = interpolate(format, [input_date, input_time], true);
|
||||
$thisSection.find('.section-published-date').html(
|
||||
'<span class="published-status">' + willReleaseAt + '</span>' +
|
||||
'<a href="#" class="edit-button" ' +
|
||||
'" data-date="' + input_date +
|
||||
'" data-time="' + input_time +
|
||||
'" data-id="' + id + '">' +
|
||||
gettext('Edit') + '</a>');
|
||||
$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('<strong>Will Release:</strong> %(date)s at %(time)s UTC');
|
||||
var willReleaseAt = interpolate(format, {
|
||||
'date': input_date,
|
||||
'time': input_time
|
||||
},
|
||||
true);
|
||||
$thisSection.find('.section-published-date').html(
|
||||
'<span class="published-status">' + willReleaseAt + '</span>' +
|
||||
'<a href="#" class="edit-button" ' +
|
||||
'" data-date="' + input_date +
|
||||
'" data-time="' + input_time +
|
||||
'" data-id="' + id + '">' + gettext('Edit') + '</a>');
|
||||
$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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -80,6 +80,6 @@ $(document).ready(function(){
|
||||
$('section.problem-edit').show();
|
||||
return false;
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,5 +6,5 @@ CMS.Models.ModuleInfo = Backbone.Model.extend({
|
||||
"data": null,
|
||||
"metadata" : null,
|
||||
"children" : null
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -66,7 +66,7 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({
|
||||
save_videosource: function(newsource) {
|
||||
// newsource either is <video youtube="speed:key, *"/> 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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -93,4 +93,4 @@ CMS.Views.Checklists = Backbone.View.extend({
|
||||
error : CMS.ServerError
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
'<a data-tooltip="Mark/unmark this subsection as graded" class="menu-toggle" href="#">' +
|
||||
'<% if (!hideSymbol) {%><span class="ss-icon ss-standard">✓</span><%};%>' +
|
||||
'</a>' +
|
||||
'<ul class="menu">' +
|
||||
'<ul class="menu">' +
|
||||
'<% graders.each(function(option) { %>' +
|
||||
'<li><a <% if (option.get("type") == assignmentType) {%>class="is-selected" <%}%> href="#"><%= option.get("type") %></a></li>' +
|
||||
'<% }) %>' +
|
||||
'<li><a class="gradable-status-notgraded" href="#">Not Graded</a></li>' +
|
||||
'</ul>');
|
||||
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();
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 : [],
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +114,7 @@
|
||||
<li><a href="#alert-announcement2" class="show-alert">Show Announcement</a></li>
|
||||
<li><a href="#alert-announcement1" class="show-alert">Show Announcement with Actions</a></li>
|
||||
<li><a href="#alert-activation" class="show-alert">Show Activiation</a></li>
|
||||
<li><a href="#alert-threeActions" class="show-alert">Alert with three actions</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
@@ -128,6 +129,10 @@
|
||||
<h3 class="title-3">Different Static Examples of Notifications</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<a href="#notification-changesMade" class="show-notification">Show Changes Made (used in Advanced Settings)</a>
|
||||
<a href="#notification-changesMade" class="hide-notification">Hide Changes Made (used in Advanced Settings)</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#notification-change" class="show-notification">Show Change Warning</a>
|
||||
<a href="#notification-change" class="hide-notification">Hide Change Warning</a>
|
||||
@@ -151,6 +156,10 @@
|
||||
<a href="#notification-help" class="show-notification">Show Help</a>
|
||||
<a href="#notification-help" class="hide-notification">Hide Help</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#notification-threeActions" class="show-notification">Show Notification with three actions</a>
|
||||
<a href="#notification-threeActions" class="hide-notification">Hide Notification with three actions</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
@@ -182,6 +191,33 @@
|
||||
</%block>
|
||||
|
||||
<%block name="view_alerts">
|
||||
<!-- alert: 3 actions -->
|
||||
<div class="wrapper wrapper-alert wrapper-alert-warning" id="alert-threeActions">
|
||||
<div class="alert warning has-actions">
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-warning">⚠</i>
|
||||
|
||||
<div class="copy">
|
||||
<h2 class="title title-3">You are editing a draft</h2>
|
||||
<p class="message">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.</p>
|
||||
</div>
|
||||
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">Alert Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="action action-save action-primary">Save Draft</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="action action-cancel action-secondary">Disgard Draft</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="action action-secondary">Do Something Elsee</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- alert: you're editing a draft -->
|
||||
<div class="wrapper wrapper-alert wrapper-alert-warning" id="alert-draft">
|
||||
<div class="alert warning has-actions">
|
||||
@@ -196,10 +232,10 @@
|
||||
<h3 class="sr">Alert Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="button save-button action-primary">Save Draft</a>
|
||||
<a href="#" class="action action-save action-primary">Save Draft</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="button cancel-button action-secondary">Disgard Draft</a>
|
||||
<a href="#" class="action action-cancel action-secondary">Disgard Draft</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
@@ -220,10 +256,10 @@
|
||||
<h3 class="sr">Alert Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="button save-button action-primary">Go to Newer Version</a>
|
||||
<a href="#" class="action action-save action-primary">Go to Newer Version</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="button cancel-button action-secondary">Continue Editing</a>
|
||||
<a href="#" class="action action-cancel action-secondary">Continue Editing</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
@@ -297,7 +333,7 @@
|
||||
<h3 class="sr">Alert Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="button cancel-button action-primary">Cancel Your Submission</a>
|
||||
<a href="#" class="action action-cancel action-primary">Cancel Your Submission</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
@@ -367,13 +403,13 @@
|
||||
|
||||
<%block name="view_notifications">
|
||||
<!-- notification: change has been made and a save is needed -->
|
||||
<div class="wrapper wrapper-notification wrapper-notification-change" id="notification-change" role="status">
|
||||
<div class="wrapper wrapper-notification wrapper-notification-change" aria-hidden="true" role="dialog" aria-labelledby="notification-change-title" aria-describedby="notification-change-description" id="notification-change">
|
||||
<div class="notification change has-actions">
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-change">📝</i>
|
||||
|
||||
<div class="copy">
|
||||
<h2 class="title title-3">You've Made Some Changes</h2>
|
||||
<p class="message">Your changes will not take effect until you <strong>save your progress</strong>.</p>
|
||||
<h2 class="title title-3" id="notification-change-title">You've Made Some Changes</h2>
|
||||
<p class="message" id="notification-change-description">Your changes will not take effect until you <strong>save your progress</strong>.</p>
|
||||
</div>
|
||||
|
||||
<nav class="nav-actions">
|
||||
@@ -390,6 +426,57 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- notification: three actions example -->
|
||||
<div class="wrapper wrapper-notification wrapper-notification-change" aria-hidden="true" role="dialog" aria-labelledby="notification-threeActions-title" aria-describedby="notification-threeActions-description" id="notification-threeActions">
|
||||
<div class="notification change has-actions">
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-change">📝</i>
|
||||
|
||||
<div class="copy">
|
||||
<h2 class="title title-3" id="notification-threeActions-title">You've Made Some Changes</h2>
|
||||
<p class="message" id="notification-threeActions-description">Your changes will not take effect until you <strong>save your progress</strong>.</p>
|
||||
</div>
|
||||
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">Notification Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="action-primary">Save Changes</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="action-secondary">Don't Save</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="action-secondary">Do something else</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- notification: change has been made and a save is needed -->
|
||||
<div class="wrapper wrapper-notification wrapper-notification-warning" aria-hidden="true" role="dialog" aria-labelledby="notification-changesMade-title" aria-describedby="notification-changesMade-description" id="notification-changesMade">
|
||||
<div class="notification warning has-actions">
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-warning">⚠</i>
|
||||
|
||||
<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>
|
||||
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">Notification Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="" class="action action-save action-primary">Save Changes</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="" class="action action-cancel action-secondary">Cancel</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- notification: newer version exists -->
|
||||
<div class="wrapper wrapper-notification wrapper-notification-warning" id="notification-version" aria-hidden="true" role="dialog" aria-labelledby="notification-warning-title" aria-describedby="notification-warning-description">
|
||||
<div class="notification warning has-actions">
|
||||
@@ -404,10 +491,10 @@
|
||||
<h3 class="sr">Notification Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="button save-button action-primary">Go to Newer Version</a>
|
||||
<a href="#" class="action action-save action-primary">Go to Newer Version</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="button cancel-button action-secondary">Continue Editing</a>
|
||||
<a href="#" class="action action-cancel action-secondary">Continue Editing</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
@@ -428,10 +515,10 @@
|
||||
<h3 class="sr">Notification Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="action-primary">Yes, I want to Edit X</a>
|
||||
<a href="#" class="action action-proceed action-primary">Yes, I want to Edit X</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="action-secondary">No, I do not</a>
|
||||
<a href="#" class="action action-cancel action-secondary">No, I do not</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<%
|
||||
import hashlib
|
||||
from xmodule.fields import StringyInteger, StringyFloat
|
||||
hlskey = hashlib.md5(module.location.url()).hexdigest()
|
||||
%>
|
||||
<section class="metadata_edit">
|
||||
@@ -7,17 +8,42 @@
|
||||
% for field_name, field_value in editable_metadata_fields.items():
|
||||
<li>
|
||||
% if field_name == 'source_code':
|
||||
<a href="#hls-modal-${hlskey}" style="color:yellow;" id="hls-trig-${hlskey}" >Edit High Level Source</a>
|
||||
% if field_value['explicitly_set'] is True:
|
||||
<a href="#hls-modal-${hlskey}" style="color:yellow;" id="hls-trig-${hlskey}" >Edit High Level Source</a>
|
||||
% endif
|
||||
% else:
|
||||
<label>${field_name}:</label>
|
||||
<input type='text' data-metadata-name='${field_name}' value='${field_value}' size='60' />
|
||||
<label>${field_value['field'].display_name}:</label>
|
||||
<input type='text' data-metadata-name='${field_value["field"].display_name}'
|
||||
## This is a hack to keep current behavior for weight and attempts (empty will parse OK as unset).
|
||||
## This hack will go away with our custom editors.
|
||||
% if field_value["value"] == None and (isinstance(field_value["field"], StringyFloat) or isinstance(field_value["field"], StringyInteger)):
|
||||
value = ''
|
||||
% else:
|
||||
value='${field_value["field"].to_json(field_value["value"])}'
|
||||
% endif
|
||||
size='60' />
|
||||
## Change to True to see all the information being passed through.
|
||||
% if False:
|
||||
<label>Help: ${field_value['field'].help}</label>
|
||||
<label>Type: ${type(field_value['field']).__name__}</label>
|
||||
<label>Inheritable: ${field_value['inheritable']}</label>
|
||||
<label>Showing inherited value: ${field_value['inheritable'] and not field_value['explicitly_set']}</label>
|
||||
<label>Explicitly set: ${field_value['explicitly_set']}</label>
|
||||
<label>Default value: ${field_value['default_value']}</label>
|
||||
% if field_value['field'].values:
|
||||
<label>Possible values:</label>
|
||||
% for value in field_value['field'].values:
|
||||
<label>${value}</label>
|
||||
% endfor
|
||||
% endif
|
||||
% endif
|
||||
% endif
|
||||
</li>
|
||||
% endfor
|
||||
</ul>
|
||||
|
||||
% 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
|
||||
|
||||
</section>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
<form id="hls-form" enctype="multipart/form-data">
|
||||
<section class="source-edit">
|
||||
<textarea name="" data-metadata-name="source_code" class="source-edit-box hls-data" rows="8" cols="40">${editable_metadata_fields['source_code']|h}</textarea>
|
||||
<textarea name="" data-metadata-name="source_code" class="source-edit-box hls-data" rows="8" cols="40">${editable_metadata_fields['source_code']['value']|h}</textarea>
|
||||
</section>
|
||||
<div class="submit">
|
||||
<button type="reset" class="hls-compile">Save & Compile to edX XML</button>
|
||||
|
||||
10
cms/urls.py
10
cms/urls.py
@@ -6,7 +6,7 @@ from . import one_time_startup
|
||||
# from django.contrib import admin
|
||||
# admin.autodiscover()
|
||||
|
||||
urlpatterns = ('',
|
||||
urlpatterns = ('', # nopep8
|
||||
url(r'^$', 'contentstore.views.howitworks', name='homepage'),
|
||||
url(r'^listing', 'contentstore.views.index', name='index'),
|
||||
url(r'^edit/(?P<location>.*?)$', 'contentstore.views.edit_unit', name='edit_unit'),
|
||||
@@ -118,17 +118,17 @@ urlpatterns += (
|
||||
|
||||
# static/proof-of-concept views
|
||||
url(r'^ux-alerts$', 'contentstore.views.ux_alerts', name='ux-alerts')
|
||||
)
|
||||
)
|
||||
|
||||
js_info_dict = {
|
||||
'domain': 'djangojs',
|
||||
'packages': ('cms',),
|
||||
}
|
||||
}
|
||||
|
||||
urlpatterns += (
|
||||
# Serve catalog of localized strings to be rendered by Javascript
|
||||
url(r'^jsi18n/$', 'django.views.i18n.javascript_catalog', js_info_dict),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if settings.ENABLE_JASMINE:
|
||||
@@ -140,5 +140,3 @@ urlpatterns = patterns(*urlpatterns)
|
||||
# Custom error pages
|
||||
handler404 = 'contentstore.views.render_404'
|
||||
handler500 = 'contentstore.views.render_500'
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from django.conf.urls import *
|
||||
|
||||
urlpatterns = patterns('',
|
||||
urlpatterns = patterns('', # nopep8
|
||||
url(r'^$', 'heartbeat.views.heartbeat', name='heartbeat'),
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ from student.models import (User, UserProfile, Registration,
|
||||
CourseEnrollmentAllowed, CourseEnrollment)
|
||||
from django.contrib.auth.models import Group
|
||||
from datetime import datetime
|
||||
from factory import DjangoModelFactory, Factory, SubFactory, PostGenerationMethodCall
|
||||
from factory import DjangoModelFactory, Factory, SubFactory, PostGenerationMethodCall, post_generation
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
@@ -45,6 +45,16 @@ class UserFactory(DjangoModelFactory):
|
||||
last_login = datetime(2012, 1, 1)
|
||||
date_joined = datetime(2011, 1, 1)
|
||||
|
||||
@post_generation
|
||||
def profile(obj, create, extracted, **kwargs):
|
||||
if create:
|
||||
obj.save()
|
||||
return UserProfileFactory.create(user=obj, **kwargs)
|
||||
elif kwargs:
|
||||
raise Exception("Cannot build a user profile without saving the user")
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class AdminFactory(UserFactory):
|
||||
is_staff = True
|
||||
|
||||
@@ -77,8 +77,9 @@ def index(request, extra_context={}, user=None):
|
||||
'''
|
||||
|
||||
# The course selection work is done in courseware.courses.
|
||||
domain = settings.MITX_FEATURES.get('FORCE_UNIVERSITY_DOMAIN') # normally False
|
||||
if domain == False: # do explicit check, because domain=None is valid
|
||||
domain = settings.MITX_FEATURES.get('FORCE_UNIVERSITY_DOMAIN') # normally False
|
||||
# do explicit check, because domain=None is valid
|
||||
if domain == False:
|
||||
domain = request.META.get('HTTP_HOST')
|
||||
|
||||
courses = get_courses(None, domain=domain)
|
||||
|
||||
@@ -8,15 +8,42 @@ import urllib
|
||||
|
||||
|
||||
def fasthash(string):
|
||||
m = hashlib.new("md4")
|
||||
m.update(string)
|
||||
return m.hexdigest()
|
||||
"""
|
||||
Hashes `string` into a string representation of a 128-bit digest.
|
||||
"""
|
||||
md4 = hashlib.new("md4")
|
||||
md4.update(string)
|
||||
return md4.hexdigest()
|
||||
|
||||
|
||||
def cleaned_string(val):
|
||||
"""
|
||||
Converts `val` to unicode and URL-encodes special characters
|
||||
(including quotes and spaces)
|
||||
"""
|
||||
return urllib.quote_plus(smart_str(val))
|
||||
|
||||
|
||||
def safe_key(key, key_prefix, version):
|
||||
safe_key = urllib.quote_plus(smart_str(key))
|
||||
"""
|
||||
Given a `key`, `key_prefix`, and `version`,
|
||||
return a key that is safe to use with memcache.
|
||||
|
||||
if len(safe_key) > 250:
|
||||
safe_key = fasthash(safe_key)
|
||||
`key`, `key_prefix`, and `version` can be numbers, strings, or unicode.
|
||||
"""
|
||||
|
||||
return ":".join([key_prefix, str(version), safe_key])
|
||||
# Clean for whitespace and control characters, which
|
||||
# cause memcache to raise an exception
|
||||
key = cleaned_string(key)
|
||||
key_prefix = cleaned_string(key_prefix)
|
||||
version = cleaned_string(version)
|
||||
|
||||
# Attempt to combine the prefix, version, and key
|
||||
combined = ":".join([key_prefix, version, key])
|
||||
|
||||
# If the total length is too long for memcache, hash it
|
||||
if len(combined) > 250:
|
||||
combined = fasthash(combined)
|
||||
|
||||
# Return the result
|
||||
return combined
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
"""
|
||||
This file demonstrates writing tests using the unittest module. These will pass
|
||||
when you run "manage.py test".
|
||||
|
||||
Replace this with more appropriate tests for your application.
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class SimpleTest(TestCase):
|
||||
def test_basic_addition(self):
|
||||
"""
|
||||
Tests that 1 + 1 always equals 2.
|
||||
"""
|
||||
self.assertEqual(1 + 1, 2)
|
||||
1
common/djangoapps/util/tests/__init__.py
Normal file
1
common/djangoapps/util/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
124
common/djangoapps/util/tests/test_memcache.py
Normal file
124
common/djangoapps/util/tests/test_memcache.py
Normal file
@@ -0,0 +1,124 @@
|
||||
"""
|
||||
Tests for memcache in util app
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
from django.core.cache import get_cache
|
||||
from django.conf import settings
|
||||
from util.memcache import safe_key
|
||||
|
||||
|
||||
class MemcacheTest(TestCase):
|
||||
"""
|
||||
Test memcache key cleanup
|
||||
"""
|
||||
|
||||
# Test whitespace, control characters, and some non-ASCII UTF-16
|
||||
UNICODE_CHAR_CODES = ([c for c in range(0, 30)] + [127] +
|
||||
[129, 500, 2 ** 8 - 1, 2 ** 8 + 1, 2 ** 16 - 1])
|
||||
|
||||
def setUp(self):
|
||||
self.cache = get_cache('default')
|
||||
|
||||
def test_safe_key(self):
|
||||
key = safe_key('test', 'prefix', 'version')
|
||||
self.assertEqual(key, 'prefix:version:test')
|
||||
|
||||
def test_numeric_inputs(self):
|
||||
|
||||
# Numeric key
|
||||
self.assertEqual(safe_key(1, 'prefix', 'version'), 'prefix:version:1')
|
||||
|
||||
# Numeric prefix
|
||||
self.assertEqual(safe_key('test', 5, 'version'), '5:version:test')
|
||||
|
||||
# Numeric version
|
||||
self.assertEqual(safe_key('test', 'prefix', 5), 'prefix:5:test')
|
||||
|
||||
def test_safe_key_long(self):
|
||||
|
||||
# Choose lengths close to memcached's cutoff (250)
|
||||
for length in [248, 249, 250, 251, 252]:
|
||||
|
||||
# Generate a key of that length
|
||||
key = 'a' * length
|
||||
|
||||
# Make the key safe
|
||||
key = safe_key(key, '', '')
|
||||
|
||||
# The key should now be valid
|
||||
self.assertTrue(self._is_valid_key(key),
|
||||
msg="Failed for key length {0}".format(length))
|
||||
|
||||
def test_long_key_prefix_version(self):
|
||||
|
||||
# Long key
|
||||
key = safe_key('a' * 300, 'prefix', 'version')
|
||||
self.assertTrue(self._is_valid_key(key))
|
||||
|
||||
# Long prefix
|
||||
key = safe_key('key', 'a' * 300, 'version')
|
||||
self.assertTrue(self._is_valid_key(key))
|
||||
|
||||
# Long version
|
||||
key = safe_key('key', 'prefix', 'a' * 300)
|
||||
self.assertTrue(self._is_valid_key(key))
|
||||
|
||||
def test_safe_key_unicode(self):
|
||||
|
||||
for unicode_char in self.UNICODE_CHAR_CODES:
|
||||
|
||||
# Generate a key with that character
|
||||
key = unichr(unicode_char)
|
||||
|
||||
# Make the key safe
|
||||
key = safe_key(key, '', '')
|
||||
|
||||
# The key should now be valid
|
||||
self.assertTrue(self._is_valid_key(key),
|
||||
msg="Failed for unicode character {0}".format(unicode_char))
|
||||
|
||||
def test_safe_key_prefix_unicode(self):
|
||||
|
||||
for unicode_char in self.UNICODE_CHAR_CODES:
|
||||
|
||||
# Generate a prefix with that character
|
||||
prefix = unichr(unicode_char)
|
||||
|
||||
# Make the key safe
|
||||
key = safe_key('test', prefix, '')
|
||||
|
||||
# The key should now be valid
|
||||
self.assertTrue(self._is_valid_key(key),
|
||||
msg="Failed for unicode character {0}".format(unicode_char))
|
||||
|
||||
def test_safe_key_version_unicode(self):
|
||||
|
||||
for unicode_char in self.UNICODE_CHAR_CODES:
|
||||
|
||||
# Generate a version with that character
|
||||
version = unichr(unicode_char)
|
||||
|
||||
# Make the key safe
|
||||
key = safe_key('test', '', version)
|
||||
|
||||
# The key should now be valid
|
||||
self.assertTrue(self._is_valid_key(key),
|
||||
msg="Failed for unicode character {0}".format(unicode_char))
|
||||
|
||||
def _is_valid_key(self, key):
|
||||
"""
|
||||
Test that a key is memcache-compatible.
|
||||
Based on Django's validator in core.cache.backends.base
|
||||
"""
|
||||
|
||||
# Check the length
|
||||
if len(key) > 250:
|
||||
return False
|
||||
|
||||
# Check that there are no spaces or control characters
|
||||
for char in key:
|
||||
if ord(char) < 33 or ord(char) == 127:
|
||||
return False
|
||||
|
||||
return True
|
||||
280
common/djangoapps/util/tests/test_zendesk.py
Normal file
280
common/djangoapps/util/tests/test_zendesk.py
Normal file
@@ -0,0 +1,280 @@
|
||||
"""Tests for the Zendesk"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.http import Http404
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.test.utils import override_settings
|
||||
from student.tests.factories import UserFactory
|
||||
from util import views
|
||||
from zendesk import ZendeskError
|
||||
import json
|
||||
import mock
|
||||
|
||||
|
||||
@mock.patch.dict("django.conf.settings.MITX_FEATURES", {"ENABLE_FEEDBACK_SUBMISSION": True})
|
||||
@override_settings(ZENDESK_URL="dummy", ZENDESK_USER="dummy", ZENDESK_API_KEY="dummy")
|
||||
@mock.patch("util.views._ZendeskApi", autospec=True)
|
||||
class SubmitFeedbackViaZendeskTest(TestCase):
|
||||
def setUp(self):
|
||||
"""Set up data for the test case"""
|
||||
self._request_factory = RequestFactory()
|
||||
self._anon_user = AnonymousUser()
|
||||
self._auth_user = UserFactory.create(
|
||||
email="test@edx.org",
|
||||
username="test",
|
||||
profile__name="Test User"
|
||||
)
|
||||
# This contains a tag to ensure that tags are submitted correctly
|
||||
self._anon_fields = {
|
||||
"email": "test@edx.org",
|
||||
"name": "Test User",
|
||||
"subject": "a subject",
|
||||
"details": "some details",
|
||||
"tag": "a tag"
|
||||
}
|
||||
# This does not contain a tag to ensure that tag is optional
|
||||
self._auth_fields = {"subject": "a subject", "details": "some details"}
|
||||
|
||||
def _test_request(self, user, fields):
|
||||
"""
|
||||
Generate a request and invoke the view, returning the response.
|
||||
|
||||
The request will be a POST request from the given `user`, with the given
|
||||
`fields` in the POST body.
|
||||
"""
|
||||
req = self._request_factory.post(
|
||||
"/submit_feedback",
|
||||
data=fields,
|
||||
HTTP_REFERER="test_referer",
|
||||
HTTP_USER_AGENT="test_user_agent"
|
||||
)
|
||||
req.user = user
|
||||
return views.submit_feedback_via_zendesk(req)
|
||||
|
||||
def _assert_bad_request(self, response, field, zendesk_mock_class):
|
||||
"""
|
||||
Assert that the given `response` contains correct failure data.
|
||||
|
||||
It should have a 400 status code, and its content should be a JSON
|
||||
object containing the specified `field` and an `error`.
|
||||
"""
|
||||
self.assertEqual(response.status_code, 400)
|
||||
resp_json = json.loads(response.content)
|
||||
self.assertTrue("field" in resp_json)
|
||||
self.assertEqual(resp_json["field"], field)
|
||||
self.assertTrue("error" in resp_json)
|
||||
# There should be absolutely no interaction with Zendesk
|
||||
self.assertFalse(zendesk_mock_class.return_value.mock_calls)
|
||||
|
||||
def _test_bad_request_omit_field(self, user, fields, omit_field, zendesk_mock_class):
|
||||
"""
|
||||
Invoke the view with a request missing a field and assert correctness.
|
||||
|
||||
The request will be a POST request from the given `user`, with POST
|
||||
fields taken from `fields` minus the entry specified by `omit_field`.
|
||||
The response should have a 400 (bad request) status code and specify
|
||||
the invalid field and an error message, and the Zendesk API should not
|
||||
have been invoked.
|
||||
"""
|
||||
filtered_fields = {k: v for (k, v) in fields.items() if k != omit_field}
|
||||
resp = self._test_request(user, filtered_fields)
|
||||
self._assert_bad_request(resp, omit_field, zendesk_mock_class)
|
||||
|
||||
def _test_bad_request_empty_field(self, user, fields, empty_field, zendesk_mock_class):
|
||||
"""
|
||||
Invoke the view with an empty field and assert correctness.
|
||||
|
||||
The request will be a POST request from the given `user`, with POST
|
||||
fields taken from `fields`, replacing the entry specified by
|
||||
`empty_field` with the empty string. The response should have a 400
|
||||
(bad request) status code and specify the invalid field and an error
|
||||
message, and the Zendesk API should not have been invoked.
|
||||
"""
|
||||
altered_fields = fields.copy()
|
||||
altered_fields[empty_field] = ""
|
||||
resp = self._test_request(user, altered_fields)
|
||||
self._assert_bad_request(resp, empty_field, zendesk_mock_class)
|
||||
|
||||
def _test_success(self, user, fields):
|
||||
"""
|
||||
Generate a request, invoke the view, and assert success.
|
||||
|
||||
The request will be a POST request from the given `user`, with the given
|
||||
`fields` in the POST body. The response should have a 200 (success)
|
||||
status code.
|
||||
"""
|
||||
resp = self._test_request(user, fields)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
def test_bad_request_anon_user_no_name(self, zendesk_mock_class):
|
||||
"""Test a request from an anonymous user not specifying `name`."""
|
||||
self._test_bad_request_omit_field(self._anon_user, self._anon_fields, "name", zendesk_mock_class)
|
||||
self._test_bad_request_empty_field(self._anon_user, self._anon_fields, "name", zendesk_mock_class)
|
||||
|
||||
def test_bad_request_anon_user_no_email(self, zendesk_mock_class):
|
||||
"""Test a request from an anonymous user not specifying `email`."""
|
||||
self._test_bad_request_omit_field(self._anon_user, self._anon_fields, "email", zendesk_mock_class)
|
||||
self._test_bad_request_empty_field(self._anon_user, self._anon_fields, "email", zendesk_mock_class)
|
||||
|
||||
def test_bad_request_anon_user_no_subject(self, zendesk_mock_class):
|
||||
"""Test a request from an anonymous user not specifying `subject`."""
|
||||
self._test_bad_request_omit_field(self._anon_user, self._anon_fields, "subject", zendesk_mock_class)
|
||||
self._test_bad_request_empty_field(self._anon_user, self._anon_fields, "subject", zendesk_mock_class)
|
||||
|
||||
def test_bad_request_anon_user_no_details(self, zendesk_mock_class):
|
||||
"""Test a request from an anonymous user not specifying `details`."""
|
||||
self._test_bad_request_omit_field(self._anon_user, self._anon_fields, "details", zendesk_mock_class)
|
||||
self._test_bad_request_empty_field(self._anon_user, self._anon_fields, "details", zendesk_mock_class)
|
||||
|
||||
def test_valid_request_anon_user(self, zendesk_mock_class):
|
||||
"""
|
||||
Test a valid request from an anonymous user.
|
||||
|
||||
The response should have a 200 (success) status code, and a ticket with
|
||||
the given information should have been submitted via the Zendesk API.
|
||||
"""
|
||||
zendesk_mock_instance = zendesk_mock_class.return_value
|
||||
zendesk_mock_instance.create_ticket.return_value = 42
|
||||
self._test_success(self._anon_user, self._anon_fields)
|
||||
expected_calls = [
|
||||
mock.call.create_ticket(
|
||||
{
|
||||
"ticket": {
|
||||
"requester": {"name": "Test User", "email": "test@edx.org"},
|
||||
"subject": "a subject",
|
||||
"comment": {"body": "some details"},
|
||||
"tags": ["a tag"]
|
||||
}
|
||||
}
|
||||
),
|
||||
mock.call.update_ticket(
|
||||
42,
|
||||
{
|
||||
"ticket": {
|
||||
"comment": {
|
||||
"public": False,
|
||||
"body":
|
||||
"Additional information:\n\n"
|
||||
"HTTP_USER_AGENT: test_user_agent\n"
|
||||
"HTTP_REFERER: test_referer"
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
self.assertEqual(zendesk_mock_instance.mock_calls, expected_calls)
|
||||
|
||||
def test_bad_request_auth_user_no_subject(self, zendesk_mock_class):
|
||||
"""Test a request from an authenticated user not specifying `subject`."""
|
||||
self._test_bad_request_omit_field(self._auth_user, self._auth_fields, "subject", zendesk_mock_class)
|
||||
self._test_bad_request_empty_field(self._auth_user, self._auth_fields, "subject", zendesk_mock_class)
|
||||
|
||||
def test_bad_request_auth_user_no_details(self, zendesk_mock_class):
|
||||
"""Test a request from an authenticated user not specifying `details`."""
|
||||
self._test_bad_request_omit_field(self._auth_user, self._auth_fields, "details", zendesk_mock_class)
|
||||
self._test_bad_request_empty_field(self._auth_user, self._auth_fields, "details", zendesk_mock_class)
|
||||
|
||||
def test_valid_request_auth_user(self, zendesk_mock_class):
|
||||
"""
|
||||
Test a valid request from an authenticated user.
|
||||
|
||||
The response should have a 200 (success) status code, and a ticket with
|
||||
the given information should have been submitted via the Zendesk API.
|
||||
"""
|
||||
zendesk_mock_instance = zendesk_mock_class.return_value
|
||||
zendesk_mock_instance.create_ticket.return_value = 42
|
||||
self._test_success(self._auth_user, self._auth_fields)
|
||||
expected_calls = [
|
||||
mock.call.create_ticket(
|
||||
{
|
||||
"ticket": {
|
||||
"requester": {"name": "Test User", "email": "test@edx.org"},
|
||||
"subject": "a subject",
|
||||
"comment": {"body": "some details"},
|
||||
"tags": []
|
||||
}
|
||||
}
|
||||
),
|
||||
mock.call.update_ticket(
|
||||
42,
|
||||
{
|
||||
"ticket": {
|
||||
"comment": {
|
||||
"public": False,
|
||||
"body":
|
||||
"Additional information:\n\n"
|
||||
"username: test\n"
|
||||
"HTTP_USER_AGENT: test_user_agent\n"
|
||||
"HTTP_REFERER: test_referer"
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
self.assertEqual(zendesk_mock_instance.mock_calls, expected_calls)
|
||||
|
||||
def test_get_request(self, zendesk_mock_class):
|
||||
"""Test that a GET results in a 405 even with all required fields"""
|
||||
req = self._request_factory.get("/submit_feedback", data=self._anon_fields)
|
||||
req.user = self._anon_user
|
||||
resp = views.submit_feedback_via_zendesk(req)
|
||||
self.assertEqual(resp.status_code, 405)
|
||||
self.assertIn("Allow", resp)
|
||||
self.assertEqual(resp["Allow"], "POST")
|
||||
# There should be absolutely no interaction with Zendesk
|
||||
self.assertFalse(zendesk_mock_class.mock_calls)
|
||||
|
||||
def test_zendesk_error_on_create(self, zendesk_mock_class):
|
||||
"""
|
||||
Test Zendesk returning an error on ticket creation.
|
||||
|
||||
We should return a 500 error with no body
|
||||
"""
|
||||
err = ZendeskError(msg="", error_code=404)
|
||||
zendesk_mock_instance = zendesk_mock_class.return_value
|
||||
zendesk_mock_instance.create_ticket.side_effect = err
|
||||
resp = self._test_request(self._anon_user, self._anon_fields)
|
||||
self.assertEqual(resp.status_code, 500)
|
||||
self.assertFalse(resp.content)
|
||||
|
||||
def test_zendesk_error_on_update(self, zendesk_mock_class):
|
||||
"""
|
||||
Test for Zendesk returning an error on ticket update.
|
||||
|
||||
If Zendesk returns any error on ticket update, we return a 200 to the
|
||||
browser because the update contains additional information that is not
|
||||
necessary for the user to have submitted their feedback.
|
||||
"""
|
||||
err = ZendeskError(msg="", error_code=500)
|
||||
zendesk_mock_instance = zendesk_mock_class.return_value
|
||||
zendesk_mock_instance.update_ticket.side_effect = err
|
||||
resp = self._test_request(self._anon_user, self._anon_fields)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
@mock.patch.dict("django.conf.settings.MITX_FEATURES", {"ENABLE_FEEDBACK_SUBMISSION": False})
|
||||
def test_not_enabled(self, zendesk_mock_class):
|
||||
"""
|
||||
Test for Zendesk submission not enabled in `settings`.
|
||||
|
||||
We should raise Http404.
|
||||
"""
|
||||
with self.assertRaises(Http404):
|
||||
self._test_request(self._anon_user, self._anon_fields)
|
||||
|
||||
def test_zendesk_not_configured(self, zendesk_mock_class):
|
||||
"""
|
||||
Test for Zendesk not fully configured in `settings`.
|
||||
|
||||
For each required configuration parameter, test that setting it to
|
||||
`None` causes an otherwise valid request to return a 500 error.
|
||||
"""
|
||||
def test_case(missing_config):
|
||||
with mock.patch(missing_config, None):
|
||||
with self.assertRaises(Exception):
|
||||
self._test_request(self._anon_user, self._anon_fields)
|
||||
|
||||
test_case("django.conf.settings.ZENDESK_URL")
|
||||
test_case("django.conf.settings.ZENDESK_USER")
|
||||
test_case("django.conf.settings.ZENDESK_API_KEY")
|
||||
@@ -1,5 +1,6 @@
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import pprint
|
||||
import sys
|
||||
|
||||
@@ -7,15 +8,21 @@ from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.context_processors import csrf
|
||||
from django.core.mail import send_mail
|
||||
from django.http import Http404
|
||||
from django.http import HttpResponse
|
||||
from django.core.validators import ValidationError, validate_email
|
||||
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed, HttpResponseServerError
|
||||
from django.shortcuts import redirect
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
from urllib import urlencode
|
||||
import zendesk
|
||||
|
||||
import capa.calc
|
||||
import track.views
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def calculate(request):
|
||||
''' Calculator in footer of every page. '''
|
||||
equation = request.GET['equation']
|
||||
@@ -29,36 +36,145 @@ def calculate(request):
|
||||
return HttpResponse(json.dumps({'result': str(result)}))
|
||||
|
||||
|
||||
def send_feedback(request):
|
||||
''' Feeback mechanism in footer of every page. '''
|
||||
try:
|
||||
username = request.user.username
|
||||
class _ZendeskApi(object):
|
||||
def __init__(self):
|
||||
"""
|
||||
Instantiate the Zendesk API.
|
||||
|
||||
All of `ZENDESK_URL`, `ZENDESK_USER`, and `ZENDESK_API_KEY` must be set
|
||||
in `django.conf.settings`.
|
||||
"""
|
||||
self._zendesk_instance = zendesk.Zendesk(
|
||||
settings.ZENDESK_URL,
|
||||
settings.ZENDESK_USER,
|
||||
settings.ZENDESK_API_KEY,
|
||||
use_api_token=True,
|
||||
api_version=2,
|
||||
# As of 2012-05-08, Zendesk is using a CA that is not
|
||||
# installed on our servers
|
||||
client_args={"disable_ssl_certificate_validation": True}
|
||||
)
|
||||
|
||||
def create_ticket(self, ticket):
|
||||
"""
|
||||
Create the given `ticket` in Zendesk.
|
||||
|
||||
The ticket should have the format specified by the zendesk package.
|
||||
"""
|
||||
ticket_url = self._zendesk_instance.create_ticket(data=ticket)
|
||||
return zendesk.get_id_from_url(ticket_url)
|
||||
|
||||
def update_ticket(self, ticket_id, update):
|
||||
"""
|
||||
Update the Zendesk ticket with id `ticket_id` using the given `update`.
|
||||
|
||||
The update should have the format specified by the zendesk package.
|
||||
"""
|
||||
self._zendesk_instance.update_ticket(ticket_id=ticket_id, data=update)
|
||||
|
||||
|
||||
def submit_feedback_via_zendesk(request):
|
||||
"""
|
||||
Create a new user-requested Zendesk ticket.
|
||||
|
||||
If Zendesk submission is not enabled, any request will raise `Http404`.
|
||||
If any configuration parameter (`ZENDESK_URL`, `ZENDESK_USER`, or
|
||||
`ZENDESK_API_KEY`) is missing, any request will raise an `Exception`.
|
||||
The request must be a POST request specifying `subject` and `details`.
|
||||
If the user is not authenticated, the request must also specify `name` and
|
||||
`email`. If the user is authenticated, the `name` and `email` will be
|
||||
populated from the user's information. If any required parameter is
|
||||
missing, a 400 error will be returned indicating which field is missing and
|
||||
providing an error message. If Zendesk returns any error on ticket
|
||||
creation, a 500 error will be returned with no body. Once created, the
|
||||
ticket will be updated with a private comment containing additional
|
||||
information from the browser and server, such as HTTP headers and user
|
||||
state. Whether or not the update succeeds, if the user's ticket is
|
||||
successfully created, an empty successful response (200) will be returned.
|
||||
"""
|
||||
if not settings.MITX_FEATURES.get('ENABLE_FEEDBACK_SUBMISSION', False):
|
||||
raise Http404()
|
||||
if request.method != "POST":
|
||||
return HttpResponseNotAllowed(["POST"])
|
||||
if (
|
||||
not settings.ZENDESK_URL or
|
||||
not settings.ZENDESK_USER or
|
||||
not settings.ZENDESK_API_KEY
|
||||
):
|
||||
raise Exception("Zendesk enabled but not configured")
|
||||
|
||||
def build_error_response(status_code, field, err_msg):
|
||||
return HttpResponse(json.dumps({"field": field, "error": err_msg}), status=status_code)
|
||||
|
||||
additional_info = {}
|
||||
|
||||
required_fields = ["subject", "details"]
|
||||
if not request.user.is_authenticated():
|
||||
required_fields += ["name", "email"]
|
||||
required_field_errs = {
|
||||
"subject": "Please provide a subject.",
|
||||
"details": "Please provide details.",
|
||||
"name": "Please provide your name.",
|
||||
"email": "Please provide a valid e-mail.",
|
||||
}
|
||||
|
||||
for field in required_fields:
|
||||
if field not in request.POST or not request.POST[field]:
|
||||
return build_error_response(400, field, required_field_errs[field])
|
||||
|
||||
subject = request.POST["subject"]
|
||||
details = request.POST["details"]
|
||||
tags = []
|
||||
if "tag" in request.POST:
|
||||
tags = [request.POST["tag"]]
|
||||
|
||||
if request.user.is_authenticated():
|
||||
realname = request.user.profile.name
|
||||
email = request.user.email
|
||||
except:
|
||||
username = "anonymous"
|
||||
email = "anonymous"
|
||||
additional_info["username"] = request.user.username
|
||||
else:
|
||||
realname = request.POST["name"]
|
||||
email = request.POST["email"]
|
||||
try:
|
||||
validate_email(email)
|
||||
except ValidationError:
|
||||
return build_error_response(400, "email", required_field_errs["email"])
|
||||
|
||||
for header in ["HTTP_REFERER", "HTTP_USER_AGENT"]:
|
||||
additional_info[header] = request.META.get(header)
|
||||
|
||||
zendesk_api = _ZendeskApi()
|
||||
|
||||
additional_info_string = (
|
||||
"Additional information:\n\n" +
|
||||
"\n".join("%s: %s" % (key, value) for (key, value) in additional_info.items() if value is not None)
|
||||
)
|
||||
|
||||
new_ticket = {
|
||||
"ticket": {
|
||||
"requester": {"name": realname, "email": email},
|
||||
"subject": subject,
|
||||
"comment": {"body": details},
|
||||
"tags": tags
|
||||
}
|
||||
}
|
||||
try:
|
||||
browser = request.META['HTTP_USER_AGENT']
|
||||
except:
|
||||
browser = "Unknown"
|
||||
ticket_id = zendesk_api.create_ticket(new_ticket)
|
||||
except zendesk.ZendeskError as err:
|
||||
log.error("%s", str(err))
|
||||
return HttpResponse(status=500)
|
||||
|
||||
feedback = render_to_string("feedback_email.txt",
|
||||
{"subject": request.POST['subject'],
|
||||
"url": request.POST['url'],
|
||||
"time": datetime.datetime.now().isoformat(),
|
||||
"feedback": request.POST['message'],
|
||||
"email": email,
|
||||
"browser": browser,
|
||||
"user": username})
|
||||
# Additional information is provided as a private update so the information
|
||||
# is not visible to the user.
|
||||
ticket_update = {"ticket": {"comment": {"public": False, "body": additional_info_string}}}
|
||||
try:
|
||||
zendesk_api.update_ticket(ticket_id, ticket_update)
|
||||
except zendesk.ZendeskError as err:
|
||||
log.error("%s", str(err))
|
||||
# The update is not strictly necessary, so do not indicate failure to the user
|
||||
pass
|
||||
|
||||
send_mail("MITx Feedback / " + request.POST['subject'],
|
||||
feedback,
|
||||
settings.DEFAULT_FROM_EMAIL,
|
||||
[settings.DEFAULT_FEEDBACK_EMAIL],
|
||||
fail_silently=False
|
||||
)
|
||||
return HttpResponse(json.dumps({'success': True}))
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
def info(request):
|
||||
|
||||
@@ -1147,9 +1147,9 @@ def sympy_check2():
|
||||
messages = []
|
||||
for input_dict in input_list:
|
||||
correct.append('correct'
|
||||
if input_dict['ok'] else 'incorrect')
|
||||
if input_dict['ok'] else 'incorrect')
|
||||
msg = (self.clean_message_html(input_dict['msg'])
|
||||
if 'msg' in input_dict else None)
|
||||
if 'msg' in input_dict else None)
|
||||
messages.append(msg)
|
||||
|
||||
# Otherwise, we do not recognize the dictionary
|
||||
@@ -1174,7 +1174,7 @@ def sympy_check2():
|
||||
|
||||
for k in range(len(idset)):
|
||||
npoints = (self.maxpoints[idset[k]]
|
||||
if correct[k] == 'correct' else 0)
|
||||
if correct[k] == 'correct' else 0)
|
||||
correct_map.set(idset[k], correct[k], msg=messages[k],
|
||||
npoints=npoints)
|
||||
return correct_map
|
||||
@@ -1851,6 +1851,19 @@ class FormulaResponse(LoncapaResponse):
|
||||
'formularesponse: undefined variable in given=%s' % given)
|
||||
raise StudentInputError(
|
||||
"Invalid input: " + uv.message + " not permitted in answer")
|
||||
except ValueError as ve:
|
||||
if 'factorial' in ve.message:
|
||||
# This is thrown when fact() or factorial() is used in a formularesponse answer
|
||||
# that tests on negative and/or non-integer inputs
|
||||
# ve.message will be: `factorial() only accepts integral values` or `factorial() not defined for negative values`
|
||||
log.debug(
|
||||
'formularesponse: factorial function used in response that tests negative and/or non-integer inputs. given={0}'.format(given))
|
||||
raise StudentInputError(
|
||||
"factorial function not permitted in answer for this problem. Provided answer was: {0}".format(given))
|
||||
# If non-factorial related ValueError thrown, handle it the same as any other Exception
|
||||
log.debug('formularesponse: error {0} in formula'.format(ve))
|
||||
raise StudentInputError("Invalid input: Could not parse '%s' as a formula" %
|
||||
cgi.escape(given))
|
||||
except Exception as err:
|
||||
# traceback.print_exc()
|
||||
log.debug('formularesponse: error %s in formula' % err)
|
||||
@@ -1983,7 +1996,6 @@ class ImageResponse(LoncapaResponse):
|
||||
self.ielements = self.inputfields
|
||||
self.answer_ids = [ie.get('id') for ie in self.ielements]
|
||||
|
||||
|
||||
def get_score(self, student_answers):
|
||||
correct_map = CorrectMap()
|
||||
expectedset = self.get_mapped_answers()
|
||||
@@ -2052,7 +2064,7 @@ class ImageResponse(LoncapaResponse):
|
||||
rectangles (dict) - a map of inputs to the defined rectangle for that input
|
||||
regions (dict) - a map of inputs to the defined region for that input
|
||||
'''
|
||||
answers = (
|
||||
answers = (
|
||||
dict([(ie.get('id'), ie.get(
|
||||
'rectangle')) for ie in self.ielements]),
|
||||
dict([(ie.get('id'), ie.get('regions')) for ie in self.ielements]))
|
||||
@@ -2074,8 +2086,6 @@ class ImageResponse(LoncapaResponse):
|
||||
answers[ie_id] = (ie.get('rectangle'), ie.get('regions'))
|
||||
|
||||
return answers
|
||||
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -65,7 +65,8 @@ class CapaFields(object):
|
||||
max_attempts = StringyInteger(help="Maximum number of attempts that a student is allowed", scope=Scope.settings)
|
||||
due = Date(help="Date that this problem is due by", scope=Scope.settings)
|
||||
graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings)
|
||||
showanswer = String(help="When to show the problem answer to the student", scope=Scope.settings, default="closed")
|
||||
showanswer = String(help="When to show the problem answer to the student", scope=Scope.settings, default="closed",
|
||||
values=["answered", "always", "attempted", "closed", "never"])
|
||||
force_save_button = Boolean(help="Whether to force the save button to appear on the page", scope=Scope.settings, default=False)
|
||||
rerandomize = Randomization(help="When to rerandomize the problem", default="always", scope=Scope.settings)
|
||||
data = String(help="XML data for the problem", scope=Scope.content)
|
||||
@@ -882,16 +883,6 @@ class CapaDescriptor(CapaFields, RawDescriptor):
|
||||
'enable_markdown': self.markdown is not None})
|
||||
return _context
|
||||
|
||||
@property
|
||||
def editable_metadata_fields(self):
|
||||
"""Remove metadata from the editable fields since it has its own editor"""
|
||||
subset = super(CapaDescriptor, self).editable_metadata_fields
|
||||
if 'markdown' in subset:
|
||||
del subset['markdown']
|
||||
if 'empty' in subset:
|
||||
del subset['empty']
|
||||
return subset
|
||||
|
||||
# VS[compat]
|
||||
# TODO (cpennington): Delete this method once all fall 2012 course are being
|
||||
# edited in the cms
|
||||
@@ -901,3 +892,10 @@ class CapaDescriptor(CapaFields, RawDescriptor):
|
||||
'problems/' + path[8:],
|
||||
path[8:],
|
||||
]
|
||||
|
||||
@property
|
||||
def non_editable_metadata_fields(self):
|
||||
non_editable_fields = super(CapaDescriptor, self).non_editable_metadata_fields
|
||||
non_editable_fields.extend([CapaDescriptor.due, CapaDescriptor.graceperiod,
|
||||
CapaDescriptor.force_save_button, CapaDescriptor.markdown])
|
||||
return non_editable_fields
|
||||
|
||||
@@ -203,9 +203,7 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
|
||||
|
||||
def save_instance_data(self):
|
||||
for attribute in self.student_attributes:
|
||||
child_attr = getattr(self.child_module, attribute)
|
||||
if child_attr != getattr(self, attribute):
|
||||
setattr(self, attribute, getattr(self.child_module, attribute))
|
||||
setattr(self, attribute, getattr(self.child_module, attribute))
|
||||
|
||||
|
||||
class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor):
|
||||
|
||||
@@ -162,8 +162,7 @@ class CourseFields(object):
|
||||
discussion_blackouts = List(help="List of pairs of start/end dates for discussion blackouts", scope=Scope.settings)
|
||||
discussion_topics = Object(
|
||||
help="Map of topics names to ids",
|
||||
scope=Scope.settings,
|
||||
computed_default=lambda c: {'General': {'id': c.location.html_id()}},
|
||||
scope=Scope.settings
|
||||
)
|
||||
testcenter_info = Object(help="Dictionary of Test Center info", scope=Scope.settings)
|
||||
announcement = Date(help="Date this course is announced", scope=Scope.settings)
|
||||
@@ -234,6 +233,8 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
|
||||
self._grading_policy = {}
|
||||
|
||||
self.set_grading_policy(self.grading_policy)
|
||||
if self.discussion_topics == {}:
|
||||
self.discussion_topics = {'General': {'id': self.location.html_id()}}
|
||||
|
||||
self.test_center_exams = []
|
||||
test_center_info = self.testcenter_info
|
||||
|
||||
@@ -37,3 +37,10 @@ class DiscussionDescriptor(DiscussionFields, MetadataOnlyEditingDescriptor, RawD
|
||||
metadata_translations = dict(RawDescriptor.metadata_translations)
|
||||
metadata_translations['id'] = 'discussion_id'
|
||||
metadata_translations['for'] = 'discussion_target'
|
||||
|
||||
@property
|
||||
def non_editable_metadata_fields(self):
|
||||
non_editable_fields = super(DiscussionDescriptor, self).non_editable_metadata_fields
|
||||
# We may choose to enable sort_keys in the future, but while Kevin is investigating....
|
||||
non_editable_fields.extend([DiscussionDescriptor.discussion_id, DiscussionDescriptor.sort_key])
|
||||
return non_editable_fields
|
||||
|
||||
@@ -19,6 +19,7 @@ log = logging.getLogger("mitx.courseware")
|
||||
|
||||
class HtmlFields(object):
|
||||
data = String(help="Html contents to display for this module", scope=Scope.content)
|
||||
source_code = String(help="Source code for LaTeX documents. This feature is not well-supported.", scope=Scope.settings)
|
||||
|
||||
|
||||
class HtmlModule(HtmlFields, XModule):
|
||||
@@ -166,16 +167,6 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor):
|
||||
elt.set("filename", relname)
|
||||
return elt
|
||||
|
||||
@property
|
||||
def editable_metadata_fields(self):
|
||||
"""Remove any metadata from the editable fields which have their own editor or shouldn't be edited by user."""
|
||||
subset = super(HtmlDescriptor, self).editable_metadata_fields
|
||||
|
||||
if 'empty' in subset:
|
||||
del subset['empty']
|
||||
|
||||
return subset
|
||||
|
||||
|
||||
class AboutDescriptor(HtmlDescriptor):
|
||||
"""
|
||||
|
||||
5
common/lib/xmodule/xmodule/js/src/.gitignore
vendored
5
common/lib/xmodule/xmodule/js/src/.gitignore
vendored
@@ -1 +1,4 @@
|
||||
# Please do not ignore *.js files. Some xmodules are written in JS.
|
||||
# Ignore .js files in this folder as they are compiled from coffeescript
|
||||
# For each of the xmodules subdirectories, add a .gitignore file that
|
||||
# will version any *.js file that is specifically written, not compiled.
|
||||
*.js
|
||||
|
||||
2
common/lib/xmodule/xmodule/js/src/capa/.gitignore
vendored
Normal file
2
common/lib/xmodule/xmodule/js/src/capa/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
!imageinput.js
|
||||
!schematic.js
|
||||
1
common/lib/xmodule/xmodule/js/src/graphical_slider_tool/.gitignore
vendored
Normal file
1
common/lib/xmodule/xmodule/js/src/graphical_slider_tool/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!*.js
|
||||
@@ -8,20 +8,23 @@ class @PeerGrading
|
||||
@use_single_location = @peer_grading_container.data('use-single-location')
|
||||
@peer_grading_outer_container = $('.peer-grading-container')
|
||||
@ajax_url = @peer_grading_container.data('ajax-url')
|
||||
@error_container = $('.error-container')
|
||||
@error_container.toggle(not @error_container.is(':empty'))
|
||||
|
||||
@message_container = $('.message-container')
|
||||
@message_container.toggle(not @message_container.is(':empty'))
|
||||
|
||||
@problem_button = $('.problem-button')
|
||||
@problem_button.click @show_results
|
||||
|
||||
@problem_list = $('.problem-list')
|
||||
@construct_progress_bar()
|
||||
|
||||
if @use_single_location
|
||||
if @use_single_location.toLowerCase() == "true"
|
||||
#If the peer grading element is linked to a single location, then activate the backend for that location
|
||||
@activate_problem()
|
||||
else
|
||||
#Otherwise, activate the panel view.
|
||||
@error_container = $('.error-container')
|
||||
@error_container.toggle(not @error_container.is(':empty'))
|
||||
|
||||
@message_container = $('.message-container')
|
||||
@message_container.toggle(not @message_container.is(':empty'))
|
||||
|
||||
@problem_button = $('.problem-button')
|
||||
@problem_button.click @show_results
|
||||
|
||||
@problem_list = $('.problem-list')
|
||||
@construct_progress_bar()
|
||||
|
||||
construct_progress_bar: () =>
|
||||
problems = @problem_list.find('tr').next()
|
||||
|
||||
1
common/lib/xmodule/xmodule/js/src/poll/.gitignore
vendored
Normal file
1
common/lib/xmodule/xmodule/js/src/poll/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!*.js
|
||||
1
common/lib/xmodule/xmodule/js/src/sequence/display/.gitignore
vendored
Normal file
1
common/lib/xmodule/xmodule/js/src/sequence/display/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!*.js
|
||||
1
common/lib/xmodule/xmodule/js/src/videoalpha/display/.gitignore
vendored
Normal file
1
common/lib/xmodule/xmodule/js/src/videoalpha/display/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!html5_video.js
|
||||
@@ -1,5 +1,4 @@
|
||||
from .x_module import XModuleDescriptor, DescriptorSystem
|
||||
from .modulestore.inheritance import own_metadata
|
||||
|
||||
|
||||
class MakoDescriptorSystem(DescriptorSystem):
|
||||
@@ -34,20 +33,10 @@ class MakoModuleDescriptor(XModuleDescriptor):
|
||||
"""
|
||||
return {
|
||||
'module': self,
|
||||
'editable_metadata_fields': self.editable_metadata_fields,
|
||||
'editable_metadata_fields': self.editable_metadata_fields
|
||||
}
|
||||
|
||||
def get_html(self):
|
||||
return self.system.render_template(
|
||||
self.mako_template, self.get_context())
|
||||
|
||||
# cdodge: encapsulate a means to expose "editable" metadata fields (i.e. not internal system metadata)
|
||||
@property
|
||||
def editable_metadata_fields(self):
|
||||
fields = {}
|
||||
for field, value in own_metadata(self).items():
|
||||
if field in self.system_metadata_fields:
|
||||
continue
|
||||
|
||||
fields[field] = value
|
||||
return fields
|
||||
|
||||
@@ -31,15 +31,22 @@ def inherit_metadata(descriptor, model_data):
|
||||
Only metadata specified in self.inheritable_metadata will
|
||||
be inherited
|
||||
"""
|
||||
# The inherited values that are actually being used.
|
||||
if not hasattr(descriptor, '_inherited_metadata'):
|
||||
setattr(descriptor, '_inherited_metadata', {})
|
||||
|
||||
# All inheritable metadata values (for which a value exists in model_data).
|
||||
if not hasattr(descriptor, '_inheritable_metadata'):
|
||||
setattr(descriptor, '_inheritable_metadata', {})
|
||||
|
||||
# Set all inheritable metadata from kwargs that are
|
||||
# in self.inheritable_metadata and aren't already set in metadata
|
||||
for attr in INHERITABLE_METADATA:
|
||||
if attr not in descriptor._model_data and attr in model_data:
|
||||
descriptor._inherited_metadata[attr] = model_data[attr]
|
||||
descriptor._model_data[attr] = model_data[attr]
|
||||
if attr in model_data:
|
||||
descriptor._inheritable_metadata[attr] = model_data[attr]
|
||||
if attr not in descriptor._model_data:
|
||||
descriptor._inherited_metadata[attr] = model_data[attr]
|
||||
descriptor._model_data[attr] = model_data[attr]
|
||||
|
||||
|
||||
def own_metadata(module):
|
||||
|
||||
@@ -37,11 +37,17 @@ class XModuleCourseFactory(Factory):
|
||||
new_course.display_name = display_name
|
||||
|
||||
new_course.lms.start = gmtime()
|
||||
new_course.tabs = [{"type": "courseware"},
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
{"type": "discussion", "name": "Discussion"},
|
||||
{"type": "wiki", "name": "Wiki"},
|
||||
{"type": "progress", "name": "Progress"}]
|
||||
new_course.tabs = kwargs.get(
|
||||
'tabs',
|
||||
[
|
||||
{"type": "courseware"},
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
{"type": "discussion", "name": "Discussion"},
|
||||
{"type": "wiki", "name": "Wiki"},
|
||||
{"type": "progress", "name": "Progress"}
|
||||
]
|
||||
)
|
||||
new_course.discussion_link = kwargs.get('discussion_link')
|
||||
|
||||
# Update the data in the mongo datastore
|
||||
store.update_metadata(new_course.location.url(), own_metadata(new_course))
|
||||
|
||||
@@ -11,7 +11,7 @@ from xmodule.raw_module import RawDescriptor
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from .timeinfo import TimeInfo
|
||||
from xblock.core import Object, Integer, Boolean, String, Scope
|
||||
from xmodule.fields import Date, StringyFloat
|
||||
from xmodule.fields import Date, StringyFloat, StringyInteger, StringyBoolean
|
||||
|
||||
from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService
|
||||
from open_ended_grading_classes import combined_open_ended_rubric
|
||||
@@ -28,14 +28,14 @@ EXTERNAL_GRADER_NO_CONTACT_ERROR = "Failed to contact external graders. Please
|
||||
|
||||
|
||||
class PeerGradingFields(object):
|
||||
use_for_single_location = Boolean(help="Whether to use this for a single location or as a panel.",
|
||||
use_for_single_location = StringyBoolean(help="Whether to use this for a single location or as a panel.",
|
||||
default=USE_FOR_SINGLE_LOCATION, scope=Scope.settings)
|
||||
link_to_location = String(help="The location this problem is linked to.", default=LINK_TO_LOCATION,
|
||||
scope=Scope.settings)
|
||||
is_graded = Boolean(help="Whether or not this module is scored.", default=IS_GRADED, scope=Scope.settings)
|
||||
is_graded = StringyBoolean(help="Whether or not this module is scored.", default=IS_GRADED, scope=Scope.settings)
|
||||
due_date = Date(help="Due date that should be displayed.", default=None, scope=Scope.settings)
|
||||
grace_period_string = String(help="Amount of grace to give on the due date.", default=None, scope=Scope.settings)
|
||||
max_grade = Integer(help="The maximum grade that a student can receieve for this problem.", default=MAX_SCORE,
|
||||
max_grade = StringyInteger(help="The maximum grade that a student can receieve for this problem.", default=MAX_SCORE,
|
||||
scope=Scope.settings)
|
||||
student_data_for_location = Object(help="Student data for a given peer grading problem.",
|
||||
scope=Scope.user_state)
|
||||
@@ -93,9 +93,9 @@ class PeerGradingModule(PeerGradingFields, XModule):
|
||||
if not self.ajax_url.endswith("/"):
|
||||
self.ajax_url = self.ajax_url + "/"
|
||||
|
||||
if not isinstance(self.max_grade, (int, long)):
|
||||
#This could result in an exception, but not wrapping in a try catch block so it moves up the stack
|
||||
self.max_grade = int(self.max_grade)
|
||||
#StringyInteger could return None, so keep this check.
|
||||
if not isinstance(self.max_grade, int):
|
||||
raise TypeError("max_grade needs to be an integer.")
|
||||
|
||||
def closed(self):
|
||||
return self._closed(self.timeinfo)
|
||||
|
||||
@@ -40,34 +40,20 @@ class DummySystem(ImportSystem):
|
||||
)
|
||||
|
||||
|
||||
class IsNewCourseTestCase(unittest.TestCase):
|
||||
"""Make sure the property is_new works on courses"""
|
||||
def get_dummy_course(start, announcement=None, is_new=None, advertised_start=None, end=None):
|
||||
"""Get a dummy course"""
|
||||
|
||||
def setUp(self):
|
||||
# Needed for test_is_newish
|
||||
datetime_patcher = patch.object(
|
||||
xmodule.course_module, 'datetime',
|
||||
Mock(wraps=datetime.datetime)
|
||||
)
|
||||
mocked_datetime = datetime_patcher.start()
|
||||
mocked_datetime.utcnow.return_value = time_to_datetime(NOW)
|
||||
self.addCleanup(datetime_patcher.stop)
|
||||
system = DummySystem(load_error_modules=True)
|
||||
|
||||
@staticmethod
|
||||
def get_dummy_course(start, announcement=None, is_new=None, advertised_start=None, end=None):
|
||||
"""Get a dummy course"""
|
||||
def to_attrb(n, v):
|
||||
return '' if v is None else '{0}="{1}"'.format(n, v).lower()
|
||||
|
||||
system = DummySystem(load_error_modules=True)
|
||||
is_new = to_attrb('is_new', is_new)
|
||||
announcement = to_attrb('announcement', announcement)
|
||||
advertised_start = to_attrb('advertised_start', advertised_start)
|
||||
end = to_attrb('end', end)
|
||||
|
||||
def to_attrb(n, v):
|
||||
return '' if v is None else '{0}="{1}"'.format(n, v).lower()
|
||||
|
||||
is_new = to_attrb('is_new', is_new)
|
||||
announcement = to_attrb('announcement', announcement)
|
||||
advertised_start = to_attrb('advertised_start', advertised_start)
|
||||
end = to_attrb('end', end)
|
||||
|
||||
start_xml = '''
|
||||
start_xml = '''
|
||||
<course org="{org}" course="{course}"
|
||||
graceperiod="1 day" url_name="test"
|
||||
start="{start}"
|
||||
@@ -80,9 +66,23 @@ class IsNewCourseTestCase(unittest.TestCase):
|
||||
</chapter>
|
||||
</course>
|
||||
'''.format(org=ORG, course=COURSE, start=start, is_new=is_new,
|
||||
announcement=announcement, advertised_start=advertised_start, end=end)
|
||||
announcement=announcement, advertised_start=advertised_start, end=end)
|
||||
|
||||
return system.process_xml(start_xml)
|
||||
return system.process_xml(start_xml)
|
||||
|
||||
|
||||
class IsNewCourseTestCase(unittest.TestCase):
|
||||
"""Make sure the property is_new works on courses"""
|
||||
|
||||
def setUp(self):
|
||||
# Needed for test_is_newish
|
||||
datetime_patcher = patch.object(
|
||||
xmodule.course_module, 'datetime',
|
||||
Mock(wraps=datetime.datetime)
|
||||
)
|
||||
mocked_datetime = datetime_patcher.start()
|
||||
mocked_datetime.utcnow.return_value = time_to_datetime(NOW)
|
||||
self.addCleanup(datetime_patcher.stop)
|
||||
|
||||
@patch('xmodule.course_module.time.gmtime')
|
||||
def test_sorting_score(self, gmtime_mock):
|
||||
@@ -120,8 +120,8 @@ class IsNewCourseTestCase(unittest.TestCase):
|
||||
]
|
||||
|
||||
for a, b, assertion in dates:
|
||||
a_score = self.get_dummy_course(start=a[0], announcement=a[1], advertised_start=a[2]).sorting_score
|
||||
b_score = self.get_dummy_course(start=b[0], announcement=b[1], advertised_start=b[2]).sorting_score
|
||||
a_score = get_dummy_course(start=a[0], announcement=a[1], advertised_start=a[2]).sorting_score
|
||||
b_score = get_dummy_course(start=b[0], announcement=b[1], advertised_start=b[2]).sorting_score
|
||||
print "Comparing %s to %s" % (a, b)
|
||||
assertion(a_score, b_score)
|
||||
|
||||
@@ -138,36 +138,42 @@ class IsNewCourseTestCase(unittest.TestCase):
|
||||
]
|
||||
|
||||
for s in settings:
|
||||
d = self.get_dummy_course(start=s[0], advertised_start=s[1])
|
||||
d = get_dummy_course(start=s[0], advertised_start=s[1])
|
||||
print "Checking start=%s advertised=%s" % (s[0], s[1])
|
||||
self.assertEqual(d.start_date_text, s[2])
|
||||
|
||||
def test_is_newish(self):
|
||||
descriptor = self.get_dummy_course(start='2012-12-02T12:00', is_new=True)
|
||||
descriptor = get_dummy_course(start='2012-12-02T12:00', is_new=True)
|
||||
assert(descriptor.is_newish is True)
|
||||
|
||||
descriptor = self.get_dummy_course(start='2013-02-02T12:00', is_new=False)
|
||||
descriptor = get_dummy_course(start='2013-02-02T12:00', is_new=False)
|
||||
assert(descriptor.is_newish is False)
|
||||
|
||||
descriptor = self.get_dummy_course(start='2013-02-02T12:00', is_new=True)
|
||||
descriptor = get_dummy_course(start='2013-02-02T12:00', is_new=True)
|
||||
assert(descriptor.is_newish is True)
|
||||
|
||||
descriptor = self.get_dummy_course(start='2013-01-15T12:00')
|
||||
descriptor = get_dummy_course(start='2013-01-15T12:00')
|
||||
assert(descriptor.is_newish is True)
|
||||
|
||||
descriptor = self.get_dummy_course(start='2013-03-01T12:00')
|
||||
descriptor = get_dummy_course(start='2013-03-01T12:00')
|
||||
assert(descriptor.is_newish is True)
|
||||
|
||||
descriptor = self.get_dummy_course(start='2012-10-15T12:00')
|
||||
descriptor = get_dummy_course(start='2012-10-15T12:00')
|
||||
assert(descriptor.is_newish is False)
|
||||
|
||||
descriptor = self.get_dummy_course(start='2012-12-31T12:00')
|
||||
descriptor = get_dummy_course(start='2012-12-31T12:00')
|
||||
assert(descriptor.is_newish is True)
|
||||
|
||||
def test_end_date_text(self):
|
||||
# No end date set, returns empty string.
|
||||
d = self.get_dummy_course('2012-12-02T12:00')
|
||||
d = get_dummy_course('2012-12-02T12:00')
|
||||
self.assertEqual('', d.end_date_text)
|
||||
|
||||
d = self.get_dummy_course('2012-12-02T12:00', end='2014-9-04T12:00')
|
||||
d = get_dummy_course('2012-12-02T12:00', end='2014-9-04T12:00')
|
||||
self.assertEqual('Sep 04, 2014', d.end_date_text)
|
||||
|
||||
|
||||
class DiscussionTopicsTestCase(unittest.TestCase):
|
||||
def test_default_discussion_topics(self):
|
||||
d = get_dummy_course('2012-12-02T12:00')
|
||||
self.assertEqual({'General': {'id': 'i4x-test_org-test_course-course-test'}}, d.discussion_topics)
|
||||
|
||||
@@ -151,6 +151,10 @@ class ImportTestCase(BaseCourseTestCase):
|
||||
# Check that the child inherits due correctly
|
||||
child = descriptor.get_children()[0]
|
||||
self.assertEqual(child.lms.due, Date().from_json(v))
|
||||
self.assertEqual(child._inheritable_metadata, child._inherited_metadata)
|
||||
self.assertEqual(2, len(child._inherited_metadata))
|
||||
self.assertEqual('1970-01-01T00:00:00Z', child._inherited_metadata['start'])
|
||||
self.assertEqual(v, child._inherited_metadata['due'])
|
||||
|
||||
# Now export and check things
|
||||
resource_fs = MemoryFS()
|
||||
@@ -184,6 +188,60 @@ class ImportTestCase(BaseCourseTestCase):
|
||||
self.assertEqual(chapter_xml.tag, 'chapter')
|
||||
self.assertFalse('due' in chapter_xml.attrib)
|
||||
|
||||
def test_metadata_no_inheritance(self):
|
||||
"""
|
||||
Checks that default value of None (for due) does not get marked as inherited.
|
||||
"""
|
||||
system = self.get_system()
|
||||
url_name = 'test1'
|
||||
start_xml = '''
|
||||
<course org="{org}" course="{course}"
|
||||
url_name="{url_name}" unicorn="purple">
|
||||
<chapter url="hi" url_name="ch" display_name="CH">
|
||||
<html url_name="h" display_name="H">Two houses, ...</html>
|
||||
</chapter>
|
||||
</course>'''.format(org=ORG, course=COURSE, url_name=url_name)
|
||||
descriptor = system.process_xml(start_xml)
|
||||
compute_inherited_metadata(descriptor)
|
||||
|
||||
self.assertEqual(descriptor.lms.due, None)
|
||||
|
||||
# Check that the child does not inherit a value for due
|
||||
child = descriptor.get_children()[0]
|
||||
self.assertEqual(child.lms.due, None)
|
||||
self.assertEqual(child._inheritable_metadata, child._inherited_metadata)
|
||||
self.assertEqual(1, len(child._inherited_metadata))
|
||||
self.assertEqual('1970-01-01T00:00:00Z', child._inherited_metadata['start'])
|
||||
|
||||
def test_metadata_override_default(self):
|
||||
"""
|
||||
Checks that due date can be overriden at child level.
|
||||
"""
|
||||
system = self.get_system()
|
||||
course_due = 'March 20 17:00'
|
||||
child_due = 'April 10 00:00'
|
||||
url_name = 'test1'
|
||||
start_xml = '''
|
||||
<course org="{org}" course="{course}"
|
||||
due="{due}" url_name="{url_name}" unicorn="purple">
|
||||
<chapter url="hi" url_name="ch" display_name="CH">
|
||||
<html url_name="h" display_name="H">Two houses, ...</html>
|
||||
</chapter>
|
||||
</course>'''.format(due=course_due, org=ORG, course=COURSE, url_name=url_name)
|
||||
descriptor = system.process_xml(start_xml)
|
||||
child = descriptor.get_children()[0]
|
||||
child._model_data['due'] = child_due
|
||||
compute_inherited_metadata(descriptor)
|
||||
|
||||
self.assertEqual(descriptor.lms.due, Date().from_json(course_due))
|
||||
self.assertEqual(child.lms.due, Date().from_json(child_due))
|
||||
# Test inherited metadata. Due does not appear here (because explicitly set on child).
|
||||
self.assertEqual(1, len(child._inherited_metadata))
|
||||
self.assertEqual('1970-01-01T00:00:00Z', child._inherited_metadata['start'])
|
||||
# Test inheritable metadata. This has the course inheritable value for due.
|
||||
self.assertEqual(2, len(child._inheritable_metadata))
|
||||
self.assertEqual(course_due, child._inheritable_metadata['due'])
|
||||
|
||||
def test_is_pointer_tag(self):
|
||||
"""
|
||||
Check that is_pointer_tag works properly.
|
||||
|
||||
93
common/lib/xmodule/xmodule/tests/test_xml_module.py
Normal file
93
common/lib/xmodule/xmodule/tests/test_xml_module.py
Normal file
@@ -0,0 +1,93 @@
|
||||
from xmodule.x_module import XModuleFields
|
||||
from xblock.core import Scope, String, Object
|
||||
from xmodule.fields import Date, StringyInteger
|
||||
from xmodule.xml_module import XmlDescriptor
|
||||
import unittest
|
||||
from . import test_system
|
||||
from mock import Mock
|
||||
|
||||
|
||||
class TestFields(object):
|
||||
# Will be returned by editable_metadata_fields.
|
||||
max_attempts = StringyInteger(scope=Scope.settings, default=1000)
|
||||
# Will not be returned by editable_metadata_fields because filtered out by non_editable_metadata_fields.
|
||||
due = Date(scope=Scope.settings)
|
||||
# Will not be returned by editable_metadata_fields because is not Scope.settings.
|
||||
student_answers = Object(scope=Scope.user_state)
|
||||
# Will be returned, and can override the inherited value from XModule.
|
||||
display_name = String(scope=Scope.settings, default='local default')
|
||||
|
||||
|
||||
class EditableMetadataFieldsTest(unittest.TestCase):
|
||||
|
||||
def test_display_name_field(self):
|
||||
editable_fields = self.get_xml_editable_fields({})
|
||||
# Tests that the xblock fields (currently tags and name) get filtered out.
|
||||
# Also tests that xml_attributes is filtered out of XmlDescriptor.
|
||||
self.assertEqual(1, len(editable_fields), "Expected only 1 editable field for xml descriptor.")
|
||||
self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name,
|
||||
explicitly_set=False, inheritable=False, value=None, default_value=None)
|
||||
|
||||
def test_override_default(self):
|
||||
# Tests that explicitly_set is correct when a value overrides the default (not inheritable).
|
||||
editable_fields = self.get_xml_editable_fields({'display_name': 'foo'})
|
||||
self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name,
|
||||
explicitly_set=True, inheritable=False, value='foo', default_value=None)
|
||||
|
||||
def test_additional_field(self):
|
||||
descriptor = self.get_descriptor({'max_attempts' : '7'})
|
||||
editable_fields = descriptor.editable_metadata_fields
|
||||
self.assertEqual(2, len(editable_fields))
|
||||
self.assert_field_values(editable_fields, 'max_attempts', TestFields.max_attempts,
|
||||
explicitly_set=True, inheritable=False, value=7, default_value=1000)
|
||||
self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name,
|
||||
explicitly_set=False, inheritable=False, value='local default', default_value='local default')
|
||||
|
||||
editable_fields = self.get_descriptor({}).editable_metadata_fields
|
||||
self.assert_field_values(editable_fields, 'max_attempts', TestFields.max_attempts,
|
||||
explicitly_set=False, inheritable=False, value=1000, default_value=1000)
|
||||
|
||||
def test_inherited_field(self):
|
||||
model_val = {'display_name' : 'inherited'}
|
||||
descriptor = self.get_descriptor(model_val)
|
||||
# Mimic an inherited value for display_name (inherited and inheritable are the same in this case).
|
||||
descriptor._inherited_metadata = model_val
|
||||
descriptor._inheritable_metadata = model_val
|
||||
editable_fields = descriptor.editable_metadata_fields
|
||||
self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name,
|
||||
explicitly_set=False, inheritable=True, value='inherited', default_value='inherited')
|
||||
|
||||
descriptor = self.get_descriptor({'display_name' : 'explicit'})
|
||||
# Mimic the case where display_name WOULD have been inherited, except we explicitly set it.
|
||||
descriptor._inheritable_metadata = {'display_name' : 'inheritable value'}
|
||||
descriptor._inherited_metadata = {}
|
||||
editable_fields = descriptor.editable_metadata_fields
|
||||
self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name,
|
||||
explicitly_set=True, inheritable=True, value='explicit', default_value='inheritable value')
|
||||
|
||||
# Start of helper methods
|
||||
def get_xml_editable_fields(self, model_data):
|
||||
system = test_system()
|
||||
system.render_template = Mock(return_value="<div>Test Template HTML</div>")
|
||||
return XmlDescriptor(system=system, location=None, model_data=model_data).editable_metadata_fields
|
||||
|
||||
def get_descriptor(self, model_data):
|
||||
class TestModuleDescriptor(TestFields, XmlDescriptor):
|
||||
|
||||
@property
|
||||
def non_editable_metadata_fields(self):
|
||||
non_editable_fields = super(TestModuleDescriptor, self).non_editable_metadata_fields
|
||||
non_editable_fields.append(TestModuleDescriptor.due)
|
||||
return non_editable_fields
|
||||
|
||||
system = test_system()
|
||||
system.render_template = Mock(return_value="<div>Test Template HTML</div>")
|
||||
return TestModuleDescriptor(system=system, location=None, model_data=model_data)
|
||||
|
||||
def assert_field_values(self, editable_fields, name, field, explicitly_set, inheritable, value, default_value):
|
||||
test_field = editable_fields[name]
|
||||
self.assertEqual(field, test_field['field'])
|
||||
self.assertEqual(explicitly_set, test_field['explicitly_set'])
|
||||
self.assertEqual(inheritable, test_field['inheritable'])
|
||||
self.assertEqual(value, test_field['value'])
|
||||
self.assertEqual(default_value, test_field['default_value'])
|
||||
@@ -82,7 +82,7 @@ class XModuleFields(object):
|
||||
display_name = String(
|
||||
help="Display name for this module",
|
||||
scope=Scope.settings,
|
||||
default=None,
|
||||
default=None
|
||||
)
|
||||
|
||||
|
||||
@@ -334,12 +334,6 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
|
||||
# (like a practice problem).
|
||||
has_score = False
|
||||
|
||||
# cdodge: this is a list of metadata names which are 'system' metadata
|
||||
# and should not be edited by an end-user
|
||||
|
||||
system_metadata_fields = ['data_dir', 'published_date', 'published_by', 'is_draft',
|
||||
'discussion_id', 'xml_attributes']
|
||||
|
||||
# A list of descriptor attributes that must be equal for the descriptors to
|
||||
# be equal
|
||||
equality_attributes = ('_model_data', 'location')
|
||||
@@ -612,6 +606,49 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
|
||||
model_data=self._model_data,
|
||||
))
|
||||
|
||||
@property
|
||||
def non_editable_metadata_fields(self):
|
||||
"""
|
||||
Return the list of fields that should not be editable in Studio.
|
||||
|
||||
When overriding, be sure to append to the superclasses' list.
|
||||
"""
|
||||
# We are not allowing editing of xblock tag and name fields at this time (for any component).
|
||||
return [XBlock.tags, XBlock.name]
|
||||
|
||||
@property
|
||||
def editable_metadata_fields(self):
|
||||
"""
|
||||
Returns the metadata fields to be edited in Studio. These are fields with scope `Scope.settings`.
|
||||
|
||||
Can be limited by extending `non_editable_metadata_fields`.
|
||||
"""
|
||||
inherited_metadata = getattr(self, '_inherited_metadata', {})
|
||||
inheritable_metadata = getattr(self, '_inheritable_metadata', {})
|
||||
metadata = {}
|
||||
for field in self.fields:
|
||||
|
||||
if field.scope != Scope.settings or field in self.non_editable_metadata_fields:
|
||||
continue
|
||||
|
||||
inheritable = False
|
||||
value = getattr(self, field.name)
|
||||
default_value = field.default
|
||||
explicitly_set = field.name in self._model_data
|
||||
if field.name in inheritable_metadata:
|
||||
inheritable = True
|
||||
default_value = field.from_json(inheritable_metadata.get(field.name))
|
||||
if field.name in inherited_metadata:
|
||||
explicitly_set = False
|
||||
|
||||
metadata[field.name] = {'field': field,
|
||||
'value': value,
|
||||
'default_value': default_value,
|
||||
'inheritable': inheritable,
|
||||
'explicitly_set': explicitly_set }
|
||||
|
||||
return metadata
|
||||
|
||||
|
||||
class DescriptorSystem(object):
|
||||
def __init__(self, load_item, resources_fs, error_tracker, **kwargs):
|
||||
|
||||
@@ -84,7 +84,8 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
Mixin class for standardized parsing of from xml
|
||||
"""
|
||||
|
||||
xml_attributes = Object(help="Map of unhandled xml attributes, used only for storage between import and export", default={}, scope=Scope.settings)
|
||||
xml_attributes = Object(help="Map of unhandled xml attributes, used only for storage between import and export",
|
||||
default={}, scope=Scope.settings)
|
||||
|
||||
# Extension to append to filename paths
|
||||
filename_extension = 'xml'
|
||||
@@ -418,3 +419,9 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"%s does not implement definition_to_xml" % self.__class__.__name__)
|
||||
|
||||
@property
|
||||
def non_editable_metadata_fields(self):
|
||||
non_editable_fields = super(XmlDescriptor, self).non_editable_metadata_fields
|
||||
non_editable_fields.append(XmlDescriptor.xml_attributes)
|
||||
return non_editable_fields
|
||||
|
||||
46
common/static/js/vendor/backbone-min.js
vendored
46
common/static/js/vendor/backbone-min.js
vendored
File diff suppressed because one or more lines are too long
2
common/static/js/vendor/underscore-min.js
vendored
2
common/static/js/vendor/underscore-min.js
vendored
File diff suppressed because one or more lines are too long
@@ -31,6 +31,14 @@ Check out the course data directories that you want to work with into the
|
||||
|
||||
rake resetdb
|
||||
|
||||
## Installing
|
||||
|
||||
To create your development environment, run the shell script in the root of
|
||||
the repo:
|
||||
|
||||
create-dev-env.sh
|
||||
|
||||
|
||||
## Starting development servers
|
||||
|
||||
Both the LMS and Studio can be started using the following shortcut tasks
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
-e git://github.com/edx/django-pipeline.git#egg=django-pipeline
|
||||
-e git://github.com/edx/django-wiki.git@e2e84558#egg=django-wiki
|
||||
-e git://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev
|
||||
-e git://github.com/eventbrite/zendesk.git@d53fe0e81b623f084e91776bcf6369f8b7b63879#egg=zendesk
|
||||
|
||||
# Our libraries:
|
||||
-e git+https://github.com/edx/XBlock.git@5ce6f70a#egg=XBlock
|
||||
-e git+https://github.com/edx/XBlock.git@483e0cb1#egg=XBlock
|
||||
|
||||
74
install.txt
74
install.txt
@@ -1,74 +0,0 @@
|
||||
This document describes how to set up the MITx development environment
|
||||
for both Linux (Ubuntu) and MacOS (OSX Lion).
|
||||
|
||||
There is also a script "create-dev-env.sh" that automates these steps.
|
||||
|
||||
1) Make an mitx_all directory and clone the repos
|
||||
(download and install git and mercurial if you don't have them already)
|
||||
|
||||
mkdir ~/mitx_all
|
||||
cd ~/mitx_all
|
||||
git clone git@github.com:MITx/mitx.git
|
||||
hg clone ssh://hg-content@gp.mitx.mit.edu/data
|
||||
|
||||
2) Install OSX dependencies (Mac users only)
|
||||
|
||||
a) Install the brew utility if necessary
|
||||
/usr/bin/ruby -e "$(curl -fsSL https://raw.github.com/mxcl/homebrew/master/Library/Contributions/install_homebrew.rb)"
|
||||
|
||||
b) Install the brew package list
|
||||
cat ~/mitx_all/mitx/brew-formulas.txt | xargs brew install
|
||||
|
||||
c) Install python pip if necessary
|
||||
sudo easy_install pip
|
||||
|
||||
d) Install python virtualenv if necessary
|
||||
sudo pip install virtualenv virtualenvwrapper
|
||||
|
||||
e) Install coffee script
|
||||
curl http://npmjs.org/install.sh | sh
|
||||
npm install -g coffee-script
|
||||
|
||||
3) Install Ubuntu dependencies (Linux users only)
|
||||
|
||||
sudo apt-get install curl python-virtualenv build-essential python-dev gfortran liblapack-dev libfreetype6-dev libpng12-dev libxml2-dev libxslt-dev yui-compressor coffeescript
|
||||
|
||||
|
||||
4) Install rvm, ruby, and libraries
|
||||
|
||||
echo "export rvm_path=$HOME/mitx_all/ruby" > $HOME/.rvmrc
|
||||
curl -sL get.rvm.io | bash -s stable
|
||||
source ~/mitx_all/ruby/scripts/rvm
|
||||
rvm install 1.9.3
|
||||
gem install bundler
|
||||
cd ~/mitx_all/mitx
|
||||
bundle install
|
||||
|
||||
5) Install python libraries
|
||||
|
||||
source ~/mitx_all/python/bin/activate
|
||||
cd ~/mitx_all
|
||||
pip install -r mitx/pre-requirements.txt
|
||||
pip install -r mitx/requirements.txt
|
||||
|
||||
6) Create log and db dirs
|
||||
|
||||
mkdir ~/mitx_all/log
|
||||
mkdir ~/mitx_all/db
|
||||
|
||||
7) Start the dev server
|
||||
|
||||
To start using Django you will need
|
||||
to activate the local Python and Ruby
|
||||
environment:
|
||||
|
||||
$ source ~/mitx_all/ruby/scripts/rvm
|
||||
$ source ~/mitx_all/python/bin/activate
|
||||
|
||||
To initialize and start a local instance of Django:
|
||||
|
||||
$ cd ~/mitx_all/mitx
|
||||
$ django-admin.py syncdb --settings=envs.dev --pythonpath=.
|
||||
$ django-admin.py migrate --settings=envs.dev --pythonpath=.
|
||||
$ django-admin.py runserver --settings=envs.dev --pythonpath=.
|
||||
|
||||
@@ -294,6 +294,27 @@ def get_course_tabs(user, course, active_page):
|
||||
return tabs
|
||||
|
||||
|
||||
def get_discussion_link(course):
|
||||
"""
|
||||
Return the URL for the discussion tab for the given `course`.
|
||||
|
||||
If they have a discussion link specified, use that even if we disable
|
||||
discussions. Disabling discsussions is mostly a server safety feature at
|
||||
this point, and we don't need to worry about external sites. Otherwise,
|
||||
if the course has a discussion tab or uses the default tabs, return the
|
||||
discussion view URL. Otherwise, return None to indicate the lack of a
|
||||
discussion tab.
|
||||
"""
|
||||
if course.discussion_link:
|
||||
return course.discussion_link
|
||||
elif not settings.MITX_FEATURES.get('ENABLE_DISCUSSION_SERVICE'):
|
||||
return None
|
||||
elif hasattr(course, 'tabs') and course.tabs and not any([tab['type'] == 'discussion' for tab in course.tabs]):
|
||||
return None
|
||||
else:
|
||||
return reverse('django_comment_client.forum.views.forum_form_discussion', args=[course.id])
|
||||
|
||||
|
||||
def get_default_tabs(user, course, active_page):
|
||||
|
||||
# When calling the various _tab methods, can omit the 'type':'blah' from the
|
||||
@@ -308,15 +329,9 @@ def get_default_tabs(user, course, active_page):
|
||||
|
||||
tabs.extend(_textbooks({}, user, course, active_page))
|
||||
|
||||
## If they have a discussion link specified, use that even if we feature
|
||||
## flag discussions off. Disabling that is mostly a server safety feature
|
||||
## at this point, and we don't need to worry about external sites.
|
||||
if course.discussion_link:
|
||||
tabs.append(CourseTab('Discussion', course.discussion_link, active_page == 'discussion'))
|
||||
elif settings.MITX_FEATURES.get('ENABLE_DISCUSSION_SERVICE'):
|
||||
link = reverse('django_comment_client.forum.views.forum_form_discussion',
|
||||
args=[course.id])
|
||||
tabs.append(CourseTab('Discussion', link, active_page == 'discussion'))
|
||||
discussion_link = get_discussion_link(course)
|
||||
if discussion_link:
|
||||
tabs.append(CourseTab('Discussion', discussion_link, active_page == 'discussion'))
|
||||
|
||||
tabs.extend(_wiki({'name': 'Wiki', 'type': 'wiki'}, user, course, active_page))
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
from django.test import TestCase
|
||||
from mock import MagicMock
|
||||
from mock import patch
|
||||
|
||||
import courseware.tabs as tabs
|
||||
|
||||
from django.test.utils import override_settings
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
class ProgressTestCase(TestCase):
|
||||
|
||||
@@ -257,3 +261,62 @@ class ValidateTabsTestCase(TestCase):
|
||||
self.assertRaises(tabs.InvalidTabsException, tabs.validate_tabs, self.courses[2])
|
||||
self.assertIsNone(tabs.validate_tabs(self.courses[3]))
|
||||
self.assertRaises(tabs.InvalidTabsException, tabs.validate_tabs, self.courses[4])
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
|
||||
class DiscussionLinkTestCase(ModuleStoreTestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.tabs_with_discussion = [
|
||||
{'type':'courseware'},
|
||||
{'type':'course_info'},
|
||||
{'type':'discussion'},
|
||||
{'type':'textbooks'},
|
||||
]
|
||||
self.tabs_without_discussion = [
|
||||
{'type':'courseware'},
|
||||
{'type':'course_info'},
|
||||
{'type':'textbooks'},
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _patch_reverse(course):
|
||||
def patched_reverse(viewname, args):
|
||||
if viewname == "django_comment_client.forum.views.forum_form_discussion" and args == [course.id]:
|
||||
return "default_discussion_link"
|
||||
else:
|
||||
return None
|
||||
return patch("courseware.tabs.reverse", patched_reverse)
|
||||
|
||||
@patch.dict("django.conf.settings.MITX_FEATURES", {"ENABLE_DISCUSSION_SERVICE": False})
|
||||
def test_explicit_discussion_link(self):
|
||||
"""Test that setting discussion_link overrides everything else"""
|
||||
course = CourseFactory.create(discussion_link="other_discussion_link", tabs=self.tabs_with_discussion)
|
||||
self.assertEqual(tabs.get_discussion_link(course), "other_discussion_link")
|
||||
|
||||
@patch.dict("django.conf.settings.MITX_FEATURES", {"ENABLE_DISCUSSION_SERVICE": False})
|
||||
def test_discussions_disabled(self):
|
||||
"""Test that other cases return None with discussions disabled"""
|
||||
for i, t in enumerate([None, self.tabs_with_discussion, self.tabs_without_discussion]):
|
||||
course = CourseFactory.create(tabs=t, number=str(i))
|
||||
self.assertEqual(tabs.get_discussion_link(course), None)
|
||||
|
||||
@patch.dict("django.conf.settings.MITX_FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
||||
def test_no_tabs(self):
|
||||
"""Test a course without tabs configured"""
|
||||
course = CourseFactory.create(tabs=None)
|
||||
with self._patch_reverse(course):
|
||||
self.assertEqual(tabs.get_discussion_link(course), "default_discussion_link")
|
||||
|
||||
@patch.dict("django.conf.settings.MITX_FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
||||
def test_tabs_with_discussion(self):
|
||||
"""Test a course with a discussion tab configured"""
|
||||
course = CourseFactory.create(tabs=self.tabs_with_discussion)
|
||||
with self._patch_reverse(course):
|
||||
self.assertEqual(tabs.get_discussion_link(course), "default_discussion_link")
|
||||
|
||||
@patch.dict("django.conf.settings.MITX_FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
||||
def test_tabs_without_discussion(self):
|
||||
"""Test a course with tabs configured but without a discussion tab"""
|
||||
course = CourseFactory.create(tabs=self.tabs_without_discussion)
|
||||
self.assertEqual(tabs.get_discussion_link(course), None)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
'''
|
||||
Test for lms courseware app
|
||||
'''
|
||||
|
||||
import logging
|
||||
import json
|
||||
import time
|
||||
import random
|
||||
|
||||
from urlparse import urlsplit, urlunsplit
|
||||
from uuid import uuid4
|
||||
|
||||
from django.contrib.auth.models import User, Group
|
||||
from django.test import TestCase
|
||||
@@ -62,7 +62,7 @@ def mongo_store_config(data_dir):
|
||||
'default_class': 'xmodule.raw_module.RawDescriptor',
|
||||
'host': 'localhost',
|
||||
'db': 'test_xmodule',
|
||||
'collection': 'modulestore',
|
||||
'collection': 'modulestore_%s' % uuid4().hex,
|
||||
'fs_root': data_dir,
|
||||
'render_template': 'mitxmako.shortcuts.render_to_string',
|
||||
}
|
||||
@@ -81,7 +81,7 @@ def draft_mongo_store_config(data_dir):
|
||||
'default_class': 'xmodule.raw_module.RawDescriptor',
|
||||
'host': 'localhost',
|
||||
'db': 'test_xmodule',
|
||||
'collection': 'modulestore',
|
||||
'collection': 'modulestore_%s' % uuid4().hex,
|
||||
'fs_root': data_dir,
|
||||
'render_template': 'mitxmako.shortcuts.render_to_string',
|
||||
}
|
||||
@@ -92,7 +92,7 @@ def draft_mongo_store_config(data_dir):
|
||||
'default_class': 'xmodule.raw_module.RawDescriptor',
|
||||
'host': 'localhost',
|
||||
'db': 'test_xmodule',
|
||||
'collection': 'modulestore',
|
||||
'collection': 'modulestore_%s' % uuid4().hex,
|
||||
'fs_root': data_dir,
|
||||
'render_template': 'mitxmako.shortcuts.render_to_string',
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
from django.conf.urls.defaults import url, patterns
|
||||
import django_comment_client.base.views
|
||||
|
||||
urlpatterns = patterns('django_comment_client.base.views',
|
||||
|
||||
urlpatterns = patterns('django_comment_client.base.views', # nopep8
|
||||
url(r'upload$', 'upload', name='upload'),
|
||||
url(r'users/(?P<user_id>\w+)/update_moderator_status$', 'update_moderator_status', name='update_moderator_status'),
|
||||
url(r'threads/tags/autocomplete$', 'tags_autocomplete', name='tags_autocomplete'),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from django.conf.urls.defaults import url, patterns
|
||||
import django_comment_client.forum.views
|
||||
|
||||
urlpatterns = patterns('django_comment_client.forum.views',
|
||||
urlpatterns = patterns('django_comment_client.forum.views', # nopep8
|
||||
url(r'users/(?P<user_id>\w+)/followed$', 'followed_threads', name='followed_threads'),
|
||||
url(r'users/(?P<user_id>\w+)$', 'user_profile', name='user_profile'),
|
||||
url(r'^(?P<discussion_id>[\w\-.]+)/threads/(?P<thread_id>\w+)$', 'single_thread', name='single_thread'),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from django.conf.urls.defaults import url, patterns, include
|
||||
|
||||
urlpatterns = patterns('',
|
||||
urlpatterns = patterns('', # nopep8
|
||||
url(r'forum/?', include('django_comment_client.forum.urls')),
|
||||
url(r'', include('django_comment_client.base.urls')),
|
||||
)
|
||||
|
||||
@@ -49,7 +49,6 @@ class TestGradebook(ModuleStoreTestCase):
|
||||
]
|
||||
|
||||
for user in self.users:
|
||||
UserProfileFactory.create(user=user)
|
||||
CourseEnrollmentFactory.create(user=user, course_id=self.course.id)
|
||||
|
||||
for i in xrange(USER_COUNT-1):
|
||||
@@ -151,4 +150,4 @@ class TestLetterCutoffPolicy(TestGradebook):
|
||||
# User 0 has 0 on Homeworks [1]
|
||||
# User 0 has 0 on the class [1]
|
||||
# One use at the top of the page [1]
|
||||
self.assertEquals(3, self.response.content.count('grade_None'))
|
||||
self.assertEquals(3, self.response.content.count('grade_None'))
|
||||
|
||||
@@ -11,6 +11,7 @@ from util.cache import cache
|
||||
import datetime
|
||||
from xmodule.x_module import ModuleSystem
|
||||
from mitxmako.shortcuts import render_to_string
|
||||
import datetime
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -104,6 +105,25 @@ def peer_grading_notifications(course, user):
|
||||
|
||||
|
||||
def combined_notifications(course, user):
|
||||
"""
|
||||
Show notifications to a given user for a given course. Get notifications from the cache if possible,
|
||||
or from the grading controller server if not.
|
||||
@param course: The course object for which we are getting notifications
|
||||
@param user: The user object for which we are getting notifications
|
||||
@return: A dictionary with boolean pending_grading (true if there is pending grading), img_path (for notification
|
||||
image), and response (actual response from grading controller server).
|
||||
"""
|
||||
#Set up return values so that we can return them for error cases
|
||||
pending_grading = False
|
||||
img_path = ""
|
||||
notifications={}
|
||||
notification_dict = {'pending_grading': pending_grading, 'img_path': img_path, 'response': notifications}
|
||||
|
||||
#We don't want to show anonymous users anything.
|
||||
if not user.is_authenticated():
|
||||
return notification_dict
|
||||
|
||||
#Define a mock modulesystem
|
||||
system = ModuleSystem(
|
||||
ajax_url=None,
|
||||
track_function=None,
|
||||
@@ -112,41 +132,44 @@ def combined_notifications(course, user):
|
||||
replace_urls=None,
|
||||
xblock_model_data= {}
|
||||
)
|
||||
#Initialize controller query service using our mock system
|
||||
controller_qs = ControllerQueryService(settings.OPEN_ENDED_GRADING_INTERFACE, system)
|
||||
student_id = unique_id_for_user(user)
|
||||
user_is_staff = has_access(user, course, 'staff')
|
||||
course_id = course.id
|
||||
notification_type = "combined"
|
||||
|
||||
#See if we have a stored value in the cache
|
||||
success, notification_dict = get_value_from_cache(student_id, course_id, notification_type)
|
||||
if success:
|
||||
return notification_dict
|
||||
|
||||
min_time_to_query = user.last_login
|
||||
#Get the time of the last login of the user
|
||||
last_login = user.last_login
|
||||
|
||||
#Find the modules they have seen since they logged in
|
||||
last_module_seen = StudentModule.objects.filter(student=user, course_id=course_id,
|
||||
modified__gt=min_time_to_query).values('modified').order_by(
|
||||
modified__gt=last_login).values('modified').order_by(
|
||||
'-modified')
|
||||
last_module_seen_count = last_module_seen.count()
|
||||
|
||||
if last_module_seen_count > 0:
|
||||
#The last time they viewed an updated notification (last module seen minus how long notifications are cached)
|
||||
last_time_viewed = last_module_seen[0]['modified'] - datetime.timedelta(seconds=(NOTIFICATION_CACHE_TIME + 60))
|
||||
else:
|
||||
last_time_viewed = user.last_login
|
||||
#If they have not seen any modules since they logged in, then don't refresh
|
||||
return {'pending_grading': False, 'img_path': img_path, 'response': notifications}
|
||||
|
||||
pending_grading = False
|
||||
|
||||
img_path = ""
|
||||
try:
|
||||
#Get the notifications from the grading controller
|
||||
controller_response = controller_qs.check_combined_notifications(course.id, student_id, user_is_staff,
|
||||
last_time_viewed)
|
||||
log.debug(controller_response)
|
||||
notifications = json.loads(controller_response)
|
||||
if notifications['success']:
|
||||
if notifications['overall_need_to_check']:
|
||||
pending_grading = True
|
||||
except:
|
||||
#Non catastrophic error, so no real action
|
||||
notifications = {}
|
||||
#This is a dev_facing_error
|
||||
log.exception(
|
||||
"Problem with getting notifications from controller query service for course {0} user {1}.".format(
|
||||
@@ -157,6 +180,7 @@ def combined_notifications(course, user):
|
||||
|
||||
notification_dict = {'pending_grading': pending_grading, 'img_path': img_path, 'response': notifications}
|
||||
|
||||
#Store the notifications in the cache
|
||||
set_value_in_cache(student_id, course_id, notification_type, notification_dict)
|
||||
|
||||
return notification_dict
|
||||
|
||||
@@ -4,16 +4,16 @@ namespace_regex = r"[a-zA-Z\d._-]+"
|
||||
article_slug = r'/(?P<article_path>' + namespace_regex + r'/[a-zA-Z\d_-]*)'
|
||||
namespace = r'/(?P<namespace>' + namespace_regex + r')'
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^$', 'simplewiki.views.root_redirect', name='wiki_root'),
|
||||
url(r'^view' + article_slug, 'simplewiki.views.view', name='wiki_view'),
|
||||
url(r'^view_revision/(?P<revision_number>[0-9]+)' + article_slug, 'simplewiki.views.view_revision', name='wiki_view_revision'),
|
||||
url(r'^edit' + article_slug, 'simplewiki.views.edit', name='wiki_edit'),
|
||||
url(r'^create' + article_slug, 'simplewiki.views.create', name='wiki_create'),
|
||||
url(r'^history' + article_slug + r'(?:/(?P<page>[0-9]+))?$', 'simplewiki.views.history', name='wiki_history'),
|
||||
url(r'^search_related' + article_slug, 'simplewiki.views.search_add_related', name='search_related'),
|
||||
url(r'^random/?$', 'simplewiki.views.random_article', name='wiki_random'),
|
||||
url(r'^revision_feed' + namespace + r'/(?P<page>[0-9]+)?$', 'simplewiki.views.revision_feed', name='wiki_revision_feed'),
|
||||
url(r'^search' + namespace + r'?$', 'simplewiki.views.search_articles', name='wiki_search_articles'),
|
||||
url(r'^list' + namespace + r'?$', 'simplewiki.views.search_articles', name='wiki_list_articles'), # Just an alias for the search, but you usually don't submit a search term
|
||||
urlpatterns = patterns('', # nopep8
|
||||
url(r'^$', 'simplewiki.views.root_redirect', name='wiki_root'),
|
||||
url(r'^view' + article_slug, 'simplewiki.views.view', name='wiki_view'),
|
||||
url(r'^view_revision/(?P<revision_number>[0-9]+)' + article_slug, 'simplewiki.views.view_revision', name='wiki_view_revision'),
|
||||
url(r'^edit' + article_slug, 'simplewiki.views.edit', name='wiki_edit'),
|
||||
url(r'^create' + article_slug, 'simplewiki.views.create', name='wiki_create'),
|
||||
url(r'^history' + article_slug + r'(?:/(?P<page>[0-9]+))?$', 'simplewiki.views.history', name='wiki_history'),
|
||||
url(r'^search_related' + article_slug, 'simplewiki.views.search_add_related', name='search_related'),
|
||||
url(r'^random/?$', 'simplewiki.views.random_article', name='wiki_random'),
|
||||
url(r'^revision_feed' + namespace + r'/(?P<page>[0-9]+)?$', 'simplewiki.views.revision_feed', name='wiki_revision_feed'),
|
||||
url(r'^search' + namespace + r'?$', 'simplewiki.views.search_articles', name='wiki_search_articles'),
|
||||
url(r'^list' + namespace + r'?$', 'simplewiki.views.search_articles', name='wiki_list_articles'), # Just an alias for the search, but you usually don't submit a search term
|
||||
)
|
||||
|
||||
@@ -60,6 +60,15 @@ LOG_DIR = ENV_TOKENS['LOG_DIR']
|
||||
|
||||
CACHES = ENV_TOKENS['CACHES']
|
||||
|
||||
#Email overrides
|
||||
DEFAULT_FROM_EMAIL = ENV_TOKENS.get('DEFAULT_FROM_EMAIL', DEFAULT_FROM_EMAIL)
|
||||
DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS.get('DEFAULT_FEEDBACK_EMAIL', DEFAULT_FEEDBACK_EMAIL)
|
||||
ADMINS = ENV_TOKENS.get('ADMINS', ADMINS)
|
||||
SERVER_EMAIL = ENV_TOKENS.get('SERVER_EMAIL', SERVER_EMAIL)
|
||||
|
||||
#Timezone overrides
|
||||
TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE)
|
||||
|
||||
for feature, value in ENV_TOKENS.get('MITX_FEATURES', {}).items():
|
||||
MITX_FEATURES[feature] = value
|
||||
|
||||
@@ -80,6 +89,8 @@ META_UNIVERSITIES = ENV_TOKENS.get('META_UNIVERSITIES', {})
|
||||
COMMENTS_SERVICE_URL = ENV_TOKENS.get("COMMENTS_SERVICE_URL", '')
|
||||
COMMENTS_SERVICE_KEY = ENV_TOKENS.get("COMMENTS_SERVICE_KEY", '')
|
||||
CERT_QUEUE = ENV_TOKENS.get("CERT_QUEUE", 'test-pull')
|
||||
ZENDESK_URL = ENV_TOKENS.get("ZENDESK_URL")
|
||||
FEEDBACK_SUBMISSION_EMAIL = ENV_TOKENS.get("FEEDBACK_SUBMISSION_EMAIL")
|
||||
MKTG_URLS = ENV_TOKENS.get('MKTG_URLS', MKTG_URLS)
|
||||
|
||||
############################## SECURE AUTH ITEMS ###############
|
||||
@@ -116,3 +127,6 @@ DATADOG_API = AUTH_TOKENS.get("DATADOG_API")
|
||||
# Analytics dashboard server
|
||||
ANALYTICS_SERVER_URL = ENV_TOKENS.get("ANALYTICS_SERVER_URL")
|
||||
ANALYTICS_API_KEY = AUTH_TOKENS.get("ANALYTICS_API_KEY", "")
|
||||
|
||||
ZENDESK_USER = AUTH_TOKENS.get("ZENDESK_USER")
|
||||
ZENDESK_API_KEY = AUTH_TOKENS.get("ZENDESK_API_KEY")
|
||||
|
||||
@@ -91,7 +91,10 @@ MITX_FEATURES = {
|
||||
|
||||
# Give a UI to show a student's submission history in a problem by the
|
||||
# Staff Debug tool.
|
||||
'ENABLE_STUDENT_HISTORY_VIEW': True
|
||||
'ENABLE_STUDENT_HISTORY_VIEW': True,
|
||||
|
||||
# Provide a UI to allow users to submit feedback from the LMS
|
||||
'ENABLE_FEEDBACK_SUBMISSION': False,
|
||||
}
|
||||
|
||||
# Used for A/B testing
|
||||
@@ -263,6 +266,7 @@ IGNORABLE_404_ENDS = ('favicon.ico')
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||
DEFAULT_FROM_EMAIL = 'registration@edx.org'
|
||||
DEFAULT_FEEDBACK_EMAIL = 'feedback@edx.org'
|
||||
SERVER_EMAIL = 'devops@edx.org'
|
||||
ADMINS = (
|
||||
('edX Admins', 'admin@edx.org'),
|
||||
)
|
||||
@@ -324,6 +328,14 @@ WIKI_LINK_DEFAULT_LEVEL = 2
|
||||
PEARSONVUE_SIGNINPAGE_URL = "https://www1.pearsonvue.com/testtaker/signin/SignInPage/EDX"
|
||||
# TESTCENTER_ACCOMMODATION_REQUEST_EMAIL = "exam-help@edx.org"
|
||||
|
||||
##### Feedback submission mechanism #####
|
||||
FEEDBACK_SUBMISSION_EMAIL = None
|
||||
|
||||
##### Zendesk #####
|
||||
ZENDESK_URL = None
|
||||
ZENDESK_USER = None
|
||||
ZENDESK_API_KEY = None
|
||||
|
||||
################################# open ended grading config #####################
|
||||
|
||||
#By setting up the default settings with an incorrect user name and password,
|
||||
|
||||
@@ -37,6 +37,10 @@ def perform_request(method, url, data_or_params=None, *args, **kwargs):
|
||||
else:
|
||||
response = requests.request(method, url, params=data_or_params, timeout=5)
|
||||
except Exception as err:
|
||||
# remove API key if it is in the params
|
||||
if 'api_key' in data_or_params:
|
||||
log.info('Deleting API key from params')
|
||||
del data_or_params['api_key']
|
||||
log.exception("Trying to call {method} on {url} with params {params}".format(
|
||||
method=method, url=url, params=data_or_params))
|
||||
# Reraise with a single exception type
|
||||
|
||||
BIN
lms/static/images/press/cbsnews_178x138.jpg
Normal file
BIN
lms/static/images/press/cbsnews_178x138.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
BIN
lms/static/images/press/nytimes_240x180.png
Normal file
BIN
lms/static/images/press/nytimes_240x180.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
BIN
lms/static/images/press/wash_post_logo_178x138.jpg
Normal file
BIN
lms/static/images/press/wash_post_logo_178x138.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 266 KiB After Width: | Height: | Size: 120 KiB |
@@ -211,3 +211,63 @@ mark {
|
||||
.sr {
|
||||
@include text-sr();
|
||||
}
|
||||
|
||||
.help-tab {
|
||||
@include transform(rotate(-90deg));
|
||||
@include transform-origin(0 0);
|
||||
top: 50%;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
z-index: 99;
|
||||
|
||||
a:link, a:visited {
|
||||
cursor: pointer;
|
||||
border: 1px solid #ccc;
|
||||
border-top-style: none;
|
||||
@include border-radius(0px 0px 10px 10px);
|
||||
background: transparentize(#fff, 0.25);
|
||||
color: transparentize(#333, 0.25);
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
padding: 6px 22px 11px;
|
||||
display: inline-block;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background: #1D9DD9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.help-buttons {
|
||||
padding: 10px 50px;
|
||||
|
||||
a:link, a:visited {
|
||||
padding: 15px 0px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
border: 1px solid #ccc;
|
||||
|
||||
&#feedback_link_problem {
|
||||
border-bottom-style: none;
|
||||
@include border-radius(10px 10px 0px 0px);
|
||||
}
|
||||
|
||||
&#feedback_link_question {
|
||||
border-top-style: none;
|
||||
@include border-radius(0px 0px 10px 10px);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background: #1D9DD9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#feedback_form textarea[name="details"] {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
@@ -149,13 +149,13 @@
|
||||
}
|
||||
|
||||
label {
|
||||
color: #999;
|
||||
color: #646464;
|
||||
|
||||
&.field-error {
|
||||
display: block;
|
||||
color: #8F0E0E;
|
||||
|
||||
+ input {
|
||||
+ input, + textarea {
|
||||
border: 1px solid #CA1111;
|
||||
color: #8F0E0E;
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
<div id="calculator_wrapper">
|
||||
<form id="calculator">
|
||||
<div class="input-wrapper">
|
||||
<input type="text" id="calculator_input" />
|
||||
<input type="text" id="calculator_input" title="Calculator Input Field" />
|
||||
|
||||
<div class="help-wrapper">
|
||||
<a href="#">Hints</a>
|
||||
@@ -176,8 +176,8 @@
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<input id="calculator_button" type="submit" value="="/>
|
||||
<input type="text" id="calculator_output" readonly />
|
||||
<input id="calculator_button" type="submit" title="Calculate" value="="/>
|
||||
<input type="text" id="calculator_output" title="Calculator Output Field" readonly />
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -6,9 +6,18 @@
|
||||
<link type="text/html" rel="alternate" href="http://blog.edx.org/"/>
|
||||
<link type="application/atom+xml" rel="self" href="https://github.com/blog.atom"/>
|
||||
<title>EdX Blog</title>
|
||||
<updated>2013-04-03T14:00:12-07:00</updated>
|
||||
<updated>2013-05-03T14:00:12-07:00</updated>
|
||||
<entry>
|
||||
<id>tag:www.edx.org,2012:Post/17</id>
|
||||
<id>tag:www.edx.org,2013:Post/18</id>
|
||||
<published>2013-05-02T14:00:00-07:00</published>
|
||||
<updated>2013-05-02T14:00:00-07:00</updated>
|
||||
<link type="text/html" rel="alternate" href="http://www.nytimes.com/2013/04/30/education/colleges-adapt-online-courses-to-ease-burden.html?pagewanted=all"/>
|
||||
<title>edX project at San Jose State featured in New York Times</title>
|
||||
<content type="html"><img src="${static.url('images/press/nytimes_240x180.png')}" />
|
||||
<p></p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>tag:www.edx.org,2013:Post/17</id>
|
||||
<published>2012-12-19T14:00:00-07:00</published>
|
||||
<updated>2012-12-19T14:00:00-07:00</updated>
|
||||
<link type="text/html" rel="alternate" href="${reverse('press_release', args=['stanford-to-work-with-edx'])}"/>
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
<ol class="list-input">
|
||||
<li class="field required text" id="field-email">
|
||||
<label for="id_email">Your E-mail Address</label>
|
||||
<input class="" id="id_email" type="email" name="email" value="" placeholder="example: username@domain.com" />
|
||||
<label for="pwd_reset_email">Your E-mail Address</label>
|
||||
<input class="" id="pwd_reset_email" type="email" name="email" value="" placeholder="example: username@domain.com" />
|
||||
<span class="tip tip-input">This is the email address you used to register with edX</span>
|
||||
</li>
|
||||
</ol>
|
||||
@@ -29,11 +29,11 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="close-modal">
|
||||
<a href="#" class="close-modal" title="Close Modal">
|
||||
<div class="inner">
|
||||
<p>✕</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -49,5 +49,10 @@
|
||||
$('#pwd_error').stop().css("display", "block");
|
||||
}
|
||||
});
|
||||
|
||||
// removing close link's default behavior
|
||||
$('#login-modal .close-modal').click(function(e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
})(this)
|
||||
</script>
|
||||
|
||||
176
lms/templates/help_modal.html
Normal file
176
lms/templates/help_modal.html
Normal file
@@ -0,0 +1,176 @@
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
<%! from datetime import datetime %>
|
||||
<%! import pytz %>
|
||||
<%! from django.conf import settings %>
|
||||
<%! from courseware.tabs import get_discussion_link %>
|
||||
|
||||
% if settings.MITX_FEATURES.get('ENABLE_FEEDBACK_SUBMISSION', False):
|
||||
|
||||
<div class="help-tab">
|
||||
<a href="#help-modal" rel="leanModal">Help</a>
|
||||
</div>
|
||||
|
||||
<section id="help-modal" class="modal">
|
||||
<div class="inner-wrapper" id="help_wrapper">
|
||||
<header>
|
||||
<h2><span class="edx">edX</span> Help</h2>
|
||||
<hr>
|
||||
</header>
|
||||
|
||||
<%
|
||||
discussion_link = get_discussion_link(course) if course else None
|
||||
%>
|
||||
% if discussion_link:
|
||||
<p>
|
||||
Have a course-specific question?
|
||||
<a href="${discussion_link}" target="_blank"/>
|
||||
Post it on the course forums.
|
||||
</a>
|
||||
</p>
|
||||
<hr>
|
||||
% endif
|
||||
|
||||
<p>Have a general question about edX? <a href="/help" target="_blank">Check the FAQ</a>.</p>
|
||||
<hr>
|
||||
|
||||
<div class="help-buttons">
|
||||
<a href="#" id="feedback_link_problem">Report a problem</a>
|
||||
<a href="#" id="feedback_link_suggestion">Make a suggestion</a>
|
||||
<a href="#" id="feedback_link_question">Ask a question</a>
|
||||
</div>
|
||||
|
||||
## TODO: find a way to refactor this
|
||||
<div class="close-modal">
|
||||
<div class="inner">
|
||||
<p>✕</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="inner-wrapper" id="feedback_form_wrapper">
|
||||
<header></header>
|
||||
|
||||
<form id="feedback_form" class="feedback_form" method="post" data-remote="true" action="/submit_feedback">
|
||||
<div id="feedback_error" class="modal-form-error"></div>
|
||||
% if not user.is_authenticated():
|
||||
<label data-field="name">Name*</label>
|
||||
<input name="name" type="text">
|
||||
<label data-field="email">E-mail*</label>
|
||||
<input name="email" type="text">
|
||||
% endif
|
||||
<label data-field="subject">Subject*</label>
|
||||
<input name="subject" type="text">
|
||||
<label data-field="details">Details*</label>
|
||||
<textarea name="details"></textarea>
|
||||
<input name="tag" type="hidden">
|
||||
<div class="submit">
|
||||
<input name="submit" type="submit" value="Submit">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="close-modal">
|
||||
<div class="inner">
|
||||
<p>✕</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="inner-wrapper" id="feedback_success_wrapper">
|
||||
<header>
|
||||
<h2>Thank You!</h2>
|
||||
<hr>
|
||||
</header>
|
||||
|
||||
<%
|
||||
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"
|
||||
%>
|
||||
<p>
|
||||
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
|
||||
<a href="/help" target="_blank">detailed FAQs</a>
|
||||
where most questions have already been answered.
|
||||
</p>
|
||||
|
||||
<div class="close-modal">
|
||||
<div class="inner">
|
||||
<p>✕</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script type="text/javascript">
|
||||
(function() {
|
||||
$(".help-tab").click(function() {
|
||||
$(".field-error").removeClass("field-error");
|
||||
$("#feedback_form")[0].reset();
|
||||
$("#feedback_form input[type='submit']").removeAttr("disabled");
|
||||
$("#feedback_form_wrapper").css("display", "none");
|
||||
$("#feedback_error").css("display", "none");
|
||||
$("#feedback_success_wrapper").css("display", "none");
|
||||
$("#help_wrapper").css("display", "block");
|
||||
});
|
||||
showFeedback = function(e, tag, title) {
|
||||
$("#help_wrapper").css("display", "none");
|
||||
$("#feedback_form input[name='tag']").val(tag);
|
||||
$("#feedback_form_wrapper").css("display", "block");
|
||||
$("#feedback_form_wrapper header").html("<h2>" + title + "</h2><hr>");
|
||||
e.preventDefault();
|
||||
};
|
||||
$("#feedback_link_problem").click(function(e) {
|
||||
showFeedback(e, "problem", "Report a Problem");
|
||||
});
|
||||
$("#feedback_link_suggestion").click(function(e) {
|
||||
showFeedback(e, "suggestion", "Make a Suggestion");
|
||||
});
|
||||
$("#feedback_link_question").click(function(e) {
|
||||
showFeedback(e, "question", "Ask a Question");
|
||||
});
|
||||
$("#feedback_form").submit(function() {
|
||||
$("input[type='submit']", this).attr("disabled", "disabled");
|
||||
});
|
||||
$("#feedback_form").on("ajax:complete", function() {
|
||||
$("input[type='submit']", this).removeAttr("disabled");
|
||||
});
|
||||
$("#feedback_form").on("ajax:success", function(event, data, status, xhr) {
|
||||
$("#feedback_form_wrapper").css("display", "none");
|
||||
$("#feedback_success_wrapper").css("display", "block");
|
||||
});
|
||||
$("#feedback_form").on("ajax:error", function(event, xhr, status, error) {
|
||||
$(".field-error").removeClass("field-error");
|
||||
var responseData;
|
||||
try {
|
||||
responseData = jQuery.parseJSON(xhr.responseText);
|
||||
} catch(err) {
|
||||
}
|
||||
if (responseData) {
|
||||
$("[data-field='"+responseData.field+"']").addClass("field-error");
|
||||
$("#feedback_error").html(responseData.error).stop().css("display", "block");
|
||||
} else {
|
||||
// If no data (or malformed data) is returned, a server error occurred
|
||||
htmlStr = "An error has occurred.";
|
||||
% if settings.FEEDBACK_SUBMISSION_EMAIL:
|
||||
htmlStr += " Please <a href='#' id='feedback_email'>send us e-mail</a>.";
|
||||
% else:
|
||||
// If no email is configured, we can't do much other than
|
||||
// ask the user to try again later
|
||||
htmlStr += " Please try again later.";
|
||||
% endif
|
||||
$("#feedback_error").html(htmlStr).stop().css("display", "block");
|
||||
% if settings.FEEDBACK_SUBMISSION_EMAIL:
|
||||
$("#feedback_email").click(function(e) {
|
||||
mailto = "mailto:" + "${settings.FEEDBACK_SUBMISSION_EMAIL}" +
|
||||
"?subject=" + $("#feedback_form input[name='subject']").val() +
|
||||
"&body=" + $("#feedback_form textarea[name='details']").val();
|
||||
window.open(mailto);
|
||||
e.preventDefault();
|
||||
});
|
||||
%endif
|
||||
}
|
||||
});
|
||||
})(this)
|
||||
</script>
|
||||
|
||||
%endif
|
||||
@@ -190,18 +190,11 @@
|
||||
</section>
|
||||
<section class="press-links">
|
||||
<h3>edX in the News:</h3>
|
||||
<a target="_blank" href="http://www.bbc.co.uk/news/business-19661899">BBC</a>,
|
||||
<a target="_blank" href="http://www.technologyreview.com/news/506351/the-most-important-education-technology-in-200-years/">Technology Review</a>,
|
||||
<a target="_blank" href="http://tech.mit.edu/V132/N48/edxvmware.html">The Tech</a>,
|
||||
<a target="_blank" href="http://www.nytimes.com/2012/11/04/education/edlife/massive-open-online-courses-are-multiplying-at-a-rapid-pace.html">The New York Times</a>,
|
||||
<a target="_blank" href="http://www.reuters.com/article/2012/10/19/us-education-courses-online-idUSBRE89I17120121019">Reuters</a>,
|
||||
<a target="_blank" href="http://www.ft.com/intl/cms/s/2/73030f44-d4dd-11e1-9444-00144feabdc0.html#axzz2A9qvk48A">Financial Times</a>,
|
||||
<a target="_blank" href="http://campustechnology.com/articles/2012/10/25/vmware-offers-free-virtualization-software-for-edx-computer-science-students.aspx">Campus Technology</a>,
|
||||
<a target="_blank" href="http://chronicle.com/blogs/wiredcampus/san-jose-state-u-says-replacing-live-lectures-with-videos-increased-test-scores/40470">Chronicle of Higher Education</a>,
|
||||
<a target="_blank" href="http://www.timeshighereducation.co.uk/story.asp?sectioncode=26&storycode=421577&c=1">Times Higher Education</a>,
|
||||
<a target="_blank" href="http://www.bloomberg.com/news/2012-10-15/university-of-texas-joining-harvard-mit-online-venture.html">Bloomberg.com</a>,
|
||||
<a target="_blank" href="http://www.businessweek.com/news/2012-10-15/university-of-texas-joining-harvard-mit-online-venture">BusinessWeek</a>,
|
||||
<a target="_blank" href="http://news.yahoo.com/univ-texas-joins-online-course-program-edx-172202035--finance.html">Associated Press</a>
|
||||
<a target="_blank" href="http://www.nytimes.com/2013/04/30/education/adapting-to-blended-courses-and-finding-early-benefits.html?ref=education">The New York Times</a>,
|
||||
<a target="_blank" href="http://online.wsj.com/article/SB10001424127887323741004578414861572832182.html?mod=googlenews_wsj">The Wall Street Journal</a>,
|
||||
<a target="_blank" href="http://www.washingtonpost.com/local/education/stanford-to-help-build-edx-mooc-platform/2013/04/02/5b53bb3e-9bbe-11e2-9a79-eb5280c81c63_story.html">The Washington Post</a>,
|
||||
<a target="_blank" href="http://www.cbsnews.com/video/watch/?id=50143164n">CBS Television</a>,
|
||||
<a target="_blank" href="http://bostonglobe.com/2012/12/04/edx/AqnQ808q4IEcaUa8KuZuBO/story.html">The Boston Globe</a>
|
||||
<a href="${reverse('press')}" class="read-more">Read More →</a>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@@ -9,14 +9,17 @@
|
||||
</header>
|
||||
|
||||
<form id="login_form" class="login_form" method="post" data-remote="true" action="/login">
|
||||
<label>E-mail</label>
|
||||
<input name="email" type="email">
|
||||
<label>Password</label>
|
||||
<input name="password" type="password">
|
||||
<label class="remember-me">
|
||||
<input name="remember" type="checkbox" value="true">
|
||||
<label for="login_email">E-mail</label>
|
||||
<input id="login_email" type="email" name="email" placeholder="e.g. yourname@domain.com" />
|
||||
|
||||
<label for="login_password">Password</label>
|
||||
<input id="login_password" type="password" name="password" placeholder="••••••••" />
|
||||
|
||||
<label for="login_remember_me" class="remember-me">
|
||||
<input id="login_remember_me" type="checkbox" name="remember" value="true" />
|
||||
Remember me
|
||||
</label>
|
||||
|
||||
<div class="submit">
|
||||
<input name="submit" type="submit" value="Access My Courses">
|
||||
</div>
|
||||
@@ -34,11 +37,11 @@
|
||||
% endif
|
||||
</section>
|
||||
|
||||
<div class="close-modal">
|
||||
<a href="#" class="close-modal" title="Close Modal">
|
||||
<div class="inner">
|
||||
<p>✕</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -59,5 +62,10 @@
|
||||
$('#login_error').html(json.value).stop().css("display", "block");
|
||||
}
|
||||
});
|
||||
|
||||
// removing close link's default behavior
|
||||
$('#login-modal .close-modal').click(function(e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
})(this)
|
||||
</script>
|
||||
|
||||
@@ -111,3 +111,5 @@ site_status_msg = get_site_status_msg(course_id)
|
||||
%if not user.is_authenticated():
|
||||
<%include file="forgot_password_modal.html" />
|
||||
%endif
|
||||
|
||||
<%include file="help_modal.html"/>
|
||||
|
||||
@@ -1,4 +1,106 @@
|
||||
[
|
||||
{
|
||||
"title": "Adapting to Blended Courses, and Finding Early Benefits",
|
||||
"url": "http://www.nytimes.com/2013/04/30/education/adapting-to-blended-courses-and-finding-early-benefits.html?ref=education",
|
||||
"author": "Tamar Lewin",
|
||||
"image": "nyt_logo_178x138.jpeg",
|
||||
"deck": null,
|
||||
"publication": "The New York Times",
|
||||
"publish_date": "April 29, 2013"
|
||||
},
|
||||
|
||||
{
|
||||
"title": "Colleges Adapt Online Courses to Ease Burden",
|
||||
"url": "http://www.nytimes.com/2013/04/30/education/colleges-adapt-online-courses-to-ease-burden.html?pagewanted=all",
|
||||
"author": "Tamar Lewin",
|
||||
"image": "nyt_logo_178x138.jpeg",
|
||||
"deck": null,
|
||||
"publication": "The New York Times",
|
||||
"publish_date": "April 29, 2013"
|
||||
},
|
||||
|
||||
{
|
||||
"title": "Online Education Lifts Pass Rates at University",
|
||||
"url": "http://online.wsj.com/article/SB10001424127887323741004578414861572832182.html?mod=googlenews_wsj",
|
||||
"author": "Geoffrey Fowler",
|
||||
"image": "wsj_logo_178x138.jpg",
|
||||
"deck": null,
|
||||
"publication": "The Wall Street Journal",
|
||||
"publish_date": "April 10, 2013"
|
||||
},
|
||||
|
||||
{
|
||||
"title": "Software Seen Giving Grades on Essay Tests",
|
||||
"url": "http://www.nytimes.com/2013/04/05/science/new-test-for-computers-grading-essays-at-college-level.html?pagewanted=all&_r=0",
|
||||
"author": "John Markoff",
|
||||
"image": "nyt_logo_178x138.jpeg",
|
||||
"deck": null,
|
||||
"publication": "The New York Times",
|
||||
"publish_date": "April 4, 2013"
|
||||
},
|
||||
|
||||
{
|
||||
"title": "Stanford to help build edX MOOC platform",
|
||||
"url": "http://www.washingtonpost.com/local/education/stanford-to-help-build-edx-mooc-platform/2013/04/02/5b53bb3e-9bbe-11e2-9a79-eb5280c81c63_story.html",
|
||||
"author": "Nick Anderson",
|
||||
"image": "wash_post_logo_178x138.jpg",
|
||||
"deck": null,
|
||||
"publication": "The Washington Post",
|
||||
"publish_date": "April 3, 2013"
|
||||
},
|
||||
|
||||
{
|
||||
"title": "Could online ed end college as we know it?",
|
||||
"url": "http://www.cbsnews.com/video/watch/?id=50143164n",
|
||||
"author": "CBS This Morning",
|
||||
"image": "cbsnews_178x138.jpg",
|
||||
"deck": null,
|
||||
"publication": "CBS Television Network",
|
||||
"publish_date": "March 19, 2013"
|
||||
},
|
||||
|
||||
{
|
||||
"title": "The Professors’ Big Stage",
|
||||
"url": "http://www.nytimes.com/2013/03/06/opinion/friedman-the-professors-big-stage.html?_r=1&#commentsContainer",
|
||||
"author": "Thomas L. Friedman",
|
||||
"image": "nyt_logo_178x138.jpeg",
|
||||
"deck": null,
|
||||
"publication": "The New York Times",
|
||||
"publish_date": "March 6, 2013"
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
"title": "Universities Abroad Join Partnerships On the Web",
|
||||
"url": "http://www.nytimes.com/2013/02/21/education/universities-abroad-join-mooc-course-projects.html",
|
||||
"author": "Tamar Lewin",
|
||||
"image": "nyt_logo_178x138.jpeg",
|
||||
"deck": null,
|
||||
"publication": "The New York Times",
|
||||
"publish_date": "February 20, 2013"
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
"title": "Georgetown to offer free online courses",
|
||||
"url": "http://www.washingtonpost.com/local/education/georgetown-to-offer-free-online-courses/2012/12/09/365c4612-3fd3-11e2-bca3-aadc9b7e29c5_story.html",
|
||||
"author": "Nick Anderson",
|
||||
"image": "wash_post_logo_178x138.jpg",
|
||||
"deck": null,
|
||||
"publication": "The Washington Post",
|
||||
"publish_date": "December 9, 2012"
|
||||
},
|
||||
|
||||
{
|
||||
"title": "Wellesley College teams up with online provider edX",
|
||||
"url": "http://bostonglobe.com/2012/12/04/edx/AqnQ808q4IEcaUa8KuZuBO/story.html",
|
||||
"author": "Peter Schworm",
|
||||
"image": "bostonglobe_logo_178x138.jpeg",
|
||||
"deck": null,
|
||||
"publication": "The Boston Globe",
|
||||
"publish_date": "December 4, 2012"
|
||||
},
|
||||
|
||||
{
|
||||
"title": "The Year of the MOOC",
|
||||
"url": "http://www.nytimes.com/2012/11/04/education/edlife/massive-open-online-courses-are-multiplying-at-a-rapid-pace.html",
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
<li>
|
||||
<a class="seq_${item['type']} inactive progress-${item['progress_status']}"
|
||||
data-id="${item['id']}"
|
||||
data-element="${idx+1}">
|
||||
data-element="${idx+1}"
|
||||
href="javascript:void(0);">
|
||||
<p>${item['title']}</p>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -20,27 +20,31 @@
|
||||
|
||||
<div class="input-group">
|
||||
% if has_extauth_info is UNDEFINED:
|
||||
<label data-field="email">E-mail*</label>
|
||||
<input name="email" type="email" placeholder="eg. yourname@domain.com">
|
||||
<label data-field="password">Password*</label>
|
||||
<input name="password" type="password" placeholder="****">
|
||||
<label data-field="username">Public Username*</label>
|
||||
<input name="username" type="text" placeholder="Shown on forums">
|
||||
<label data-field="name">Full Name*</label>
|
||||
<input name="name" type="text" placeholder="For your certificate">
|
||||
<label data-field="email" for="signup_email">E-mail *</label>
|
||||
<input id="signup_email" type="email" name="email" placeholder="e.g. yourname@domain.com" required />
|
||||
|
||||
<label data-field="password" for="signup_password">Password *</label>
|
||||
<input id="signup_password" type="password" name="password" placeholder="••••••••" required />
|
||||
|
||||
<label data-field="username" for="signup_username">Public Username *</label>
|
||||
<input id="signup_username" type="text" name="username" placeholder="e.g. yourname (shown on forums)" required />
|
||||
|
||||
<label data-field="name" for="signup_fullname">Full Name *</label>
|
||||
<input id="signup_fullname" type="text" name="name" placeholder="e.g. Your Name (for certificates)" required />
|
||||
% else:
|
||||
<p><i>Welcome</i> ${extauth_email}</p><br/>
|
||||
<p><i>Enter a public username:</i></p>
|
||||
<label data-field="username">Public Username*</label>
|
||||
<input name="username" type="text" value="${extauth_username}" placeholder="Shown on forums">
|
||||
|
||||
<label data-field="username" for="signup_username">Public Username *</label>
|
||||
<input id="signup_username" type="text" name="username" value="${extauth_username}" placeholder="e.g. yourname (shown on forums)" required />
|
||||
% endif
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<section class="citizenship">
|
||||
<label data-field="level_of_education">Ed. completed</label>
|
||||
<label data-field="level_of_education" for="signup_ed_level">Ed. Completed</label>
|
||||
<div class="input-wrapper">
|
||||
<select name="level_of_education">
|
||||
<select id="signup_ed_level" name="level_of_education">
|
||||
<option value="">--</option>
|
||||
%for code, ed_level in UserProfile.LEVEL_OF_EDUCATION_CHOICES:
|
||||
<option value="${code}">${ed_level}</option>
|
||||
@@ -50,9 +54,9 @@
|
||||
</section>
|
||||
|
||||
<section class="gender">
|
||||
<label data-field="gender">Gender</label>
|
||||
<label data-field="gender" for="signup_gender">Gender</label>
|
||||
<div class="input-wrapper">
|
||||
<select name="gender">
|
||||
<select id="signup_gender" name="gender">
|
||||
<option value="">--</option>
|
||||
%for code, gender in UserProfile.GENDER_CHOICES:
|
||||
<option value="${code}">${gender}</option>
|
||||
@@ -62,9 +66,9 @@
|
||||
</section>
|
||||
|
||||
<section class="date-of-birth">
|
||||
<label data-field="date-of-birth">Year of birth</label>
|
||||
<label data-field="date-of-birth" for="signup_birth_year">Year of birth</label>
|
||||
<div class="input-wrapper">
|
||||
<select name="year_of_birth">
|
||||
<select id="signup_birth_year" name="year_of_birth">
|
||||
<option value="">--</option>
|
||||
%for year in UserProfile.VALID_YEARS:
|
||||
<option value="${year}">${year}</option>
|
||||
@@ -74,22 +78,23 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<label data-field="mailing_address">Mailing address</label>
|
||||
<textarea name="mailing_address"></textarea>
|
||||
<label data-field="goals">Goals in signing up for edX</label>
|
||||
<textarea name="goals"></textarea>
|
||||
<label data-field="mailing_address" for="signup_mailing_address">Mailing address</label>
|
||||
<textarea id="signup_mailing_address" name="mailing_address"></textarea>
|
||||
|
||||
<label data-field="goals" for="signup_goals">Goals in signing up for edX</label>
|
||||
<textarea name="goals" id="signup_goals"></textarea>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label data-field="terms_of_service" class="terms-of-service">
|
||||
<input name="terms_of_service" type="checkbox" value="true">
|
||||
<label data-field="terms_of_service" class="terms-of-service" for="signup_tos">
|
||||
<input id="signup_tos" name="terms_of_service" type="checkbox" value="true">
|
||||
I agree to the
|
||||
<a href="${reverse('tos')}" target="_blank">Terms of Service</a>*
|
||||
</label>
|
||||
|
||||
<label data-field="honor_code" class="honor-code">
|
||||
<input name="honor_code" type="checkbox" value="true">
|
||||
<label data-field="honor_code" class="honor-code" for="signup_honor">
|
||||
<input id="signup_honor" name="honor_code" type="checkbox" value="true">
|
||||
I agree to the
|
||||
<a href="${reverse('honor')}" target="_blank">Honor Code</a>*
|
||||
</label>
|
||||
@@ -110,11 +115,11 @@
|
||||
|
||||
</div>
|
||||
|
||||
<div class="close-modal">
|
||||
<a href="#" class="close-modal" title="Close Modal">
|
||||
<div class="inner">
|
||||
<p>✕</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -129,5 +134,10 @@
|
||||
$("[data-field='"+json.field+"']").addClass('field-error')
|
||||
}
|
||||
});
|
||||
|
||||
// removing close link's default behavior
|
||||
$('#login-modal .close-modal').click(function(e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
})(this)
|
||||
</script>
|
||||
|
||||
@@ -374,35 +374,6 @@
|
||||
</div>
|
||||
</article>
|
||||
|
||||
|
||||
<article id="devops-engineer-systems-administrator" class="job">
|
||||
<div class="inner-wrapper">
|
||||
<h3><strong>DEVOPS ENGINEER – SYSTEMS ADMINISTRATOR</strong></h3>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<p><strong>Responsibilities:</strong></p>
|
||||
<ul>
|
||||
<li>Work with developers and staff to maintain and improve the infrastructure of edX.</li>
|
||||
<li>Assist where needed with other technical support tasks to support the fast moving pace of edX.</li>
|
||||
<li>Rapidly diagnose and resolve faults with organization-wide servers and services, and communicate to users as appropriate.</li>
|
||||
</ul>
|
||||
<p><strong>Requirements:</strong></p>
|
||||
<ul>
|
||||
<li>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.</li>
|
||||
<li>Three or more years of systems administration. </li>
|
||||
<li>Must have an excellent working knowledge of Linux both as an end-user and as an administrator.</li>
|
||||
<li>Must be adept in programming/scripting languages such as Python, Ruby, Bash.</li>
|
||||
<li>Must be familiar with a configuration management system such as Puppet, Chef, Ansible.</li>
|
||||
<li>Must have experience running web applications in a production environment.</li>
|
||||
<li>Must have excellent personal interaction skills as the position requires interfacing with a wide range of people up to board level.</li>
|
||||
<li>Ideally possesses experience with some of the following technologies: nginx, mysql, mongodb, django environments, splunk, git.</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>
|
||||
|
||||
|
||||
<article id="learning-sciences-engineer" class="job">
|
||||
<div class="inner-wrapper">
|
||||
<h3><strong>LEARNING SCIENCES ENGINEER</strong></h3>
|
||||
@@ -483,39 +454,7 @@ development and program management teams.</p>
|
||||
<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>
|
||||
|
||||
<article id="web-designer" class="job">
|
||||
<div class="inner-wrapper">
|
||||
<h3><strong>WEB DESIGNER, PRODUCT TEAM</strong></h3>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>As an edX Designer, you:</p>
|
||||
<ul>
|
||||
<li>Have an innate sense of – and strong opinion about – good usability when it comes to web applications, and an ability to clearly articulate both.</li>
|
||||
<li>Understand established interactive technologies and possess an undying thirst to learn about new ones.</li>
|
||||
<li>Define and work within visual themes based on your excellent understanding of grids, typography, color, and design principles.</li>
|
||||
<li>Marry design aesthetics to user experiences while keeping in mind accessibility, usability, and web standards.</li>
|
||||
<li>Can use HTML5, CSS3, and DOM-manipulating JavaScript to represent your designs in the browser.</li>
|
||||
<li>Conceptualize and articulate complex ideas to drive decisions, facilitate understanding, and reach consensus.</li>
|
||||
<li>Document your thinking using appropriately chosen, informed deliverables such as sketches, wireframes, prototypes, site maps/flows, personas, style tiles, and design comps.</li>
|
||||
<li>Have a perfectionist mindset, but won’t lose momentum in projects because of it.</li>
|
||||
<li>Expertly present user experience and design recommendations to team members.</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Requirements:</strong></p>
|
||||
<ul>
|
||||
<li>Have at least 2 years of professional, post-collegiate experience.</li>
|
||||
<li>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.</li>
|
||||
</ul>
|
||||
<p><strong>About the Product Design Team:</strong></p>
|
||||
<p>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.</p>
|
||||
|
||||
<p>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 <a href="mailto:jobs@edx.org">jobs@edx.org</a>. 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.</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article id="front-end-developer" class="job">
|
||||
<article id="front-end-developer" class="job">
|
||||
<div class="inner-wrapper">
|
||||
<h3><strong>FRONT END DEVELOPER</strong></h3>
|
||||
<p>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.</p>
|
||||
@@ -545,6 +484,72 @@ development and program management teams.</p>
|
||||
<p>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 <a href="mailto:jobs@edx.org">jobs@edx.org</a>. 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.</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
|
||||
<article id="test-engineer" class="job">
|
||||
<div class="inner-wrapper">
|
||||
<h3><strong>TEST ENGINEER</strong></h3>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<p><strong>Responsibilities:</strong></p>
|
||||
<ul>
|
||||
<li>Review software designs with a focus on code quality, risk, and testability</li>
|
||||
<li>Build tools and frameworks that enable fellow engineers be more productive, write better code and test it themselves</li>
|
||||
<li>Code test automation at all levels including class library, web application framework, javascript, and end-to-end</li>
|
||||
<li>Enable metrics collection to measure adoption and expand the reach of the delivered tools</li>
|
||||
<li>Fix framework bugs and improve test architecture, including adding required unit tests</li>
|
||||
<li>Train and mentor other team members</li>
|
||||
</ul>
|
||||
<p><strong>Qualifications:</strong></p>
|
||||
<ul>
|
||||
<li>Excellent coding skills across a number of languages: Python or other high level programming languages, Javascript, bash, etc.</li>
|
||||
<li>Experience in building test automation frameworks</li>
|
||||
<li>Comfortable with source code in various languages (Python/Django, Ruby/Rails, Javascript/Backbone/JQuery, etc.)</li>
|
||||
<li>Highly proficient in a Unix/Linux environment</li>
|
||||
<li>Experience with database technologies from SQLite to MongoDB</li>
|
||||
<li>Familiar with deployment automation (Puppet, Jenkins, AWS)</li>
|
||||
<li>Open Source development experience preferred, extra points for sharing your GitHub / StackOverflow / etc. profile</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>
|
||||
<article id="coordinator-university-business-affairs" class="job">
|
||||
<div class="inner-wrapper">
|
||||
<h3><strong>COORDINATOR OF UNIVERSITY AND BUSINESS AFFAIRS</strong></h3>
|
||||
<p>EdX is looking for a Coordinator of External Affairs, to streamline, organize and maintain our efforts in Business Development and University Relations.</p>
|
||||
<p><strong>There are 4 primary areas of responsibility:</strong></p>
|
||||
<ol>
|
||||
<li>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.</li>
|
||||
<li>To maintain a database of partners and prospects and manage any data flows/reporting required.</li>
|
||||
<li>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.</li>
|
||||
<li>To act as a central point of contact for all relationship and event activity within this scope.</li>
|
||||
</ol>
|
||||
<p><strong>Detailed Responsibilities:</strong></p>
|
||||
<ul>
|
||||
<li>Provide support and coordinate activities for these 3 executives</li>
|
||||
<li>Acquire strong user knowledge of related systems, processes and tools</li>
|
||||
<li>Participate in the new partner on-boarding process</li>
|
||||
<li>Provide an escalation point for Sales personnel for systems, procedures and policies</li>
|
||||
<li>Maintain Salesforce database for client/partner set up and support information, generating reports as needed</li>
|
||||
<li>Document proofreading, editing as directed for proposals, contracts, contact and call reports</li>
|
||||
<li>Coordinate and manage travel, events and meetings, including invitations, RSVP’s, hotel/meeting space contracts, and providing event materials to attendees</li>
|
||||
</ul>
|
||||
<p><strong>Qualifications:</strong></p>
|
||||
<ul>
|
||||
<li>5-7 years of experience in a similar project/coordinator type position with progressively responsible administrative experience</li>
|
||||
<li>Self-starter, possessing tenacity and a desire for challenges, not afraid to take risks, and the initiative to get things done with little direction </li>
|
||||
<li>Superior interpersonal and communications skills, including concise writing and editing skills</li>
|
||||
<li>Strong organizational skills to manage multiple competing priorities and projects with attention to detail </li>
|
||||
<li>Exceptional ability to effectively interact with multiple external and internal stakeholders </li>
|
||||
<li>Adept at analyzing complex issues with the ability to synthesize data and perform gap analyses</li>
|
||||
<li>Performs well with a variety of disciplines while remaining effective in a high-volume, fast-pace start-up environment with high workload</li>
|
||||
<li>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</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>
|
||||
|
||||
@@ -559,11 +564,11 @@ development and program management teams.</p>
|
||||
<a href="#director-of-product-management">Director, Product Management</a>
|
||||
<a href="#content-engineer">Content Engineer</a>
|
||||
<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>
|
||||
<a href="#web-designer">Web Designer, Product Team</a>
|
||||
<a href="#front-end-developer">Front End Developer</a>
|
||||
<a href="#test-engineer">Test Engineer</a>
|
||||
<a href="#coordinator-university-business-affairs">Coordinator of University and Business Affairs</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>
|
||||
|
||||
@@ -18,10 +18,13 @@
|
||||
</%block>
|
||||
|
||||
<%block name="university_description">
|
||||
<p>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. </p>
|
||||
|
||||
<p>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.
|
||||
</p>
|
||||
<p>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.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
</%block>
|
||||
|
||||
${parent.body()}
|
||||
|
||||
15
lms/urls.py
15
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<key>[^/]*)$', 'student.views.reactivation_email'),
|
||||
)
|
||||
|
||||
@@ -309,12 +308,12 @@ if settings.COURSEWARE_ENABLED:
|
||||
'courseware.views.news', name="news"),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/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<course_id>[^/]+/[^/]+/[^/]+)/(?P<tab_slug>[^/]+)/$',
|
||||
'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<reload_dir>[^/]+)/(?P<commit_id>[^/]+)$', 'lms_migration.migrate.manage_modulestores'),
|
||||
url(r'^gitreload$', 'lms_migration.migrate.gitreload'),
|
||||
url(r'^gitreload/(?P<reload_dir>[^/]+)$', '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<args>.+)$', 'track.views.view_tracking_log'),
|
||||
)
|
||||
)
|
||||
|
||||
# FoldIt views
|
||||
urlpatterns += (
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user