Commit a291f4a9 by Will Daly

Standardize build tools using make

Quiet the doc and i18n scripts
Add locale to i18n script
Use PhantomJS as the headless test runner.
Refactor file upload tests
parent c3b36f0a
......@@ -60,7 +60,4 @@ storage/*
openassessment/xblock/static/js/fixtures/*.html
# logging
logs/*/*.log*
# Vagrant
.vagrant
logs/*.log*
language: python
python:
- "2.7"
before_install:
- sudo apt-get update
- sudo xargs -a apt-packages.txt apt-get install --fix-missing
install:
- "pip install coveralls"
- "./scripts/install-wheels.sh"
- travis_retry ./scripts/download-nltk-data.sh
- "make install"
before_script:
- "export DISPLAY=:99.0"
- "sh -e /etc/init.d/xvfb start"
script:
- "./scripts/test.sh"
- "./scripts/i18n.sh eo"
- "make test"
- "./scripts/i18n.sh eo > /dev/null"
after_success:
coveralls
all: install test
install-system:
sudo apt-get update -y -qq
sudo xargs -a apt-packages.txt apt-get install -y -qq --fix-missing
install-node:
sudo add-apt-repository -y ppa:chris-lea/node.js
sudo apt-get update -y -qq
sudo apt-get install -y -qq nodejs
install-wheels:
./scripts/install-wheels.sh
install-python:
./scripts/install-python.sh
install-js:
npm config set loglevel warn
npm install
install-nltk-data:
./scripts/download-nltk-data.sh
STATIC_JS = openassessment/xblock/static/js
minimize-js:
node_modules/.bin/uglifyjs $(STATIC_JS)/src/oa_shared.js $(STATIC_JS)/src/*.js > "$(STATIC_JS)/openassessment.min.js"
install-test:
pip install -q -r requirements/test.txt
install: install-system install-node install-wheels install-python install-js install-nltk-data install-test minimize-js
test:
./scripts/test.sh
......@@ -12,20 +12,21 @@
__ http://edx.readthedocs.org/projects/edx-open-response-assessments
This is an initial prototype for redesigning Peer Grading and general Open Ended
Submission Evaluation. This project is in the early stages of development and is
not ready for general use.
Installation
============
The intent of this project is to be installed as Django apps that will be
included in `edx-platform <https://github.com/edx/edx-platform>`_.
For JavaScript minification and unit tests, you must `install NodeJS <http://nodejs.org/>`_.
To install all dependencies (assumes Ubuntu 12.04):
.. code:: bash
make install
To install dependencies and start the development ("workbench") server:
Running the Development Server
==============================
.. code:: bash
......@@ -46,30 +47,14 @@ to start the server on port 8001:
./scripts/workbench.sh 8001
Celery Workers
==============
Some of the OpenAssessment APIs execute tasks asynchronously using `celery <http://docs.celeryproject.org>`_.
The tasks are executed by worker processes.
First, you will need to `install RabbitMQ <http://http://www.rabbitmq.com/download.html>`_.
Once RabbitMQ is installed, you can start a worker process locally:
.. code:: bash
./scripts/celery-worker.sh
Running Tests
=============
To run the Python and Javascript unit test suites:
To run all tests:
.. code:: bash
./scripts/test.sh
make test
To limit Python tests to a particular module:
......@@ -90,70 +75,6 @@ To run the JavaScript tests in Chrome so you can use the debugger:
./scripts/js-debugger.sh
Quality Check
=============
Install pylint:
.. code:: bash
pip install pylint==0.28.0
Check for quality violations:
.. code:: bash
pylint openassessment
Disable quality violations on a line or file:
.. code:: python
# pylint: disable=W0123,E4567
Vagrant
=======
This repository includes a Vagrant configuration file, which is useful for testing
ORA2 in an environment that is closer to production:
* Uses `gunicorn <http://gunicorn.org/>`_ to serve the workbench application.
Unlike Django ``runserver``, gunicorn will process requests in parallel.
* Uses `mysql <http://www.mysql.com/>`_ as the database, which (unlike
`sqlite <http://www.sqlite.org/>`_) allows for simultaneous writes.
* Serves static files using `nginx <http://wiki.nginx.org/Main>`_ instead
of Django `staticfiles <https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/>`_.
* Runs multiple `celery workers <http://celery.readthedocs.org/en/latest/>`_.
* Uses `memcached <http://memcached.org/>`_.
* Installs `EASE <https://github.com/edx/ease>`_ for AI grading, including
its many requirements.
To use the Vagrant VM:
1) `Install Vagrant <https://docs.vagrantup.com/v2/installation/>`_.
2) ``vagrant up`` to start and provision the Vagrant VM.
3) Visit `http://192.168.44.10 <http://192.168.44.10>`_
4) You should see the workbench index page load.
After making a change to the code in the ``edx-ora2`` directory,
you must restart the services on the Vagrant VM:
1) ``vagrant ssh`` to ssh into the Vagrant VM.
2) ``./update.sh`` to restart the services, run database migrations, and collect static assets.
3) Visit `http://192.168.44.10 <http://192.168.44.10>`_
By default, the Vagrant VM also includes a monitoring tool for Celery tasks called `Flower <https://github.com/mher/flower>`_.
To use the tool, visit: `http://192.168.44.10:5555 <http://192.168.44.10:5555>`_
The log files from the Vagrant VM are located in ``edx-ora2/logs/vagrant``, which is shared with the host machine.
i18n
====
......@@ -177,9 +98,7 @@ Please see ``LICENSE.txt`` for details.
How to Contribute
=================
Due to the very early stage of development we're at, we are not accepting
contributions at this time. Large portions of the API can change with little
notice.
Contributions are very welcome. The easiest way is to fork this repo, and then make a pull request from your fork. The first time you make a pull request, you may be asked to sign a Contributor Agreement.
Reporting Security Issues
=========================
......
# -*- mode: ruby -*-
# vi: set ft=ruby :
# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
VAGRANTFILE_API_VERSION = "2"
$script = <<END
set -e
echo "Updating apt packages..."
apt-get update -y
echo "Installing basic system requirements..."
apt-get install -y curl git vim libxml2-dev libxslt1-dev memcached nginx
echo "Installing mysql server..."
DEBIAN_FRONTEND=noninteractive apt-get install -y mysql-server-5.5
echo "CREATE DATABASE IF NOT EXISTS workbench DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;" | mysql -u root
echo "Installing Python system requirements..."
apt-get install -y python2.7 python2.7-dev python-pip python-software-properties python-mysqldb libmysqlclient-dev
pip install virtualenv
echo "Installing FireFox and xvfb (for JavaScript tests)..."
add-apt-repository "ppa:ubuntu-mozilla-security/ppa"
apt-get install -y firefox dbus-x11 xvfb
cat /home/vagrant/edx-ora2/vagrant/xvfb.conf > /etc/init/xvfb.conf
start xvfb || true
echo "Installing RabbitMQ..."
add-apt-repository "deb http://www.rabbitmq.com/debian/ testing main"
cd /tmp && wget http://www.rabbitmq.com/rabbitmq-signing-key-public.asc && apt-key add rabbitmq-signing-key-public.asc
apt-get update -y
apt-get install -y rabbitmq-server
echo "Installing NodeJS..."
add-apt-repository ppa:chris-lea/node.js
apt-get update -y
apt-get install -y nodejs
# Stop all Python upstart jobs
sudo stop workbench || true
sudo stop celery || true
sudo stop flower || true
su vagrant <<EOF
set -e
echo "Creating a virtualenv..."
mkdir -p /home/vagrant/.virtualenvs
virtualenv /home/vagrant/.virtualenvs/edx-ora2
source /home/vagrant/.virtualenvs/edx-ora2/bin/activate
echo "Configuring login script..."
cat /home/vagrant/edx-ora2/vagrant/bash_profile > /home/vagrant/.bash_profile
echo "Installing EASE..."
if [ ! -d /home/vagrant/ease ]; then
git clone https://github.com/edx/ease.git /home/vagrant/ease
fi
cat /home/vagrant/ease/apt-packages.txt | xargs sudo apt-get -y install
cd /home/vagrant/ease && pip install -r pre-requirements.txt
cd /home/vagrant/ease && python setup.py install
echo "Downloading NLTK corpus..."
cd /home/vagrant/ease && ./download-nltk-corpus.sh
echo "Installing gunicorn..."
pip install gunicorn
echo "Instally Python MySQL library..."
pip install MySQL-python
echo "Installing celery flower..."
pip install flower
echo "Install edx-ora2..."
cd /home/vagrant/edx-ora2 && ./scripts/install.sh
echo "Update the database..."
cd /home/vagrant/edx-ora2 && python manage.py syncdb --migrate --noinput --settings settings.vagrant
echo "Collect static assets..."
mkdir -p /home/vagrant/static
cd /home/vagrant/edx-ora2 && python manage.py collectstatic --noinput --settings settings.vagrant
echo "Creating the update script..."
cp /home/vagrant/edx-ora2/vagrant/update.sh /home/vagrant/update.sh
EOF
echo "Creating upstart script for workbench..."
cat /home/vagrant/edx-ora2/vagrant/workbench_upstart.conf > /etc/init/workbench.conf
start workbench || true
echo "Create upstart script for Celery workers..."
cat /home/vagrant/edx-ora2/vagrant/celery_upstart.conf > /etc/init/celery.conf
start celery || true
echo "Create upstart script for Celery flower..."
cat /home/vagrant/edx-ora2/vagrant/flower_upstart.conf > /etc/init/flower.conf
start flower || true
echo "Configure nginx"
cat /home/vagrant/edx-ora2/vagrant/nginx.conf > /etc/nginx/sites-enabled/workbench.conf
echo "Restart nginx"
sudo service nginx stop || true
sudo service nginx start || true
END
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.box = "precise64"
config.vm.box_url = "http://files.vagrantup.com/precise64.box"
config.vm.network "private_network", ip: "192.168.44.10"
config.vm.synced_folder ".", "/home/vagrant/edx-ora2"
config.vm.provider :virtualbox do |vb|
# Increase memory and CPU
vb.customize ["modifyvm", :id, "--memory", "2048"]
vb.customize ["modifyvm", :id, "--cpus", "2"]
# Allow DNS to work for Ubuntu 12.10 host
# http://askubuntu.com/questions/238040/how-do-i-fix-name-service-for-vagrant-client
vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
end
config.vm.provision "shell", inline: $script
end
aspell
gcc
g++
gcc
git
gfortran
libblas3gf
libblas-dev
liblapack3gf
liblapack-dev
libatlas-base-dev
libfontconfig1
libmysqlclient-dev
libxml2-dev
libxslt1-dev
nodejs
python2.7
python2.7-dev
python-mysqldb
python-pip
python-software-properties
......@@ -7,6 +7,13 @@ module.exports = function(config) {
basePath: 'openassessment/xblock/static/js',
plugins: [
'karma-coverage',
'karma-jasmine',
'karma-chrome-launcher',
'karma-phantomjs-launcher',
],
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['jasmine'],
......@@ -66,11 +73,9 @@ module.exports = function(config) {
// enable / disable watching file and executing tests whenever any file changes
autoWatch: false,
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['Firefox'],
browsers: ['PhantomJS'],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
......
......@@ -4,12 +4,20 @@ Tests for OpenAssessment response (submission) view.
describe("OpenAssessment.ResponseView", function() {
// Stub server
var FAKE_URL = "http://www.example.com";
var StubServer = function() {
var successPromise = $.Deferred(
function(defer) {
defer.resolve();
}
function(defer) { defer.resolve(); }
).promise();
var successPromiseWithUrl = $.Deferred(
function(defer) { defer.resolveWith(this, [FAKE_URL]); }
).promise();
var errorPromise = $.Deferred(
function(defer) { defer.rejectWith(this, ["ERROR"]); }
).promise();
this.save = function(submission) {
......@@ -24,17 +32,37 @@ describe("OpenAssessment.ResponseView", function() {
return successPromise;
};
this.uploadUrlError = false;
this.getUploadUrl = function(contentType) {
return successPromise;
return this.uploadUrlError ? errorPromise : successPromiseWithUrl;
};
this.getDownloadUrl = function() {
return successPromise;
}
return successPromiseWithUrl;
};
};
var StubFileUploader = function() {
var successPromise = $.Deferred(function(defer) { defer.resolve(); }).promise();
var errorPromise = $.Deferred(function(defer) { defer.rejectWith(this, ["ERROR"]); }).promise();
this.uploadError = false;
this.uploadArgs = null;
this.upload = function(url, data, contentType) {
// Store the args we were passed so we can verify them
this.uploadArgs = {
url: url,
data: data,
contentType: contentType
};
// Return a promise indicating success or error
return this.uploadError ? errorPromise : successPromise;
};
};
// Stub base view
var StubBaseView = function() {
this.loadAssessmentModules = function() {};
this.peerView = { load: function() {} };
......@@ -47,6 +75,7 @@ describe("OpenAssessment.ResponseView", function() {
// Stubs
var baseView = null;
var server = null;
var fileUploader = null;
// View under test
var view = null;
......@@ -71,15 +100,14 @@ describe("OpenAssessment.ResponseView", function() {
jasmine.getFixtures().fixturesPath = 'base/fixtures';
loadFixtures('oa_response.html');
// Create the stub server
// Create stub objects
server = new StubServer();
// Create the stub base view
fileUploader = new StubFileUploader();
baseView = new StubBaseView();
// Create and install the view
var el = $('#openassessment-base').get(0);
view = new OpenAssessment.ResponseView(el, server, baseView);
view = new OpenAssessment.ResponseView(el, server, fileUploader, baseView);
view.installHandlers();
// Stub the confirmation step
......@@ -377,23 +405,50 @@ describe("OpenAssessment.ResponseView", function() {
spyOn(baseView, 'toggleActionError').andCallThrough();
var files = [{type: 'image/jpg', size: 6000000, name: 'huge-picture.jpg', data: ''}];
view.prepareUpload(files);
expect(baseView.toggleActionError).toHaveBeenCalled();
expect(baseView.toggleActionError).toHaveBeenCalledWith('upload', 'File size must be 5MB or less.');
});
it("selects the wrong file type", function() {
spyOn(baseView, 'toggleActionError').andCallThrough();
var files = [{type: 'bogus/jpg', size: 1024, name: 'picture.exe', data: ''}];
view.prepareUpload(files);
expect(baseView.toggleActionError).toHaveBeenCalled();
expect(baseView.toggleActionError).toHaveBeenCalledWith('upload', 'File must be an image.');
});
it("requests a file upload", function() {
it("uploads a file using a one-time URL", function() {
var files = [{type: 'image/jpg', size: 1024, name: 'picture.jpg', data: ''}];
view.prepareUpload(files);
view.fileUpload();
expect(fileUploader.uploadArgs.url).toEqual(FAKE_URL);
expect(fileUploader.uploadArgs.data).toEqual(files[0]);
expect(fileUploader.uploadArgs.contentType).toEqual('image/jpg');
});
it("displays an error if a one-time file upload URL cannot be retrieved", function() {
// Configure the server to fail when retrieving the one-time URL
server.uploadUrlError = true;
spyOn(baseView, 'toggleActionError').andCallThrough();
spyOn(server, 'getUploadUrl').andCallThrough();
// Attempt to upload a file
var files = [{type: 'image/jpg', size: 1024, name: 'picture.jpg', data: ''}];
view.prepareUpload(files);
view.fileUpload();
expect(server.getUploadUrl).toHaveBeenCalled();
expect(baseView.toggleActionError).toHaveBeenCalled();
// Expect an error to be displayed
expect(baseView.toggleActionError).toHaveBeenCalledWith('upload', 'ERROR');
});
it("displays an error if a file could not be uploaded", function() {
// Configure the file upload server to return an error
fileUploader.uploadError = true;
spyOn(baseView, 'toggleActionError').andCallThrough();
// Attempt to upload a file
var files = [{type: 'image/jpg', size: 1024, name: 'picture.jpg', data: ''}];
view.prepareUpload(files);
view.fileUpload();
// Expect an error to be displayed
expect(baseView.toggleActionError).toHaveBeenCalledWith('upload', 'ERROR');
});
});
......@@ -13,8 +13,9 @@ OpenAssessment.BaseView = function(runtime, element, server) {
this.runtime = runtime;
this.element = element;
this.server = server;
this.fileUploader = new OpenAssessment.FileUploader();
this.responseView = new OpenAssessment.ResponseView(this.element, this.server, this);
this.responseView = new OpenAssessment.ResponseView(this.element, this.server, this.fileUploader, this);
this.trainingView = new OpenAssessment.StudentTrainingView(this.element, this.server, this);
this.selfView = new OpenAssessment.SelfView(this.element, this.server, this);
this.peerView = new OpenAssessment.PeerView(this.element, this.server, this);
......@@ -74,15 +75,15 @@ OpenAssessment.BaseView.prototype = {
this.selfView.load();
this.gradeView.load();
/**
this.messageView.load() is intentionally omitted.
Because of the asynchronous loading, there is no way to tell (from the perspective of the
messageView) whether or not the peer view was able to grab an assessment to assess. Any
asynchronous strategy would run into a race condition based around this problem at some
point. Instead, we created a field in the XBlock called no_peers, which is set by the
this.messageView.load() is intentionally omitted.
Because of the asynchronous loading, there is no way to tell (from the perspective of the
messageView) whether or not the peer view was able to grab an assessment to assess. Any
asynchronous strategy would run into a race condition based around this problem at some
point. Instead, we created a field in the XBlock called no_peers, which is set by the
Peer XBlock Handler, and which is examined by the Message XBlock Handler.
To Avoid rendering the message more than one time per update/load (and avoiding all comp-
lications that that would likely induce), we chose to load the method view only after
lications that that would likely induce), we chose to load the method view only after
the peer view has been loaded. This is achieved by having the peer view call to render
the message view after rendering itself but before exiting its load method.
*/
......
/*
Upload a file using a one-time URL.
This object doesn't do much work, but it makes it
easier to stub out the upload in tests.
This request requires appropriate CORS configuration for AJAX
PUT requests on the server.
Args:
url (string): The one-time URL we're uploading to.
data (object): The object to upload, which should have properties:
data (string)
name (string)
size (int)
type (string)
contentType (string): The MIME type of the data to upload.
Returns:
JQuery promise
*/
OpenAssessment.FileUploader = function() {
this.upload = function(url, data, contentType) {
return $.Deferred(
function(defer) {
$.ajax({
url: url,
type: 'PUT',
data: data,
async: false,
processData: false,
contentType: contentType,
}).done(
function(data, textStatus, jqXHR) { defer.resolve(); }
).fail(
function(data, textStatus, jqXHR) {
defer.rejectWith(this, [textStatus]);
}
);
}
).promise();
};
};
......@@ -9,9 +9,10 @@ Args:
Returns:
OpenAssessment.ResponseView
**/
OpenAssessment.ResponseView = function(element, server, baseView) {
OpenAssessment.ResponseView = function(element, server, fileUploader, baseView) {
this.element = element;
this.server = server;
this.fileUploader = fileUploader;
this.baseView = baseView;
this.savedResponse = "";
this.files = null;
......@@ -437,7 +438,7 @@ OpenAssessment.ResponseView.prototype = {
this.baseView.toggleActionError('upload', null);
this.files = files;
}
$("#file__upload").toggleClass("is--disabled", this.files == null);
$("#file__upload").toggleClass("is--disabled", this.files === null);
},
......@@ -452,32 +453,26 @@ OpenAssessment.ResponseView.prototype = {
var fileUpload = $("#file__upload");
fileUpload.addClass("is--disabled");
var handleError = function(errMsg) {
view.baseView.toggleActionError('upload', errMsg);
fileUpload.removeClass("is--disabled");
};
// Call getUploadUrl to get the one-time upload URL for this file. Once
// completed, execute a sequential AJAX call to upload to the returned
// URL. This request requires appropriate CORS configuration for AJAX
// PUT requests on the server.
this.server.getUploadUrl(view.imageType).done(function(url) {
var image = view.files[0];
$.ajax({
url: url,
type: 'PUT',
data: image,
async: false,
processData: false,
contentType: view.imageType,
success: function(data, textStatus, jqXHR) {
view.imageUrl();
this.baseView.toggleActionError('upload', null);
},
error: function(jqXHR, textStatus, errorThrown) {
view.baseView.toggleActionError('upload', textStatus);
fileUpload.removeClass("is--disabled");
}
});
}).fail(function(errMsg) {
view.baseView.toggleActionError('upload', errMsg);
fileUpload.removeClass("is--disabled");
});
this.server.getUploadUrl(view.imageType).done(
function(url) {
var image = view.files[0];
view.fileUploader.upload(url, image, view.imageType)
.done(function() {
view.imageUrl();
view.baseView.toggleActionError('upload', null);
})
.fail(handleError);
}
).fail(handleError);
},
/**
......
......@@ -3,11 +3,11 @@
"version": "0.0.1",
"repository": "https://github.com/edx/edx-ora2.git",
"devDependencies": {
"karma": "~0.11",
"karma-jasmine": "0.1.3",
"karma-coverage": "0.1.5",
"karma-firefox-launcher": "~0.1.3",
"karma": "~0.12",
"karma-jasmine": "0.1.5",
"karma-chrome-launcher": "0.1.3",
"karma-coverage": "0.2.4",
"karma-phantomjs-launcher": "0.1.4",
"uglify-js": "2.3.6"
},
"scripts": {
......
[MASTER]
# Profiled execution.
profile=no
# Add files or directories to the blacklist. They should be base names, not paths.
ignore=migrations,features
# Pickle collected data for later comparisons.
persistent=no
[MESSAGES CONTROL]
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once).
disable=
# Never going to use these
# I0011: Locally disabling W0232
# C0301: Line too long
# W0141: Used builtin function 'map'
# W0142: Used * or ** magic
# R0921: Abstract class not referenced
# R0922: Abstract class is only referenced 1 times
# C0302: Too many lines in module
# R0201: Method could be a function
# R0901: Too many ancestors
# R0902: Too many instance attributes
# R0903: Too few public methods (1/2)
# R0904: Too many public methods
# R0911: Too many return statements
# R0912: Too many branches
# R0913: Too many arguments
# R0914: Too many local variables
# W0232: Class has no __init__ method
I0011,C0301,W0141,W0142,R0921,R0922,
C0302,R0201,R0901,R0902,R0903,R0904,R0911,R0912,R0913,R0914, W0232,
[REPORTS]
# Set the output format. Available formats are text, parseable, colorized, msvs
# (visual studio) and html
output-format=text
# Include message's id in output
include-ids=yes
# Put messages in a separate file for each module / package specified on the
# command line instead of printing them on stdout. Reports (if any) will be
# written in a file name "pylint_global.[txt|html]".
files-output=no
# Tells whether to display a full report or only the messages
reports=no
# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Add a comment according to your evaluation note. This is used by the global
# evaluation report (RP0004).
comment=no
# Display symbolic names of messages in reports
symbols=yes
[TYPECHECK]
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# List of classes names for which member attributes should not be checked
# (useful for classes with attributes dynamically set).
ignored-classes=SQLObject
# When zope mode is activated, add a predefined set of Zope acquired attributes
# to generated-members.
zope=no
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E0201 when accessed. Python regular
# expressions are accepted.
generated-members=
REQUEST,
acl_users,
aq_parent,
objects,
DoesNotExist,
MultipleObjectsReturned,
can_read,
can_write,
get_url,
size,
content,
status_code,
# For xblocks
fields,
# Django models
criteria,
options,
parts,
points,
points_possible,
uuid,
is_valid,
save,
errors,
data,
id,
criterion
[BASIC]
# Required attributes for module, separated by a comma
required-attributes=
# List of builtins function names that should not be used, separated by a comma
bad-functions=map,filter,apply,input
# Regular expression which should only match correct module names
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Regular expression which should only match correct module level names
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__)|log|logger|urlpatterns)$
# Regular expression which should only match correct class names
class-rgx=[A-Z_][a-zA-Z0-9]+$
# Regular expression which should only match correct function names
function-rgx=([a-z_][a-z0-9_]{2,30}|test_[a-z0-9_]+)$
# Regular expression which should only match correct method names
method-rgx=([a-z_][a-z0-9_]{2,60}|setUp|set[Uu]pClass|tearDown|tear[Dd]ownClass|assert[A-Z]\w*|maxDiff|test_[a-z0-9_]+)$
# Regular expression which should only match correct instance attribute names
attr-rgx=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match correct argument names
argument-rgx=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match correct variable names
variable-rgx=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match correct list comprehension /
# generator expression variable names
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
# Good variable names which should always be accepted, separated by a comma
good-names=f,i,j,k,db,ex,Run,_,__
# Bad variable names which should always be refused, separated by a comma
bad-names=foo,bar,baz,toto,tutu,tata
# Regular expression which should only match functions or classes name which do
# not require a docstring
no-docstring-rgx=__.*__|test_.*|setUp|tearDown|Meta
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,XXX,TODO
[FORMAT]
# Maximum number of characters on a single line.
max-line-length=120
# Maximum number of lines in a module
max-module-lines=1000
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
[SIMILARITIES]
# Minimum lines number of a similarity.
min-similarity-lines=4
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
[VARIABLES]
# Tells whether we should check for unused import in __init__ files.
init-import=no
# A regular expression matching the beginning of the name of dummy variables
# (i.e. not used).
dummy-variables-rgx=_|dummy|unused|.*_unused
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
additional-builtins=
[IMPORTS]
# Deprecated modules which should not be used, separated by a comma
deprecated-modules=regsub,string,TERMIOS,Bastion,rexec
# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled)
import-graph=
# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled)
ext-import-graph=
# Create a graph of internal dependencies in the given file (report RP0402 must
# not be disabled)
int-import-graph=
[DESIGN]
# Maximum number of arguments for function / method
max-args=5
# Argument names that match this expression will be ignored. Default to name
# with leading underscore
ignored-argument-names=_.*
# Maximum number of locals for function / method body
max-locals=15
# Maximum number of return / yield for function / method body
max-returns=6
# Maximum number of branch for function / method body
max-branchs=12
# Maximum number of statements in function / method body
max-statements=50
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of attributes for a class (see R0902).
max-attributes=7
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
[CLASSES]
# List of interface methods to ignore, separated by a comma. This is used for
# instance to not check methods defines in Zope's Interface base class.
ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,__new__,setUp
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
[EXCEPTIONS]
# Exceptions that will emit a warning when being caught. Defaults to "Exception"
overgeneral-exceptions=Exception
......@@ -21,6 +21,7 @@ djangorestframework==2.3.5
lazy==1.1
loremipsum==1.0.2
python-dateutil==2.1
python-memcached==1.53
pytz==2012h
South==0.7.6
......
-r test.txt
git+https://github.com/edx/i18n-tools.git@f5303e82dff368c7595884d9325aeea1d802da25#egg=i18n_tools
# Debug tools
bpython==0.13
django-debug-panel==0.8.0
......@@ -9,17 +7,5 @@ django-debug-toolbar==1.0.1
django-pdb==0.3.2
sqlparse==0.1.10
# Doc generation
docutils==0.11
Jinja2==2.7.1
MarkupSafe==0.18
Pygments==1.6
Sphinx==1.2
sphinx-rtd-theme==0.1.5
sphinxcontrib-napoleon==0.2.3
# runserver_plus
Werkzeug==0.9.4
# caching
python-memcached==1.53
# Grab everything in base requirements
-r base.txt
# There's a unicode bug in the httpretty==0.8 (used by moto)
# Once a new version gets released on PyPi we can use that instead.
git+https://github.com/gabrielfalcao/HTTPretty.git@4c2b10925c86c9b6299c1a04ae334d89fe007ae2#egg=httpretty
ddt==0.8.0
django-nose==1.2
mock==1.0.1
......@@ -13,3 +9,5 @@ nose==1.3.0
coverage==3.7.1
pep8==1.4.6
pylint<1.0
git+https://github.com/edx/i18n-tools.git@f5303e82dff368c7595884d9325aeea1d802da25#egg=i18n_tools
#!/usr/bin/env python
"""
Benchmark the execution time of the EASE algorithm for scoring essays.
"""
import os
import json
import time
import contextlib
from openassessment.assessment.worker.algorithm import AIAlgorithm, EaseAIAlgorithm
NUM_TRIALS = 3
NUM_CRITERIA = 10
DATA_FILE_PATH = os.path.abspath(
os.path.join(
os.path.dirname(__file__),
'data/ai-test-data.json'
)
)
@contextlib.contextmanager
def benchmark(name):
"""
Print the duration in seconds for a block of code.
Args:
name (unicode): A descriptive name for the benchmark
Returns:
None
Yields:
None
"""
start = time.clock()
yield
end = time.clock()
duration = end - start
print u"{name} took {duration} seconds".format(name=name, duration=duration)
def load_training_data(data_path):
"""
Load the example essays and scores.
Args:
data_path (unicode): The path to the JSON data file.
This should be a serialized list of dictionaries
with keys 'text' (unicode) and 'score' (int).
Returns:
list of `AIAlgorithm.ExampleEssay`s
"""
print "Loading training data..."
with open(data_path) as data_file:
input_examples = json.load(data_file)
print "Done."
return [
AIAlgorithm.ExampleEssay(
text=example['text'],
score=int(example['score'])
)
for example in input_examples
]
def main():
"""
Time training/scoring using EASE.
"""
examples = load_training_data(DATA_FILE_PATH)
algorithm = EaseAIAlgorithm()
print "Training classifier..."
with benchmark('Training'):
classifier = algorithm.train_classifier(examples[:-1])
print "Done."
print u"Scoring essays ({num} criteria)...".format(num=NUM_CRITERIA)
for num in range(NUM_TRIALS):
cache = {}
with benchmark('Scoring (rubric)'):
for _ in range(NUM_CRITERIA):
with benchmark('Scoring (criteria)'):
algorithm.score(examples[-1].text, classifier, cache)
print "Finished scoring essay #{num}".format(num=num)
if __name__ == "__main__":
main()
......@@ -3,7 +3,7 @@
# Need to exit with an error code to fail the Travis build
set -e
pip install Sphinx sphinx_rtd_theme
pip install -q Sphinx sphinx_rtd_theme
# go into docs directory
cd docs/en_us
......@@ -11,7 +11,7 @@ cd docs/en_us
# build course authors docs
cd course_authors
if [ -f requirements.txt ]; then
pip install -r requirements.txt
pip install -q -r requirements.txt
fi
make html
cd ..
......@@ -19,7 +19,7 @@ cd ..
# build developer docs
cd developers
if [ -f requirements.txt ]; then
pip install -r requirements.txt
pip install -q -r requirements.txt
fi
make html
cd ..
......
#!/usr/bin/env bash
##################################################
#
# celery-worker.sh
#
# Start a worker instance for local development.
#
# Usage:
#
# ./celery-worker.sh
#
###################################################
cd `dirname $BASH_SOURCE` && cd ..
# Check whether RabbitMQ is installed
if [ -z `which rabbitmqctl` ]; then
echo "Please install RabbitMQ: http://www.rabbitmq.com/download.html"
exit 1;
fi
# Install Python and JS dependencies
./scripts/install.sh
# Configure Django settings
export DJANGO_SETTINGS_MODULE="settings.dev"
# Update the database
echo "Updating the database..."
python manage.py syncdb --migrate -v 0
# Start the RabbitMQ server (ignore errors if it's already started)
echo "Starting RabbitMQ server..."
rabbitmq-server -detached || true
# Start a RabbitMQ node
echo "Starting RabbitMQ node..."
rabbitmqctl start_app
# Start the worker
echo "Starting worker..."
python manage.py celery worker
......@@ -7,4 +7,4 @@ if [ -z $1 ]; then LOCALE='--all'; else LOCALE="-l $1"; fi
python manage.py makemessages $LOCALE
python manage.py makemessages $LOCALE -d djangojs
i18n_tool dummy
python manage.py compilemessages
python manage.py compilemessages $LOCALE
#!/usr/bin/env bash
cd `dirname $BASH_SOURCE` && cd ..
STATIC_JS="openassessment/xblock/static/js"
echo "Installing Node requirements..."
if [ -z `which npm` ]; then
echo "Please install NodeJS: http://nodejs.org/"
exit 1
fi
npm config set loglevel warn
npm install
echo "Minimizing XBlock JavaScript..."
echo "(set DEBUG_JS=1 to preserve indentation and line breaks)"
if [[ -n "$DEBUG_JS" ]]; then
UGLIFY_EXTRA_ARGS="--beautify"
fi
node_modules/.bin/uglifyjs $STATIC_JS/src/oa_shared.js $STATIC_JS/src/*.js $UGLIFY_EXTRA_ARGS > "$STATIC_JS/openassessment.min.js"
#!/usr/bin/env bash
cd `dirname $BASH_SOURCE` && cd ..
STATIC_JS="openassessment/xblock/static/js"
if [[ -n "$1" ]]; then
REQS="$1"
else
REQS="dev"
fi
echo "Installing Python requirements..."
pip install -q -r requirements/$REQS.txt
pip install -q -r requirements/base.txt
echo "Installing the OpenAssessment XBlock..."
cat <<EOF | python -
......
#!/usr/bin/env bash
cd `dirname $BASH_SOURCE` && cd ..
# Install "wheel" archives of the requirements for running the test suite.
# http://pip.readthedocs.org/en/latest/reference/pip_wheel.html
# This runs in Travis to install pre-built binary packages, which
# means the builds are faster and more reliable.
cd `dirname $BASH_SOURCE` && cd ..
pip install --upgrade pip
pip install wheel
......
#!/usr/bin/env bash
./scripts/install-python.sh
./scripts/install-js.sh
#!/usr/bin/env bash
cd `dirname $BASH_SOURCE` && cd ..
./scripts/install-js.sh
echo "Generating HTML fixtures for JavaScript tests..."
export DJANGO_SETTINGS_MODULE="settings.test"
......
......@@ -5,8 +5,6 @@ cd `dirname $BASH_SOURCE` && cd ..
# Cleanup uploaded files from previous test runs (AI classifiers)
git clean -xfd "./storage/test/"
./scripts/install-python.sh test
echo "Running Python tests..."
export DJANGO_SETTINGS_MODULE="settings.test"
python manage.py test $1
......@@ -4,7 +4,9 @@
cd `dirname $BASH_SOURCE` && cd ..
# Install dependencies
./scripts/install.sh
make install-python
make install-js
make minimize-js
# Configure Django settings
export DJANGO_SETTINGS_MODULE="settings.dev"
......
......@@ -156,3 +156,8 @@ BASE_DIR = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
# version 3.0 (same as edx-platform), we need to use an external library.
import djcelery
djcelery.setup_loader()
# We run Celery in "always eager" mode in the test suite and local dev,
# which executes tasks synchronously instead of using the task queue.
CELERY_ALWAYS_EAGER = True
CELERY_EAGER_PROPAGATES_EXCEPTIONS = True
......@@ -42,19 +42,19 @@ LOGGING = {
'apps_info': {
'level': 'INFO',
'class': 'logging.FileHandler',
'filename': 'logs/dev/apps_info.log',
'filename': 'logs/apps_info.log',
'formatter': 'simple',
},
'apps_debug': {
'level': 'DEBUG',
'class': 'logging.FileHandler',
'filename': 'logs/dev/apps_debug.log',
'filename': 'logs/apps_debug.log',
'formatter': 'simple',
},
'trace': {
'level': 'DEBUG',
'class': 'logging.handlers.RotatingFileHandler',
'filename': 'logs/dev/trace.log',
'filename': 'logs/trace.log',
'formatter': 'simple',
'maxBytes': 1000000,
'backupCount': 2,
......@@ -62,13 +62,13 @@ LOGGING = {
'events': {
'level': 'INFO',
'class': 'logging.FileHandler',
'filename': 'logs/dev/events.log',
'filename': 'logs/events.log',
'formatter': 'simple',
},
'errors': {
'level': 'ERROR',
'class': 'logging.FileHandler',
'filename': 'logs/dev/errors.log',
'filename': 'logs/errors.log',
'formatter': 'simple',
}
},
......@@ -100,16 +100,3 @@ LOGGING = {
# Store uploaded files in a dev-specific directory
MEDIA_ROOT = os.path.join(BASE_DIR, 'storage/dev')
# Celery Broker
CELERY_BROKER_TRANSPORT = "amqp"
CELERY_BROKER_HOSTNAME = "localhost:5672//"
CELERY_BROKER_USER = "guest"
CELERY_BROKER_PASSWORD = "guest"
BROKER_URL = "{0}://{1}:{2}@{3}".format(
CELERY_BROKER_TRANSPORT,
CELERY_BROKER_USER,
CELERY_BROKER_PASSWORD,
CELERY_BROKER_HOSTNAME,
)
......@@ -40,10 +40,6 @@ INSTALLED_APPS += ('django_nose',)
# Store uploaded files in a test-specific directory
MEDIA_ROOT = os.path.join(BASE_DIR, 'storage/test')
# We run Celery in "always eager" mode in the test suite,
# which executes tasks synchronously instead of using the task queue.
CELERY_ALWAYS_EAGER = True
CELERY_EAGER_PROPAGATES_EXCEPTIONS = True
# Silence cache key warnings
# https://docs.djangoproject.com/en/1.4/topics/cache/#cache-key-warnings
......
"""
Settings for running workbench in Vagrant.
To mimic production, the Vagrant setup uses:
* gunicorn (run multiple server processes)
* memcached
* mysql
* rabbitmq
"""
# Inherit from base settings
from .base import * # pylint:disable=W0614,W0401
VAGRANT_HOME = "/home/vagrant"
REPO_ROOT = u"{home}/edx-ora2".format(home=VAGRANT_HOME)
DEBUG = False
INSTALLED_APPS += ('gunicorn',)
STATIC_ROOT = u"{home}/static".format(home=VAGRANT_HOME)
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'workbench',
'USER': 'root',
'PASSWORD': '',
'HOST': '',
'PORT': '',
}
}
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': '127.0.0.1:11211',
'TIMEOUT': 60 * 60 * 8
}
}
LOG_ROOT = u"{repo}/logs/vagrant".format(repo=REPO_ROOT)
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'filters': {
'require_debug_false': {
'()': 'django.utils.log.RequireDebugFalse'
}
},
'handlers': {
'apps_info': {
'level': 'INFO',
'class': 'logging.FileHandler',
'filename': u'{}/apps_info.log'.format(LOG_ROOT),
'formatter': 'simple',
},
'apps_debug': {
'level': 'DEBUG',
'class': 'logging.FileHandler',
'filename': u'{}/apps_debug.log'.format(LOG_ROOT),
'formatter': 'simple',
},
'trace': {
'level': 'DEBUG',
'class': 'logging.handlers.RotatingFileHandler',
'filename': u'{}/trace.log'.format(LOG_ROOT),
'formatter': 'simple',
'maxBytes': 1000000,
'backupCount': 2,
},
'events': {
'level': 'INFO',
'class': 'logging.FileHandler',
'filename': u'{}/events.log'.format(LOG_ROOT),
'formatter': 'simple',
},
'errors': {
'level': 'ERROR',
'class': 'logging.FileHandler',
'filename': u'{}/errors.log'.format(LOG_ROOT),
'formatter': 'simple',
}
},
'formatters': {
'simple': {
'format': '%(asctime)s %(name)s [%(levelname)s] %(message)s'
}
},
'loggers': {
'': {
'handlers': ['trace', 'errors'],
'propagate': True,
},
'openassessment': {
'handlers': ['apps_debug', 'apps_info'],
'propagate': True,
},
'submissions': {
'handlers': ['apps_debug', 'apps_info'],
'propagate': True,
},
'workbench.runtime': {
'handlers': ['apps_debug', 'apps_info', 'events'],
'propogate': True,
}
},
}
# Store uploaded files in a dev-specific directory
MEDIA_ROOT = os.path.join(BASE_DIR, 'storage/dev')
# AI algorithm configuration
ORA2_AI_ALGORITHMS = {
'fake': 'openassessment.assessment.worker.algorithm.FakeAIAlgorithm',
'ease': 'openassessment.assessment.worker.algorithm.EaseAIAlgorithm'
}
# Celery Broker
CELERY_BROKER_TRANSPORT = "amqp"
CELERY_BROKER_HOSTNAME = "localhost:5672//"
CELERY_BROKER_USER = "guest"
CELERY_BROKER_PASSWORD = "guest"
BROKER_URL = "{0}://{1}:{2}@{3}".format(
CELERY_BROKER_TRANSPORT,
CELERY_BROKER_USER,
CELERY_BROKER_PASSWORD,
CELERY_BROKER_HOSTNAME,
)
source /home/vagrant/.virtualenvs/edx-ora2/bin/activate
export DISPLAY=:1
export LC_ALL="en_US.UTF-8"
description "Celery workers"
start on vagrant-mounted
stop on runlevel [016]
exec /home/vagrant/.virtualenvs/edx-ora2/bin/python /home/vagrant/edx-ora2/manage.py celery --settings=settings.vagrant worker --concurrency=2
respawn
respawn limit 15 5
description "Celery flower"
start on vagrant-mounted
stop on runlevel [016]
exec /home/vagrant/.virtualenvs/edx-ora2/bin/celery flower
respawn
respawn limit 15 5
server {
listen 80;
server_name 192.168.44.10;
charset utf-8;
client_max_body_size 75M;
location /static {
alias /home/vagrant/static;
}
location / {
proxy_pass http://127.0.0.1:8000/;
}
}
#!/usr/bin/env bash
echo "Stopping services..."
sudo service nginx stop || true
sudo stop workbench || true
sudo stop celery || true
sudo stop flower || true
echo "Updating ORA2..."
cd /home/vagrant/edx-ora2 && ./scripts/install.sh
echo "Updating the database..."
cd /home/vagrant/edx-ora2 && python manage.py syncdb --migrate --noinput --settings settings.vagrant
echo "Collecting static assets..."
cd /home/vagrant/edx-ora2 && python manage.py collectstatic --noinput --settings settings.vagrant
echo "Restarting services..."
sudo start workbench || true
sudo start celery || true
sudo start flower || true
sudo service nginx start || true
description "Workbench"
start on vagrant-mounted
stop on runlevel [016]
exec /home/vagrant/.virtualenvs/edx-ora2/bin/python /home/vagrant/edx-ora2/manage.py run_gunicorn --log-level=DEBUG --workers=10 -b 127.0.0.1:8000 --settings settings.vagrant
respawn
respawn limit 15 5
description "Xvfb X Server"
start on (net-device-up and local-filesystems and runlevel [2345])
stop on runlevel [016]
exec /usr/bin/Xvfb :1 -screen 0 1024x768x24
respawn
respawn limit 15 5
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment