From 9ee246e4073f2649ecdb88a58c89b6a403baabf4 Mon Sep 17 00:00:00 2001
From: e0d
Date: Thu, 2 May 2013 15:50:33 -0400
Subject: [PATCH 009/245] flexible org repos parsing tested locally
---
jenkins/base.sh | 15 ++++++++++++++-
1 file changed, 14 insertions(+), 1 deletion(-)
diff --git a/jenkins/base.sh b/jenkins/base.sh
index fc2595662a..7eb4802b8f 100644
--- a/jenkins/base.sh
+++ b/jenkins/base.sh
@@ -1,6 +1,19 @@
+##
+## requires >= 1.3.0 of the Jenkins git plugin
+##
function github_status {
- gcli status create edx mitx $GIT_COMMIT \
+
+ if [[ ! ${GIT_URL} =~ git@github.com:([^/]+)/([^\.]+).git ]]; then
+ echo "Cannot parse Github org or repo from URL, using defaults."
+ ORG="edx"
+ REPO="mitx"
+ else
+ ORG=${BASH_REMATCH[1]}
+ REPO=${BASH_REMATCH[2]}
+ fi
+
+ gcli status create $ORG $REPO $GIT_COMMIT \
--params=$1 \
target_url:$BUILD_URL \
description:"Build #$BUILD_NUMBER is running" \
From 55dd0fc8bcb40efa2557653261d9ac6a29c3683f Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Fri, 3 May 2013 12:45:55 -0400
Subject: [PATCH 010/245] workaround for gettext parser bug
---
.tx/config | 26 ++++++++++++++++
conf/locale/config | 2 +-
i18n/execute.py | 21 +++++++++++--
i18n/extract.py | 4 +--
i18n/generate.py | 16 +++++++++-
i18n/make_dummy.py | 18 +++++------
i18n/tests/__init__.py | 2 ++
i18n/tests/test_validate.py | 33 ++++++++++++++++++++
rakefile | 62 +++++++++++++++++++++++--------------
9 files changed, 145 insertions(+), 39 deletions(-)
create mode 100644 .tx/config
create mode 100644 i18n/tests/test_validate.py
diff --git a/.tx/config b/.tx/config
new file mode 100644
index 0000000000..540c4732af
--- /dev/null
+++ b/.tx/config
@@ -0,0 +1,26 @@
+[main]
+host = https://www.transifex.com
+
+[edx-studio.django-partial]
+file_filter = conf/locale//LC_MESSAGES/django-partial.po
+source_file = conf/locale/en/LC_MESSAGES/django-partial.po
+source_lang = en
+type = PO
+
+[edx-studio.djangojs]
+file_filter = conf/locale//LC_MESSAGES/djangojs.po
+source_file = conf/locale/en/LC_MESSAGES/djangojs.po
+source_lang = en
+type = PO
+
+[edx-studio.mako]
+file_filter = conf/locale//LC_MESSAGES/mako.po
+source_file = conf/locale/en/LC_MESSAGES/mako.po
+source_lang = en
+type = PO
+
+[edx-studio.messages]
+file_filter = conf/locale//LC_MESSAGES/messages.po
+source_file = conf/locale/en/LC_MESSAGES/messages.po
+source_lang = en
+type = PO
diff --git a/conf/locale/config b/conf/locale/config
index 2d01e1ea43..8afaaa9482 100644
--- a/conf/locale/config
+++ b/conf/locale/config
@@ -1 +1 @@
-{"locales" : ["en"]}
+{"locales" : ["en", "fr", "es"]}
diff --git a/i18n/execute.py b/i18n/execute.py
index 3c3416b65d..0cd152fb58 100644
--- a/i18n/execute.py
+++ b/i18n/execute.py
@@ -50,14 +50,29 @@ def execute (command, working_directory=BASE_DIR, log=LOG):
"""
Executes shell command in a given working_directory.
Command is a string to pass to the shell.
- Output is logged to log.
+ The command is logged to log, output is ignored.
"""
- log.info(command)
+ if log:
+ log.info(command)
subprocess.call(command.split(' '), cwd=working_directory)
+
+
+def call(command, working_directory=BASE_DIR, log=LOG):
+ """
+ Executes shell command in a given working_directory.
+ Command is a string to pass to the shell.
+ Returns a tuple of two strings: (stdout, stderr)
+ """
+ if log:
+ log.info(command)
+ p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=working_directory)
+ out, err = p.communicate()
+ return (out, err)
+
def get_config():
"""Returns data found in config file, or returns None if file not found"""
- config_path = os.path.abspath(CONFIG_FILENAME)
+ config_path = os.path.normpath(CONFIG_FILENAME)
if not os.path.exists(config_path):
log.warn("Configuration file cannot be found: %s" % \
os.path.relpath(config_path, BASE_DIR))
diff --git a/i18n/extract.py b/i18n/extract.py
index c6fedd3bfa..c3d4368a67 100755
--- a/i18n/extract.py
+++ b/i18n/extract.py
@@ -79,6 +79,7 @@ def fix_header(po):
"""
Replace default headers with edX headers
"""
+ po.metadata_is_fuzzy = [] # remove [u'fuzzy']
header = po.header
fixes = (
('SOME DESCRIPTIVE TITLE', 'edX translation file'),
@@ -119,10 +120,9 @@ def fix_metadata(po):
'Report-Msgid-Bugs-To': 'translation_team@edx.org',
'Project-Id-Version': '0.1a',
'Language' : 'en',
+ 'Last-Translator' : '',
'Language-Team': 'translation team ',
}
- if po.metadata.has_key('Last-Translator'):
- del po.metadata['Last-Translator']
po.metadata.update(fixes)
def strip_key_strings(po):
diff --git a/i18n/generate.py b/i18n/generate.py
index ddbaadfa70..40a6cc88ca 100755
--- a/i18n/generate.py
+++ b/i18n/generate.py
@@ -14,6 +14,8 @@
"""
import os
+from polib import pofile
+
from execute import execute, get_config, messages_dir, remove_file, \
BASE_DIR, LOG, SOURCE_LOCALE
@@ -30,11 +32,23 @@ def merge(locale, target='django.po'):
merge_cmd = 'msgcat -o merged.po ' + ' '.join(files_to_merge)
execute(merge_cmd, working_directory=locale_directory)
- # rename merged.po -> django.po (default)
+ # clean up redunancies in the metadata
merged_filename = os.path.join(locale_directory, 'merged.po')
+ clean_metadata(merged_filename)
+
+ # rename merged.po -> django.po (default)
django_filename = os.path.join(locale_directory, target)
os.rename(merged_filename, django_filename) # can't overwrite file on Windows
+def clean_metadata(file):
+ """
+ Clean up redundancies in the metadata caused by merging.
+ This reads in a PO file and simply saves it back out again.
+ """
+ po = pofile(file)
+ po.save()
+
+
def validate_files(dir, files_to_merge):
"""
Asserts that the given files exist.
diff --git a/i18n/make_dummy.py b/i18n/make_dummy.py
index c8dcde861a..9c8c3289ce 100755
--- a/i18n/make_dummy.py
+++ b/i18n/make_dummy.py
@@ -10,15 +10,15 @@
#
# $ ./make_dummy.py
#
-# $ ./make_dummy.py mitx/conf/locale/en/LC_MESSAGES/django.po
+# $ ./make_dummy.py ../conf/locale/en/LC_MESSAGES/django.po
#
# generates output to
-# mitx/conf/locale/vr/LC_MESSAGES/django.po
+# mitx/conf/locale/fr/LC_MESSAGES/django.po
import os, sys
import polib
from dummy import Dummy
-from execute import create_dir_if_necessary
+from execute import get_logger, create_dir_if_necessary
def main(file, locale):
"""
@@ -41,11 +41,11 @@ def new_filename(original_filename, new_locale):
orig_dir = os.path.dirname(original_filename)
msgs_dir = os.path.basename(orig_dir)
orig_file = os.path.basename(original_filename)
- return os.path.join(orig_dir,
- '/../..',
- new_locale,
- msgs_dir,
- orig_file)
+ return os.path.abspath(os.path.join(orig_dir,
+ '../..',
+ new_locale,
+ msgs_dir,
+ orig_file))
# Dummy language
@@ -60,7 +60,7 @@ DEFAULT_LOCALE = 'fr'
if __name__ == '__main__':
if len(sys.argv)<2:
raise Exception("missing file argument")
- if len(sys.argv)<2:
+ if len(sys.argv)<3:
locale = DEFAULT_LOCALE
else:
locale = sys.argv[2]
diff --git a/i18n/tests/__init__.py b/i18n/tests/__init__.py
index d60515c712..88216df993 100644
--- a/i18n/tests/__init__.py
+++ b/i18n/tests/__init__.py
@@ -2,3 +2,5 @@ from test_extract import TestExtract
from test_generate import TestGenerate
from test_converter import TestConverter
from test_dummy import TestDummy
+from test_validate import TestValidate
+
diff --git a/i18n/tests/test_validate.py b/i18n/tests/test_validate.py
new file mode 100644
index 0000000000..ed746db78f
--- /dev/null
+++ b/i18n/tests/test_validate.py
@@ -0,0 +1,33 @@
+import os
+from unittest import TestCase
+from nose.plugins.skip import SkipTest
+
+from execute import call, LOCALE_DIR, LOG
+
+class TestValidate(TestCase):
+ """
+ Call GNU msgfmt -c on each .po file to validate its format.
+ """
+
+ def test_validate(self):
+ # Skip this test for now because it's very noisy
+ raise SkipTest()
+ for file in self.get_po_files():
+ # Use relative paths to make output less noisy.
+ rfile = os.path.relpath(file, LOCALE_DIR)
+ (out, err) = call(['msgfmt','-c', rfile], log=None, working_directory=LOCALE_DIR)
+ if err != '':
+ LOG.warn('\n'+err)
+
+ def get_po_files(self, root=LOCALE_DIR):
+ """
+ This is a generator. It yields all of the .po files under root.
+ """
+ for (dirpath, dirnames, filenames) in os.walk(root):
+ for name in filenames:
+ (base, ext) = os.path.splitext(name)
+ if ext.lower() == '.po':
+ yield os.path.join(dirpath, name)
+
+
+
diff --git a/rakefile b/rakefile
index 32d92a0349..5914b2f0ae 100644
--- a/rakefile
+++ b/rakefile
@@ -337,12 +337,6 @@ task :migrate, [:env] do |t, args|
sh(django_admin(:lms, args.env, 'migrate'))
end
-desc "Run tests for the internationalization library"
-task :test_i18n do
- test = File.join(REPO_ROOT, "i18n", "tests")
- sh("nosetests #{test}")
-end
-
Dir["common/lib/*"].select{|lib| File.directory?(lib)}.each do |lib|
task_name = "test_#{lib}"
@@ -516,27 +510,49 @@ end
# --- Internationalization tasks
-desc "Extract localizable strings from sources"
-task :extract_dev_strings do
- sh(File.join(REPO_ROOT, "i18n", "extract.py"))
-end
+namespace :i18n do
-desc "Compile localizable strings from sources. With optional flag 'extract', will extract strings first."
-task :generate_i18n do
- if ARGV.last.downcase == 'extract'
- Rake::Task["extract_dev_strings"].execute
+ desc "Extract localizable strings from sources"
+ task :extract do
+ sh(File.join(REPO_ROOT, "i18n", "extract.py"))
end
- sh(File.join(REPO_ROOT, "i18n", "generate.py"))
-end
-desc "Simulate international translation by generating dummy strings corresponding to source strings."
-task :dummy_i18n do
- source_files = Dir["#{REPO_ROOT}/conf/locale/en/LC_MESSAGES/*.po"]
- dummy_locale = 'fr'
- cmd = File.join(REPO_ROOT, "i18n", "make_dummy.py")
- for file in source_files do
- sh("#{cmd} #{file} #{dummy_locale}")
+ desc "Compile localizable strings from sources. With optional flag 'extract', will extract strings first."
+ task :generate do
+ if ARGV.last.downcase == 'extract'
+ Rake::Task["i18n:extract"].execute
+ end
+ sh(File.join(REPO_ROOT, "i18n", "generate.py"))
end
+
+ desc "Simulate international translation by generating dummy strings corresponding to source strings."
+ task :dummy do
+ source_files = Dir["#{REPO_ROOT}/conf/locale/en/LC_MESSAGES/*.po"]
+ dummy_locale = 'fr'
+ cmd = File.join(REPO_ROOT, "i18n", "make_dummy.py")
+ for file in source_files do
+ sh("#{cmd} #{file} #{dummy_locale}")
+ end
+ end
+
+ namespace :transifex do
+ desc "Push source strings to Transifex for translation"
+ task :push do
+ sh("tx push -s")
+ end
+
+ desc "Pull transated strings from Transifex"
+ task :pull do
+ sh("tx pull")
+ end
+ end
+
+ desc "Run tests for the internationalization library"
+ task :test do
+ test = File.join(REPO_ROOT, "i18n", "tests")
+ sh("nosetests #{test}")
+ end
+
end
# --- Develop and public documentation ---
From e6334584d68272c90b139cdec43eb1bec2d01c2e Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Fri, 3 May 2013 15:18:29 -0400
Subject: [PATCH 011/245] rakefile cleanup
---
conf/locale/config | 2 +-
rakefile | 22 +++++++++++++++++++---
2 files changed, 20 insertions(+), 4 deletions(-)
diff --git a/conf/locale/config b/conf/locale/config
index 8afaaa9482..2d01e1ea43 100644
--- a/conf/locale/config
+++ b/conf/locale/config
@@ -1 +1 @@
-{"locales" : ["en", "fr", "es"]}
+{"locales" : ["en"]}
diff --git a/rakefile b/rakefile
index 5914b2f0ae..f39cbacc41 100644
--- a/rakefile
+++ b/rakefile
@@ -510,6 +510,18 @@ end
# --- Internationalization tasks
+# Make sure config file with username/password exists
+# Returns boolean: returns true if file exists and is nonzero length
+def validate_transifex_config()
+ config_file = Dir.home + "/.transifexrc"
+ if !File.file?(config_file) or File.size(config_file)==0
+ raise "Cannot connect to Transifex, config file is missing or empty: #{config_file}\n" +
+ "See http://help.transifex.com/features/client/#transifexrc\n"
+ return false
+ end
+ return true
+end
+
namespace :i18n do
desc "Extract localizable strings from sources"
@@ -538,12 +550,16 @@ namespace :i18n do
namespace :transifex do
desc "Push source strings to Transifex for translation"
task :push do
- sh("tx push -s")
+ if validate_transifex_config()
+ sh("tx push -s")
+ end
end
- desc "Pull transated strings from Transifex"
+ desc "Pull translated strings from Transifex"
task :pull do
- sh("tx pull")
+ if validate_transifex_config()
+ sh("tx pull")
+ end
end
end
From 03b9a9e22a6c8c69fd1f1b6275b334a873f54746 Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Fri, 3 May 2013 15:42:39 -0400
Subject: [PATCH 012/245] tweak file reference
---
rakefile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/rakefile b/rakefile
index f39cbacc41..240847812e 100644
--- a/rakefile
+++ b/rakefile
@@ -513,7 +513,7 @@ end
# Make sure config file with username/password exists
# Returns boolean: returns true if file exists and is nonzero length
def validate_transifex_config()
- config_file = Dir.home + "/.transifexrc"
+ config_file = "#{Dir.home}/.transifexrc"
if !File.file?(config_file) or File.size(config_file)==0
raise "Cannot connect to Transifex, config file is missing or empty: #{config_file}\n" +
"See http://help.transifex.com/features/client/#transifexrc\n"
From c0278d0ff1c75647113a9282407ed9c35bc7bb7d Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Mon, 6 May 2013 11:29:27 -0400
Subject: [PATCH 013/245] refactor config file; fix duplicate merge
---
conf/locale/config | 5 ++-
i18n/config.py | 73 +++++++++++++++++++++++++++++++++++++
i18n/execute.py | 54 +++------------------------
i18n/extract.py | 25 +++++++------
i18n/generate.py | 15 ++------
i18n/tests/__init__.py | 1 +
i18n/tests/test_config.py | 33 +++++++++++++++++
i18n/tests/test_extract.py | 4 +-
i18n/tests/test_generate.py | 52 +++++++++++++++-----------
i18n/tests/test_validate.py | 3 +-
i18n/transifex.py | 23 ++++++++++++
rakefile | 6 ++-
12 files changed, 196 insertions(+), 98 deletions(-)
create mode 100644 i18n/config.py
create mode 100644 i18n/tests/test_config.py
create mode 100755 i18n/transifex.py
diff --git a/conf/locale/config b/conf/locale/config
index 2d01e1ea43..67252b1fa0 100644
--- a/conf/locale/config
+++ b/conf/locale/config
@@ -1 +1,4 @@
-{"locales" : ["en"]}
+{
+ "locales" : ["en"],
+ "dummy-locale" : "fr"
+}
diff --git a/i18n/config.py b/i18n/config.py
new file mode 100644
index 0000000000..f0d8e366d0
--- /dev/null
+++ b/i18n/config.py
@@ -0,0 +1,73 @@
+import os, json
+
+# BASE_DIR is the working directory to execute django-admin commands from.
+# Typically this should be the 'mitx' directory.
+BASE_DIR = os.path.normpath(os.path.dirname(os.path.abspath(__file__))+'/..')
+
+# LOCALE_DIR contains the locale files.
+# Typically this should be 'mitx/conf/locale'
+LOCALE_DIR = os.path.join(BASE_DIR, 'conf', 'locale')
+
+class Configuration:
+ """
+ # Reads localization configuration in json format
+
+ """
+ _source_locale = 'en'
+
+ def __init__(self, filename):
+ self.filename = filename
+ self.config = self.get_config(self.filename)
+
+ def get_config(self, filename):
+ """
+ Returns data found in config file (as dict), or raises exception if file not found
+ """
+ if not os.path.exists(filename):
+ raise Exception("Configuration file cannot be found: %s" % filename)
+ with open(filename) as stream:
+ return json.load(stream)
+
+ def get_locales(self):
+ """
+ Returns a list of locales declared in the configuration file,
+ e.g. ['en', 'fr', 'es']
+ Each locale is a string.
+ """
+ return self.config['locales']
+
+ def get_source_locale(self):
+ """
+ Returns source language.
+ Source language is English.
+ """
+ return self._source_locale
+
+ def get_dummy_locale(self):
+ """
+ Returns a locale to use for the dummy text, e.g. 'fr'.
+ Throws exception if no dummy-locale is declared.
+ The locale is a string.
+ """
+ dummy = self.config.get('dummy-locale', None)
+ if not dummy:
+ raise Exception('Could not read dummy-locale from configuration file.')
+ return dummy
+
+ def get_messages_dir(self, locale):
+ """
+ Returns the name of the directory holding the po files for locale.
+ Example: mitx/conf/locale/fr/LC_MESSAGES
+ """
+ return os.path.join(LOCALE_DIR, locale, 'LC_MESSAGES')
+
+ def get_source_messages_dir(self):
+ """
+ Returns the name of the directory holding the source-language po files (English).
+ Example: mitx/conf/locale/en/LC_MESSAGES
+ """
+ return self.get_messages_dir(self.get_source_locale())
+
+
+CONFIGURATION = Configuration(os.path.normpath(os.path.join(LOCALE_DIR, 'config')))
+
diff --git a/i18n/execute.py b/i18n/execute.py
index 0cd152fb58..4c47680101 100644
--- a/i18n/execute.py
+++ b/i18n/execute.py
@@ -1,40 +1,8 @@
-import os, subprocess, logging, json
+import os, subprocess, logging
+from config import CONFIGURATION, BASE_DIR
-def init_module():
- """
- Initializes module parameters
- """
- global BASE_DIR, LOCALE_DIR, CONFIG_FILENAME, SOURCE_MSGS_DIR, SOURCE_LOCALE, LOG
- # BASE_DIR is the working directory to execute django-admin commands from.
- # Typically this should be the 'mitx' directory.
- BASE_DIR = os.path.normpath(os.path.dirname(os.path.abspath(__file__))+'/..')
-
- # Source language is English
- SOURCE_LOCALE = 'en'
-
- # LOCALE_DIR contains the locale files.
- # Typically this should be 'mitx/conf/locale'
- LOCALE_DIR = BASE_DIR + '/conf/locale'
-
- # CONFIG_FILENAME contains localization configuration in json format
- CONFIG_FILENAME = LOCALE_DIR + '/config'
-
- # SOURCE_MSGS_DIR contains the English po files.
- SOURCE_MSGS_DIR = messages_dir(SOURCE_LOCALE)
-
- # Default logger.
- LOG = get_logger()
-
-
-def messages_dir(locale):
- """
- Returns the name of the directory holding the po files for locale.
- Example: mitx/conf/locale/en/LC_MESSAGES
- """
- return os.path.join(LOCALE_DIR, locale, 'LC_MESSAGES')
-
-def get_logger():
+def get_default_logger():
"""Returns a default logger"""
log = logging.getLogger(__name__)
log.setLevel(logging.INFO)
@@ -43,8 +11,8 @@ def get_logger():
log.addHandler(log_handler)
return log
-# Run this after defining messages_dir and get_logger, because it depends on these.
-init_module()
+LOG = get_default_logger()
+
def execute (command, working_directory=BASE_DIR, log=LOG):
"""
@@ -69,17 +37,6 @@ def call(command, working_directory=BASE_DIR, log=LOG):
out, err = p.communicate()
return (out, err)
-
-def get_config():
- """Returns data found in config file, or returns None if file not found"""
- config_path = os.path.normpath(CONFIG_FILENAME)
- if not os.path.exists(config_path):
- log.warn("Configuration file cannot be found: %s" % \
- os.path.relpath(config_path, BASE_DIR))
- return None
- with open(config_path) as stream:
- return json.load(stream)
-
def create_dir_if_necessary(pathname):
dirname = os.path.dirname(pathname)
if not os.path.exists(dirname):
@@ -98,4 +55,3 @@ def remove_file(filename, log=LOG, verbose=True):
log.warn("File does not exist: %s" % os.path.relpath(filename, BASE_DIR))
else:
os.remove(filename)
-
diff --git a/i18n/extract.py b/i18n/extract.py
index c3d4368a67..ffac9b6270 100755
--- a/i18n/extract.py
+++ b/i18n/extract.py
@@ -18,9 +18,8 @@ See https://edx-wiki.atlassian.net/wiki/display/ENG/PO+File+workflow
import os
from datetime import datetime
from polib import pofile
-from execute import execute, create_dir_if_necessary, remove_file, \
- BASE_DIR, LOCALE_DIR, SOURCE_MSGS_DIR, LOG
-
+from config import BASE_DIR, LOCALE_DIR, CONFIGURATION
+from execute import execute, create_dir_if_necessary, remove_file, LOG
# BABEL_CONFIG contains declarations for Babel to extract strings from mako template files
# Use relpath to reduce noise in logs
@@ -28,15 +27,19 @@ BABEL_CONFIG = os.path.relpath(LOCALE_DIR + '/babel.cfg', BASE_DIR)
# Strings from mako template files are written to BABEL_OUT
# Use relpath to reduce noise in logs
-BABEL_OUT = os.path.relpath(SOURCE_MSGS_DIR + '/mako.po', BASE_DIR)
+BABEL_OUT = os.path.relpath(CONFIGURATION.get_source_messages_dir() + '/mako.po', BASE_DIR)
+SOURCE_WARN = 'This English source file is machine-generated. Do not check it into github'
def main ():
create_dir_if_necessary(LOCALE_DIR)
- generated_files = ('django-partial.po', 'djangojs.po', 'mako.po')
+ source_msgs_dir = CONFIGURATION.get_source_messages_dir()
+ remove_file(os.path.join(source_msgs_dir, 'django.po'))
+ generated_files = ('django-partial.po', 'djangojs.po', 'mako.po')
for filename in generated_files:
- remove_file(os.path.join(SOURCE_MSGS_DIR, filename))
+ remove_file(os.path.join(source_msgs_dir, filename))
+
# Extract strings from mako templates
babel_mako_cmd = 'pybabel extract -F %s -c "TRANSLATORS:" . -o %s' % (BABEL_CONFIG, BABEL_OUT)
@@ -52,13 +55,13 @@ def main ():
execute(make_django_cmd, working_directory=BASE_DIR)
# makemessages creates 'django.po'. This filename is hardcoded.
# Rename it to django-partial.po to enable merging into django.po later.
- os.rename(os.path.join(SOURCE_MSGS_DIR, 'django.po'),
- os.path.join(SOURCE_MSGS_DIR, 'django-partial.po'))
+ os.rename(os.path.join(source_msgs_dir, 'django.po'),
+ os.path.join(source_msgs_dir, 'django-partial.po'))
execute(make_djangojs_cmd, working_directory=BASE_DIR)
for filename in generated_files:
LOG.info('Cleaning %s' % filename)
- po = pofile(os.path.join(SOURCE_MSGS_DIR, filename))
+ po = pofile(os.path.join(source_msgs_dir, filename))
# replace default headers with edX headers
fix_header(po)
# replace default metadata with edX metadata
@@ -82,8 +85,8 @@ def fix_header(po):
po.metadata_is_fuzzy = [] # remove [u'fuzzy']
header = po.header
fixes = (
- ('SOME DESCRIPTIVE TITLE', 'edX translation file'),
- ('Translations template for PROJECT.', 'edX translation file'),
+ ('SOME DESCRIPTIVE TITLE', 'edX translation file\n' + SOURCE_WARN),
+ ('Translations template for PROJECT.', 'edX translation file\n' + SOURCE_WARN),
('YEAR', '%s' % datetime.utcnow().year),
('ORGANIZATION', 'edX'),
("THE PACKAGE'S COPYRIGHT HOLDER", "EdX"),
diff --git a/i18n/generate.py b/i18n/generate.py
index 40a6cc88ca..e43efc268a 100755
--- a/i18n/generate.py
+++ b/i18n/generate.py
@@ -16,15 +16,15 @@
import os
from polib import pofile
-from execute import execute, get_config, messages_dir, remove_file, \
- BASE_DIR, LOG, SOURCE_LOCALE
+from config import BASE_DIR, CONFIGURATION
+from execute import execute, remove_file, LOG
def merge(locale, target='django.po'):
"""
For the given locale, merge django-partial.po, messages.po, mako.po -> django.po
"""
LOG.info('Merging locale={0}'.format(locale))
- locale_directory = messages_dir(locale)
+ locale_directory = CONFIGURATION.get_messages_dir(locale)
files_to_merge = ('django-partial.po', 'messages.po', 'mako.po')
validate_files(locale_directory, files_to_merge)
@@ -62,15 +62,8 @@ def validate_files(dir, files_to_merge):
raise Exception("File not found: {0}".format(pathname))
def main ():
- configuration = get_config()
- if configuration == None:
- LOG.warn('Configuration file not found, using only English.')
- locales = (SOURCE_LOCALE,)
- else:
- locales = configuration['locales']
- for locale in locales:
+ for locale in CONFIGURATION.get_locales():
merge(locale)
-
compile_cmd = 'django-admin.py compilemessages'
execute(compile_cmd, working_directory=BASE_DIR)
diff --git a/i18n/tests/__init__.py b/i18n/tests/__init__.py
index 88216df993..d8fce19df7 100644
--- a/i18n/tests/__init__.py
+++ b/i18n/tests/__init__.py
@@ -1,3 +1,4 @@
+from test_config import TestConfiguration
from test_extract import TestExtract
from test_generate import TestGenerate
from test_converter import TestConverter
diff --git a/i18n/tests/test_config.py b/i18n/tests/test_config.py
new file mode 100644
index 0000000000..aea8f0bca3
--- /dev/null
+++ b/i18n/tests/test_config.py
@@ -0,0 +1,33 @@
+import os
+from unittest import TestCase
+
+from config import Configuration, LOCALE_DIR, CONFIGURATION
+
+class TestConfiguration(TestCase):
+ """
+ Tests functionality of i18n/config.py
+ """
+
+ def test_config(self):
+ config_filename = os.path.normpath(os.path.join(LOCALE_DIR, 'config'))
+ config = Configuration(config_filename)
+ self.assertEqual(config.get_source_locale(), 'en')
+
+ def test_no_config(self):
+ config_filename = os.path.normpath(os.path.join(LOCALE_DIR, 'no_such_file'))
+ with self.assertRaises(Exception):
+ Configuration(config_filename)
+
+ def test_valid_configuration(self):
+ """
+ Make sure we have a valid configuration file,
+ and that it contains an 'en' locale.
+ Also check values of dummy_locale and source_locale.
+ """
+ self.assertIsNotNone(CONFIGURATION)
+ locales = CONFIGURATION.get_locales()
+ self.assertIsNotNone(locales)
+ self.assertIsInstance(locales, list)
+ self.assertIn('en', locales)
+ self.assertEqual('fr', CONFIGURATION.get_dummy_locale())
+ self.assertEqual('en', CONFIGURATION.get_source_locale())
diff --git a/i18n/tests/test_extract.py b/i18n/tests/test_extract.py
index b14ae9872d..a9faa2bdd8 100644
--- a/i18n/tests/test_extract.py
+++ b/i18n/tests/test_extract.py
@@ -4,7 +4,7 @@ from nose.plugins.skip import SkipTest
from datetime import datetime, timedelta
import extract
-from execute import SOURCE_MSGS_DIR
+from config import CONFIGURATION
# Make sure setup runs only once
SETUP_HAS_RUN = False
@@ -39,7 +39,7 @@ class TestExtract(TestCase):
Fails assertion if one of the files doesn't exist.
"""
for filename in self.generated_files:
- path = os.path.join(SOURCE_MSGS_DIR, filename)
+ path = os.path.join(CONFIGURATION.get_source_messages_dir(), filename)
exists = os.path.exists(path)
self.assertTrue(exists, msg='Missing file: %s' % filename)
if exists:
diff --git a/i18n/tests/test_generate.py b/i18n/tests/test_generate.py
index fc22988251..bac727f671 100644
--- a/i18n/tests/test_generate.py
+++ b/i18n/tests/test_generate.py
@@ -1,9 +1,10 @@
-import os, string, random
+import os, string, random, re
+from polib import pofile
from unittest import TestCase
from datetime import datetime, timedelta
import generate
-from execute import get_config, messages_dir, SOURCE_MSGS_DIR, SOURCE_LOCALE
+from config import CONFIGURATION
class TestGenerate(TestCase):
"""
@@ -12,29 +13,16 @@ class TestGenerate(TestCase):
generated_files = ('django-partial.po', 'djangojs.po', 'mako.po')
def setUp(self):
- self.configuration = get_config()
-
# Subtract 1 second to help comparisons with file-modify time succeed,
# since os.path.getmtime() is not millisecond-accurate
self.start_time = datetime.now() - timedelta(seconds=1)
- def test_configuration(self):
- """
- Make sure we have a valid configuration file,
- and that it contains an 'en' locale.
- """
- self.assertIsNotNone(self.configuration)
- locales = self.configuration['locales']
- self.assertIsNotNone(locales)
- self.assertIsInstance(locales, list)
- self.assertIn('en', locales)
-
def test_merge(self):
"""
Tests merge script on English source files.
"""
- filename = os.path.join(SOURCE_MSGS_DIR, random_name())
- generate.merge(SOURCE_LOCALE, target=filename)
+ filename = os.path.join(CONFIGURATION.get_source_messages_dir(), random_name())
+ generate.merge(CONFIGURATION.get_source_locale(), target=filename)
self.assertTrue(os.path.exists(filename))
os.remove(filename)
@@ -47,13 +35,35 @@ class TestGenerate(TestCase):
after start of test suite)
"""
generate.main()
- for locale in self.configuration['locales']:
- for filename in ('django.mo', 'djangojs.mo'):
- path = os.path.join(messages_dir(locale), filename)
+ for locale in CONFIGURATION.get_locales():
+ for filename in ('django', 'djangojs'):
+ mofile = filename+'.mo'
+ path = os.path.join(CONFIGURATION.get_messages_dir(locale), mofile)
exists = os.path.exists(path)
- self.assertTrue(exists, msg='Missing file in locale %s: %s' % (locale, filename))
+ self.assertTrue(exists, msg='Missing file in locale %s: %s' % (locale, mofile))
self.assertTrue(datetime.fromtimestamp(os.path.getmtime(path)) >= self.start_time,
msg='File not recently modified: %s' % path)
+ self.assert_merge_headers(locale)
+
+ def assert_merge_headers(self, locale):
+ """
+ This is invoked by test_main to ensure that it runs after
+ calling generate.main().
+
+ There should be exactly three merge comment headers
+ in our merged .po file. This counts them to be sure.
+ A merge comment looks like this:
+ # #-#-#-#-# django-partial.po (0.1a) #-#-#-#-#
+
+ """
+ path = os.path.join(CONFIGURATION.get_messages_dir(locale), 'django.po')
+ po = pofile(path)
+ pattern = re.compile('^#-#-#-#-#', re.M)
+ match = pattern.findall(po.header)
+ self.assertEqual(len(match), 3,
+ msg="Found %s (should be 3) merge comments in the header for %s" % \
+ (len(match), path))
+
def random_name(size=6):
"""Returns random filename as string, like test-4BZ81W"""
diff --git a/i18n/tests/test_validate.py b/i18n/tests/test_validate.py
index ed746db78f..64579fb563 100644
--- a/i18n/tests/test_validate.py
+++ b/i18n/tests/test_validate.py
@@ -2,7 +2,8 @@ import os
from unittest import TestCase
from nose.plugins.skip import SkipTest
-from execute import call, LOCALE_DIR, LOG
+from config import LOCALE_DIR
+from execute import call, LOG
class TestValidate(TestCase):
"""
diff --git a/i18n/transifex.py b/i18n/transifex.py
new file mode 100755
index 0000000000..9def339262
--- /dev/null
+++ b/i18n/transifex.py
@@ -0,0 +1,23 @@
+#!/usr/bin/python
+
+import sys
+from execute import execute
+
+def push():
+ execute('tx push -s')
+
+def pull():
+ execute('tx pull')
+
+
+if __name__ == '__main__':
+ if len(sys.argv)<2:
+ raise Exception("missing argument: push or pull")
+ arg = sys.argv[1]
+ if arg == 'push':
+ push()
+ elif arg == 'pull':
+ pull()
+ else:
+ raise Exception("unknown argument: (%s)" % arg)
+
diff --git a/rakefile b/rakefile
index 240847812e..acb9bf6ecc 100644
--- a/rakefile
+++ b/rakefile
@@ -551,14 +551,16 @@ namespace :i18n do
desc "Push source strings to Transifex for translation"
task :push do
if validate_transifex_config()
- sh("tx push -s")
+ cmd = File.join(REPO_ROOT, "i18n", "transifex.py")
+ sh("#{cmd} push")
end
end
desc "Pull translated strings from Transifex"
task :pull do
if validate_transifex_config()
- sh("tx pull")
+ cmd = File.join(REPO_ROOT, "i18n", "transifex.py")
+ sh("#{cmd} pull")
end
end
end
From bec2ca4086cb15079ee86f28b6a33ecdb1085283 Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Mon, 6 May 2013 14:05:05 -0400
Subject: [PATCH 014/245] cleanup headers of files pulled from transifex.
---
conf/locale/config | 2 +-
i18n/make_dummy.py | 23 +++++++++++------------
i18n/transifex.py | 47 +++++++++++++++++++++++++++++++++++++++++++++-
3 files changed, 58 insertions(+), 14 deletions(-)
diff --git a/conf/locale/config b/conf/locale/config
index 67252b1fa0..58f8da0513 100644
--- a/conf/locale/config
+++ b/conf/locale/config
@@ -1,4 +1,4 @@
{
- "locales" : ["en"],
+ "locales" : ["en", "es"],
"dummy-locale" : "fr"
}
diff --git a/i18n/make_dummy.py b/i18n/make_dummy.py
index 9c8c3289ce..8d0fb95ef2 100755
--- a/i18n/make_dummy.py
+++ b/i18n/make_dummy.py
@@ -2,6 +2,12 @@
# Generate test translation files from human-readable po files.
#
+# Dummy language is specified in configuration file (see config.py)
+# two letter language codes reference:
+# see http://www.loc.gov/standards/iso639-2/php/code_list.php
+#
+# Django will not localize in languages that django itself has not been
+# localized for. So we are using a well-known language (default='fr').
#
# po files can be generated with this:
# django-admin.py makemessages --all --extension html -l en
@@ -18,7 +24,8 @@
import os, sys
import polib
from dummy import Dummy
-from execute import get_logger, create_dir_if_necessary
+from config import CONFIGURATION
+from execute import create_dir_if_necessary
def main(file, locale):
"""
@@ -47,21 +54,13 @@ def new_filename(original_filename, new_locale):
msgs_dir,
orig_file))
-
-# Dummy language
-# two letter language codes reference:
-# see http://www.loc.gov/standards/iso639-2/php/code_list.php
-#
-# Django will not localize in languages that django itself has not been
-# localized for. So we are using a well-known language: 'fr'.
-
-DEFAULT_LOCALE = 'fr'
-
if __name__ == '__main__':
+ # required arg: file
if len(sys.argv)<2:
raise Exception("missing file argument")
+ # optional arg: locale
if len(sys.argv)<3:
- locale = DEFAULT_LOCALE
+ locale = CONFIGURATION.get_dummy_locale()
else:
locale = sys.argv[2]
main(sys.argv[1], locale)
diff --git a/i18n/transifex.py b/i18n/transifex.py
index 9def339262..812ecd666f 100755
--- a/i18n/transifex.py
+++ b/i18n/transifex.py
@@ -1,15 +1,60 @@
#!/usr/bin/python
-import sys
+import os, sys
+from polib import pofile
+from config import CONFIGURATION
+from extract import SOURCE_WARN
from execute import execute
+TRANSIFEX_HEADER = 'Translations in this file have been downloaded from %s'
+TRANSIFEX_URL = 'https://www.transifex.com/projects/p/edx-studio/'
+
def push():
execute('tx push -s')
def pull():
execute('tx pull')
+ clean_translated_locales()
+def clean_translated_locales():
+ """
+ Strips out the warning from all translated po files
+ about being an English source file.
+ """
+ for locale in CONFIGURATION.get_locales():
+ if locale != CONFIGURATION.get_source_locale():
+ clean_locale(locale)
+
+def clean_locale(locale):
+ """
+ Strips out the warning from all of a locale's translated po files
+ about being an English source file.
+ Iterates over machine-generated files.
+ """
+ dirname = CONFIGURATION.get_messages_dir(locale)
+ for filename in ('django-partial.po', 'djangojs.po', 'mako.po'):
+ clean_file(os.path.join(dirname, filename))
+
+def clean_file(file):
+ """
+ Strips out the warning from a translated po file about being an English source file.
+ Replaces warning with a note about coming from Transifex.
+ """
+ po = pofile(file)
+ if po.header.find(SOURCE_WARN) != -1:
+ new_header = get_new_header(po)
+ new = po.header.replace(SOURCE_WARN, new_header)
+ po.header = new
+ po.save()
+
+def get_new_header(po):
+ team = po.metadata.get('Language-Team', None)
+ if not team:
+ return TRANSIFEX_HEADER % TRANSIFEX_URL
+ else:
+ return TRANSIFEX_HEADER % team
+
if __name__ == '__main__':
if len(sys.argv)<2:
raise Exception("missing argument: push or pull")
From c5728fb036097d841608d5e13321460a92ff0496 Mon Sep 17 00:00:00 2001
From: Arthur Barrett
Date: Mon, 6 May 2013 14:48:51 -0400
Subject: [PATCH 015/245] fix undefined variable name error in mako template
---
lms/templates/static_htmlbook.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lms/templates/static_htmlbook.html b/lms/templates/static_htmlbook.html
index 2d65e6aae7..ae04f7343a 100644
--- a/lms/templates/static_htmlbook.html
+++ b/lms/templates/static_htmlbook.html
@@ -98,7 +98,7 @@
%if chapter is not None:
options.chapterNum = ${chapter};
%endif
- %if anchor_id is not None:
+ %if anchor_id is not UNDEFINED and anchor_id is not None:
options.anchor_id = ${anchor_id};
%endif
From ceefdf01615aad16af7e3bce28da8715aa4c3e40 Mon Sep 17 00:00:00 2001
From: Arthur Barrett
Date: Mon, 6 May 2013 15:52:32 -0400
Subject: [PATCH 016/245] user must be authenticated to view notes tab
---
lms/djangoapps/courseware/tabs.py | 10 ++++------
lms/envs/common.py | 1 +
2 files changed, 5 insertions(+), 6 deletions(-)
diff --git a/lms/djangoapps/courseware/tabs.py b/lms/djangoapps/courseware/tabs.py
index 81f2f10c80..0fc86704dd 100644
--- a/lms/djangoapps/courseware/tabs.py
+++ b/lms/djangoapps/courseware/tabs.py
@@ -185,10 +185,10 @@ def _combined_open_ended_grading(tab, user, course, active_page):
return tab
return []
-def _student_notes(tab, user, course, active_page):
- if settings.MITX_FEATURES.get('ENABLE_STUDENT_NOTES'):
+def _notes_tab(tab, user, course, active_page):
+ if user.is_authenticated() and settings.MITX_FEATURES.get('ENABLE_STUDENT_NOTES'):
link = reverse('notes', args=[course.id])
- return [CourseTab('My Notes', link, active_page == 'notes')]
+ return [CourseTab(tab['name'], link, active_page == 'notes')]
return []
#### Validators
@@ -232,7 +232,7 @@ VALID_TAB_TYPES = {
'peer_grading': TabImpl(null_validator, _peer_grading),
'staff_grading': TabImpl(null_validator, _staff_grading),
'open_ended': TabImpl(null_validator, _combined_open_ended_grading),
- 'notes': TabImpl(null_validator, _student_notes)
+ 'notes': TabImpl(null_validator, _notes_tab)
}
@@ -326,8 +326,6 @@ def get_default_tabs(user, course, active_page):
tabs.extend(_wiki({'name': 'Wiki', 'type': 'wiki'}, user, course, active_page))
- tabs.extend(_student_notes({'name': 'Notes', 'type': 'notes'}, user, course, active_page))
-
if user.is_authenticated() and not course.hide_progress_tab:
tabs.extend(_progress({'name': 'Progress'}, user, course, active_page))
diff --git a/lms/envs/common.py b/lms/envs/common.py
index e9e8007f74..876cff6a2e 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -92,6 +92,7 @@ MITX_FEATURES = {
# Staff Debug tool.
'ENABLE_STUDENT_HISTORY_VIEW': True,
+ # Enables the student notes API and UI.
'ENABLE_STUDENT_NOTES': True
}
From 0e1b3999e839723656095d4cb80834d6a46523dd Mon Sep 17 00:00:00 2001
From: Arthur Barrett
Date: Mon, 6 May 2013 17:01:17 -0400
Subject: [PATCH 017/245] add limit and offset params to search endpoint
---
lms/djangoapps/notes/api.py | 40 +++++++++++++++++++++++++++++--------
1 file changed, 32 insertions(+), 8 deletions(-)
diff --git a/lms/djangoapps/notes/api.py b/lms/djangoapps/notes/api.py
index fb52683a27..296ec2766f 100644
--- a/lms/djangoapps/notes/api.py
+++ b/lms/djangoapps/notes/api.py
@@ -1,3 +1,4 @@
+from django.contrib.auth.decorators import login_required
from django.http import HttpResponse, Http404
from django.core.exceptions import ValidationError
from notes.models import Note
@@ -6,6 +7,10 @@ import logging
log = logging.getLogger(__name__)
+API_SETTINGS = {
+ 'MAX_NOTE_LIMIT': 100 # Max number of annotations retrieved per set
+}
+
#----------------------------------------------------------------------#
# API requests are routed through api_request() using the resource map.
@@ -21,6 +26,7 @@ def api_resource_map():
'search': {GET: search}
}
+@login_required
def api_request(request, course_id, **kwargs):
''' Routes API requests to the appropriate action method and formats the results
(defaults to JSON).
@@ -54,8 +60,7 @@ def api_request(request, course_id, **kwargs):
response['Content-type'] = formatted[0]
response.content = formatted[1]
- log.debug("API response:")
- log.debug(response)
+ log.debug("API response: {0}".format(formatted))
return response
@@ -74,7 +79,9 @@ def api_format(request, response, data):
# Exposed API actions via the resource map.
def index(request, course_id):
- notes = Note.objects.filter(course_id=course_id, user=request.user)
+ MAX_LIMIT = API_SETTINGS.get('MAX_NOTE_LIMIT')
+ notes = Note.objects.order_by('id').filter(course_id=course_id,
+ user=request.user)[:MAX_LIMIT]
return [HttpResponse(), [note.as_dict() for note in notes]]
def create(request, course_id):
@@ -139,17 +146,34 @@ def delete(request, course_id, note_id):
return [HttpResponse('', status=204), None]
def search(request, course_id):
+ MAX_LIMIT = API_SETTINGS.get('MAX_NOTE_LIMIT')
+
+ # search parameters
limit = request.GET.get('limit')
+ offset = request.GET.get('offset')
uri = request.GET.get('uri')
- filters = {'course_id':course_id, 'user':request.user}
+ # validate search parameters
+ if limit is not None and limit.isdigit():
+ limit = int(limit)
+ if limit == 0 or limit > MAX_LIMIT:
+ limit = MAX_LIMIT
+ else:
+ limit = MAX_LIMIT
+
+ if offset is not None and offset.isdigit():
+ offset = int(offset)
+ else:
+ offset = 0
+
+ # search filters
+ filters = {'course_id': course_id, 'user': request.user}
if uri is not None:
filters['uri'] = uri
- notes = Note.objects.filter(**filters)
- #if limit is not None and limit > 0:
- #notes = notes[:limit]
-
+ start = offset
+ end = offset + limit
+ notes = Note.objects.order_by('id').filter(**filters)[start:end]
result = {'rows': [note.as_dict() for note in notes]}
return [HttpResponse(), result]
From 45cca10fb94e119dc18bac77a6558f06e09b68b0 Mon Sep 17 00:00:00 2001
From: Arthur Barrett
Date: Mon, 6 May 2013 17:01:35 -0400
Subject: [PATCH 018/245] remove debugging from default notes view
---
lms/djangoapps/notes/views.py | 12 ++++--------
1 file changed, 4 insertions(+), 8 deletions(-)
diff --git a/lms/djangoapps/notes/views.py b/lms/djangoapps/notes/views.py
index 7d6390baed..865e894ac1 100644
--- a/lms/djangoapps/notes/views.py
+++ b/lms/djangoapps/notes/views.py
@@ -1,20 +1,16 @@
+from django.contrib.auth.decorators import login_required
from mitxmako.shortcuts import render_to_response
from courseware.courses import get_course_with_access
from notes.models import Note
import json
-import logging
-
-log = logging.getLogger(__name__)
+@login_required
def notes(request, course_id):
''' Displays a student's notes in a course. '''
course = get_course_with_access(request.user, course_id, 'load')
-
+
notes = Note.objects.filter(course_id=course_id, user=request.user).order_by('-created', 'uri')
-
- prettyprint = {'sort_keys':True, 'indent':2, 'separators':(',', ': ')}
- json_notes = json.dumps([n.as_dict() for n in notes], **prettyprint)
-
+ json_notes = json.dumps([n.as_dict() for n in notes])
context = {
'course': course,
'notes': notes,
From 4070e019e44102fcb2211393929a0453078c859b Mon Sep 17 00:00:00 2001
From: Arthur Barrett
Date: Mon, 6 May 2013 19:29:44 -0400
Subject: [PATCH 019/245] modified the html static book to only enable
annotator.js when notes are enabled for the course
---
lms/djangoapps/notes/api.py | 7 +++++++
lms/djangoapps/notes/utils.py | 5 +++++
lms/djangoapps/notes/views.py | 7 ++++++-
lms/djangoapps/staticbook/views.py | 5 ++++-
lms/templates/static_htmlbook.html | 18 +++++++++++++-----
5 files changed, 35 insertions(+), 7 deletions(-)
create mode 100644 lms/djangoapps/notes/utils.py
diff --git a/lms/djangoapps/notes/api.py b/lms/djangoapps/notes/api.py
index 296ec2766f..bf7b1005ad 100644
--- a/lms/djangoapps/notes/api.py
+++ b/lms/djangoapps/notes/api.py
@@ -2,6 +2,8 @@ from django.contrib.auth.decorators import login_required
from django.http import HttpResponse, Http404
from django.core.exceptions import ValidationError
from notes.models import Note
+from notes.utils import notes_enabled_for_course
+from courseware.courses import get_course_with_access
import json
import logging
@@ -34,6 +36,11 @@ def api_request(request, course_id, **kwargs):
Raises a 404 if the resource type doesn't exist, or if there is no action
method associated with the HTTP method.
'''
+ course = get_course_with_access(request.user, course_id, 'load')
+ if not notes_enabled_for_course(course):
+ log.debug('Notes not enabled for course')
+ raise Http404
+
resource_map = api_resource_map()
resource_name = kwargs.pop('resource')
resource = resource_map.get(resource_name)
diff --git a/lms/djangoapps/notes/utils.py b/lms/djangoapps/notes/utils.py
new file mode 100644
index 0000000000..e06df3e42a
--- /dev/null
+++ b/lms/djangoapps/notes/utils.py
@@ -0,0 +1,5 @@
+# TODO: make a separate policy setting to enable/disable notes.
+def notes_enabled_for_course(course):
+ ''' Returns True if notes are enabled for the course, False otherwise. '''
+ notes_tab_type = 'notes'
+ return next((True for tab in course.tabs if tab['type'] == notes_tab_type), False)
diff --git a/lms/djangoapps/notes/views.py b/lms/djangoapps/notes/views.py
index 865e894ac1..ba47540071 100644
--- a/lms/djangoapps/notes/views.py
+++ b/lms/djangoapps/notes/views.py
@@ -1,13 +1,18 @@
from django.contrib.auth.decorators import login_required
+from django.http import Http404
from mitxmako.shortcuts import render_to_response
from courseware.courses import get_course_with_access
from notes.models import Note
+from notes.utils import notes_enabled_for_course
import json
@login_required
def notes(request, course_id):
- ''' Displays a student's notes in a course. '''
+ ''' Displays the student's notes. '''
+
course = get_course_with_access(request.user, course_id, 'load')
+ if not notes_enabled_for_course(course):
+ raise Http404
notes = Note.objects.filter(course_id=course_id, user=request.user).order_by('-created', 'uri')
json_notes = json.dumps([n.as_dict() for n in notes])
diff --git a/lms/djangoapps/staticbook/views.py b/lms/djangoapps/staticbook/views.py
index 04433f0e0e..6d3dcbd5ca 100644
--- a/lms/djangoapps/staticbook/views.py
+++ b/lms/djangoapps/staticbook/views.py
@@ -5,6 +5,7 @@ from mitxmako.shortcuts import render_to_response
from courseware.access import has_access
from courseware.courses import get_course_with_access
+from notes.utils import notes_enabled_for_course
from static_replace import replace_static_urls
@@ -102,6 +103,7 @@ def html_index(request, course_id, book_index, chapter=None):
"""
course = get_course_with_access(request.user, course_id, 'load')
staff_access = has_access(request.user, course, 'staff')
+ notes_enabled = notes_enabled_for_course(course)
book_index = int(book_index)
if book_index < 0 or book_index >= len(course.html_textbooks):
@@ -130,4 +132,5 @@ def html_index(request, course_id, book_index, chapter=None):
'course': course,
'textbook': textbook,
'chapter': chapter,
- 'staff_access': staff_access})
+ 'staff_access': staff_access,
+ 'notes_enabled': notes_enabled})
diff --git a/lms/templates/static_htmlbook.html b/lms/templates/static_htmlbook.html
index ae04f7343a..8a3c50f680 100644
--- a/lms/templates/static_htmlbook.html
+++ b/lms/templates/static_htmlbook.html
@@ -31,11 +31,14 @@
anchorToLoad = options.anchor_id;
}
- var onComplete = function(url) {
- return function() {
- $('#viewerContainer').trigger('notes:init', [url]);
- }
- };
+ var onComplete = function() {};
+ if(options.notesEnabled) {
+ onComplete = function(url) {
+ return function() {
+ $('#viewerContainer').trigger('notes:init', [url]);
+ }
+ };
+ }
loadUrl = function htmlViewLoadUrl(url, anchorId) {
// clear out previous load, if any:
@@ -102,6 +105,11 @@
options.anchor_id = ${anchor_id};
%endif
+ options.notesEnabled = false;
+ %if notes_enabled is not UNDEFINED and notes_enabled:
+ options.notesEnabled = true;
+ %endif
+
$('#outerContainer').myHTMLViewer(options);
});
From ade6d4085dc92479c017123aae3ba8317e5e9f2f Mon Sep 17 00:00:00 2001
From: Arthur Barrett
Date: Mon, 6 May 2013 19:35:41 -0400
Subject: [PATCH 020/245] turn off notes js debugging
---
lms/static/coffee/src/notes.coffee | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/lms/static/coffee/src/notes.coffee b/lms/static/coffee/src/notes.coffee
index 46699b8466..e13707256e 100644
--- a/lms/static/coffee/src/notes.coffee
+++ b/lms/static/coffee/src/notes.coffee
@@ -1,5 +1,5 @@
class StudentNotes
- _debug: true
+ _debug: false
targets: [] # holds elements with annotator() instances
@@ -70,4 +70,4 @@ class StudentNotes
#
# Comment this line to disable notes.
-$(document).ready ($) -> new StudentNotes $, @
\ No newline at end of file
+$(document).ready ($) -> new StudentNotes $, @
From 4620f50fb151e7a235bc119a92d4adec794a871d Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Tue, 7 May 2013 13:24:38 -0400
Subject: [PATCH 021/245] addressed Cale's comments; switched to path.py paths
---
i18n/config.py | 34 +++++++++++++++++------------
i18n/execute.py | 2 +-
i18n/extract.py | 16 +++++++-------
i18n/generate.py | 34 ++++++++++++++++++-----------
i18n/tests/__init__.py | 3 +--
i18n/tests/test_config.py | 8 +++----
i18n/tests/test_extract.py | 2 +-
i18n/tests/test_generate.py | 6 +++---
i18n/tests/test_validate.py | 43 +++++++++++++++++--------------------
i18n/transifex.py | 10 +++++----
requirements.txt | 1 +
11 files changed, 87 insertions(+), 72 deletions(-)
diff --git a/i18n/config.py b/i18n/config.py
index f0d8e366d0..461b0dfd15 100644
--- a/i18n/config.py
+++ b/i18n/config.py
@@ -1,12 +1,14 @@
import os, json
+from path import path
# BASE_DIR is the working directory to execute django-admin commands from.
# Typically this should be the 'mitx' directory.
-BASE_DIR = os.path.normpath(os.path.dirname(os.path.abspath(__file__))+'/..')
+#BASE_DIR = os.path.normpath(os.path.dirname(os.path.abspath(__file__))+'/..')
+BASE_DIR = path(__file__).abspath().dirname().joinpath('..').normpath()
# LOCALE_DIR contains the locale files.
# Typically this should be 'mitx/conf/locale'
-LOCALE_DIR = os.path.join(BASE_DIR, 'conf', 'locale')
+LOCALE_DIR = BASE_DIR.joinpath('conf', 'locale')
class Configuration:
"""
@@ -16,10 +18,10 @@ class Configuration:
_source_locale = 'en'
def __init__(self, filename):
- self.filename = filename
- self.config = self.get_config(self.filename)
+ self._filename = filename
+ self._config = self.read_config(filename)
- def get_config(self, filename):
+ def read_config(self, filename):
"""
Returns data found in config file (as dict), or raises exception if file not found
"""
@@ -28,28 +30,31 @@ class Configuration:
with open(filename) as stream:
return json.load(stream)
- def get_locales(self):
+ @property
+ def locales(self):
"""
Returns a list of locales declared in the configuration file,
e.g. ['en', 'fr', 'es']
Each locale is a string.
"""
- return self.config['locales']
+ return self._config['locales']
- def get_source_locale(self):
+ @property
+ def source_locale(self):
"""
Returns source language.
Source language is English.
"""
return self._source_locale
- def get_dummy_locale(self):
+ @property
+ def dummy_locale(self):
"""
Returns a locale to use for the dummy text, e.g. 'fr'.
Throws exception if no dummy-locale is declared.
The locale is a string.
"""
- dummy = self.config.get('dummy-locale', None)
+ dummy = self._config.get('dummy-locale', None)
if not dummy:
raise Exception('Could not read dummy-locale from configuration file.')
return dummy
@@ -59,15 +64,16 @@ class Configuration:
Returns the name of the directory holding the po files for locale.
Example: mitx/conf/locale/fr/LC_MESSAGES
"""
- return os.path.join(LOCALE_DIR, locale, 'LC_MESSAGES')
+ return LOCALE_DIR.joinpath(locale, 'LC_MESSAGES')
- def get_source_messages_dir(self):
+ @property
+ def source_messages_dir(self):
"""
Returns the name of the directory holding the source-language po files (English).
Example: mitx/conf/locale/en/LC_MESSAGES
"""
- return self.get_messages_dir(self.get_source_locale())
+ return self.get_messages_dir(self.source_locale)
-CONFIGURATION = Configuration(os.path.normpath(os.path.join(LOCALE_DIR, 'config')))
+CONFIGURATION = Configuration(LOCALE_DIR.joinpath('config').normpath())
diff --git a/i18n/execute.py b/i18n/execute.py
index 4c47680101..6a7e12ac5d 100644
--- a/i18n/execute.py
+++ b/i18n/execute.py
@@ -14,7 +14,7 @@ def get_default_logger():
LOG = get_default_logger()
-def execute (command, working_directory=BASE_DIR, log=LOG):
+def execute(command, working_directory=BASE_DIR, log=LOG):
"""
Executes shell command in a given working_directory.
Command is a string to pass to the shell.
diff --git a/i18n/extract.py b/i18n/extract.py
index ffac9b6270..57da0bd76d 100755
--- a/i18n/extract.py
+++ b/i18n/extract.py
@@ -23,22 +23,22 @@ from execute import execute, create_dir_if_necessary, remove_file, LOG
# BABEL_CONFIG contains declarations for Babel to extract strings from mako template files
# Use relpath to reduce noise in logs
-BABEL_CONFIG = os.path.relpath(LOCALE_DIR + '/babel.cfg', BASE_DIR)
+BABEL_CONFIG = BASE_DIR.relpathto(LOCALE_DIR.joinpath('babel.cfg'))
# Strings from mako template files are written to BABEL_OUT
# Use relpath to reduce noise in logs
-BABEL_OUT = os.path.relpath(CONFIGURATION.get_source_messages_dir() + '/mako.po', BASE_DIR)
+BABEL_OUT = BASE_DIR.relpathto(CONFIGURATION.source_messages_dir.joinpath('mako.po'))
SOURCE_WARN = 'This English source file is machine-generated. Do not check it into github'
def main ():
create_dir_if_necessary(LOCALE_DIR)
- source_msgs_dir = CONFIGURATION.get_source_messages_dir()
+ source_msgs_dir = CONFIGURATION.source_messages_dir
- remove_file(os.path.join(source_msgs_dir, 'django.po'))
+ remove_file(source_msgs_dir.joinpath('django.po'))
generated_files = ('django-partial.po', 'djangojs.po', 'mako.po')
for filename in generated_files:
- remove_file(os.path.join(source_msgs_dir, filename))
+ remove_file(source_msgs_dir.joinpath(filename))
# Extract strings from mako templates
@@ -55,13 +55,13 @@ def main ():
execute(make_django_cmd, working_directory=BASE_DIR)
# makemessages creates 'django.po'. This filename is hardcoded.
# Rename it to django-partial.po to enable merging into django.po later.
- os.rename(os.path.join(source_msgs_dir, 'django.po'),
- os.path.join(source_msgs_dir, 'django-partial.po'))
+ os.rename(source_msgs_dir.joinpath('django.po'),
+ source_msgs_dir.joinpath('django-partial.po'))
execute(make_djangojs_cmd, working_directory=BASE_DIR)
for filename in generated_files:
LOG.info('Cleaning %s' % filename)
- po = pofile(os.path.join(source_msgs_dir, filename))
+ po = pofile(source_msgs_dir.joinpath(filename))
# replace default headers with edX headers
fix_header(po)
# replace default metadata with edX metadata
diff --git a/i18n/generate.py b/i18n/generate.py
index e43efc268a..0c7179b2c6 100755
--- a/i18n/generate.py
+++ b/i18n/generate.py
@@ -19,25 +19,35 @@ from polib import pofile
from config import BASE_DIR, CONFIGURATION
from execute import execute, remove_file, LOG
-def merge(locale, target='django.po'):
+def merge(locale, target='django.po', fail_if_missing=True):
"""
For the given locale, merge django-partial.po, messages.po, mako.po -> django.po
+ target is the resulting filename
+ If fail_if_missing is True, and the files to be merged are missing,
+ throw an Exception.
+ If fail_if_missing is False, and the files to be merged are missing,
+ just return silently.
"""
LOG.info('Merging locale={0}'.format(locale))
locale_directory = CONFIGURATION.get_messages_dir(locale)
files_to_merge = ('django-partial.po', 'messages.po', 'mako.po')
- validate_files(locale_directory, files_to_merge)
+ try:
+ validate_files(locale_directory, files_to_merge)
+ except Exception, e:
+ if not fail_if_missing:
+ return
+ raise e
# merged file is merged.po
merge_cmd = 'msgcat -o merged.po ' + ' '.join(files_to_merge)
execute(merge_cmd, working_directory=locale_directory)
# clean up redunancies in the metadata
- merged_filename = os.path.join(locale_directory, 'merged.po')
+ merged_filename = locale_directory.joinpath('merged.po')
clean_metadata(merged_filename)
# rename merged.po -> django.po (default)
- django_filename = os.path.join(locale_directory, target)
+ django_filename = locale_directory.joinpath(target)
os.rename(merged_filename, django_filename) # can't overwrite file on Windows
def clean_metadata(file):
@@ -45,25 +55,25 @@ def clean_metadata(file):
Clean up redundancies in the metadata caused by merging.
This reads in a PO file and simply saves it back out again.
"""
- po = pofile(file)
- po.save()
-
+ pofile(file).save()
def validate_files(dir, files_to_merge):
"""
Asserts that the given files exist.
files_to_merge is a list of file names (no directories).
- dir is the directory in which the files should appear.
+ dir is the directory (a path object from path.py) in which the files should appear.
raises an Exception if any of the files are not in dir.
"""
for path in files_to_merge:
- pathname = os.path.join(dir, path)
- if not os.path.exists(pathname):
- raise Exception("File not found: {0}".format(pathname))
+ pathname = dir.joinpath(path)
+ if not pathname.exists():
+ raise Exception("I18N: Cannot generate because file not found: {0}".format(pathname))
def main ():
- for locale in CONFIGURATION.get_locales():
+ for locale in CONFIGURATION.locales:
merge(locale)
+ # Dummy text is not required. Don't raise exception if files are missing.
+ merge(CONFIGURATION.dummy_locale, fail_if_missing=False)
compile_cmd = 'django-admin.py compilemessages'
execute(compile_cmd, working_directory=BASE_DIR)
diff --git a/i18n/tests/__init__.py b/i18n/tests/__init__.py
index d8fce19df7..ee6283376e 100644
--- a/i18n/tests/__init__.py
+++ b/i18n/tests/__init__.py
@@ -3,5 +3,4 @@ from test_extract import TestExtract
from test_generate import TestGenerate
from test_converter import TestConverter
from test_dummy import TestDummy
-from test_validate import TestValidate
-
+import test_validate
diff --git a/i18n/tests/test_config.py b/i18n/tests/test_config.py
index aea8f0bca3..bcec6ac354 100644
--- a/i18n/tests/test_config.py
+++ b/i18n/tests/test_config.py
@@ -11,7 +11,7 @@ class TestConfiguration(TestCase):
def test_config(self):
config_filename = os.path.normpath(os.path.join(LOCALE_DIR, 'config'))
config = Configuration(config_filename)
- self.assertEqual(config.get_source_locale(), 'en')
+ self.assertEqual(config.source_locale, 'en')
def test_no_config(self):
config_filename = os.path.normpath(os.path.join(LOCALE_DIR, 'no_such_file'))
@@ -25,9 +25,9 @@ class TestConfiguration(TestCase):
Also check values of dummy_locale and source_locale.
"""
self.assertIsNotNone(CONFIGURATION)
- locales = CONFIGURATION.get_locales()
+ locales = CONFIGURATION.locales
self.assertIsNotNone(locales)
self.assertIsInstance(locales, list)
self.assertIn('en', locales)
- self.assertEqual('fr', CONFIGURATION.get_dummy_locale())
- self.assertEqual('en', CONFIGURATION.get_source_locale())
+ self.assertEqual('fr', CONFIGURATION.dummy_locale)
+ self.assertEqual('en', CONFIGURATION.source_locale)
diff --git a/i18n/tests/test_extract.py b/i18n/tests/test_extract.py
index a9faa2bdd8..7e8b1a9d2b 100644
--- a/i18n/tests/test_extract.py
+++ b/i18n/tests/test_extract.py
@@ -39,7 +39,7 @@ class TestExtract(TestCase):
Fails assertion if one of the files doesn't exist.
"""
for filename in self.generated_files:
- path = os.path.join(CONFIGURATION.get_source_messages_dir(), filename)
+ path = os.path.join(CONFIGURATION.source_messages_dir, filename)
exists = os.path.exists(path)
self.assertTrue(exists, msg='Missing file: %s' % filename)
if exists:
diff --git a/i18n/tests/test_generate.py b/i18n/tests/test_generate.py
index bac727f671..468858664f 100644
--- a/i18n/tests/test_generate.py
+++ b/i18n/tests/test_generate.py
@@ -21,8 +21,8 @@ class TestGenerate(TestCase):
"""
Tests merge script on English source files.
"""
- filename = os.path.join(CONFIGURATION.get_source_messages_dir(), random_name())
- generate.merge(CONFIGURATION.get_source_locale(), target=filename)
+ filename = os.path.join(CONFIGURATION.source_messages_dir, random_name())
+ generate.merge(CONFIGURATION.source_locale, target=filename)
self.assertTrue(os.path.exists(filename))
os.remove(filename)
@@ -35,7 +35,7 @@ class TestGenerate(TestCase):
after start of test suite)
"""
generate.main()
- for locale in CONFIGURATION.get_locales():
+ for locale in CONFIGURATION.locales:
for filename in ('django', 'djangojs'):
mofile = filename+'.mo'
path = os.path.join(CONFIGURATION.get_messages_dir(locale), mofile)
diff --git a/i18n/tests/test_validate.py b/i18n/tests/test_validate.py
index 64579fb563..7f0cdd7a25 100644
--- a/i18n/tests/test_validate.py
+++ b/i18n/tests/test_validate.py
@@ -4,31 +4,28 @@ from nose.plugins.skip import SkipTest
from config import LOCALE_DIR
from execute import call, LOG
+
+def test_po_files():
+ """
+ This is a generator. It yields all of the .po files under root, and tests each one.
+ """
+ for (dirpath, dirnames, filenames) in os.walk(LOCALE_DIR):
+ for name in filenames:
+ print name
+ (base, ext) = os.path.splitext(name)
+ if ext.lower() == '.po':
+ yield validate_po_file, os.path.join(dirpath, name)
-class TestValidate(TestCase):
+
+def validate_po_file(filename):
"""
Call GNU msgfmt -c on each .po file to validate its format.
"""
-
- def test_validate(self):
- # Skip this test for now because it's very noisy
- raise SkipTest()
- for file in self.get_po_files():
- # Use relative paths to make output less noisy.
- rfile = os.path.relpath(file, LOCALE_DIR)
- (out, err) = call(['msgfmt','-c', rfile], log=None, working_directory=LOCALE_DIR)
- if err != '':
- LOG.warn('\n'+err)
+ # Skip this test for now because it's very noisy
+ raise SkipTest()
+ # Use relative paths to make output less noisy.
+ rfile = os.path.relpath(filename, LOCALE_DIR)
+ (out, err) = call(['msgfmt','-c', rfile], log=None, working_directory=LOCALE_DIR)
+ if err != '':
+ LOG.warn('\n'+err)
- def get_po_files(self, root=LOCALE_DIR):
- """
- This is a generator. It yields all of the .po files under root.
- """
- for (dirpath, dirnames, filenames) in os.walk(root):
- for name in filenames:
- (base, ext) = os.path.splitext(name)
- if ext.lower() == '.po':
- yield os.path.join(dirpath, name)
-
-
-
diff --git a/i18n/transifex.py b/i18n/transifex.py
index 812ecd666f..d08a77b1c0 100755
--- a/i18n/transifex.py
+++ b/i18n/transifex.py
@@ -13,7 +13,9 @@ def push():
execute('tx push -s')
def pull():
- execute('tx pull')
+ for locale in CONFIGURATION.locales:
+ if locale != CONFIGURATION.source_locale:
+ execute('tx pull -l %s' % locale)
clean_translated_locales()
@@ -22,8 +24,8 @@ def clean_translated_locales():
Strips out the warning from all translated po files
about being an English source file.
"""
- for locale in CONFIGURATION.get_locales():
- if locale != CONFIGURATION.get_source_locale():
+ for locale in CONFIGURATION.locales:
+ if locale != CONFIGURATION.source_locale:
clean_locale(locale)
def clean_locale(locale):
@@ -34,7 +36,7 @@ def clean_locale(locale):
"""
dirname = CONFIGURATION.get_messages_dir(locale)
for filename in ('django-partial.po', 'djangojs.po', 'mako.po'):
- clean_file(os.path.join(dirname, filename))
+ clean_file(dirname.joinpath(filename))
def clean_file(file):
"""
diff --git a/requirements.txt b/requirements.txt
index d3fdd46b81..c6ee47becb 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -33,6 +33,7 @@ paramiko==1.9.0
path.py==3.0.1
Pillow==1.7.8
pip
+polib==1.0.3
pygments==1.5
pygraphviz==1.1
pymongo==2.4.1
From cf8d6b89657295ed55fc7ca8a70bf3a7de3db17f Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Tue, 7 May 2013 14:10:59 -0400
Subject: [PATCH 022/245] factor logging out of library calls
---
i18n/config.py | 1 -
i18n/execute.py | 17 ++++-------------
i18n/extract.py | 7 +++++--
i18n/generate.py | 12 ++++++++----
i18n/logger.py | 13 +++++++++++++
i18n/tests/test_validate.py | 15 +++++++++------
6 files changed, 39 insertions(+), 26 deletions(-)
create mode 100644 i18n/logger.py
diff --git a/i18n/config.py b/i18n/config.py
index 461b0dfd15..d78fc0ca45 100644
--- a/i18n/config.py
+++ b/i18n/config.py
@@ -76,4 +76,3 @@ class Configuration:
CONFIGURATION = Configuration(LOCALE_DIR.joinpath('config').normpath())
-
diff --git a/i18n/execute.py b/i18n/execute.py
index 6a7e12ac5d..e3f3478d12 100644
--- a/i18n/execute.py
+++ b/i18n/execute.py
@@ -1,18 +1,9 @@
-import os, subprocess, logging
+import os, subprocess
+
+from logger import get_logger
from config import CONFIGURATION, BASE_DIR
-
-def get_default_logger():
- """Returns a default logger"""
- log = logging.getLogger(__name__)
- log.setLevel(logging.INFO)
- log_handler = logging.StreamHandler()
- log_handler.setFormatter(logging.Formatter('%(asctime)s [%(levelname)s] %(message)s'))
- log.addHandler(log_handler)
- return log
-
-LOG = get_default_logger()
-
+LOG = get_logger(__name__)
def execute(command, working_directory=BASE_DIR, log=LOG):
"""
diff --git a/i18n/extract.py b/i18n/extract.py
index 57da0bd76d..9b0ad3829c 100755
--- a/i18n/extract.py
+++ b/i18n/extract.py
@@ -18,8 +18,10 @@ See https://edx-wiki.atlassian.net/wiki/display/ENG/PO+File+workflow
import os
from datetime import datetime
from polib import pofile
+
+from logger import get_logger
from config import BASE_DIR, LOCALE_DIR, CONFIGURATION
-from execute import execute, create_dir_if_necessary, remove_file, LOG
+from execute import execute, create_dir_if_necessary, remove_file
# BABEL_CONFIG contains declarations for Babel to extract strings from mako template files
# Use relpath to reduce noise in logs
@@ -32,6 +34,7 @@ BABEL_OUT = BASE_DIR.relpathto(CONFIGURATION.source_messages_dir.joinpath('mako.
SOURCE_WARN = 'This English source file is machine-generated. Do not check it into github'
def main ():
+ log = get_logger(__name__)
create_dir_if_necessary(LOCALE_DIR)
source_msgs_dir = CONFIGURATION.source_messages_dir
@@ -60,7 +63,7 @@ def main ():
execute(make_djangojs_cmd, working_directory=BASE_DIR)
for filename in generated_files:
- LOG.info('Cleaning %s' % filename)
+ log.info('Cleaning %s' % filename)
po = pofile(source_msgs_dir.joinpath(filename))
# replace default headers with edX headers
fix_header(po)
diff --git a/i18n/generate.py b/i18n/generate.py
index 0c7179b2c6..ffc88b64d0 100755
--- a/i18n/generate.py
+++ b/i18n/generate.py
@@ -16,10 +16,11 @@
import os
from polib import pofile
+from logger import get_logger
from config import BASE_DIR, CONFIGURATION
-from execute import execute, remove_file, LOG
+from execute import execute, remove_file
-def merge(locale, target='django.po', fail_if_missing=True):
+def merge(locale, target='django.po', fail_if_missing=True, log=None):
"""
For the given locale, merge django-partial.po, messages.po, mako.po -> django.po
target is the resulting filename
@@ -28,7 +29,8 @@ def merge(locale, target='django.po', fail_if_missing=True):
If fail_if_missing is False, and the files to be merged are missing,
just return silently.
"""
- LOG.info('Merging locale={0}'.format(locale))
+ if log:
+ log.info('Merging locale={0}'.format(locale))
locale_directory = CONFIGURATION.get_messages_dir(locale)
files_to_merge = ('django-partial.po', 'messages.po', 'mako.po')
try:
@@ -70,10 +72,12 @@ def validate_files(dir, files_to_merge):
raise Exception("I18N: Cannot generate because file not found: {0}".format(pathname))
def main ():
+ log = get_logger(__name__)
+
for locale in CONFIGURATION.locales:
merge(locale)
# Dummy text is not required. Don't raise exception if files are missing.
- merge(CONFIGURATION.dummy_locale, fail_if_missing=False)
+ merge(CONFIGURATION.dummy_locale, fail_if_missing=False, log=log)
compile_cmd = 'django-admin.py compilemessages'
execute(compile_cmd, working_directory=BASE_DIR)
diff --git a/i18n/logger.py b/i18n/logger.py
new file mode 100644
index 0000000000..20d767a032
--- /dev/null
+++ b/i18n/logger.py
@@ -0,0 +1,13 @@
+import logging
+
+def get_logger(name):
+ """
+ Returns a default logger.
+ logging.basicConfig does not render to the console
+ """
+ log = logging.getLogger()
+ log.setLevel(logging.INFO)
+ log_handler = logging.StreamHandler()
+ log_handler.setFormatter(logging.Formatter('%(asctime)s [%(levelname)s] %(message)s'))
+ log.addHandler(log_handler)
+ return log
diff --git a/i18n/tests/test_validate.py b/i18n/tests/test_validate.py
index 7f0cdd7a25..6bb7164a50 100644
--- a/i18n/tests/test_validate.py
+++ b/i18n/tests/test_validate.py
@@ -2,24 +2,27 @@ import os
from unittest import TestCase
from nose.plugins.skip import SkipTest
+from logger import get_logger
from config import LOCALE_DIR
-from execute import call, LOG
+from execute import call
-def test_po_files():
+def test_po_files(root=LOCALE_DIR):
"""
This is a generator. It yields all of the .po files under root, and tests each one.
"""
- for (dirpath, dirnames, filenames) in os.walk(LOCALE_DIR):
+ log = get_logger(__name__)
+ for (dirpath, dirnames, filenames) in os.walk(root):
for name in filenames:
print name
(base, ext) = os.path.splitext(name)
if ext.lower() == '.po':
- yield validate_po_file, os.path.join(dirpath, name)
+ yield validate_po_file, os.path.join(dirpath, name), log
-def validate_po_file(filename):
+def validate_po_file(filename, log):
"""
Call GNU msgfmt -c on each .po file to validate its format.
+ Any errors caught by msgfmt are logged to log.
"""
# Skip this test for now because it's very noisy
raise SkipTest()
@@ -27,5 +30,5 @@ def validate_po_file(filename):
rfile = os.path.relpath(filename, LOCALE_DIR)
(out, err) = call(['msgfmt','-c', rfile], log=None, working_directory=LOCALE_DIR)
if err != '':
- LOG.warn('\n'+err)
+ log.warn('\n'+err)
From 129c02f0b296dc5863eb7708cd67bd77ebd0124f Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Tue, 7 May 2013 14:12:58 -0400
Subject: [PATCH 023/245] forgot to remove stale comment
---
i18n/config.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/i18n/config.py b/i18n/config.py
index d78fc0ca45..4f246ed942 100644
--- a/i18n/config.py
+++ b/i18n/config.py
@@ -3,7 +3,6 @@ from path import path
# BASE_DIR is the working directory to execute django-admin commands from.
# Typically this should be the 'mitx' directory.
-#BASE_DIR = os.path.normpath(os.path.dirname(os.path.abspath(__file__))+'/..')
BASE_DIR = path(__file__).abspath().dirname().joinpath('..').normpath()
# LOCALE_DIR contains the locale files.
From 0ed07779c8ea9456baece621b9c8121e7c8028bc Mon Sep 17 00:00:00 2001
From: Arthur Barrett
Date: Tue, 7 May 2013 17:27:06 -0400
Subject: [PATCH 024/245] kludged in the note tab panel so it can be turned on
and off using the policy editor by adding "notes" to "advanced_modules."
---
cms/djangoapps/contentstore/utils.py | 24 ++++++++-----
cms/djangoapps/contentstore/views.py | 54 +++++++++++++++++-----------
2 files changed, 49 insertions(+), 29 deletions(-)
diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py
index a5a3b47bce..ea3e3ecd6a 100644
--- a/cms/djangoapps/contentstore/utils.py
+++ b/cms/djangoapps/contentstore/utils.py
@@ -9,6 +9,8 @@ DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_ta
#In order to instantiate an open ended tab automatically, need to have this data
OPEN_ENDED_PANEL = {"name": "Open Ended Panel", "type": "open_ended"}
+NOTES_PANEL = {"name": "My Notes", "type": "notes"}
+EXTRA_TAB_PANELS = dict([(p['type'], p) for p in [OPEN_ENDED_PANEL, NOTES_PANEL]])
def get_modulestore(location):
@@ -192,9 +194,10 @@ class CoursePageNames:
Checklists = "checklists"
-def add_open_ended_panel_tab(course):
+def add_extra_panel_tab(tab_type, course):
"""
- Used to add the open ended panel tab to a course if it does not exist.
+ Used to add the panel tab to a course if it does not exist.
+ @param tab_type: A string representing the tab type.
@param course: A course object from the modulestore.
@return: Boolean indicating whether or not a tab was added and a list of tabs for the course.
"""
@@ -202,16 +205,19 @@ def add_open_ended_panel_tab(course):
course_tabs = copy.copy(course.tabs)
changed = False
#Check to see if open ended panel is defined in the course
- if OPEN_ENDED_PANEL not in course_tabs:
+
+ tab_panel = EXTRA_TAB_PANELS.get(tab_type)
+ if tab_panel not in course_tabs:
#Add panel to the tabs if it is not defined
- course_tabs.append(OPEN_ENDED_PANEL)
+ course_tabs.append(tab_panel)
changed = True
return changed, course_tabs
-def remove_open_ended_panel_tab(course):
+def remove_extra_panel_tab(tab_type, course):
"""
- Used to remove the open ended panel tab from a course if it exists.
+ Used to remove the panel tab from a course if it exists.
+ @param tab_type: A string representing the tab type.
@param course: A course object from the modulestore.
@return: Boolean indicating whether or not a tab was added and a list of tabs for the course.
"""
@@ -219,8 +225,10 @@ def remove_open_ended_panel_tab(course):
course_tabs = copy.copy(course.tabs)
changed = False
#Check to see if open ended panel is defined in the course
- if OPEN_ENDED_PANEL in course_tabs:
+
+ tab_panel = EXTRA_TAB_PANELS.get(tab_type)
+ if tab_panel in course_tabs:
#Add panel to the tabs if it is not defined
- course_tabs = [ct for ct in course_tabs if ct != OPEN_ENDED_PANEL]
+ course_tabs = [ct for ct in course_tabs if ct != tab_panel]
changed = True
return changed, course_tabs
diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py
index caf3901e03..8d0878809e 100644
--- a/cms/djangoapps/contentstore/views.py
+++ b/cms/djangoapps/contentstore/views.py
@@ -49,8 +49,8 @@ from auth.authz import is_user_in_course_group_role, get_users_in_course_group_b
from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group
from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME, create_all_course_groups
from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state, \
- UnitState, get_course_for_item, get_url_reverse, add_open_ended_panel_tab, \
- remove_open_ended_panel_tab
+ UnitState, get_course_for_item, get_url_reverse, add_extra_panel_tab, \
+ remove_extra_panel_tab
from xmodule.modulestore.xml_importer import import_from_xml
from contentstore.course_info_model import get_course_updates, \
@@ -72,7 +72,8 @@ log = logging.getLogger(__name__)
COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video']
OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"]
-ADVANCED_COMPONENT_TYPES = ['annotatable'] + OPEN_ENDED_COMPONENT_TYPES
+NOTE_COMPONENT_TYPES = ['notes']
+ADVANCED_COMPONENT_TYPES = ['annotatable'] + OPEN_ENDED_COMPONENT_TYPES + NOTE_COMPONENT_TYPES
ADVANCED_COMPONENT_CATEGORY = 'advanced'
ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
@@ -1274,32 +1275,43 @@ def course_advanced_updates(request, org, course, name):
#Check to see if the user instantiated any advanced components. This is a hack to add the open ended panel tab
#to a course automatically if the user has indicated that they want to edit the combinedopenended or peergrading
#module, and to remove it if they have removed the open ended elements.
+ #Note: it has also been extended to include additional component types beyond openended.
if ADVANCED_COMPONENT_POLICY_KEY in request_body:
#Check to see if the user instantiated any open ended components
- found_oe_type = False
#Get the course so that we can scrape current tabs
course_module = modulestore().get_item(location)
- for oe_type in OPEN_ENDED_COMPONENT_TYPES:
- if oe_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]:
- #Add an open ended tab to the course if needed
- changed, new_tabs = add_open_ended_panel_tab(course_module)
- #If a tab has been added to the course, then send the metadata along to CourseMetadata.update_from_json
+ tab_component_types = ['open_ended', 'notes']
+ tab_component_map = {
+ 'open_ended': OPEN_ENDED_COMPONENT_TYPES,
+ 'notes': NOTE_COMPONENT_TYPES,
+ }
+
+ for tab_type in tab_component_types:
+ component_types = tab_component_map.get(tab_type)
+ found_ac_type = False
+ for ac_type in component_types:
+ if ac_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]:
+ #Add an open ended tab to the course if needed
+ changed, new_tabs = add_extra_panel_tab(tab_type, course_module)
+ #If a tab has been added to the course, then send the metadata along to CourseMetadata.update_from_json
+ if changed:
+ course_module.tabs = new_tabs
+ request_body.update({'tabs': new_tabs})
+ #Indicate that tabs should not be filtered out of the metadata
+ filter_tabs = False
+ #Set this flag to avoid the open ended tab removal code below.
+ found_ac_type = True
+ break
+ #If we did not find an open ended module type in the advanced settings,
+ # we may need to remove the open ended tab from the course.
+ if not found_ac_type:
+ #Remove open ended tab to the course if needed
+ changed, new_tabs = remove_extra_panel_tab(tab_type, course_module)
if changed:
+ course_module.tabs = new_tabs
request_body.update({'tabs': new_tabs})
#Indicate that tabs should not be filtered out of the metadata
filter_tabs = False
- #Set this flag to avoid the open ended tab removal code below.
- found_oe_type = True
- break
- #If we did not find an open ended module type in the advanced settings,
- # we may need to remove the open ended tab from the course.
- if not found_oe_type:
- #Remove open ended tab to the course if needed
- changed, new_tabs = remove_open_ended_panel_tab(course_module)
- if changed:
- request_body.update({'tabs': new_tabs})
- #Indicate that tabs should not be filtered out of the metadata
- filter_tabs = False
response_json = json.dumps(CourseMetadata.update_from_json(location, request_body, filter_tabs=filter_tabs))
return HttpResponse(response_json, mimetype="application/json")
From 894cfd1f4817c3215c5c800b161b3a5d496f840e Mon Sep 17 00:00:00 2001
From: Arthur Barrett
Date: Tue, 7 May 2013 18:45:40 -0400
Subject: [PATCH 025/245] update doc strings in api
---
lms/djangoapps/notes/api.py | 21 ++++++++++++++-------
1 file changed, 14 insertions(+), 7 deletions(-)
diff --git a/lms/djangoapps/notes/api.py b/lms/djangoapps/notes/api.py
index bf7b1005ad..a5f39e3cea 100644
--- a/lms/djangoapps/notes/api.py
+++ b/lms/djangoapps/notes/api.py
@@ -10,7 +10,7 @@ import logging
log = logging.getLogger(__name__)
API_SETTINGS = {
- 'MAX_NOTE_LIMIT': 100 # Max number of annotations retrieved per set
+ 'MAX_NOTE_LIMIT': 100 # Max number of annotations to retrieve at one time
}
#----------------------------------------------------------------------#
@@ -18,11 +18,9 @@ API_SETTINGS = {
def api_resource_map():
''' Maps API resources to (method, action) pairs. '''
-
- (GET, PUT, POST, DELETE) = ('GET', 'PUT', 'POST', 'DELETE') # for convenience
-
+ (GET, PUT, POST, DELETE) = ('GET', 'PUT', 'POST', 'DELETE')
return {
- 'root': {GET: version},
+ 'root': {GET: root},
'notes': {GET: index, POST: create},
'note': {GET: read, PUT: update, DELETE: delete},
'search': {GET: search}
@@ -83,15 +81,19 @@ def api_format(request, response, data):
return [content_type, content]
#----------------------------------------------------------------------#
-# Exposed API actions via the resource map.
+# API actions exposed via the resource map.
def index(request, course_id):
+ ''' Returns a list of annotation objects. '''
MAX_LIMIT = API_SETTINGS.get('MAX_NOTE_LIMIT')
+
notes = Note.objects.order_by('id').filter(course_id=course_id,
user=request.user)[:MAX_LIMIT]
+
return [HttpResponse(), [note.as_dict() for note in notes]]
def create(request, course_id):
+ ''' Receives an annotation object to create and returns a 303 with the read location. '''
note = Note(course_id=course_id, user=request.user)
try:
@@ -107,6 +109,7 @@ def create(request, course_id):
return [response, None]
def read(request, course_id, note_id):
+ ''' Returns a single annotation object. '''
try:
note = Note.objects.get(id=note_id)
except:
@@ -118,6 +121,7 @@ def read(request, course_id, note_id):
return [HttpResponse(), note.as_dict()]
def update(request, course_id, note_id):
+ ''' Updates an annotation object and returns a 303 with the read location. '''
try:
note = Note.objects.get(id=note_id)
except:
@@ -140,6 +144,7 @@ def update(request, course_id, note_id):
return [response, None]
def delete(request, course_id, note_id):
+ ''' Deletes the annotation object and returns a 204 with no content. '''
try:
note = Note.objects.get(id=note_id)
except:
@@ -153,6 +158,7 @@ def delete(request, course_id, note_id):
return [HttpResponse('', status=204), None]
def search(request, course_id):
+ ''' Returns a subset of annotation objects based on a search query.. '''
MAX_LIMIT = API_SETTINGS.get('MAX_NOTE_LIMIT')
# search parameters
@@ -185,5 +191,6 @@ def search(request, course_id):
return [HttpResponse(), result]
-def version(request, course_id):
+def root(request, course_id):
+ ''' Returns version information about the API. '''
return [HttpResponse(), {'name': 'Notes API', 'version': '1.0'}]
From 5c55595e8bae9f77731e82a5063f1612895d90d8 Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Wed, 8 May 2013 12:10:49 -0400
Subject: [PATCH 026/245] Start to add in some more open ended module tests
---
common/lib/xmodule/xmodule/tests/__init__.py | 2 +-
.../xmodule/tests/test_combined_open_ended.py | 63 ++++++++++++++++++-
common/test/data/open_ended/README.md | 1 +
common/test/data/open_ended/course.xml | 1 +
.../test/data/open_ended/course/2012_Fall.xml | 5 ++
.../data/open_ended/policies/2012_Fall.json | 14 +++++
.../test/data/open_ended/roots/2012_Fall.xml | 1 +
.../selfassessment/SampleQuestion.xml | 26 ++++----
8 files changed, 98 insertions(+), 15 deletions(-)
create mode 100644 common/test/data/open_ended/README.md
create mode 100644 common/test/data/open_ended/course.xml
create mode 100644 common/test/data/open_ended/course/2012_Fall.xml
create mode 100644 common/test/data/open_ended/policies/2012_Fall.json
create mode 100644 common/test/data/open_ended/roots/2012_Fall.xml
diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py
index 1a10654f6c..59495048a1 100644
--- a/common/lib/xmodule/xmodule/tests/__init__.py
+++ b/common/lib/xmodule/xmodule/tests/__init__.py
@@ -20,7 +20,7 @@ from xmodule.x_module import ModuleSystem
from mock import Mock
open_ended_grading_interface = {
- 'url': 'http://sandbox-grader-001.m.edx.org/peer_grading',
+ 'url': 'blah',
'username': 'incorrect_user',
'password': 'incorrect_pass',
'staff_grading' : 'staff_grading',
diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
index 59f0e222ee..8e9e63b530 100644
--- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
@@ -2,12 +2,19 @@ import json
from mock import Mock, MagicMock, ANY
import unittest
+from fs.memoryfs import MemoryFS
+from mock import patch
+
from xmodule.open_ended_grading_classes.openendedchild import OpenEndedChild
from xmodule.open_ended_grading_classes.open_ended_module import OpenEndedModule
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module
from xmodule.combined_open_ended_module import CombinedOpenEndedModule
-
+from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location
+from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
+
+from xmodule.tests.test_export import DATA_DIR
+
from lxml import etree
import capa.xqueue_interface as xqueue_interface
from datetime import datetime
@@ -17,8 +24,36 @@ log = logging.getLogger(__name__)
from . import test_system
+ORG = 'test_org'
+COURSE = 'open_ended' # name of directory with course data
+
import test_util_open_ended
+class DummySystem(ImportSystem):
+
+ @patch('xmodule.modulestore.xml.OSFS', lambda dir: MemoryFS())
+ def __init__(self, load_error_modules):
+
+ xmlstore = XMLModuleStore("data_dir", course_dirs=[], load_error_modules=load_error_modules)
+ course_id = "/".join([ORG, COURSE, 'test_run'])
+ course_dir = "test_dir"
+ policy = {}
+ error_tracker = Mock()
+ parent_tracker = Mock()
+
+ super(DummySystem, self).__init__(
+ xmlstore,
+ course_id,
+ course_dir,
+ policy,
+ error_tracker,
+ parent_tracker,
+ load_error_modules=load_error_modules,
+ )
+
+ def render_template(self, template, context):
+ raise Exception("Shouldn't be called")
+
"""
Tests for the various pieces of the CombinedOpenEndedGrading system
@@ -436,6 +471,9 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
changed = combinedoe.update_task_states()
self.assertFalse(changed)
+ def test_ajax_actions(self):
+ self.combinedoe_container.handle_ajax('save_answer', {'student_answer' : "This is my answer"})
+
def test_get_score_realistic(self):
instance_state = r"""{"ready_to_reset": false, "skip_spelling_checks": true, "current_task_number": 1, "weight": 5.0, "graceperiod": "1 day 12 hours 59 minutes 59 seconds", "is_graded": "True", "task_states": ["{\"child_created\": false, \"child_attempts\": 4, \"version\": 1, \"child_history\": [{\"answer\": \"The students\\u2019 data are recorded in the table below.\\r\\n\\r\\nStarting Mass (g)\\tEnding Mass (g)\\tDifference in Mass (g)\\r\\nMarble\\t 9.8\\t 9.4\\t\\u20130.4\\r\\nLimestone\\t10.4\\t 9.1\\t\\u20131.3\\r\\nWood\\t11.2\\t11.2\\t 0.0\\r\\nPlastic\\t 7.2\\t 7.1\\t\\u20130.1\\r\\nAfter reading the group\\u2019s procedure, describe what additional information you would need in order to replicate the expe\", \"post_assessment\": \"{\\\"submission_id\\\": 3097, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: More grammar errors than average.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"the students data are recorded in the table below . starting mass g ending mass g difference in mass g marble . . . limestone . . . wood . . . plastic . . . after reading the groups procedure , describe what additional information you would need in order to replicate the expe\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3233, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Response Quality0\\\"}\", \"score\": 0}, {\"answer\": \"After 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\\r\\nStarting Mass (g)\\tEnding Mass (g)\\tDifference in Mass (g)\\r\\nMarble\\t 9.8\\t 9.4\\t\\u20130.4\\r\\nLimestone\\t10.4\\t 9.1\\t\\u20131.3\\r\\nWood\\t11.2\\t11.2\\t 0.0\\r\\nPlastic\\t 7.2\\t 7.1\\t\\u20130.1\\r\\nAfter reading the\", \"post_assessment\": \"[3]\", \"score\": 3}, {\"answer\": \"To replicate the experiment, the procedure would require more detail. One piece of information that is omitted is the amount of vinegar used in the experiment. It is also important to know what temperature the experiment was kept at during the 24 hours. Finally, the procedure needs to include details about the experiment, for example if the whole sample must be submerged.\", \"post_assessment\": \"[3]\", \"score\": 3}, {\"answer\": \"e the mass of four different samples.\\r\\nPour vinegar in each of four separate, but identical, containers.\\r\\nPlace a sample of one material into one container and label. Repeat with remaining samples, placing a single sample into a single container.\\r\\nAfter 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\", \"post_assessment\": \"[3]\", \"score\": 3}, {\"answer\": \"\", \"post_assessment\": \"[3]\", \"score\": 3}], \"max_score\": 3, \"child_state\": \"done\"}", "{\"child_created\": false, \"child_attempts\": 0, \"version\": 1, \"child_history\": [{\"answer\": \"The students\\u2019 data are recorded in the table below.\\r\\n\\r\\nStarting Mass (g)\\tEnding Mass (g)\\tDifference in Mass (g)\\r\\nMarble\\t 9.8\\t 9.4\\t\\u20130.4\\r\\nLimestone\\t10.4\\t 9.1\\t\\u20131.3\\r\\nWood\\t11.2\\t11.2\\t 0.0\\r\\nPlastic\\t 7.2\\t 7.1\\t\\u20130.1\\r\\nAfter reading the group\\u2019s procedure, describe what additional information you would need in order to replicate the expe\", \"post_assessment\": \"{\\\"submission_id\\\": 3097, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: More grammar errors than average.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"the students data are recorded in the table below . starting mass g ending mass g difference in mass g marble . . . limestone . . . wood . . . plastic . . . after reading the groups procedure , describe what additional information you would need in order to replicate the expe\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3233, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Response Quality0\\\"}\", \"score\": 0}, {\"answer\": \"After 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\\r\\nStarting Mass (g)\\tEnding Mass (g)\\tDifference in Mass (g)\\r\\nMarble\\t 9.8\\t 9.4\\t\\u20130.4\\r\\nLimestone\\t10.4\\t 9.1\\t\\u20131.3\\r\\nWood\\t11.2\\t11.2\\t 0.0\\r\\nPlastic\\t 7.2\\t 7.1\\t\\u20130.1\\r\\nAfter reading the\", \"post_assessment\": \"{\\\"submission_id\\\": 3098, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"after hours , remove the samples from the containers and rinse each sample with distilled water . allow the samples to sit and dry for minutes . determine the mass of each sample . the students data are recorded in the table below . starting mass g ending mass g difference in mass g marble . . . limestone . . . wood . . . plastic . . . after reading the\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3235, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Response Quality0\\\"}\", \"score\": 0}, {\"answer\": \"To replicate the experiment, the procedure would require more detail. One piece of information that is omitted is the amount of vinegar used in the experiment. It is also important to know what temperature the experiment was kept at during the 24 hours. Finally, the procedure needs to include details about the experiment, for example if the whole sample must be submerged.\", \"post_assessment\": \"{\\\"submission_id\\\": 3099, \\\"score\\\": 3, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"to replicate the experiment , the procedure would require more detail . one piece of information that is omitted is the amount of vinegar used in the experiment . it is also important to know what temperature the experiment was kept at during the hours . finally , the procedure needs to include details about the experiment , for example if the whole sample must be submerged .\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3237, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Response Quality3\\\"}\", \"score\": 3}, {\"answer\": \"e the mass of four different samples.\\r\\nPour vinegar in each of four separate, but identical, containers.\\r\\nPlace a sample of one material into one container and label. Repeat with remaining samples, placing a single sample into a single container.\\r\\nAfter 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\", \"post_assessment\": \"{\\\"submission_id\\\": 3100, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"e the mass of four different samples . pour vinegar in each of four separate , but identical , containers . place a sample of one material into one container and label . repeat with remaining samples , placing a single sample into a single container . after hours , remove the samples from the containers and rinse each sample with distilled water . allow the samples to sit and dry for minutes . determine the mass of each sample . the students data are recorded in the table below . \\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3239, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Response Quality0\\\"}\", \"score\": 0}, {\"answer\": \"\", \"post_assessment\": \"{\\\"submission_id\\\": 3101, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"invalid essay .\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3241, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Response Quality0\\\"}\", \"score\": 0}], \"max_score\": 3, \"child_state\": \"done\"}"], "attempts": "10000", "student_attempts": 0, "due": null, "state": "done", "accept_file_upload": false, "display_name": "Science Question -- Machine Assessed"}"""
instance_state = json.loads(instance_state)
@@ -466,6 +504,29 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
self.assertEqual(score_dict['score'], 15.0)
self.assertEqual(score_dict['total'], 15.0)
+class OpenEndedModuleXmlTest(unittest.TestCase):
+ def setUp(self):
+ self.test_system = test_system()
+
+ @staticmethod
+ def get_import_system(load_error_modules=True):
+ '''Get a dummy system'''
+ return DummySystem(load_error_modules)
+
+ def get_course(self, name):
+ """Get a test course by directory name. If there's more than one, error."""
+ print "Importing {0}".format(name)
+
+ modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name])
+ courses = modulestore.get_courses()
+ self.modulestore = modulestore
+ self.assertEquals(len(courses), 1)
+ return courses[0]
+
+ def test_open_ended_load(self):
+ course = self.get_course('open_ended')
+ log.info(course.id)
+
diff --git a/common/test/data/open_ended/README.md b/common/test/data/open_ended/README.md
new file mode 100644
index 0000000000..7fe58ac17f
--- /dev/null
+++ b/common/test/data/open_ended/README.md
@@ -0,0 +1 @@
+This is a very very simple course, useful for debugging self assessment code.
diff --git a/common/test/data/open_ended/course.xml b/common/test/data/open_ended/course.xml
new file mode 100644
index 0000000000..ea7d5c420d
--- /dev/null
+++ b/common/test/data/open_ended/course.xml
@@ -0,0 +1 @@
+
diff --git a/common/test/data/open_ended/course/2012_Fall.xml b/common/test/data/open_ended/course/2012_Fall.xml
new file mode 100644
index 0000000000..f2d16488a7
--- /dev/null
+++ b/common/test/data/open_ended/course/2012_Fall.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/common/test/data/open_ended/policies/2012_Fall.json b/common/test/data/open_ended/policies/2012_Fall.json
new file mode 100644
index 0000000000..09b68ab400
--- /dev/null
+++ b/common/test/data/open_ended/policies/2012_Fall.json
@@ -0,0 +1,14 @@
+{
+ "course/2012_Fall": {
+ "graceperiod": "2 days 5 hours 59 minutes 59 seconds",
+ "start": "2015-07-17T12:00",
+ "display_name": "Self Assessment Test",
+ "graded": "true"
+ },
+ "chapter/Overview": {
+ "display_name": "Overview"
+ },
+ "combinedopenended/SampleQuestion": {
+ "display_name": "Sample Question",
+ },
+}
diff --git a/common/test/data/open_ended/roots/2012_Fall.xml b/common/test/data/open_ended/roots/2012_Fall.xml
new file mode 100644
index 0000000000..ea7d5c420d
--- /dev/null
+++ b/common/test/data/open_ended/roots/2012_Fall.xml
@@ -0,0 +1 @@
+
diff --git a/common/test/data/self_assessment/selfassessment/SampleQuestion.xml b/common/test/data/self_assessment/selfassessment/SampleQuestion.xml
index 6c383763b1..f8affa903d 100644
--- a/common/test/data/self_assessment/selfassessment/SampleQuestion.xml
+++ b/common/test/data/self_assessment/selfassessment/SampleQuestion.xml
@@ -1,14 +1,14 @@
-
- What is the meaning of life?
-
-
- This is a rubric.
-
-
- Thanks for your submission!
-
-
- Enter a hint below:
-
-
+
+ What is the meaning of life?
+
+
+ This is a rubric.
+
+
+ Thanks for your submission!
+
+
+ Enter a hint below:
+
+
\ No newline at end of file
From d05ba84f1317820ce7265745b11e99e68c275c0c Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Wed, 8 May 2013 13:49:42 -0400
Subject: [PATCH 027/245] /usr/bin/env in shebang line
---
i18n/extract.py | 2 +-
i18n/generate.py | 2 +-
i18n/make_dummy.py | 2 +-
i18n/transifex.py | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/i18n/extract.py b/i18n/extract.py
index 9b0ad3829c..2cb4ebe118 100755
--- a/i18n/extract.py
+++ b/i18n/extract.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
"""
See https://edx-wiki.atlassian.net/wiki/display/ENG/PO+File+workflow
diff --git a/i18n/generate.py b/i18n/generate.py
index ffc88b64d0..1deb1beeae 100755
--- a/i18n/generate.py
+++ b/i18n/generate.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
"""
See https://edx-wiki.atlassian.net/wiki/display/ENG/PO+File+workflow
diff --git a/i18n/make_dummy.py b/i18n/make_dummy.py
index 8d0fb95ef2..6c14edd45a 100755
--- a/i18n/make_dummy.py
+++ b/i18n/make_dummy.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
# Generate test translation files from human-readable po files.
#
diff --git a/i18n/transifex.py b/i18n/transifex.py
index d08a77b1c0..ac203f3eea 100755
--- a/i18n/transifex.py
+++ b/i18n/transifex.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
import os, sys
from polib import pofile
From dc473e6f7b5f919844493e6782a017f56d589194 Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Wed, 8 May 2013 14:07:57 -0400
Subject: [PATCH 028/245] more verbose messages.po
---
conf/locale/en/LC_MESSAGES/messages.po | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/conf/locale/en/LC_MESSAGES/messages.po b/conf/locale/en/LC_MESSAGES/messages.po
index 1bb8bf6d7f..e5961753c5 100644
--- a/conf/locale/en/LC_MESSAGES/messages.po
+++ b/conf/locale/en/LC_MESSAGES/messages.po
@@ -1 +1,20 @@
+# edX translation file
+# Copyright (C) 2013 edX
+# This file is distributed under the GNU AFFERO GENERAL PUBLIC LICENSE.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: EdX Studio\n"
+"Report-Msgid-Bugs-To: translation_team@edx.org\n"
+"POT-Creation-Date: 2013-05-02 13:13-0400\n"
+"PO-Revision-Date: 2013-05-02 13:27-0400\n"
+"Last-Translator: \n"
+"Language-Team: translation team \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: en\n"
+
# empty
+msgid "This is a key string."
+msgstr ""
From 0f70b0aa8ac0914d5b0cb7fed1ebe2510e3931f1 Mon Sep 17 00:00:00 2001
From: Arthur Barrett
Date: Wed, 8 May 2013 14:09:29 -0400
Subject: [PATCH 029/245] refactored api resource map and added total to search
query
---
lms/djangoapps/notes/api.py | 129 +++++++++++++++++++++-------------
lms/djangoapps/notes/utils.py | 6 +-
2 files changed, 83 insertions(+), 52 deletions(-)
diff --git a/lms/djangoapps/notes/api.py b/lms/djangoapps/notes/api.py
index a5f39e3cea..7252fdd232 100644
--- a/lms/djangoapps/notes/api.py
+++ b/lms/djangoapps/notes/api.py
@@ -9,55 +9,66 @@ import logging
log = logging.getLogger(__name__)
-API_SETTINGS = {
- 'MAX_NOTE_LIMIT': 100 # Max number of annotations to retrieve at one time
+API_SETTINGS = {
+ # Version
+ 'META': {'name': 'Notes API', 'version': '1.0'},
+
+ # Maps resources to HTTP methods
+ 'RESOURCE_MAP': {
+ 'root': {'GET': 'root'},
+ 'notes': {'GET': 'index', 'POST': 'create'},
+ 'note': {'GET': 'read', 'PUT': 'update', 'DELETE': 'delete'},
+ 'search': {'GET': 'search'},
+ },
+
+ # Cap the number of notes that can be returned in one request
+ 'MAX_NOTE_LIMIT': 1000,
}
#----------------------------------------------------------------------#
# API requests are routed through api_request() using the resource map.
-def api_resource_map():
- ''' Maps API resources to (method, action) pairs. '''
- (GET, PUT, POST, DELETE) = ('GET', 'PUT', 'POST', 'DELETE')
- return {
- 'root': {GET: root},
- 'notes': {GET: index, POST: create},
- 'note': {GET: read, PUT: update, DELETE: delete},
- 'search': {GET: search}
- }
-
@login_required
def api_request(request, course_id, **kwargs):
- ''' Routes API requests to the appropriate action method and formats the results
- (defaults to JSON).
-
- Raises a 404 if the resource type doesn't exist, or if there is no action
- method associated with the HTTP method.
+ '''
+ Routes API requests to the appropriate action method and returns JSON.
+ Raises a 404 if the requested resource does not exist or notes are
+ disabled for the course.
'''
+
+ # Verify that notes are enabled for the course
course = get_course_with_access(request.user, course_id, 'load')
if not notes_enabled_for_course(course):
log.debug('Notes not enabled for course')
raise Http404
- resource_map = api_resource_map()
+ # Locate and validate the requested resource
+ resource_map = API_SETTINGS.get('RESOURCE_MAP', {})
resource_name = kwargs.pop('resource')
+ resource_method = request.method
resource = resource_map.get(resource_name)
if resource is None:
log.debug('Resource "{0}" does not exist'.format(resource_name))
raise Http404
- if request.method not in resource.keys():
- log.debug('Resource "{0}" does not support method "{1}"'.format(resource_name, request.method))
+ if resource_method not in resource.keys():
+ log.debug('Resource "{0}" does not support method "{1}"'.format(resource_name, resource_method))
raise Http404
- log.debug("API request: {0} {1}".format(request.method, resource_name))
+ # Find the associated function definition and execute the request
+ func = resource.get(resource_method)
+ module = globals()
+ if func not in module:
+ log.debug('Function "{0}" does not exist for request {1} {2}'.format(action, resource_method, resource_name))
+ raise Http404
- action = resource.get(request.method)
- result = action(request, course_id, **kwargs)
+ log.debug('API request: {0} {1}'.format(resource_method, resource_name))
+ result = module[func](request, course_id, **kwargs)
- response = result[0]
+ # Format and output the results
data = None
+ response = result[0]
if len(result) == 2:
data = result[1]
@@ -65,13 +76,13 @@ def api_request(request, course_id, **kwargs):
response['Content-type'] = formatted[0]
response.content = formatted[1]
- log.debug("API response: {0}".format(formatted))
+ log.debug('API response: {0}'.format(formatted))
return response
def api_format(request, response, data):
- ''' Returns a two-element list containing the content type and content.
- This method does not modify the request or response.
+ '''
+ Returns a two-element list containing the content type and content.
'''
content_type = 'application/json'
if data is None:
@@ -84,7 +95,9 @@ def api_format(request, response, data):
# API actions exposed via the resource map.
def index(request, course_id):
- ''' Returns a list of annotation objects. '''
+ '''
+ Returns a list of annotation objects.
+ '''
MAX_LIMIT = API_SETTINGS.get('MAX_NOTE_LIMIT')
notes = Note.objects.order_by('id').filter(course_id=course_id,
@@ -93,7 +106,9 @@ def index(request, course_id):
return [HttpResponse(), [note.as_dict() for note in notes]]
def create(request, course_id):
- ''' Receives an annotation object to create and returns a 303 with the read location. '''
+ '''
+ Receives an annotation object to create and returns a 303 with the read location.
+ '''
note = Note(course_id=course_id, user=request.user)
try:
@@ -109,7 +124,9 @@ def create(request, course_id):
return [response, None]
def read(request, course_id, note_id):
- ''' Returns a single annotation object. '''
+ '''
+ Returns a single annotation object.
+ '''
try:
note = Note.objects.get(id=note_id)
except:
@@ -121,7 +138,9 @@ def read(request, course_id, note_id):
return [HttpResponse(), note.as_dict()]
def update(request, course_id, note_id):
- ''' Updates an annotation object and returns a 303 with the read location. '''
+ '''
+ Updates an annotation object and returns a 303 with the read location.
+ '''
try:
note = Note.objects.get(id=note_id)
except:
@@ -144,7 +163,9 @@ def update(request, course_id, note_id):
return [response, None]
def delete(request, course_id, note_id):
- ''' Deletes the annotation object and returns a 204 with no content. '''
+ '''
+ Deletes the annotation object and returns a 204 with no content.
+ '''
try:
note = Note.objects.get(id=note_id)
except:
@@ -158,39 +179,47 @@ def delete(request, course_id, note_id):
return [HttpResponse('', status=204), None]
def search(request, course_id):
- ''' Returns a subset of annotation objects based on a search query.. '''
+ '''
+ Returns a subset of annotation objects based on a search query.
+ '''
MAX_LIMIT = API_SETTINGS.get('MAX_NOTE_LIMIT')
# search parameters
- limit = request.GET.get('limit')
- offset = request.GET.get('offset')
- uri = request.GET.get('uri')
+ offset = request.GET.get('offset', '')
+ limit = request.GET.get('limit', '')
+ uri = request.GET.get('uri', '')
# validate search parameters
- if limit is not None and limit.isdigit():
+ if offset.isdigit():
+ offset = int(offset)
+ else:
+ offset = 0
+
+ if limit.isdigit():
limit = int(limit)
if limit == 0 or limit > MAX_LIMIT:
limit = MAX_LIMIT
else:
limit = MAX_LIMIT
- if offset is not None and offset.isdigit():
- offset = int(offset)
- else:
- offset = 0
-
- # search filters
+ # set filters
filters = {'course_id': course_id, 'user': request.user}
- if uri is not None:
+ if uri != '':
filters['uri'] = uri
- start = offset
- end = offset + limit
- notes = Note.objects.order_by('id').filter(**filters)[start:end]
- result = {'rows': [note.as_dict() for note in notes]}
+ # retrieve notes
+ notes = Note.objects.order_by('id').filter(**filters)
+ total = notes.count()
+ rows = notes[offset:offset+limit]
+ result = {
+ 'total': notes.count(),
+ 'rows': [note.as_dict() for note in rows]
+ }
return [HttpResponse(), result]
def root(request, course_id):
- ''' Returns version information about the API. '''
- return [HttpResponse(), {'name': 'Notes API', 'version': '1.0'}]
+ '''
+ Returns version information about the API.
+ '''
+ return [HttpResponse(), API_SETTINGS.get('META')]
diff --git a/lms/djangoapps/notes/utils.py b/lms/djangoapps/notes/utils.py
index e06df3e42a..5e3c0182fa 100644
--- a/lms/djangoapps/notes/utils.py
+++ b/lms/djangoapps/notes/utils.py
@@ -1,5 +1,7 @@
-# TODO: make a separate policy setting to enable/disable notes.
def notes_enabled_for_course(course):
- ''' Returns True if notes are enabled for the course, False otherwise. '''
+ '''
+ Returns True if the notes app is enabled for the course, False otherwise.
+ '''
+ # TODO: create a separate policy setting to enable/disable notes
notes_tab_type = 'notes'
return next((True for tab in course.tabs if tab['type'] == notes_tab_type), False)
From beb4b39b73f84dbd839f9802e1508c3e0abc2fbe Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Wed, 8 May 2013 14:25:31 -0400
Subject: [PATCH 030/245] fix logging
---
i18n/extract.py | 6 +++---
i18n/generate.py | 6 +++---
i18n/logger.py | 13 -------------
i18n/tests/test_validate.py | 8 ++++----
4 files changed, 10 insertions(+), 23 deletions(-)
delete mode 100644 i18n/logger.py
diff --git a/i18n/extract.py b/i18n/extract.py
index 2cb4ebe118..c517de3b51 100755
--- a/i18n/extract.py
+++ b/i18n/extract.py
@@ -15,11 +15,10 @@ See https://edx-wiki.atlassian.net/wiki/display/ENG/PO+File+workflow
"""
-import os
+import os, sys, logging
from datetime import datetime
from polib import pofile
-from logger import get_logger
from config import BASE_DIR, LOCALE_DIR, CONFIGURATION
from execute import execute, create_dir_if_necessary, remove_file
@@ -34,7 +33,8 @@ BABEL_OUT = BASE_DIR.relpathto(CONFIGURATION.source_messages_dir.joinpath('mako.
SOURCE_WARN = 'This English source file is machine-generated. Do not check it into github'
def main ():
- log = get_logger(__name__)
+ log = logging.getLogger(__name__)
+ logging.basicConfig(stream=sys.stdout, level=logging.INFO)
create_dir_if_necessary(LOCALE_DIR)
source_msgs_dir = CONFIGURATION.source_messages_dir
diff --git a/i18n/generate.py b/i18n/generate.py
index 1deb1beeae..48470796a2 100755
--- a/i18n/generate.py
+++ b/i18n/generate.py
@@ -13,10 +13,9 @@
languages to generate.
"""
-import os
+import os, sys, logging
from polib import pofile
-from logger import get_logger
from config import BASE_DIR, CONFIGURATION
from execute import execute, remove_file
@@ -72,7 +71,8 @@ def validate_files(dir, files_to_merge):
raise Exception("I18N: Cannot generate because file not found: {0}".format(pathname))
def main ():
- log = get_logger(__name__)
+ log = logging.getLogger(__name__)
+ logging.basicConfig(stream=sys.stdout, level=logging.INFO)
for locale in CONFIGURATION.locales:
merge(locale)
diff --git a/i18n/logger.py b/i18n/logger.py
deleted file mode 100644
index 20d767a032..0000000000
--- a/i18n/logger.py
+++ /dev/null
@@ -1,13 +0,0 @@
-import logging
-
-def get_logger(name):
- """
- Returns a default logger.
- logging.basicConfig does not render to the console
- """
- log = logging.getLogger()
- log.setLevel(logging.INFO)
- log_handler = logging.StreamHandler()
- log_handler.setFormatter(logging.Formatter('%(asctime)s [%(levelname)s] %(message)s'))
- log.addHandler(log_handler)
- return log
diff --git a/i18n/tests/test_validate.py b/i18n/tests/test_validate.py
index 6bb7164a50..67057a30e7 100644
--- a/i18n/tests/test_validate.py
+++ b/i18n/tests/test_validate.py
@@ -1,8 +1,7 @@
-import os
+import os, sys, logging
from unittest import TestCase
from nose.plugins.skip import SkipTest
-from logger import get_logger
from config import LOCALE_DIR
from execute import call
@@ -10,10 +9,11 @@ def test_po_files(root=LOCALE_DIR):
"""
This is a generator. It yields all of the .po files under root, and tests each one.
"""
- log = get_logger(__name__)
+ log = logging.getLogger(__name__)
+ logging.basicConfig(stream=sys.stdout, level=logging.INFO)
+
for (dirpath, dirnames, filenames) in os.walk(root):
for name in filenames:
- print name
(base, ext) = os.path.splitext(name)
if ext.lower() == '.po':
yield validate_po_file, os.path.join(dirpath, name), log
From dfcbb73662ae6d845213ace78ba450c010ad61b7 Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Wed, 8 May 2013 14:55:16 -0400
Subject: [PATCH 031/245] add guard code to ensure gnu gettext utilities are
loaded before rake tests
---
rakefile | 34 ++++++++++++++++++++++++++++------
1 file changed, 28 insertions(+), 6 deletions(-)
diff --git a/rakefile b/rakefile
index acb9bf6ecc..64afb2e389 100644
--- a/rakefile
+++ b/rakefile
@@ -522,19 +522,39 @@ def validate_transifex_config()
return true
end
+# Make sure GNU gettext utilities are available
+# Returns boolean: returns true if utilities are available, else returns false
+def validate_gnu_gettext()
+ begin
+ select_executable('xgettext')
+ return true
+ rescue
+ puts "Error:".red
+ puts "Cannot locate GNU gettext utilities, which are required by django for internationalization.".red
+ puts "(see https://docs.djangoproject.com/en/dev/topics/i18n/translation/#message-files)".red
+ puts "Try downloading them from http://www.gnu.org/software/gettext/".red
+ return false
+ end
+end
+
+
namespace :i18n do
desc "Extract localizable strings from sources"
task :extract do
- sh(File.join(REPO_ROOT, "i18n", "extract.py"))
+ if validate_gnu_gettext()
+ sh(File.join(REPO_ROOT, "i18n", "extract.py"))
+ end
end
desc "Compile localizable strings from sources. With optional flag 'extract', will extract strings first."
task :generate do
- if ARGV.last.downcase == 'extract'
- Rake::Task["i18n:extract"].execute
+ if validate_gnu_gettext()
+ if ARGV.last.downcase == 'extract'
+ Rake::Task["i18n:extract"].execute
+ end
+ sh(File.join(REPO_ROOT, "i18n", "generate.py"))
end
- sh(File.join(REPO_ROOT, "i18n", "generate.py"))
end
desc "Simulate international translation by generating dummy strings corresponding to source strings."
@@ -567,8 +587,10 @@ namespace :i18n do
desc "Run tests for the internationalization library"
task :test do
- test = File.join(REPO_ROOT, "i18n", "tests")
- sh("nosetests #{test}")
+ if validate_gnu_gettext()
+ test = File.join(REPO_ROOT, "i18n", "tests")
+ sh("nosetests #{test}")
+ end
end
end
From 1eb05cfd4e753d107fcefb4d29dbdcce5cbaf74a Mon Sep 17 00:00:00 2001
From: Arthur Barrett
Date: Wed, 8 May 2013 15:07:36 -0400
Subject: [PATCH 032/245] remove unnecessary count
---
lms/djangoapps/notes/api.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lms/djangoapps/notes/api.py b/lms/djangoapps/notes/api.py
index 7252fdd232..884b87ad55 100644
--- a/lms/djangoapps/notes/api.py
+++ b/lms/djangoapps/notes/api.py
@@ -212,7 +212,7 @@ def search(request, course_id):
total = notes.count()
rows = notes[offset:offset+limit]
result = {
- 'total': notes.count(),
+ 'total': total,
'rows': [note.as_dict() for note in rows]
}
From 165e7059c81c10005f6412594dc3167eaef13da6 Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Wed, 8 May 2013 15:34:35 -0400
Subject: [PATCH 033/245] guard predicates are rake tasks, not functions
---
rakefile | 88 ++++++++++++++++++++++++++------------------------------
1 file changed, 40 insertions(+), 48 deletions(-)
diff --git a/rakefile b/rakefile
index 64afb2e389..3edbc39067 100644
--- a/rakefile
+++ b/rakefile
@@ -510,51 +510,21 @@ end
# --- Internationalization tasks
-# Make sure config file with username/password exists
-# Returns boolean: returns true if file exists and is nonzero length
-def validate_transifex_config()
- config_file = "#{Dir.home}/.transifexrc"
- if !File.file?(config_file) or File.size(config_file)==0
- raise "Cannot connect to Transifex, config file is missing or empty: #{config_file}\n" +
- "See http://help.transifex.com/features/client/#transifexrc\n"
- return false
- end
- return true
-end
-
-# Make sure GNU gettext utilities are available
-# Returns boolean: returns true if utilities are available, else returns false
-def validate_gnu_gettext()
- begin
- select_executable('xgettext')
- return true
- rescue
- puts "Error:".red
- puts "Cannot locate GNU gettext utilities, which are required by django for internationalization.".red
- puts "(see https://docs.djangoproject.com/en/dev/topics/i18n/translation/#message-files)".red
- puts "Try downloading them from http://www.gnu.org/software/gettext/".red
- return false
- end
-end
-
-
namespace :i18n do
desc "Extract localizable strings from sources"
task :extract do
- if validate_gnu_gettext()
- sh(File.join(REPO_ROOT, "i18n", "extract.py"))
- end
+ Rake::Task["i18n:validate:gettext"].execute
+ sh(File.join(REPO_ROOT, "i18n", "extract.py"))
end
desc "Compile localizable strings from sources. With optional flag 'extract', will extract strings first."
task :generate do
- if validate_gnu_gettext()
- if ARGV.last.downcase == 'extract'
- Rake::Task["i18n:extract"].execute
- end
- sh(File.join(REPO_ROOT, "i18n", "generate.py"))
+ Rake::Task["i18n:validate:gettext"].execute
+ if ARGV.last.downcase == 'extract'
+ Rake::Task["i18n:extract"].execute
end
+ sh(File.join(REPO_ROOT, "i18n", "generate.py"))
end
desc "Simulate international translation by generating dummy strings corresponding to source strings."
@@ -567,30 +537,52 @@ namespace :i18n do
end
end
+ namespace :validate do
+
+ desc "Make sure GNU gettext utilities are available"
+ task :gettext do
+ begin
+ select_executable('xgettext')
+ rescue
+ msg = "Cannot locate GNU gettext utilities, which are required by django for internationalization.\n"
+ msg += "(see https://docs.djangoproject.com/en/dev/topics/i18n/translation/#message-files)\n"
+ msg += "Try downloading them from http://www.gnu.org/software/gettext/"
+ abort(msg.red)
+ end
+ end
+
+ desc "Make sure config file with username/password exists"
+ task :transifex_config do
+ config_file = "#{Dir.home}/.transifexrc"
+ if !File.file?(config_file) or File.size(config_file)==0
+ msg ="Cannot connect to Transifex, config file is missing or empty: #{config_file}\n"
+ msg += "See http://help.transifex.com/features/client/#transifexrc"
+ abort(msg.red)
+ end
+ end
+ end
+
namespace :transifex do
desc "Push source strings to Transifex for translation"
task :push do
- if validate_transifex_config()
- cmd = File.join(REPO_ROOT, "i18n", "transifex.py")
- sh("#{cmd} push")
- end
+ Rake::Task["i18n:validate:transifex_config"].execute
+ cmd = File.join(REPO_ROOT, "i18n", "transifex.py")
+ sh("#{cmd} push")
end
desc "Pull translated strings from Transifex"
task :pull do
- if validate_transifex_config()
- cmd = File.join(REPO_ROOT, "i18n", "transifex.py")
- sh("#{cmd} pull")
- end
+ Rake::Task["i18n:validate:transifex_config"].execute
+ cmd = File.join(REPO_ROOT, "i18n", "transifex.py")
+ sh("#{cmd} pull")
end
end
desc "Run tests for the internationalization library"
task :test do
- if validate_gnu_gettext()
- test = File.join(REPO_ROOT, "i18n", "tests")
- sh("nosetests #{test}")
- end
+ Rake::Task["i18n:validate:gettext"].execute
+ test = File.join(REPO_ROOT, "i18n", "tests")
+ sh("nosetests #{test}")
end
end
From 1d53625673a33dec59fc9bd2558936517bfec350 Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Wed, 8 May 2013 15:37:25 -0400
Subject: [PATCH 034/245] Add in ability to mock a server, a lot more testing
code for open ended
---
common/lib/xmodule/xmodule/tests/__init__.py | 2 +-
.../lib/xmodule/xmodule/tests/mock_server.py | 34 +++++++++
.../xmodule/tests/test_combined_open_ended.py | 75 +++++++++++++++++--
common/test/data/open_ended/README.md | 2 +-
.../combinedopenended/SampleQuestion.xml | 33 ++++++++
common/test/data/open_ended/course.xml | 2 +-
6 files changed, 140 insertions(+), 8 deletions(-)
create mode 100644 common/lib/xmodule/xmodule/tests/mock_server.py
create mode 100644 common/test/data/open_ended/combinedopenended/SampleQuestion.xml
diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py
index 59495048a1..b58308252e 100644
--- a/common/lib/xmodule/xmodule/tests/__init__.py
+++ b/common/lib/xmodule/xmodule/tests/__init__.py
@@ -20,7 +20,7 @@ from xmodule.x_module import ModuleSystem
from mock import Mock
open_ended_grading_interface = {
- 'url': 'blah',
+ 'url': 'blah/',
'username': 'incorrect_user',
'password': 'incorrect_pass',
'staff_grading' : 'staff_grading',
diff --git a/common/lib/xmodule/xmodule/tests/mock_server.py b/common/lib/xmodule/xmodule/tests/mock_server.py
new file mode 100644
index 0000000000..d8ae2f9124
--- /dev/null
+++ b/common/lib/xmodule/xmodule/tests/mock_server.py
@@ -0,0 +1,34 @@
+from threading import Thread
+import socket
+import threading
+
+import SimpleHTTPServer
+import SocketServer
+
+class ThreadedRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
+ def handle(self):
+ data = self.request.recv(1024)
+ cur_thread = threading.current_thread()
+ response = "{}: {}".format(cur_thread.name, data)
+ self.request.sendall(response)
+ return
+
+class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
+ pass
+
+def create_server(host,port):
+ """
+ Mock a server to be used for the open ended grading tests
+ @param host: the hostname ie "localhost" or "127.0.0.1"
+ @param port: the integer of the port to open a connection on
+ @return: The created server object
+ """
+ server = ThreadedTCPServer((host,port), ThreadedRequestHandler)
+
+ # Start a thread with the server -- that thread will then start one
+ # more thread for each request
+ server_thread = threading.Thread(target=server.serve_forever)
+ # Exit the server thread when the main thread terminates
+ server_thread.daemon = True
+ server_thread.start()
+ return server
\ No newline at end of file
diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
index 8e9e63b530..0f7db73db4 100644
--- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
@@ -20,6 +20,8 @@ import capa.xqueue_interface as xqueue_interface
from datetime import datetime
import logging
+from mock_server import create_server
+
log = logging.getLogger(__name__)
from . import test_system
@@ -29,6 +31,18 @@ COURSE = 'open_ended' # name of directory with course data
import test_util_open_ended
+class MockQueryDict(dict):
+ """
+ Mock a query set so that it can be used with default authorization
+ """
+ def getlist(self, key, default=None):
+ try:
+ return super(MockQueryDict, self).__getitem__(key)
+ except KeyError:
+ if default is None:
+ return []
+ return default
+
class DummySystem(ImportSystem):
@patch('xmodule.modulestore.xml.OSFS', lambda dir: MemoryFS())
@@ -471,9 +485,6 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
changed = combinedoe.update_task_states()
self.assertFalse(changed)
- def test_ajax_actions(self):
- self.combinedoe_container.handle_ajax('save_answer', {'student_answer' : "This is my answer"})
-
def test_get_score_realistic(self):
instance_state = r"""{"ready_to_reset": false, "skip_spelling_checks": true, "current_task_number": 1, "weight": 5.0, "graceperiod": "1 day 12 hours 59 minutes 59 seconds", "is_graded": "True", "task_states": ["{\"child_created\": false, \"child_attempts\": 4, \"version\": 1, \"child_history\": [{\"answer\": \"The students\\u2019 data are recorded in the table below.\\r\\n\\r\\nStarting Mass (g)\\tEnding Mass (g)\\tDifference in Mass (g)\\r\\nMarble\\t 9.8\\t 9.4\\t\\u20130.4\\r\\nLimestone\\t10.4\\t 9.1\\t\\u20131.3\\r\\nWood\\t11.2\\t11.2\\t 0.0\\r\\nPlastic\\t 7.2\\t 7.1\\t\\u20130.1\\r\\nAfter reading the group\\u2019s procedure, describe what additional information you would need in order to replicate the expe\", \"post_assessment\": \"{\\\"submission_id\\\": 3097, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: More grammar errors than average.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"the students data are recorded in the table below . starting mass g ending mass g difference in mass g marble . . . limestone . . . wood . . . plastic . . . after reading the groups procedure , describe what additional information you would need in order to replicate the expe\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3233, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Response Quality0\\\"}\", \"score\": 0}, {\"answer\": \"After 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\\r\\nStarting Mass (g)\\tEnding Mass (g)\\tDifference in Mass (g)\\r\\nMarble\\t 9.8\\t 9.4\\t\\u20130.4\\r\\nLimestone\\t10.4\\t 9.1\\t\\u20131.3\\r\\nWood\\t11.2\\t11.2\\t 0.0\\r\\nPlastic\\t 7.2\\t 7.1\\t\\u20130.1\\r\\nAfter reading the\", \"post_assessment\": \"[3]\", \"score\": 3}, {\"answer\": \"To replicate the experiment, the procedure would require more detail. One piece of information that is omitted is the amount of vinegar used in the experiment. It is also important to know what temperature the experiment was kept at during the 24 hours. Finally, the procedure needs to include details about the experiment, for example if the whole sample must be submerged.\", \"post_assessment\": \"[3]\", \"score\": 3}, {\"answer\": \"e the mass of four different samples.\\r\\nPour vinegar in each of four separate, but identical, containers.\\r\\nPlace a sample of one material into one container and label. Repeat with remaining samples, placing a single sample into a single container.\\r\\nAfter 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\", \"post_assessment\": \"[3]\", \"score\": 3}, {\"answer\": \"\", \"post_assessment\": \"[3]\", \"score\": 3}], \"max_score\": 3, \"child_state\": \"done\"}", "{\"child_created\": false, \"child_attempts\": 0, \"version\": 1, \"child_history\": [{\"answer\": \"The students\\u2019 data are recorded in the table below.\\r\\n\\r\\nStarting Mass (g)\\tEnding Mass (g)\\tDifference in Mass (g)\\r\\nMarble\\t 9.8\\t 9.4\\t\\u20130.4\\r\\nLimestone\\t10.4\\t 9.1\\t\\u20131.3\\r\\nWood\\t11.2\\t11.2\\t 0.0\\r\\nPlastic\\t 7.2\\t 7.1\\t\\u20130.1\\r\\nAfter reading the group\\u2019s procedure, describe what additional information you would need in order to replicate the expe\", \"post_assessment\": \"{\\\"submission_id\\\": 3097, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: More grammar errors than average.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"the students data are recorded in the table below . starting mass g ending mass g difference in mass g marble . . . limestone . . . wood . . . plastic . . . after reading the groups procedure , describe what additional information you would need in order to replicate the expe\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3233, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Response Quality0\\\"}\", \"score\": 0}, {\"answer\": \"After 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\\r\\nStarting Mass (g)\\tEnding Mass (g)\\tDifference in Mass (g)\\r\\nMarble\\t 9.8\\t 9.4\\t\\u20130.4\\r\\nLimestone\\t10.4\\t 9.1\\t\\u20131.3\\r\\nWood\\t11.2\\t11.2\\t 0.0\\r\\nPlastic\\t 7.2\\t 7.1\\t\\u20130.1\\r\\nAfter reading the\", \"post_assessment\": \"{\\\"submission_id\\\": 3098, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"after hours , remove the samples from the containers and rinse each sample with distilled water . allow the samples to sit and dry for minutes . determine the mass of each sample . the students data are recorded in the table below . starting mass g ending mass g difference in mass g marble . . . limestone . . . wood . . . plastic . . . after reading the\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3235, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Response Quality0\\\"}\", \"score\": 0}, {\"answer\": \"To replicate the experiment, the procedure would require more detail. One piece of information that is omitted is the amount of vinegar used in the experiment. It is also important to know what temperature the experiment was kept at during the 24 hours. Finally, the procedure needs to include details about the experiment, for example if the whole sample must be submerged.\", \"post_assessment\": \"{\\\"submission_id\\\": 3099, \\\"score\\\": 3, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"to replicate the experiment , the procedure would require more detail . one piece of information that is omitted is the amount of vinegar used in the experiment . it is also important to know what temperature the experiment was kept at during the hours . finally , the procedure needs to include details about the experiment , for example if the whole sample must be submerged .\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3237, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Response Quality3\\\"}\", \"score\": 3}, {\"answer\": \"e the mass of four different samples.\\r\\nPour vinegar in each of four separate, but identical, containers.\\r\\nPlace a sample of one material into one container and label. Repeat with remaining samples, placing a single sample into a single container.\\r\\nAfter 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\", \"post_assessment\": \"{\\\"submission_id\\\": 3100, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"e the mass of four different samples . pour vinegar in each of four separate , but identical , containers . place a sample of one material into one container and label . repeat with remaining samples , placing a single sample into a single container . after hours , remove the samples from the containers and rinse each sample with distilled water . allow the samples to sit and dry for minutes . determine the mass of each sample . the students data are recorded in the table below . \\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3239, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Response Quality0\\\"}\", \"score\": 0}, {\"answer\": \"\", \"post_assessment\": \"{\\\"submission_id\\\": 3101, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"invalid essay .\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3241, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Response Quality0\\\"}\", \"score\": 0}], \"max_score\": 3, \"child_state\": \"done\"}"], "attempts": "10000", "student_attempts": 0, "due": null, "state": "done", "accept_file_upload": false, "display_name": "Science Question -- Machine Assessed"}"""
instance_state = json.loads(instance_state)
@@ -505,6 +516,11 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
self.assertEqual(score_dict['total'], 15.0)
class OpenEndedModuleXmlTest(unittest.TestCase):
+ problem_location = Location(["i4x", "edX", "oe_test", "combinedopenended", "SampleQuestion"])
+ answer = "blah blah"
+ assessment = [0,1]
+ hint = "blah"
+ test_server = create_server("127.0.0.1", 3034)
def setUp(self):
self.test_system = test_system()
@@ -523,10 +539,59 @@ class OpenEndedModuleXmlTest(unittest.TestCase):
self.assertEquals(len(courses), 1)
return courses[0]
- def test_open_ended_load(self):
+ def get_module_from_location(self, location):
course = self.get_course('open_ended')
- log.info(course.id)
+ if not isinstance(location, Location):
+ location = Location(location)
+ descriptor = self.modulestore.get_instance(course.id, location, depth=None)
+ return descriptor.xmodule(self.test_system)
+ def test_open_ended_load_and_save(self):
+ module = self.get_module_from_location(self.problem_location)
+ module.handle_ajax("save_answer", {"student_answer" : self.answer})
+ task_one_json = json.loads(module.task_states[0])
+ self.assertEqual(task_one_json['child_history'][0]['answer'], self.answer)
+ def test_open_ended_flow_reset(self):
+ assessment = [0,1]
+ module = self.get_module_from_location(self.problem_location)
+ #Simulate a student saving an answer
+ module.handle_ajax("save_answer", {"student_answer" : self.answer})
+ status = module.handle_ajax("get_status", {})
+ #Mock a student submitting an assessment
+ assessment_dict = MockQueryDict()
+ assessment_dict.update({'assessment' : sum(assessment), 'score_list[]' : assessment})
+ module.handle_ajax("save_assessment", assessment_dict)
+ task_one_json = json.loads(module.task_states[0])
+ self.assertEqual(json.loads(task_one_json['child_history'][0]['post_assessment']), assessment)
+ module.handle_ajax("get_status", {})
+
+ #Move to the next step in the problem
+ module.handle_ajax("next_problem", {})
+ module.get_html()
+ module.handle_ajax("get_combined_rubric", {})
+
+ module.handle_ajax("reset", {})
+
+ def test_open_ended_flow_correct(self):
+ assessment = [1,1]
+ module = self.get_module_from_location(self.problem_location)
+
+ #Simulate a student saving an answer
+ module.handle_ajax("save_answer", {"student_answer" : self.answer})
+ status = module.handle_ajax("get_status", {})
+
+ #Mock a student submitting an assessment
+ assessment_dict = MockQueryDict()
+ assessment_dict.update({'assessment' : sum(assessment), 'score_list[]' : assessment})
+ module.handle_ajax("save_assessment", assessment_dict)
+ task_one_json = json.loads(module.task_states[0])
+ self.assertEqual(json.loads(task_one_json['child_history'][0]['post_assessment']), assessment)
+ module.handle_ajax("get_status", {})
+
+ #Move to the next step in the problem
+ module.handle_ajax("next_problem", {})
+ module.get_html()
+ module.handle_ajax("get_combined_rubric", {})
diff --git a/common/test/data/open_ended/README.md b/common/test/data/open_ended/README.md
index 7fe58ac17f..ed1d5c771d 100644
--- a/common/test/data/open_ended/README.md
+++ b/common/test/data/open_ended/README.md
@@ -1 +1 @@
-This is a very very simple course, useful for debugging self assessment code.
+This is a very very simple course, useful for debugging open ended grading code.
diff --git a/common/test/data/open_ended/combinedopenended/SampleQuestion.xml b/common/test/data/open_ended/combinedopenended/SampleQuestion.xml
new file mode 100644
index 0000000000..5dbe285526
--- /dev/null
+++ b/common/test/data/open_ended/combinedopenended/SampleQuestion.xml
@@ -0,0 +1,33 @@
+
+
+
+
+ Writing Applications
+
+
+
+
+ Language Conventions
+
+
+
+
+
+
+
Censorship in the Libraries
+
"All of us can think of a book that we hope none of our children or any other children have taken off the shelf. But if I have the right to remove that book from the shelf -- that work I abhor -- then you also have exactly the same right and so does everyone else. And then we have no books left on the shelf for any of us." --Katherine Paterson, Author
+
Write a persuasive essay to a newspaper reflecting your vies on censorship in libraries. Do you believe that certain materials, such as books, music, movies, magazines, etc., should be removed from the shelves if they are found offensive? Support your position with convincing arguments from your own experience, observations, and/or reading.
+
+
+
+
+
+
+
+ Enter essay here.
+ This is the answer.
+ {"grader_settings" : "ml_grading.conf", "problem_id" : "6.002x/Welcome/OETest"}
+
+
+
+
\ No newline at end of file
diff --git a/common/test/data/open_ended/course.xml b/common/test/data/open_ended/course.xml
index ea7d5c420d..bf3ed687fb 100644
--- a/common/test/data/open_ended/course.xml
+++ b/common/test/data/open_ended/course.xml
@@ -1 +1 @@
-
+
From 3801f574a4f230fbd6ccb3e4a6d8aef2ffb23b79 Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Wed, 8 May 2013 16:00:07 -0400
Subject: [PATCH 035/245] Add in xqueue submission tests
---
.../open_ended_module.py | 2 +-
common/lib/xmodule/xmodule/tests/__init__.py | 2 +-
.../lib/xmodule/xmodule/tests/mock_server.py | 34 -------------------
.../xmodule/tests/test_combined_open_ended.py | 21 +++++++++---
4 files changed, 19 insertions(+), 40 deletions(-)
delete mode 100644 common/lib/xmodule/xmodule/tests/mock_server.py
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
index 8373700837..4a8604ac30 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
@@ -243,7 +243,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
})
# Submit request. When successful, 'msg' is the prior length of the queue
- (error, msg) = qinterface.send_to_queue(header=xheader,
+ qinterface.send_to_queue(header=xheader,
body=json.dumps(contents))
# State associated with the queueing request
diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py
index b58308252e..0a2f22aa68 100644
--- a/common/lib/xmodule/xmodule/tests/__init__.py
+++ b/common/lib/xmodule/xmodule/tests/__init__.py
@@ -52,7 +52,7 @@ def test_system():
user=Mock(is_staff=False),
filestore=Mock(),
debug=True,
- xqueue={'interface': None, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 10},
+ xqueue={'interface': None, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 10, 'construct_callback' : Mock(side_effect="/")},
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
xblock_model_data=lambda descriptor: descriptor._model_data,
anonymous_student_id='student',
diff --git a/common/lib/xmodule/xmodule/tests/mock_server.py b/common/lib/xmodule/xmodule/tests/mock_server.py
deleted file mode 100644
index d8ae2f9124..0000000000
--- a/common/lib/xmodule/xmodule/tests/mock_server.py
+++ /dev/null
@@ -1,34 +0,0 @@
-from threading import Thread
-import socket
-import threading
-
-import SimpleHTTPServer
-import SocketServer
-
-class ThreadedRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
- def handle(self):
- data = self.request.recv(1024)
- cur_thread = threading.current_thread()
- response = "{}: {}".format(cur_thread.name, data)
- self.request.sendall(response)
- return
-
-class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
- pass
-
-def create_server(host,port):
- """
- Mock a server to be used for the open ended grading tests
- @param host: the hostname ie "localhost" or "127.0.0.1"
- @param port: the integer of the port to open a connection on
- @return: The created server object
- """
- server = ThreadedTCPServer((host,port), ThreadedRequestHandler)
-
- # Start a thread with the server -- that thread will then start one
- # more thread for each request
- server_thread = threading.Thread(target=server.serve_forever)
- # Exit the server thread when the main thread terminates
- server_thread.daemon = True
- server_thread.start()
- return server
\ No newline at end of file
diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
index 0f7db73db4..a8127eec30 100644
--- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
@@ -8,6 +8,7 @@ from mock import patch
from xmodule.open_ended_grading_classes.openendedchild import OpenEndedChild
from xmodule.open_ended_grading_classes.open_ended_module import OpenEndedModule
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module
+from xmodule.open_ended_grading_classes.grading_service_module import GradingServiceError
from xmodule.combined_open_ended_module import CombinedOpenEndedModule
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location
@@ -520,9 +521,11 @@ class OpenEndedModuleXmlTest(unittest.TestCase):
answer = "blah blah"
assessment = [0,1]
hint = "blah"
- test_server = create_server("127.0.0.1", 3034)
def setUp(self):
self.test_system = test_system()
+ self.test_system.xqueue['interface'] = Mock(
+ send_to_queue = Mock(side_effect=[1,"queued"])
+ )
@staticmethod
def get_import_system(load_error_modules=True):
@@ -592,6 +595,16 @@ class OpenEndedModuleXmlTest(unittest.TestCase):
module.handle_ajax("get_status", {})
#Move to the next step in the problem
- module.handle_ajax("next_problem", {})
- module.get_html()
- module.handle_ajax("get_combined_rubric", {})
+ try:
+ module.handle_ajax("next_problem", {})
+ except GradingServiceError:
+ #This error is okay. We don't have a grading service to connect to!
+ pass
+ #Move to the next step in the problem
+ try:
+ module.get_html()
+ except GradingServiceError:
+ #This error is okay. We don't have a grading service to connect to!
+ pass
+
+ module.handle_ajax("get_combined_rubric", {})
From c39a21d833c51737413afee586444fa4a4ebde64 Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Wed, 8 May 2013 16:03:11 -0400
Subject: [PATCH 036/245] Remove old import
---
common/lib/xmodule/xmodule/tests/test_combined_open_ended.py | 2 --
1 file changed, 2 deletions(-)
diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
index a8127eec30..d845a9a711 100644
--- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
@@ -21,8 +21,6 @@ import capa.xqueue_interface as xqueue_interface
from datetime import datetime
import logging
-from mock_server import create_server
-
log = logging.getLogger(__name__)
from . import test_system
From bf21211b250c2ea164f64fb8f86a8ea2b7e4e9ed Mon Sep 17 00:00:00 2001
From: Arthur Barrett
Date: Wed, 8 May 2013 18:03:41 -0400
Subject: [PATCH 037/245] adding some tests
---
lms/djangoapps/notes/api.py | 2 +-
lms/djangoapps/notes/tests.py | 130 +++++++++++++++++++++++++++++++---
lms/djangoapps/notes/utils.py | 6 +-
3 files changed, 125 insertions(+), 13 deletions(-)
diff --git a/lms/djangoapps/notes/api.py b/lms/djangoapps/notes/api.py
index 884b87ad55..9849c35d33 100644
--- a/lms/djangoapps/notes/api.py
+++ b/lms/djangoapps/notes/api.py
@@ -11,7 +11,7 @@ log = logging.getLogger(__name__)
API_SETTINGS = {
# Version
- 'META': {'name': 'Notes API', 'version': '1.0'},
+ 'META': {'name': 'Notes API', 'version': 1},
# Maps resources to HTTP methods
'RESOURCE_MAP': {
diff --git a/lms/djangoapps/notes/tests.py b/lms/djangoapps/notes/tests.py
index 501deb776c..9861863561 100644
--- a/lms/djangoapps/notes/tests.py
+++ b/lms/djangoapps/notes/tests.py
@@ -1,16 +1,126 @@
"""
-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.
+Unit tests for the notes API and model.
"""
from django.test import TestCase
+from django.test.client import Client
+from django.core.urlresolvers import reverse
+from django.contrib.auth.models import User
+from collections import namedtuple
+from random import random
+import json
+import logging
-class SimpleTest(TestCase):
- def test_basic_addition(self):
- """
- Tests that 1 + 1 always equals 2.
- """
- self.assertEqual(1 + 1, 2)
+from . import utils, api, models
+
+logging.disable(logging.CRITICAL) # remove debugging from the log output
+
+class UtilsTest(TestCase):
+ def setUp(self):
+ '''
+ Setup a dummy course-like object with a tabs field that can be
+ accessed via attribute lookup.
+ '''
+ self.course = namedtuple('DummyCourse', ['tabs'])
+ self.course.tabs = []
+
+ def test_notes_not_enabled(self):
+ '''
+ Tests that notes are disabled when the course tab configuration does NOT
+ contain a tab with type "notes."
+ '''
+ self.assertFalse(utils.notes_enabled_for_course(self.course))
+
+ def test_notes_enabled(self):
+ '''
+ Tests that notes are enabled when the course tab configuration contains
+ a tab with type "notes."
+ '''
+ self.course.tabs = [
+ {'type': 'foo'},
+ {'name': 'My Notes', 'type': 'notes'},
+ {'type':'bar'}]
+ self.assertTrue(utils.notes_enabled_for_course(self.course))
+
+class ApiTest(TestCase):
+ def setUp(self):
+ self.client = Client()
+
+ # Create two accounts
+ self.password = 'abc'
+ self.student = User.objects.create_user('student', 'student@test.com', self.password)
+ self.instructor = User.objects.create_user('instructor', 'instructor@test.com', self.password)
+ self.course_id = 'HarvardX/CB22x/The_Ancient_Greek_Hero'
+ self.note = {
+ 'user':self.student,
+ 'course_id':self.course_id,
+ 'uri':'/',
+ 'text':'foo',
+ 'quote':'bar',
+ 'range_start':0,
+ 'range_start_offset':0,
+ 'range_end':100,
+ 'range_end_offset':0,
+ 'tags':'a,b,c'
+ }
+
+ def login(self):
+ self.client.login(username=self.student.username, password=self.password)
+
+ def url(self, name):
+ return reverse(name, kwargs={'course_id':self.course_id})
+
+ def create_notes(self, num_notes):
+ notes = [ models.Note(**self.note) for n in range(num_notes) ]
+ models.Note.objects.bulk_create(notes)
+ return notes
+
+ def test_root(self):
+ self.login()
+
+ resp = self.client.get(self.url('notes_api_root'))
+ self.assertEqual(resp.status_code, 200)
+ self.assertNotEqual(resp.content, '')
+
+ content = json.loads(resp.content)
+
+ self.assertEqual(set(('name','version')), set(content.keys()))
+ self.assertIsInstance(content['version'], int)
+ self.assertEqual(content['name'], 'Notes API')
+
+ def test_index_empty(self):
+ self.login()
+
+ resp = self.client.get(self.url('notes_api_notes'))
+ self.assertEqual(resp.status_code, 200)
+ self.assertNotEqual(resp.content, '')
+
+ content = json.loads(resp.content)
+ self.assertEqual(len(content), 0)
+
+ def test_index_with_notes(self):
+ num_notes = 7
+ self.login()
+ self.create_notes(num_notes)
+
+ resp = self.client.get(self.url('notes_api_notes'))
+ self.assertEqual(resp.status_code, 200)
+ self.assertNotEqual(resp.content, '')
+
+ content = json.loads(resp.content)
+ self.assertEqual(len(content), num_notes)
+
+ def test_index_max_notes(self):
+ self.login()
+
+ MAX_LIMIT = api.API_SETTINGS.get('MAX_NOTE_LIMIT')
+ num_notes = MAX_LIMIT + 1
+ self.create_notes(num_notes)
+
+ resp = self.client.get(self.url('notes_api_notes'))
+ self.assertEqual(resp.status_code, 200)
+ self.assertNotEqual(resp.content, '')
+
+ content = json.loads(resp.content)
+ self.assertEqual(len(content), MAX_LIMIT)
diff --git a/lms/djangoapps/notes/utils.py b/lms/djangoapps/notes/utils.py
index 5e3c0182fa..80a872da4e 100644
--- a/lms/djangoapps/notes/utils.py
+++ b/lms/djangoapps/notes/utils.py
@@ -3,5 +3,7 @@ def notes_enabled_for_course(course):
Returns True if the notes app is enabled for the course, False otherwise.
'''
# TODO: create a separate policy setting to enable/disable notes
- notes_tab_type = 'notes'
- return next((True for tab in course.tabs if tab['type'] == notes_tab_type), False)
+ tab_type = 'notes'
+ tabs = course.tabs
+ tab_found = next((True for t in tabs if t['type'] == tab_type), False)
+ return tab_found
From f26a2585982c796e6aa1be0835882c070e1f155c Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Wed, 8 May 2013 18:07:17 -0400
Subject: [PATCH 038/245] Add in test for grader reply
---
.../open_ended_module.py | 2 +-
.../xmodule/tests/test_combined_open_ended.py | 20 +++++++++++++++++--
2 files changed, 19 insertions(+), 3 deletions(-)
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
index 4a8604ac30..afdfeef6de 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
@@ -516,7 +516,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
try:
feedback_dict = json.loads(score_result['feedback'])
except:
- pass
+ feedback_dict = score_result.get('feedback', '')
feedback_dicts = [feedback_dict]
grader_ids = [score_result['grader_id']]
submission_ids = [score_result['submission_id']]
diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
index d845a9a711..7cfec5fb62 100644
--- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
@@ -598,11 +598,27 @@ class OpenEndedModuleXmlTest(unittest.TestCase):
except GradingServiceError:
#This error is okay. We don't have a grading service to connect to!
pass
- #Move to the next step in the problem
try:
module.get_html()
except GradingServiceError:
#This error is okay. We don't have a grading service to connect to!
pass
- module.handle_ajax("get_combined_rubric", {})
+ module.handle_ajax("get_combined_rubric", {})
+
+ queue_reply = {
+ 'queuekey' : "",
+ 'xqueue_body' : json.dumps({
+ 'score' : 0,
+ 'feedback' : json.dumps({"spelling": "Spelling: Ok.", "grammar": "Grammar: Ok.", "markup-text": " all of us can think of a book that we hope none of our children or any other children have taken off the shelf . but if i have the right to remove that book from the shelf that work i abhor then you also have exactly the same right and so does everyone else . and then we have no books left on the shelf for any of us . katherinepaterson , author write a persuasive essay to a newspaper reflecting your vies on censorship in libraries . do you believe that certain materials , such as books , music , movies , magazines , etc . , should be removed from the shelves if they are found offensive ? support your position with convincing arguments from your own experience , observations , and or reading . "}),
+ 'grader_type' : "ML",
+ 'success' : True,
+ 'grader_id' : 1,
+ 'submission_id' : 1,
+ 'rubric_xml' : "Writing Applications0 Language Conventions 0",
+ 'rubric_scores_complete' : True,
+ })
+ }
+
+ module.handle_ajax("score_update", queue_reply)
+
From 4d759e9772b690c3ded7458bb7b9ed6eaecad6a1 Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Wed, 8 May 2013 18:15:57 -0400
Subject: [PATCH 039/245] Test full flow, including reset
---
.../combined_open_ended_modulev1.py | 12 ++++++++++++
.../xmodule/tests/test_combined_open_ended.py | 17 +++++++++++++++++
2 files changed, 29 insertions(+)
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py
index 12f90ed1b3..6767851d3a 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py
@@ -803,6 +803,18 @@ class CombinedOpenEndedV1Module():
return progress_object
+ def out_of_sync_error(self, get, msg=''):
+ """
+ return dict out-of-sync error message, and also log.
+ """
+ #This is a dev_facing_error
+ log.warning("Combined module state out sync. state: %r, get: %r. %s",
+ self.state, get, msg)
+ #This is a student_facing_error
+ return {'success': False,
+ 'error': 'The problem state got out-of-sync. Please try reloading the page.'}
+
+
class CombinedOpenEndedV1Descriptor():
"""
diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
index 7cfec5fb62..3b8019290f 100644
--- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
@@ -604,8 +604,10 @@ class OpenEndedModuleXmlTest(unittest.TestCase):
#This error is okay. We don't have a grading service to connect to!
pass
+ #Try to get the rubric from the module
module.handle_ajax("get_combined_rubric", {})
+ #Make a fake reply from the queue
queue_reply = {
'queuekey' : "",
'xqueue_body' : json.dumps({
@@ -620,5 +622,20 @@ class OpenEndedModuleXmlTest(unittest.TestCase):
})
}
+ module.handle_ajax("check_for_score", {})
+
+ #Update the module with the fake queue reply
module.handle_ajax("score_update", queue_reply)
+ #Get html and other data client will request
+ html = module.get_html()
+ legend = module.handle_ajax("get_legend", {})
+ status = module.handle_ajax("get_status", {})
+ legend = module.handle_ajax("skip_post_assessment", {})
+
+ #Get all results
+ legend = module.handle_ajax("get_results", {})
+
+ log.info(module.task_states)
+ #reset the problem
+ module.handle_ajax("reset", {})
From ff84545f3155c00e3824b79845e6ab3fac160b2e Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Wed, 8 May 2013 18:49:28 -0400
Subject: [PATCH 040/245] Start to add peer grading tests, make dummy system a
separate thing
---
.../peer_grading_service.py | 27 +++++----
.../lib/xmodule/xmodule/tests/dummy_system.py | 55 ++++++++++++++++++
.../xmodule/tests/test_combined_open_ended.py | 57 ++-----------------
.../xmodule/tests/test_peer_grading.py | 49 ++++++++++++++++
.../test/data/open_ended/course/2012_Fall.xml | 1 +
.../peergrading/PeerGradingSample.xml | 1 +
.../data/open_ended/policies/2012_Fall.json | 5 +-
7 files changed, 131 insertions(+), 64 deletions(-)
create mode 100644 common/lib/xmodule/xmodule/tests/dummy_system.py
create mode 100644 common/lib/xmodule/xmodule/tests/test_peer_grading.py
create mode 100644 common/test/data/open_ended/peergrading/PeerGradingSample.xml
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py
index 85c7a98132..19cc013cb7 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py
@@ -100,29 +100,29 @@ without making actual service calls to the grading controller
class MockPeerGradingService(object):
def get_next_submission(self, problem_location, grader_id):
- return json.dumps({'success': True,
+ return {'success': True,
'submission_id': 1,
'submission_key': "",
'student_response': 'fake student response',
'prompt': 'fake submission prompt',
'rubric': 'fake rubric',
- 'max_score': 4})
+ 'max_score': 4}
def save_grade(self, location, grader_id, submission_id,
score, feedback, submission_key, rubric_scores, submission_flagged):
- return json.dumps({'success': True})
+ return {'success': True}
def is_student_calibrated(self, problem_location, grader_id):
- return json.dumps({'success': True, 'calibrated': True})
+ return {'success': True, 'calibrated': True}
def show_calibration_essay(self, problem_location, grader_id):
- return json.dumps({'success': True,
+ return {'success': True,
'submission_id': 1,
'submission_key': '',
'student_response': 'fake student response',
'prompt': 'fake submission prompt',
'rubric': 'fake rubric',
- 'max_score': 4})
+ 'max_score': 4}
def save_calibration_essay(self, problem_location, grader_id,
calibration_essay_id, submission_key, score,
@@ -130,10 +130,13 @@ class MockPeerGradingService(object):
return {'success': True, 'actual_score': 2}
def get_problem_list(self, course_id, grader_id):
- return json.dumps({'success': True,
+ return {'success': True,
'problem_list': [
- json.dumps({'location': 'i4x://MITx/3.091x/problem/open_ended_demo1',
- 'problem_name': "Problem 1", 'num_graded': 3, 'num_pending': 5}),
- json.dumps({'location': 'i4x://MITx/3.091x/problem/open_ended_demo2',
- 'problem_name': "Problem 2", 'num_graded': 1, 'num_pending': 5})
- ]})
+ {'location': 'i4x://MITx/3.091x/problem/open_ended_demo1',
+ 'problem_name': "Problem 1", 'num_graded': 3, 'num_pending': 5},
+ {'location': 'i4x://MITx/3.091x/problem/open_ended_demo2',
+ 'problem_name': "Problem 2", 'num_graded': 1, 'num_pending': 5}
+ ]}
+
+ def get_data_for_location(self, problem_location, student_id):
+ return {"version": 1, "count_graded": 3, "count_required": 3, "success": True, "student_sub_count": 1}
diff --git a/common/lib/xmodule/xmodule/tests/dummy_system.py b/common/lib/xmodule/xmodule/tests/dummy_system.py
new file mode 100644
index 0000000000..b4ca6136eb
--- /dev/null
+++ b/common/lib/xmodule/xmodule/tests/dummy_system.py
@@ -0,0 +1,55 @@
+from . import test_system
+import unittest
+from xmodule.modulestore import Location
+from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
+from xmodule.tests.test_export import DATA_DIR
+from fs.memoryfs import MemoryFS
+from mock import patch, Mock
+
+class DummySystem(ImportSystem):
+
+ @patch('xmodule.modulestore.xml.OSFS', lambda dir: MemoryFS())
+ def __init__(self, load_error_modules, org, course):
+
+ xmlstore = XMLModuleStore("data_dir", course_dirs=[], load_error_modules=load_error_modules)
+ course_id = "/".join([org, course, 'test_run'])
+ course_dir = "test_dir"
+ policy = {}
+ error_tracker = Mock()
+ parent_tracker = Mock()
+
+ super(DummySystem, self).__init__(
+ xmlstore,
+ course_id,
+ course_dir,
+ policy,
+ error_tracker,
+ parent_tracker,
+ load_error_modules=load_error_modules,
+ )
+
+ def render_template(self, template, context):
+ raise Exception("Shouldn't be called")
+
+class DummySystemUser(object):
+ test_system = test_system()
+ @staticmethod
+ def get_import_system(org, course, load_error_modules=True):
+ '''Get a dummy system'''
+ return DummySystem(load_error_modules, org, course)
+
+ def get_course(self, name):
+ """Get a test course by directory name. If there's more than one, error."""
+
+ modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name])
+ courses = modulestore.get_courses()
+ self.modulestore = modulestore
+ self.assertEquals(len(courses), 1)
+ return courses[0]
+
+ def get_module_from_location(self, location, course):
+ course = self.get_course(course)
+ if not isinstance(location, Location):
+ location = Location(location)
+ descriptor = self.modulestore.get_instance(course.id, location, depth=None)
+ return descriptor.xmodule(self.test_system)
\ No newline at end of file
diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
index 3b8019290f..fdbd37c6ab 100644
--- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
@@ -5,6 +5,8 @@ import unittest
from fs.memoryfs import MemoryFS
from mock import patch
+from dummy_system import DummySystemUser
+
from xmodule.open_ended_grading_classes.openendedchild import OpenEndedChild
from xmodule.open_ended_grading_classes.open_ended_module import OpenEndedModule
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module
@@ -42,31 +44,6 @@ class MockQueryDict(dict):
return []
return default
-class DummySystem(ImportSystem):
-
- @patch('xmodule.modulestore.xml.OSFS', lambda dir: MemoryFS())
- def __init__(self, load_error_modules):
-
- xmlstore = XMLModuleStore("data_dir", course_dirs=[], load_error_modules=load_error_modules)
- course_id = "/".join([ORG, COURSE, 'test_run'])
- course_dir = "test_dir"
- policy = {}
- error_tracker = Mock()
- parent_tracker = Mock()
-
- super(DummySystem, self).__init__(
- xmlstore,
- course_id,
- course_dir,
- policy,
- error_tracker,
- parent_tracker,
- load_error_modules=load_error_modules,
- )
-
- def render_template(self, template, context):
- raise Exception("Shouldn't be called")
-
"""
Tests for the various pieces of the CombinedOpenEndedGrading system
@@ -514,7 +491,7 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
self.assertEqual(score_dict['score'], 15.0)
self.assertEqual(score_dict['total'], 15.0)
-class OpenEndedModuleXmlTest(unittest.TestCase):
+class OpenEndedModuleXmlTest(unittest.TestCase, DummySystemUser):
problem_location = Location(["i4x", "edX", "oe_test", "combinedopenended", "SampleQuestion"])
answer = "blah blah"
assessment = [0,1]
@@ -525,37 +502,15 @@ class OpenEndedModuleXmlTest(unittest.TestCase):
send_to_queue = Mock(side_effect=[1,"queued"])
)
- @staticmethod
- def get_import_system(load_error_modules=True):
- '''Get a dummy system'''
- return DummySystem(load_error_modules)
-
- def get_course(self, name):
- """Get a test course by directory name. If there's more than one, error."""
- print "Importing {0}".format(name)
-
- modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name])
- courses = modulestore.get_courses()
- self.modulestore = modulestore
- self.assertEquals(len(courses), 1)
- return courses[0]
-
- def get_module_from_location(self, location):
- course = self.get_course('open_ended')
- if not isinstance(location, Location):
- location = Location(location)
- descriptor = self.modulestore.get_instance(course.id, location, depth=None)
- return descriptor.xmodule(self.test_system)
-
def test_open_ended_load_and_save(self):
- module = self.get_module_from_location(self.problem_location)
+ module = self.get_module_from_location(self.problem_location, COURSE)
module.handle_ajax("save_answer", {"student_answer" : self.answer})
task_one_json = json.loads(module.task_states[0])
self.assertEqual(task_one_json['child_history'][0]['answer'], self.answer)
def test_open_ended_flow_reset(self):
assessment = [0,1]
- module = self.get_module_from_location(self.problem_location)
+ module = self.get_module_from_location(self.problem_location, COURSE)
#Simulate a student saving an answer
module.handle_ajax("save_answer", {"student_answer" : self.answer})
@@ -578,7 +533,7 @@ class OpenEndedModuleXmlTest(unittest.TestCase):
def test_open_ended_flow_correct(self):
assessment = [1,1]
- module = self.get_module_from_location(self.problem_location)
+ module = self.get_module_from_location(self.problem_location, COURSE)
#Simulate a student saving an answer
module.handle_ajax("save_answer", {"student_answer" : self.answer})
diff --git a/common/lib/xmodule/xmodule/tests/test_peer_grading.py b/common/lib/xmodule/xmodule/tests/test_peer_grading.py
new file mode 100644
index 0000000000..01b3da0778
--- /dev/null
+++ b/common/lib/xmodule/xmodule/tests/test_peer_grading.py
@@ -0,0 +1,49 @@
+import unittest
+from xmodule.modulestore import Location
+import json
+from lxml import etree
+from mock import Mock
+from . import test_system
+from dummy_system import DummySystem, DummySystemUser
+
+from xmodule.peer_grading_module import PeerGradingModule, PeerGradingDescriptor
+from xmodule.open_ended_grading_classes.grading_service_module import GradingServiceError
+
+ORG = "edX"
+COURSE="open_ended"
+
+
+class PeerGradingModuleTest(unittest.TestCase, DummySystemUser):
+ location = Location(["i4x", "edX", "open_ended", "peergrading",
+ "SampleQuestion"])
+ max_score = 1
+
+ definition = ""
+ descriptor = Mock(data=definition)
+
+ def setUp(self):
+ self.test_system = test_system()
+ self.test_system.open_ended_grading_interface = None
+ self.peer_grading = PeerGradingModule(self.test_system, self.location,self.descriptor, model_data={'data': self.definition})
+
+ def test_module_closed(self):
+ closed = self.peer_grading.closed()
+ self.assertEqual(closed, False)
+
+ def test_get_html(self):
+ html = self.peer_grading.get_html()
+
+ def test_get_data(self):
+ try:
+ success, data = self.peer_grading.query_data_for_location()
+ except GradingServiceError:
+ pass
+
+ def test_get_score(self):
+ score = self.peer_grading.get_score()
+
+ def test_get_max_score(self):
+ max_score = self.peer_grading.max_score()
+
+ def get_next_submission(self):
+ success, next_submission = self.peer_grading.get_next_submission({'location' : 'blah'})
\ No newline at end of file
diff --git a/common/test/data/open_ended/course/2012_Fall.xml b/common/test/data/open_ended/course/2012_Fall.xml
index f2d16488a7..34369979ca 100644
--- a/common/test/data/open_ended/course/2012_Fall.xml
+++ b/common/test/data/open_ended/course/2012_Fall.xml
@@ -1,5 +1,6 @@
+
diff --git a/common/test/data/open_ended/peergrading/PeerGradingSample.xml b/common/test/data/open_ended/peergrading/PeerGradingSample.xml
new file mode 100644
index 0000000000..7e3afddf3a
--- /dev/null
+++ b/common/test/data/open_ended/peergrading/PeerGradingSample.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/common/test/data/open_ended/policies/2012_Fall.json b/common/test/data/open_ended/policies/2012_Fall.json
index 09b68ab400..8f8ba13437 100644
--- a/common/test/data/open_ended/policies/2012_Fall.json
+++ b/common/test/data/open_ended/policies/2012_Fall.json
@@ -9,6 +9,9 @@
"display_name": "Overview"
},
"combinedopenended/SampleQuestion": {
- "display_name": "Sample Question",
+ "display_name": "Sample Question"
},
+ "peergrading/PeerGradingSample": {
+ "display_name": "Sample Question"
+ }
}
From fd46ebd1fa950c86b8147093a644bd8b0d1561be Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Wed, 8 May 2013 19:04:07 -0400
Subject: [PATCH 041/245] Move some functions, make notification tests more
robust
---
.../lib/xmodule/xmodule/tests/dummy_system.py | 15 +++++-
.../xmodule/tests/test_combined_open_ended.py | 25 ++--------
.../xmodule/tests/test_peer_grading.py | 10 ++--
common/test/data/open_ended/course.xml | 2 +-
lms/djangoapps/open_ended_grading/tests.py | 46 +++++++++++++++----
5 files changed, 57 insertions(+), 41 deletions(-)
diff --git a/common/lib/xmodule/xmodule/tests/dummy_system.py b/common/lib/xmodule/xmodule/tests/dummy_system.py
index b4ca6136eb..d0b7513321 100644
--- a/common/lib/xmodule/xmodule/tests/dummy_system.py
+++ b/common/lib/xmodule/xmodule/tests/dummy_system.py
@@ -44,7 +44,6 @@ class DummySystemUser(object):
modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name])
courses = modulestore.get_courses()
self.modulestore = modulestore
- self.assertEquals(len(courses), 1)
return courses[0]
def get_module_from_location(self, location, course):
@@ -52,4 +51,16 @@ class DummySystemUser(object):
if not isinstance(location, Location):
location = Location(location)
descriptor = self.modulestore.get_instance(course.id, location, depth=None)
- return descriptor.xmodule(self.test_system)
\ No newline at end of file
+ return descriptor.xmodule(self.test_system)
+
+class MockQueryDict(dict):
+ """
+ Mock a query set so that it can be used with default authorization
+ """
+ def getlist(self, key, default=None):
+ try:
+ return super(MockQueryDict, self).__getitem__(key)
+ except KeyError:
+ if default is None:
+ return []
+ return default
\ No newline at end of file
diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
index fdbd37c6ab..665addefa2 100644
--- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
@@ -2,21 +2,14 @@ import json
from mock import Mock, MagicMock, ANY
import unittest
-from fs.memoryfs import MemoryFS
-from mock import patch
-
-from dummy_system import DummySystemUser
+from dummy_system import DummySystemUser, MockQueryDict
from xmodule.open_ended_grading_classes.openendedchild import OpenEndedChild
from xmodule.open_ended_grading_classes.open_ended_module import OpenEndedModule
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module
from xmodule.open_ended_grading_classes.grading_service_module import GradingServiceError
from xmodule.combined_open_ended_module import CombinedOpenEndedModule
-from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location
-from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
-
-from xmodule.tests.test_export import DATA_DIR
from lxml import etree
import capa.xqueue_interface as xqueue_interface
@@ -27,23 +20,11 @@ log = logging.getLogger(__name__)
from . import test_system
-ORG = 'test_org'
+ORG = 'edX'
COURSE = 'open_ended' # name of directory with course data
import test_util_open_ended
-class MockQueryDict(dict):
- """
- Mock a query set so that it can be used with default authorization
- """
- def getlist(self, key, default=None):
- try:
- return super(MockQueryDict, self).__getitem__(key)
- except KeyError:
- if default is None:
- return []
- return default
-
"""
Tests for the various pieces of the CombinedOpenEndedGrading system
@@ -492,7 +473,7 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
self.assertEqual(score_dict['total'], 15.0)
class OpenEndedModuleXmlTest(unittest.TestCase, DummySystemUser):
- problem_location = Location(["i4x", "edX", "oe_test", "combinedopenended", "SampleQuestion"])
+ problem_location = Location(["i4x", "edX", "open_ended", "combinedopenended", "SampleQuestion"])
answer = "blah blah"
assessment = [0,1]
hint = "blah"
diff --git a/common/lib/xmodule/xmodule/tests/test_peer_grading.py b/common/lib/xmodule/xmodule/tests/test_peer_grading.py
index 01b3da0778..3ab2e9301f 100644
--- a/common/lib/xmodule/xmodule/tests/test_peer_grading.py
+++ b/common/lib/xmodule/xmodule/tests/test_peer_grading.py
@@ -14,17 +14,13 @@ COURSE="open_ended"
class PeerGradingModuleTest(unittest.TestCase, DummySystemUser):
- location = Location(["i4x", "edX", "open_ended", "peergrading",
- "SampleQuestion"])
- max_score = 1
-
- definition = ""
- descriptor = Mock(data=definition)
+ problem_location = Location(["i4x", "edX", "open_ended", "peergrading",
+ "PeerGradingSample"])
def setUp(self):
self.test_system = test_system()
self.test_system.open_ended_grading_interface = None
- self.peer_grading = PeerGradingModule(self.test_system, self.location,self.descriptor, model_data={'data': self.definition})
+ self.peer_grading = self.get_module_from_location(self.problem_location, COURSE)
def test_module_closed(self):
closed = self.peer_grading.closed()
diff --git a/common/test/data/open_ended/course.xml b/common/test/data/open_ended/course.xml
index bf3ed687fb..9848343f58 100644
--- a/common/test/data/open_ended/course.xml
+++ b/common/test/data/open_ended/course.xml
@@ -1 +1 @@
-
+
diff --git a/lms/djangoapps/open_ended_grading/tests.py b/lms/djangoapps/open_ended_grading/tests.py
index 93d27d8e24..542c366cab 100644
--- a/lms/djangoapps/open_ended_grading/tests.py
+++ b/lms/djangoapps/open_ended_grading/tests.py
@@ -84,7 +84,13 @@ class TestStaffGradingService(LoginEnrollmentTestCase):
data = {'location': self.location}
r = self.check_for_post_code(200, url, data)
- d = json.loads(r.content)
+
+ d = r.content
+ try:
+ d = json.loads(d)
+ except Exception:
+ pass
+
self.assertTrue(d['success'])
self.assertEquals(d['submission_id'], self.mock_service.cnt)
self.assertIsNotNone(d['submission'])
@@ -112,7 +118,11 @@ class TestStaffGradingService(LoginEnrollmentTestCase):
data.update({'skipped' : True})
r = self.check_for_post_code(200, url, data)
- d = json.loads(r.content)
+ d = r.content
+ try:
+ d = json.loads(d)
+ except Exception:
+ pass
self.assertTrue(d['success'], str(d))
self.assertEquals(d['submission_id'], self.mock_service.cnt)
@@ -129,7 +139,11 @@ class TestStaffGradingService(LoginEnrollmentTestCase):
data = {}
r = self.check_for_post_code(200, url, data)
- d = json.loads(r.content)
+ d = r.content
+ try:
+ d = json.loads(d)
+ except Exception:
+ pass
self.assertTrue(d['success'], str(d))
self.assertIsNotNone(d['problem_list'])
@@ -179,7 +193,11 @@ class TestPeerGradingService(LoginEnrollmentTestCase):
data = {'location': self.location}
r = self.peer_module.get_next_submission(data)
- d = json.loads(r)
+ d = r
+ try:
+ d = json.loads(d)
+ except Exception:
+ pass
self.assertTrue(d['success'])
self.assertIsNotNone(d['submission_id'])
self.assertIsNotNone(d['prompt'])
@@ -213,7 +231,11 @@ class TestPeerGradingService(LoginEnrollmentTestCase):
qdict.keys = data.keys
r = self.peer_module.save_grade(qdict)
- d = json.loads(r)
+ d = r
+ try:
+ d = json.loads(d)
+ except Exception:
+ pass
self.assertTrue(d['success'])
def test_save_grade_missing_keys(self):
@@ -225,7 +247,11 @@ class TestPeerGradingService(LoginEnrollmentTestCase):
def test_is_calibrated_success(self):
data = {'location': self.location}
r = self.peer_module.is_student_calibrated(data)
- d = json.loads(r)
+ d = r
+ try:
+ d = json.loads(d)
+ except Exception:
+ pass
self.assertTrue(d['success'])
self.assertTrue('calibrated' in d)
@@ -239,9 +265,11 @@ class TestPeerGradingService(LoginEnrollmentTestCase):
data = {'location': self.location}
r = self.peer_module.show_calibration_essay(data)
- d = json.loads(r)
- log.debug(d)
- log.debug(type(d))
+ d = r
+ try:
+ d = json.loads(r)
+ except Exception:
+ pass
self.assertTrue(d['success'])
self.assertIsNotNone(d['submission_id'])
self.assertIsNotNone(d['prompt'])
From a2e5bd071b57f19fc36d0a9f8f41c0a46a3a27bf Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Wed, 8 May 2013 19:17:14 -0400
Subject: [PATCH 042/245] Add in tests for peer grading module
---
.../peer_grading_service.py | 4 --
.../xmodule/tests/test_peer_grading.py | 38 ++++++++++++++++++-
2 files changed, 36 insertions(+), 6 deletions(-)
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py
index 19cc013cb7..418784f618 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py
@@ -132,10 +132,6 @@ class MockPeerGradingService(object):
def get_problem_list(self, course_id, grader_id):
return {'success': True,
'problem_list': [
- {'location': 'i4x://MITx/3.091x/problem/open_ended_demo1',
- 'problem_name': "Problem 1", 'num_graded': 3, 'num_pending': 5},
- {'location': 'i4x://MITx/3.091x/problem/open_ended_demo2',
- 'problem_name': "Problem 2", 'num_graded': 1, 'num_pending': 5}
]}
def get_data_for_location(self, problem_location, student_id):
diff --git a/common/lib/xmodule/xmodule/tests/test_peer_grading.py b/common/lib/xmodule/xmodule/tests/test_peer_grading.py
index 3ab2e9301f..3ecfc759e5 100644
--- a/common/lib/xmodule/xmodule/tests/test_peer_grading.py
+++ b/common/lib/xmodule/xmodule/tests/test_peer_grading.py
@@ -4,7 +4,7 @@ import json
from lxml import etree
from mock import Mock
from . import test_system
-from dummy_system import DummySystem, DummySystemUser
+from dummy_system import DummySystem, DummySystemUser, MockQueryDict
from xmodule.peer_grading_module import PeerGradingModule, PeerGradingDescriptor
from xmodule.open_ended_grading_classes.grading_service_module import GradingServiceError
@@ -16,6 +16,17 @@ COURSE="open_ended"
class PeerGradingModuleTest(unittest.TestCase, DummySystemUser):
problem_location = Location(["i4x", "edX", "open_ended", "peergrading",
"PeerGradingSample"])
+ calibrated_dict = {'location' : "blah"}
+ save_dict = MockQueryDict()
+ save_dict.update({
+ 'location' : "blah",
+ 'submission_id' : 1,
+ 'submission_key' : "",
+ 'score': 1,
+ 'feedback' : "",
+ 'rubric_scores[]' : [0,1],
+ 'submission_flagged': False,
+ })
def setUp(self):
self.test_system = test_system()
@@ -42,4 +53,27 @@ class PeerGradingModuleTest(unittest.TestCase, DummySystemUser):
max_score = self.peer_grading.max_score()
def get_next_submission(self):
- success, next_submission = self.peer_grading.get_next_submission({'location' : 'blah'})
\ No newline at end of file
+ success, next_submission = self.peer_grading.get_next_submission({'location' : 'blah'})
+
+ def test_save_grade(self):
+ self.peer_grading.save_grade(self.save_dict)
+
+ def test_is_student_calibrated(self):
+ calibrated_dict = {'location' : "blah"}
+ self.peer_grading.is_student_calibrated(self.calibrated_dict)
+
+ def test_show_calibration_essay(self):
+
+ self.peer_grading.show_calibration_essay(self.calibrated_dict)
+
+ def test_save_calibration_essay(self):
+ self.peer_grading.save_calibration_essay(self.save_dict)
+
+ def test_peer_grading_closed(self):
+ self.peer_grading.peer_grading_closed()
+
+ def test_peer_grading_problem(self):
+ self.peer_grading.peer_grading_problem(self.calibrated_dict)
+
+ def test_get_instance_state(self):
+ self.peer_grading.get_instance_state()
\ No newline at end of file
From 7b8b168f2e8f005f7e89e4e68012e5e7c0e85aa1 Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Wed, 8 May 2013 19:21:12 -0400
Subject: [PATCH 043/245] Move the mockquerydict
---
common/lib/xmodule/xmodule/tests/dummy_system.py | 14 +-------------
.../xmodule/tests/test_combined_open_ended.py | 3 ++-
.../xmodule/xmodule/tests/test_peer_grading.py | 3 ++-
.../xmodule/tests/test_util_open_ended.py | 16 ++++++++++++++--
4 files changed, 19 insertions(+), 17 deletions(-)
diff --git a/common/lib/xmodule/xmodule/tests/dummy_system.py b/common/lib/xmodule/xmodule/tests/dummy_system.py
index d0b7513321..02fa0450f6 100644
--- a/common/lib/xmodule/xmodule/tests/dummy_system.py
+++ b/common/lib/xmodule/xmodule/tests/dummy_system.py
@@ -51,16 +51,4 @@ class DummySystemUser(object):
if not isinstance(location, Location):
location = Location(location)
descriptor = self.modulestore.get_instance(course.id, location, depth=None)
- return descriptor.xmodule(self.test_system)
-
-class MockQueryDict(dict):
- """
- Mock a query set so that it can be used with default authorization
- """
- def getlist(self, key, default=None):
- try:
- return super(MockQueryDict, self).__getitem__(key)
- except KeyError:
- if default is None:
- return []
- return default
\ No newline at end of file
+ return descriptor.xmodule(self.test_system)
\ No newline at end of file
diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
index 665addefa2..157d403ffe 100644
--- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
@@ -2,7 +2,8 @@ import json
from mock import Mock, MagicMock, ANY
import unittest
-from dummy_system import DummySystemUser, MockQueryDict
+from dummy_system import DummySystemUser
+from test_util_open_ended import MockQueryDict
from xmodule.open_ended_grading_classes.openendedchild import OpenEndedChild
from xmodule.open_ended_grading_classes.open_ended_module import OpenEndedModule
diff --git a/common/lib/xmodule/xmodule/tests/test_peer_grading.py b/common/lib/xmodule/xmodule/tests/test_peer_grading.py
index 3ecfc759e5..630f693333 100644
--- a/common/lib/xmodule/xmodule/tests/test_peer_grading.py
+++ b/common/lib/xmodule/xmodule/tests/test_peer_grading.py
@@ -4,7 +4,8 @@ import json
from lxml import etree
from mock import Mock
from . import test_system
-from dummy_system import DummySystem, DummySystemUser, MockQueryDict
+from dummy_system import DummySystem, DummySystemUser
+from test_util_open_ended import MockQueryDict
from xmodule.peer_grading_module import PeerGradingModule, PeerGradingDescriptor
from xmodule.open_ended_grading_classes.grading_service_module import GradingServiceError
diff --git a/common/lib/xmodule/xmodule/tests/test_util_open_ended.py b/common/lib/xmodule/xmodule/tests/test_util_open_ended.py
index db580f1e0e..088a5af87d 100644
--- a/common/lib/xmodule/xmodule/tests/test_util_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_util_open_ended.py
@@ -1,5 +1,5 @@
OPEN_ENDED_GRADING_INTERFACE = {
- 'url': 'http://127.0.0.1:3033/',
+ 'url': 'blah/',
'username': 'incorrect',
'password': 'incorrect',
'staff_grading': 'staff_grading',
@@ -11,4 +11,16 @@ S3_INTERFACE = {
'aws_access_key': "",
'aws_secret_key': "",
"aws_bucket_name": "",
-}
\ No newline at end of file
+}
+
+class MockQueryDict(dict):
+ """
+ Mock a query set so that it can be used with default authorization
+ """
+ def getlist(self, key, default=None):
+ try:
+ return super(MockQueryDict, self).__getitem__(key)
+ except KeyError:
+ if default is None:
+ return []
+ return default
\ No newline at end of file
From 8323cc7c60174105361262b912ac1c25fa1fc93d Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Thu, 9 May 2013 09:44:16 -0400
Subject: [PATCH 044/245] Refactor tests
---
.../lib/xmodule/xmodule/tests/dummy_system.py | 54 ----------
.../xmodule/tests/test_combined_open_ended.py | 102 +++++++++---------
.../xmodule/tests/test_peer_grading.py | 37 ++++---
.../xmodule/tests/test_util_open_ended.py | 27 ++++-
.../test/data/open_ended/course/2012_Fall.xml | 1 +
.../peergrading/PeerGradingScored.xml | 1 +
6 files changed, 98 insertions(+), 124 deletions(-)
delete mode 100644 common/lib/xmodule/xmodule/tests/dummy_system.py
create mode 100644 common/test/data/open_ended/peergrading/PeerGradingScored.xml
diff --git a/common/lib/xmodule/xmodule/tests/dummy_system.py b/common/lib/xmodule/xmodule/tests/dummy_system.py
deleted file mode 100644
index 02fa0450f6..0000000000
--- a/common/lib/xmodule/xmodule/tests/dummy_system.py
+++ /dev/null
@@ -1,54 +0,0 @@
-from . import test_system
-import unittest
-from xmodule.modulestore import Location
-from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
-from xmodule.tests.test_export import DATA_DIR
-from fs.memoryfs import MemoryFS
-from mock import patch, Mock
-
-class DummySystem(ImportSystem):
-
- @patch('xmodule.modulestore.xml.OSFS', lambda dir: MemoryFS())
- def __init__(self, load_error_modules, org, course):
-
- xmlstore = XMLModuleStore("data_dir", course_dirs=[], load_error_modules=load_error_modules)
- course_id = "/".join([org, course, 'test_run'])
- course_dir = "test_dir"
- policy = {}
- error_tracker = Mock()
- parent_tracker = Mock()
-
- super(DummySystem, self).__init__(
- xmlstore,
- course_id,
- course_dir,
- policy,
- error_tracker,
- parent_tracker,
- load_error_modules=load_error_modules,
- )
-
- def render_template(self, template, context):
- raise Exception("Shouldn't be called")
-
-class DummySystemUser(object):
- test_system = test_system()
- @staticmethod
- def get_import_system(org, course, load_error_modules=True):
- '''Get a dummy system'''
- return DummySystem(load_error_modules, org, course)
-
- def get_course(self, name):
- """Get a test course by directory name. If there's more than one, error."""
-
- modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name])
- courses = modulestore.get_courses()
- self.modulestore = modulestore
- return courses[0]
-
- def get_module_from_location(self, location, course):
- course = self.get_course(course)
- if not isinstance(location, Location):
- location = Location(location)
- descriptor = self.modulestore.get_instance(course.id, location, depth=None)
- return descriptor.xmodule(self.test_system)
\ No newline at end of file
diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
index 157d403ffe..2faecce08d 100644
--- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
@@ -2,8 +2,7 @@ import json
from mock import Mock, MagicMock, ANY
import unittest
-from dummy_system import DummySystemUser
-from test_util_open_ended import MockQueryDict
+from test_util_open_ended import MockQueryDict, DummyModulestore
from xmodule.open_ended_grading_classes.openendedchild import OpenEndedChild
from xmodule.open_ended_grading_classes.open_ended_module import OpenEndedModule
@@ -19,7 +18,7 @@ import logging
log = logging.getLogger(__name__)
-from . import test_system
+from .import test_system
ORG = 'edX'
COURSE = 'open_ended' # name of directory with course data
@@ -68,7 +67,7 @@ class OpenEndedChildTest(unittest.TestCase):
def setUp(self):
self.test_system = test_system()
self.openendedchild = OpenEndedChild(self.test_system, self.location,
- self.definition, self.descriptor, self.static_data, self.metadata)
+ self.definition, self.descriptor, self.static_data, self.metadata)
def test_latest_answer_empty(self):
@@ -115,7 +114,7 @@ class OpenEndedChildTest(unittest.TestCase):
post_assessment = "Post assessment"
self.openendedchild.record_latest_post_assessment(post_assessment)
self.assertEqual(post_assessment,
- self.openendedchild.latest_post_assessment(self.test_system))
+ self.openendedchild.latest_post_assessment(self.test_system))
def test_get_score(self):
new_answer = "New Answer"
@@ -142,12 +141,12 @@ class OpenEndedChildTest(unittest.TestCase):
self.openendedchild.new_history_entry(new_answer)
self.openendedchild.record_latest_score(self.static_data['max_score'])
self.assertEqual(self.openendedchild.is_last_response_correct(),
- 'correct')
+ 'correct')
self.openendedchild.new_history_entry(new_answer)
self.openendedchild.record_latest_score(0)
self.assertEqual(self.openendedchild.is_last_response_correct(),
- 'incorrect')
+ 'incorrect')
class OpenEndedModuleTest(unittest.TestCase):
@@ -202,7 +201,7 @@ class OpenEndedModuleTest(unittest.TestCase):
'default_queuename': 'testqueue',
'waittime': 1}
self.openendedmodule = OpenEndedModule(self.test_system, self.location,
- self.definition, self.descriptor, self.static_data, self.metadata)
+ self.definition, self.descriptor, self.static_data, self.metadata)
def test_message_post(self):
get = {'feedback': 'feedback text',
@@ -364,21 +363,21 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
descriptor = Mock(data=full_definition)
test_system = test_system()
combinedoe_container = CombinedOpenEndedModule(test_system,
- location,
- descriptor,
- model_data={'data': full_definition, 'weight': '1'})
+ location,
+ descriptor,
+ model_data={'data': full_definition, 'weight': '1'})
def setUp(self):
# TODO: this constructor call is definitely wrong, but neither branch
# of the merge matches the module constructor. Someone (Vik?) should fix this.
self.combinedoe = CombinedOpenEndedV1Module(self.test_system,
- self.location,
- self.definition,
- self.descriptor,
- static_data=self.static_data,
- metadata=self.metadata,
- instance_state=self.static_data)
+ self.location,
+ self.definition,
+ self.descriptor,
+ static_data=self.static_data,
+ metadata=self.metadata,
+ instance_state=self.static_data)
def test_get_tag_name(self):
name = self.combinedoe.get_tag_name("Tag")
@@ -433,12 +432,12 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
definition = {'prompt': etree.XML(self.prompt), 'rubric': etree.XML(self.rubric), 'task_xml': xml}
descriptor = Mock(data=definition)
combinedoe = CombinedOpenEndedV1Module(self.test_system,
- self.location,
- definition,
- descriptor,
- static_data=self.static_data,
- metadata=self.metadata,
- instance_state=self.static_data)
+ self.location,
+ definition,
+ descriptor,
+ static_data=self.static_data,
+ metadata=self.metadata,
+ instance_state=self.static_data)
changed = combinedoe.update_task_states()
self.assertFalse(changed)
@@ -463,44 +462,46 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
'task_xml': [self.task_xml1, self.task_xml2]}
descriptor = Mock(data=definition)
combinedoe = CombinedOpenEndedV1Module(self.test_system,
- self.location,
- definition,
- descriptor,
- static_data=self.static_data,
- metadata=self.metadata,
- instance_state=instance_state)
+ self.location,
+ definition,
+ descriptor,
+ static_data=self.static_data,
+ metadata=self.metadata,
+ instance_state=instance_state)
score_dict = combinedoe.get_score()
self.assertEqual(score_dict['score'], 15.0)
self.assertEqual(score_dict['total'], 15.0)
-class OpenEndedModuleXmlTest(unittest.TestCase, DummySystemUser):
+
+class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
problem_location = Location(["i4x", "edX", "open_ended", "combinedopenended", "SampleQuestion"])
answer = "blah blah"
- assessment = [0,1]
+ assessment = [0, 1]
hint = "blah"
+
def setUp(self):
self.test_system = test_system()
self.test_system.xqueue['interface'] = Mock(
- send_to_queue = Mock(side_effect=[1,"queued"])
- )
+ send_to_queue=Mock(side_effect=[1, "queued"])
+ )
def test_open_ended_load_and_save(self):
module = self.get_module_from_location(self.problem_location, COURSE)
- module.handle_ajax("save_answer", {"student_answer" : self.answer})
+ module.handle_ajax("save_answer", {"student_answer": self.answer})
task_one_json = json.loads(module.task_states[0])
self.assertEqual(task_one_json['child_history'][0]['answer'], self.answer)
def test_open_ended_flow_reset(self):
- assessment = [0,1]
+ assessment = [0, 1]
module = self.get_module_from_location(self.problem_location, COURSE)
#Simulate a student saving an answer
- module.handle_ajax("save_answer", {"student_answer" : self.answer})
+ module.handle_ajax("save_answer", {"student_answer": self.answer})
status = module.handle_ajax("get_status", {})
#Mock a student submitting an assessment
assessment_dict = MockQueryDict()
- assessment_dict.update({'assessment' : sum(assessment), 'score_list[]' : assessment})
+ assessment_dict.update({'assessment': sum(assessment), 'score_list[]': assessment})
module.handle_ajax("save_assessment", assessment_dict)
task_one_json = json.loads(module.task_states[0])
self.assertEqual(json.loads(task_one_json['child_history'][0]['post_assessment']), assessment)
@@ -514,16 +515,16 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummySystemUser):
module.handle_ajax("reset", {})
def test_open_ended_flow_correct(self):
- assessment = [1,1]
+ assessment = [1, 1]
module = self.get_module_from_location(self.problem_location, COURSE)
#Simulate a student saving an answer
- module.handle_ajax("save_answer", {"student_answer" : self.answer})
+ module.handle_ajax("save_answer", {"student_answer": self.answer})
status = module.handle_ajax("get_status", {})
#Mock a student submitting an assessment
assessment_dict = MockQueryDict()
- assessment_dict.update({'assessment' : sum(assessment), 'score_list[]' : assessment})
+ assessment_dict.update({'assessment': sum(assessment), 'score_list[]': assessment})
module.handle_ajax("save_assessment", assessment_dict)
task_one_json = json.loads(module.task_states[0])
self.assertEqual(json.loads(task_one_json['child_history'][0]['post_assessment']), assessment)
@@ -546,16 +547,17 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummySystemUser):
#Make a fake reply from the queue
queue_reply = {
- 'queuekey' : "",
- 'xqueue_body' : json.dumps({
- 'score' : 0,
- 'feedback' : json.dumps({"spelling": "Spelling: Ok.", "grammar": "Grammar: Ok.", "markup-text": " all of us can think of a book that we hope none of our children or any other children have taken off the shelf . but if i have the right to remove that book from the shelf that work i abhor then you also have exactly the same right and so does everyone else . and then we have no books left on the shelf for any of us . katherinepaterson , author write a persuasive essay to a newspaper reflecting your vies on censorship in libraries . do you believe that certain materials , such as books , music , movies , magazines , etc . , should be removed from the shelves if they are found offensive ? support your position with convincing arguments from your own experience , observations , and or reading . "}),
- 'grader_type' : "ML",
- 'success' : True,
- 'grader_id' : 1,
- 'submission_id' : 1,
- 'rubric_xml' : "Writing Applications0 Language Conventions 0",
- 'rubric_scores_complete' : True,
+ 'queuekey': "",
+ 'xqueue_body': json.dumps({
+ 'score': 0,
+ 'feedback': json.dumps({"spelling": "Spelling: Ok.", "grammar": "Grammar: Ok.",
+ "markup-text": " all of us can think of a book that we hope none of our children or any other children have taken off the shelf . but if i have the right to remove that book from the shelf that work i abhor then you also have exactly the same right and so does everyone else . and then we have no books left on the shelf for any of us . katherinepaterson , author write a persuasive essay to a newspaper reflecting your vies on censorship in libraries . do you believe that certain materials , such as books , music , movies , magazines , etc . , should be removed from the shelves if they are found offensive ? support your position with convincing arguments from your own experience , observations , and or reading . "}),
+ 'grader_type': "ML",
+ 'success': True,
+ 'grader_id': 1,
+ 'submission_id': 1,
+ 'rubric_xml': "Writing Applications0 Language Conventions 0",
+ 'rubric_scores_complete': True,
})
}
diff --git a/common/lib/xmodule/xmodule/tests/test_peer_grading.py b/common/lib/xmodule/xmodule/tests/test_peer_grading.py
index 630f693333..d7cd7a4afd 100644
--- a/common/lib/xmodule/xmodule/tests/test_peer_grading.py
+++ b/common/lib/xmodule/xmodule/tests/test_peer_grading.py
@@ -1,33 +1,33 @@
import unittest
from xmodule.modulestore import Location
-import json
-from lxml import etree
-from mock import Mock
-from . import test_system
-from dummy_system import DummySystem, DummySystemUser
-from test_util_open_ended import MockQueryDict
+from .import test_system
+from test_util_open_ended import MockQueryDict, DummyModulestore
from xmodule.peer_grading_module import PeerGradingModule, PeerGradingDescriptor
from xmodule.open_ended_grading_classes.grading_service_module import GradingServiceError
+import logging
+
+log = logging.getLogger(__name__)
+
ORG = "edX"
-COURSE="open_ended"
+COURSE = "open_ended"
-class PeerGradingModuleTest(unittest.TestCase, DummySystemUser):
+class PeerGradingModuleTest(unittest.TestCase, DummyModulestore):
problem_location = Location(["i4x", "edX", "open_ended", "peergrading",
- "PeerGradingSample"])
- calibrated_dict = {'location' : "blah"}
+ "PeerGradingSample"])
+ calibrated_dict = {'location': "blah"}
save_dict = MockQueryDict()
save_dict.update({
- 'location' : "blah",
- 'submission_id' : 1,
- 'submission_key' : "",
+ 'location': "blah",
+ 'submission_id': 1,
+ 'submission_key': "",
'score': 1,
- 'feedback' : "",
- 'rubric_scores[]' : [0,1],
+ 'feedback': "",
+ 'rubric_scores[]': [0, 1],
'submission_flagged': False,
- })
+ })
def setUp(self):
self.test_system = test_system()
@@ -54,17 +54,16 @@ class PeerGradingModuleTest(unittest.TestCase, DummySystemUser):
max_score = self.peer_grading.max_score()
def get_next_submission(self):
- success, next_submission = self.peer_grading.get_next_submission({'location' : 'blah'})
+ success, next_submission = self.peer_grading.get_next_submission({'location': 'blah'})
def test_save_grade(self):
self.peer_grading.save_grade(self.save_dict)
def test_is_student_calibrated(self):
- calibrated_dict = {'location' : "blah"}
+ calibrated_dict = {'location': "blah"}
self.peer_grading.is_student_calibrated(self.calibrated_dict)
def test_show_calibration_essay(self):
-
self.peer_grading.show_calibration_essay(self.calibrated_dict)
def test_save_calibration_essay(self):
diff --git a/common/lib/xmodule/xmodule/tests/test_util_open_ended.py b/common/lib/xmodule/xmodule/tests/test_util_open_ended.py
index 088a5af87d..38f083de38 100644
--- a/common/lib/xmodule/xmodule/tests/test_util_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_util_open_ended.py
@@ -1,3 +1,8 @@
+from .import test_system
+from xmodule.modulestore import Location
+from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
+from xmodule.tests.test_export import DATA_DIR
+
OPEN_ENDED_GRADING_INTERFACE = {
'url': 'blah/',
'username': 'incorrect',
@@ -17,10 +22,30 @@ class MockQueryDict(dict):
"""
Mock a query set so that it can be used with default authorization
"""
+
def getlist(self, key, default=None):
try:
return super(MockQueryDict, self).__getitem__(key)
except KeyError:
if default is None:
return []
- return default
\ No newline at end of file
+ return default
+
+
+class DummyModulestore(object):
+ test_system = test_system()
+
+ def get_course(self, name):
+ """Get a test course by directory name. If there's more than one, error."""
+
+ modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name])
+ courses = modulestore.get_courses()
+ self.modulestore = modulestore
+ return courses[0]
+
+ def get_module_from_location(self, location, course):
+ course = self.get_course(course)
+ if not isinstance(location, Location):
+ location = Location(location)
+ descriptor = self.modulestore.get_instance(course.id, location, depth=None)
+ return descriptor.xmodule(self.test_system)
\ No newline at end of file
diff --git a/common/test/data/open_ended/course/2012_Fall.xml b/common/test/data/open_ended/course/2012_Fall.xml
index 34369979ca..32c810174b 100644
--- a/common/test/data/open_ended/course/2012_Fall.xml
+++ b/common/test/data/open_ended/course/2012_Fall.xml
@@ -2,5 +2,6 @@
+
diff --git a/common/test/data/open_ended/peergrading/PeerGradingScored.xml b/common/test/data/open_ended/peergrading/PeerGradingScored.xml
new file mode 100644
index 0000000000..b2380b1e1b
--- /dev/null
+++ b/common/test/data/open_ended/peergrading/PeerGradingScored.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
From 09d34a02ccd3d36c2b1b2d4b014d100a698c07e4 Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Thu, 9 May 2013 09:50:32 -0400
Subject: [PATCH 045/245] Add in some comments
---
.../xmodule/tests/test_combined_open_ended.py | 29 +++++++++
.../xmodule/tests/test_peer_grading.py | 59 ++++++++++++++++++-
.../xmodule/tests/test_util_open_ended.py | 5 +-
3 files changed, 89 insertions(+), 4 deletions(-)
diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
index 2faecce08d..1d67e94376 100644
--- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
@@ -35,6 +35,9 @@ OpenEndedModule
class OpenEndedChildTest(unittest.TestCase):
+ """
+ Test the open ended child class
+ """
location = Location(["i4x", "edX", "sa_test", "selfassessment",
"SampleQuestion"])
@@ -150,6 +153,9 @@ class OpenEndedChildTest(unittest.TestCase):
class OpenEndedModuleTest(unittest.TestCase):
+ """
+ Test the open ended module class
+ """
location = Location(["i4x", "edX", "sa_test", "selfassessment",
"SampleQuestion"])
@@ -291,6 +297,9 @@ class OpenEndedModuleTest(unittest.TestCase):
class CombinedOpenEndedModuleTest(unittest.TestCase):
+ """
+ Unit tests for the combined open ended xmodule
+ """
location = Location(["i4x", "edX", "open_ended", "combinedopenended",
"SampleQuestion"])
definition_template = """
@@ -474,6 +483,9 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
+ """
+ Test the student flow in the combined open ended xmodule
+ """
problem_location = Location(["i4x", "edX", "open_ended", "combinedopenended", "SampleQuestion"])
answer = "blah blah"
assessment = [0, 1]
@@ -486,12 +498,23 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
)
def test_open_ended_load_and_save(self):
+ """
+ See if we can load the module and save an answer
+ @return:
+ """
+ #Load the module
module = self.get_module_from_location(self.problem_location, COURSE)
+
+ #Try saving an answer
module.handle_ajax("save_answer", {"student_answer": self.answer})
task_one_json = json.loads(module.task_states[0])
self.assertEqual(task_one_json['child_history'][0]['answer'], self.answer)
def test_open_ended_flow_reset(self):
+ """
+ Test the flow of the module if we complete the self assessment step and then reset
+ @return:
+ """
assessment = [0, 1]
module = self.get_module_from_location(self.problem_location, COURSE)
@@ -515,7 +538,13 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
module.handle_ajax("reset", {})
def test_open_ended_flow_correct(self):
+ """
+ Test a two step problem where the student first goes through the self assessment step, and then the
+ open ended step.
+ @return:
+ """
assessment = [1, 1]
+ #Load the module
module = self.get_module_from_location(self.problem_location, COURSE)
#Simulate a student saving an answer
diff --git a/common/lib/xmodule/xmodule/tests/test_peer_grading.py b/common/lib/xmodule/xmodule/tests/test_peer_grading.py
index d7cd7a4afd..49c696f741 100644
--- a/common/lib/xmodule/xmodule/tests/test_peer_grading.py
+++ b/common/lib/xmodule/xmodule/tests/test_peer_grading.py
@@ -15,6 +15,10 @@ COURSE = "open_ended"
class PeerGradingModuleTest(unittest.TestCase, DummyModulestore):
+ """
+ Test peer grading xmodule at the unit level. More detailed tests are difficult, as the module relies on an
+ external grading service.
+ """
problem_location = Location(["i4x", "edX", "open_ended", "peergrading",
"PeerGradingSample"])
calibrated_dict = {'location': "blah"}
@@ -30,50 +34,99 @@ class PeerGradingModuleTest(unittest.TestCase, DummyModulestore):
})
def setUp(self):
+ """
+ Create a peer grading module from a test system
+ @return:
+ """
self.test_system = test_system()
self.test_system.open_ended_grading_interface = None
self.peer_grading = self.get_module_from_location(self.problem_location, COURSE)
def test_module_closed(self):
+ """
+ Test if peer grading is closed
+ @return:
+ """
closed = self.peer_grading.closed()
self.assertEqual(closed, False)
def test_get_html(self):
+ """
+ Test to see if the module can be rendered
+ @return:
+ """
html = self.peer_grading.get_html()
def test_get_data(self):
+ """
+ Try getting data from the external grading service
+ @return:
+ """
try:
success, data = self.peer_grading.query_data_for_location()
except GradingServiceError:
pass
def test_get_score(self):
+ """
+ Test getting the score
+ @return:
+ """
score = self.peer_grading.get_score()
def test_get_max_score(self):
+ """
+ Test getting the max score
+ @return:
+ """
max_score = self.peer_grading.max_score()
def get_next_submission(self):
+ """
+ Test to see if we can get the next mock submission
+ @return:
+ """
success, next_submission = self.peer_grading.get_next_submission({'location': 'blah'})
def test_save_grade(self):
+ """
+ Test if we can save the grade
+ @return:
+ """
self.peer_grading.save_grade(self.save_dict)
def test_is_student_calibrated(self):
+ """
+ Check to see if the student has calibrated yet
+ @return:
+ """
calibrated_dict = {'location': "blah"}
self.peer_grading.is_student_calibrated(self.calibrated_dict)
def test_show_calibration_essay(self):
+ """
+ Test showing the calibration essay
+ @return:
+ """
self.peer_grading.show_calibration_essay(self.calibrated_dict)
def test_save_calibration_essay(self):
+ """
+ Test saving the calibration essay
+ @return:
+ """
self.peer_grading.save_calibration_essay(self.save_dict)
- def test_peer_grading_closed(self):
- self.peer_grading.peer_grading_closed()
-
def test_peer_grading_problem(self):
+ """
+ See if we can render a single problem
+ @return:
+ """
self.peer_grading.peer_grading_problem(self.calibrated_dict)
def test_get_instance_state(self):
+ """
+ Get the instance state dict
+ @return:
+ """
self.peer_grading.get_instance_state()
\ No newline at end of file
diff --git a/common/lib/xmodule/xmodule/tests/test_util_open_ended.py b/common/lib/xmodule/xmodule/tests/test_util_open_ended.py
index 38f083de38..f269a8a002 100644
--- a/common/lib/xmodule/xmodule/tests/test_util_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_util_open_ended.py
@@ -20,7 +20,7 @@ S3_INTERFACE = {
class MockQueryDict(dict):
"""
- Mock a query set so that it can be used with default authorization
+ Mock a query dict so that it can be used in test classes
"""
def getlist(self, key, default=None):
@@ -33,6 +33,9 @@ class MockQueryDict(dict):
class DummyModulestore(object):
+ """
+ A mixin that allows test classes to have convenience functions to get a module given a location
+ """
test_system = test_system()
def get_course(self, name):
From 8c12eb78c92c76b26b6d42ba16362e60a84f078a Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Thu, 9 May 2013 09:54:57 -0400
Subject: [PATCH 046/245] Fix some exceptions
---
.../xmodule/open_ended_grading_classes/open_ended_module.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
index afdfeef6de..a4b4afe499 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
@@ -496,8 +496,8 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
grader_types.append(score_result['grader_type'])
try:
feedback_dict = json.loads(score_result['feedback'][i])
- except:
- pass
+ except Exception:
+ feedback_dict = score_result['feedback'][i]
feedback_dicts.append(feedback_dict)
grader_ids.append(score_result['grader_id'][i])
submission_ids.append(score_result['submission_id'])
@@ -515,7 +515,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
feedback_items = [feedback]
try:
feedback_dict = json.loads(score_result['feedback'])
- except:
+ except Exception:
feedback_dict = score_result.get('feedback', '')
feedback_dicts = [feedback_dict]
grader_ids = [score_result['grader_id']]
From 7a1ef62ee31fe7d52fc619514ac4dbc31f1bddce Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Thu, 9 May 2013 09:56:02 -0400
Subject: [PATCH 047/245] Do some code reformatting
---
.../open_ended_module.py | 14 ++++-----
.../openendedchild.py | 12 ++++----
.../peer_grading_service.py | 30 +++++++++----------
3 files changed, 28 insertions(+), 28 deletions(-)
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
index a4b4afe499..266d332a7f 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
@@ -191,7 +191,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
}
(error, msg) = qinterface.send_to_queue(header=xheader,
- body=json.dumps(contents))
+ body=json.dumps(contents))
#Convert error to a success value
success = True
@@ -225,8 +225,8 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
str(len(self.child_history)))
xheader = xqueue_interface.make_xheader(lms_callback_url=system.xqueue['construct_callback'](),
- lms_key=queuekey,
- queue_name=self.queue_name)
+ lms_key=queuekey,
+ queue_name=self.queue_name)
contents = self.payload.copy()
@@ -244,7 +244,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
# Submit request. When successful, 'msg' is the prior length of the queue
qinterface.send_to_queue(header=xheader,
- body=json.dumps(contents))
+ body=json.dumps(contents))
# State associated with the queueing request
queuestate = {'key': queuekey,
@@ -402,7 +402,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
if not response_items['success']:
return system.render_template("{0}/open_ended_error.html".format(self.TEMPLATE_DIR),
- {'errors': feedback})
+ {'errors': feedback})
feedback_template = system.render_template("{0}/open_ended_feedback.html".format(self.TEMPLATE_DIR), {
'grader_type': response_items['grader_type'],
@@ -546,7 +546,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
return ""
feedback_dict = self._parse_score_msg(self.child_history[-1].get('post_assessment', ""), system,
- join_feedback=join_feedback)
+ join_feedback=join_feedback)
if not short_feedback:
return feedback_dict['feedback'] if feedback_dict['valid'] else ''
if feedback_dict['valid']:
@@ -711,7 +711,7 @@ class OpenEndedDescriptor():
template_dir_name = "openended"
def __init__(self, system):
- self.system =system
+ self.system = system
@classmethod
def definition_from_xml(cls, xml_object, system):
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py
index d5889636ed..2d8d3805f1 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py
@@ -102,7 +102,7 @@ class OpenEndedChild(object):
if system.open_ended_grading_interface:
self.peer_gs = PeerGradingService(system.open_ended_grading_interface, system)
self.controller_qs = controller_query_service.ControllerQueryService(system.open_ended_grading_interface,
- system)
+ system)
else:
self.peer_gs = MockPeerGradingService()
self.controller_qs = None
@@ -180,8 +180,8 @@ class OpenEndedChild(object):
try:
answer = autolink_html(answer)
cleaner = Cleaner(style=True, links=True, add_nofollow=False, page_structure=True, safe_attrs_only=True,
- host_whitelist=open_ended_image_submission.TRUSTED_IMAGE_DOMAINS,
- whitelist_tags=set(['embed', 'iframe', 'a', 'img']))
+ host_whitelist=open_ended_image_submission.TRUSTED_IMAGE_DOMAINS,
+ whitelist_tags=set(['embed', 'iframe', 'a', 'img']))
clean_html = cleaner.clean_html(answer)
clean_html = re.sub(r'
$', '', re.sub(r'^
', '', clean_html))
except:
@@ -282,7 +282,7 @@ class OpenEndedChild(object):
"""
#This is a dev_facing_error
log.warning("Open ended child state out sync. state: %r, get: %r. %s",
- self.child_state, get, msg)
+ self.child_state, get, msg)
#This is a student_facing_error
return {'success': False,
'error': 'The problem state got out-of-sync. Please try reloading the page.'}
@@ -343,7 +343,7 @@ class OpenEndedChild(object):
try:
image_data.seek(0)
success, s3_public_url = open_ended_image_submission.upload_to_s3(image_data, image_key,
- self.s3_interface)
+ self.s3_interface)
except:
log.exception("Could not upload image to S3.")
@@ -462,7 +462,7 @@ class OpenEndedChild(object):
allowed_to_submit = False
#This is a student_facing_error
error_message = error_string.format(count_required - count_graded, count_graded, count_required,
- student_sub_count)
+ student_sub_count)
return success, allowed_to_submit, error_message
def get_eta(self):
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py
index 418784f618..56bd1ec0a8 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py
@@ -37,7 +37,7 @@ class PeerGradingService(GradingService):
def get_next_submission(self, problem_location, grader_id):
response = self.get(self.get_next_submission_url,
- {'location': problem_location, 'grader_id': grader_id})
+ {'location': problem_location, 'grader_id': grader_id})
return self.try_to_decode(self._render_rubric(response))
def save_grade(self, location, grader_id, submission_id, score, feedback, submission_key, rubric_scores,
@@ -101,12 +101,12 @@ without making actual service calls to the grading controller
class MockPeerGradingService(object):
def get_next_submission(self, problem_location, grader_id):
return {'success': True,
- 'submission_id': 1,
- 'submission_key': "",
- 'student_response': 'fake student response',
- 'prompt': 'fake submission prompt',
- 'rubric': 'fake rubric',
- 'max_score': 4}
+ 'submission_id': 1,
+ 'submission_key': "",
+ 'student_response': 'fake student response',
+ 'prompt': 'fake submission prompt',
+ 'rubric': 'fake rubric',
+ 'max_score': 4}
def save_grade(self, location, grader_id, submission_id,
score, feedback, submission_key, rubric_scores, submission_flagged):
@@ -117,12 +117,12 @@ class MockPeerGradingService(object):
def show_calibration_essay(self, problem_location, grader_id):
return {'success': True,
- 'submission_id': 1,
- 'submission_key': '',
- 'student_response': 'fake student response',
- 'prompt': 'fake submission prompt',
- 'rubric': 'fake rubric',
- 'max_score': 4}
+ 'submission_id': 1,
+ 'submission_key': '',
+ 'student_response': 'fake student response',
+ 'prompt': 'fake submission prompt',
+ 'rubric': 'fake rubric',
+ 'max_score': 4}
def save_calibration_essay(self, problem_location, grader_id,
calibration_essay_id, submission_key, score,
@@ -131,8 +131,8 @@ class MockPeerGradingService(object):
def get_problem_list(self, course_id, grader_id):
return {'success': True,
- 'problem_list': [
- ]}
+ 'problem_list': [
+ ]}
def get_data_for_location(self, problem_location, student_id):
return {"version": 1, "count_graded": 3, "count_required": 3, "success": True, "student_sub_count": 1}
From f9e97cb935e943baed19f1192e6dc371952245eb Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Thu, 9 May 2013 10:05:48 -0400
Subject: [PATCH 048/245] Add test for proper saving
---
.../xmodule/tests/test_combined_open_ended.py | 5 ++++
.../xmodule/tests/test_peer_grading.py | 23 ++++++++++++++++++-
.../xmodule/tests/test_util_open_ended.py | 8 +++----
.../peergrading/PeerGradingScored.xml | 2 +-
4 files changed, 32 insertions(+), 6 deletions(-)
diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
index 1d67e94376..48ea6e7911 100644
--- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
@@ -496,6 +496,7 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
self.test_system.xqueue['interface'] = Mock(
send_to_queue=Mock(side_effect=[1, "queued"])
)
+ self.setup_modulestore(COURSE)
def test_open_ended_load_and_save(self):
"""
@@ -510,6 +511,10 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
task_one_json = json.loads(module.task_states[0])
self.assertEqual(task_one_json['child_history'][0]['answer'], self.answer)
+ module = self.get_module_from_location(self.problem_location, COURSE)
+ task_one_json = json.loads(module.task_states[0])
+ self.assertEqual(task_one_json['child_history'][0]['answer'], self.answer)
+
def test_open_ended_flow_reset(self):
"""
Test the flow of the module if we complete the self assessment step and then reset
diff --git a/common/lib/xmodule/xmodule/tests/test_peer_grading.py b/common/lib/xmodule/xmodule/tests/test_peer_grading.py
index 49c696f741..036ab4a85b 100644
--- a/common/lib/xmodule/xmodule/tests/test_peer_grading.py
+++ b/common/lib/xmodule/xmodule/tests/test_peer_grading.py
@@ -40,6 +40,7 @@ class PeerGradingModuleTest(unittest.TestCase, DummyModulestore):
"""
self.test_system = test_system()
self.test_system.open_ended_grading_interface = None
+ self.setup_modulestore(COURSE)
self.peer_grading = self.get_module_from_location(self.problem_location, COURSE)
def test_module_closed(self):
@@ -129,4 +130,24 @@ class PeerGradingModuleTest(unittest.TestCase, DummyModulestore):
Get the instance state dict
@return:
"""
- self.peer_grading.get_instance_state()
\ No newline at end of file
+ self.peer_grading.get_instance_state()
+
+class PeerGradingModuleScoredTest(unittest.TestCase, DummyModulestore):
+ """
+ Test peer grading xmodule at the unit level. More detailed tests are difficult, as the module relies on an
+ external grading service.
+ """
+ problem_location = Location(["i4x", "edX", "open_ended", "peergrading",
+ "PeerGradingScored"])
+ def setUp(self):
+ """
+ Create a peer grading module from a test system
+ @return:
+ """
+ self.test_system = test_system()
+ self.test_system.open_ended_grading_interface = None
+ self.setup_modulestore(COURSE)
+
+ def test_metadata_load(self):
+ peer_grading = self.get_module_from_location(self.problem_location, COURSE)
+ self.assertEqual(peer_grading.closed(), False)
\ No newline at end of file
diff --git a/common/lib/xmodule/xmodule/tests/test_util_open_ended.py b/common/lib/xmodule/xmodule/tests/test_util_open_ended.py
index f269a8a002..3737586232 100644
--- a/common/lib/xmodule/xmodule/tests/test_util_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_util_open_ended.py
@@ -38,12 +38,12 @@ class DummyModulestore(object):
"""
test_system = test_system()
+ def setup_modulestore(self, name):
+ self.modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name])
+
def get_course(self, name):
"""Get a test course by directory name. If there's more than one, error."""
-
- modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name])
- courses = modulestore.get_courses()
- self.modulestore = modulestore
+ courses = self.modulestore.get_courses()
return courses[0]
def get_module_from_location(self, location, course):
diff --git a/common/test/data/open_ended/peergrading/PeerGradingScored.xml b/common/test/data/open_ended/peergrading/PeerGradingScored.xml
index b2380b1e1b..6398a9b4c5 100644
--- a/common/test/data/open_ended/peergrading/PeerGradingScored.xml
+++ b/common/test/data/open_ended/peergrading/PeerGradingScored.xml
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
From f72659fa2e34fabda38260f75b250585e6e71800 Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Thu, 9 May 2013 10:15:07 -0400
Subject: [PATCH 049/245] Add in asserts
---
.../xmodule/tests/test_peer_grading.py | 25 ++++++++++++-------
1 file changed, 16 insertions(+), 9 deletions(-)
diff --git a/common/lib/xmodule/xmodule/tests/test_peer_grading.py b/common/lib/xmodule/xmodule/tests/test_peer_grading.py
index 036ab4a85b..a0877eab81 100644
--- a/common/lib/xmodule/xmodule/tests/test_peer_grading.py
+++ b/common/lib/xmodule/xmodule/tests/test_peer_grading.py
@@ -2,6 +2,7 @@ import unittest
from xmodule.modulestore import Location
from .import test_system
from test_util_open_ended import MockQueryDict, DummyModulestore
+import json
from xmodule.peer_grading_module import PeerGradingModule, PeerGradingDescriptor
from xmodule.open_ended_grading_classes.grading_service_module import GradingServiceError
@@ -63,10 +64,8 @@ class PeerGradingModuleTest(unittest.TestCase, DummyModulestore):
Try getting data from the external grading service
@return:
"""
- try:
- success, data = self.peer_grading.query_data_for_location()
- except GradingServiceError:
- pass
+ success, data = self.peer_grading.query_data_for_location()
+ self.assertEqual(success, True)
def test_get_score(self):
"""
@@ -74,6 +73,7 @@ class PeerGradingModuleTest(unittest.TestCase, DummyModulestore):
@return:
"""
score = self.peer_grading.get_score()
+ self.assertEquals(score['score'], None)
def test_get_max_score(self):
"""
@@ -81,6 +81,7 @@ class PeerGradingModuleTest(unittest.TestCase, DummyModulestore):
@return:
"""
max_score = self.peer_grading.max_score()
+ self.assertEquals(max_score, None)
def get_next_submission(self):
"""
@@ -88,13 +89,15 @@ class PeerGradingModuleTest(unittest.TestCase, DummyModulestore):
@return:
"""
success, next_submission = self.peer_grading.get_next_submission({'location': 'blah'})
+ self.assertEqual(success, True)
def test_save_grade(self):
"""
Test if we can save the grade
@return:
"""
- self.peer_grading.save_grade(self.save_dict)
+ response = self.peer_grading.save_grade(self.save_dict)
+ self.assertEqual(response['success'], True)
def test_is_student_calibrated(self):
"""
@@ -102,28 +105,32 @@ class PeerGradingModuleTest(unittest.TestCase, DummyModulestore):
@return:
"""
calibrated_dict = {'location': "blah"}
- self.peer_grading.is_student_calibrated(self.calibrated_dict)
+ response = self.peer_grading.is_student_calibrated(self.calibrated_dict)
+ self.assertEqual(response['success'], True)
def test_show_calibration_essay(self):
"""
Test showing the calibration essay
@return:
"""
- self.peer_grading.show_calibration_essay(self.calibrated_dict)
+ response = self.peer_grading.show_calibration_essay(self.calibrated_dict)
+ self.assertEqual(response['success'], True)
def test_save_calibration_essay(self):
"""
Test saving the calibration essay
@return:
"""
- self.peer_grading.save_calibration_essay(self.save_dict)
+ response = self.peer_grading.save_calibration_essay(self.save_dict)
+ self.assertEqual(response['success'], True)
def test_peer_grading_problem(self):
"""
See if we can render a single problem
@return:
"""
- self.peer_grading.peer_grading_problem(self.calibrated_dict)
+ response = self.peer_grading.peer_grading_problem(self.calibrated_dict)
+ self.assertEqual(response['success'], True)
def test_get_instance_state(self):
"""
From 682d06bcff1fd07b8ba158ee18562e0b729073b3 Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Thu, 9 May 2013 11:27:44 -0400
Subject: [PATCH 050/245] Fix a lot of pep8 violations
---
.../xmodule/combined_open_ended_module.py | 13 ++--
.../combined_open_ended_modulev1.py | 12 ++-
.../grading_service_module.py | 3 +-
.../open_ended_module.py | 76 ++++++++++++-------
.../openendedchild.py | 15 ++--
.../xmodule/xmodule/peer_grading_module.py | 1 -
.../xmodule/tests/test_combined_open_ended.py | 56 +++++++-------
.../xmodule/tests/test_util_open_ended.py | 4 +-
8 files changed, 99 insertions(+), 81 deletions(-)
diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py
index 67ff206e89..f4074283fe 100644
--- a/common/lib/xmodule/xmodule/combined_open_ended_module.py
+++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py
@@ -104,11 +104,14 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
icon_class = 'problem'
- js = {'coffee':
- [resource_string(__name__, 'js/src/combinedopenended/display.coffee'),
- resource_string(__name__, 'js/src/collapsible.coffee'),
- resource_string(__name__, 'js/src/javascript_loader.coffee'),
- ]}
+ js = {
+ 'coffee':
+ [
+ resource_string(__name__, 'js/src/combinedopenended/display.coffee'),
+ resource_string(__name__, 'js/src/collapsible.coffee'),
+ resource_string(__name__, 'js/src/javascript_loader.coffee'),
+ ]
+ }
js_module_name = "CombinedOpenEnded"
css = {'scss': [resource_string(__name__, 'css/combinedopenended/display.scss')]}
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py
index 6767851d3a..1404f52300 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py
@@ -294,9 +294,8 @@ class CombinedOpenEndedV1Module():
if self.current_task_number > 0:
last_response_data = self.get_last_response(self.current_task_number - 1)
current_response_data = self.get_current_attributes(self.current_task_number)
-
if (current_response_data['min_score_to_attempt'] > last_response_data['score']
- or current_response_data['max_score_to_attempt'] < last_response_data['score']):
+ or current_response_data['max_score_to_attempt'] < last_response_data['score']):
self.state = self.DONE
self.ready_to_reset = True
@@ -662,9 +661,10 @@ class CombinedOpenEndedV1Module():
return {
'success': False,
#This is a student_facing_error
- 'error': ('You have attempted this question {0} times. '
- 'You are only allowed to attempt it {1} times.').format(
- self.student_attempts, self.attempts)
+ 'error': (
+ 'You have attempted this question {0} times. '
+ 'You are only allowed to attempt it {1} times.'
+ ).format(self.student_attempts, self.attempts)
}
self.state = self.INITIAL
self.ready_to_reset = False
@@ -815,7 +815,6 @@ class CombinedOpenEndedV1Module():
'error': 'The problem state got out-of-sync. Please try reloading the page.'}
-
class CombinedOpenEndedV1Descriptor():
"""
Module for adding combined open ended questions
@@ -861,7 +860,6 @@ class CombinedOpenEndedV1Descriptor():
return {'task_xml': parse_task('task'), 'prompt': parse('prompt'), 'rubric': parse('rubric')}
-
def definition_to_xml(self, resource_fs):
'''Return an xml element representing this definition.'''
elt = etree.Element('combinedopenended')
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/grading_service_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/grading_service_module.py
index f3f6568b1e..b16f0618bb 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/grading_service_module.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/grading_service_module.py
@@ -76,7 +76,6 @@ class GradingService(object):
return r.text
-
def _try_with_login(self, operation):
"""
Call operation(), which should return a requests response object. If
@@ -87,7 +86,7 @@ class GradingService(object):
"""
response = operation()
if (response.json
- and response.json.get('success') == False
+ and response.json.get('success') is False
and response.json.get('error') == 'login_required'):
# apparrently we aren't logged in. Try to fix that.
r = self._login()
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
index 266d332a7f..7ba046b2ad 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
@@ -72,7 +72,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
self._parse(oeparam, self.child_prompt, self.child_rubric, system)
- if self.child_created == True and self.child_state == self.ASSESSING:
+ if self.child_created is True and self.child_state == self.ASSESSING:
self.child_created = False
self.send_to_grader(self.latest_answer(), system)
self.child_created = False
@@ -159,9 +159,11 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
score = int(survey_responses['score'])
except:
#This is a dev_facing_error
- error_message = ("Could not parse submission id, grader id, "
- "or feedback from message_post ajax call. Here is the message data: {0}".format(
- survey_responses))
+ error_message = (
+ "Could not parse submission id, grader id, "
+ "or feedback from message_post ajax call. "
+ "Here is the message data: {0}".format(survey_responses)
+ )
log.exception(error_message)
#This is a student_facing_error
return {'success': False, 'msg': "There was an error saving your feedback. Please contact course staff."}
@@ -179,8 +181,9 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
queue_name=self.message_queue_name
)
- student_info = {'anonymous_student_id': anonymous_student_id,
- 'submission_time': qtime,
+ student_info = {
+ 'anonymous_student_id': anonymous_student_id,
+ 'submission_time': qtime,
}
contents = {
'feedback': feedback,
@@ -190,8 +193,10 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
'student_info': json.dumps(student_info),
}
- (error, msg) = qinterface.send_to_queue(header=xheader,
- body=json.dumps(contents))
+ (error, msg) = qinterface.send_to_queue(
+ header=xheader,
+ body=json.dumps(contents)
+ )
#Convert error to a success value
success = True
@@ -224,15 +229,18 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
anonymous_student_id +
str(len(self.child_history)))
- xheader = xqueue_interface.make_xheader(lms_callback_url=system.xqueue['construct_callback'](),
+ xheader = xqueue_interface.make_xheader(
+ lms_callback_url=system.xqueue['construct_callback'](),
lms_key=queuekey,
- queue_name=self.queue_name)
+ queue_name=self.queue_name
+ )
contents = self.payload.copy()
# Metadata related to the student submission revealed to the external grader
- student_info = {'anonymous_student_id': anonymous_student_id,
- 'submission_time': qtime,
+ student_info = {
+ 'anonymous_student_id': anonymous_student_id,
+ 'submission_time': qtime,
}
#Update contents with student response and student info
@@ -243,12 +251,16 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
})
# Submit request. When successful, 'msg' is the prior length of the queue
- qinterface.send_to_queue(header=xheader,
- body=json.dumps(contents))
+ qinterface.send_to_queue(
+ header=xheader,
+ body=json.dumps(contents)
+ )
# State associated with the queueing request
- queuestate = {'key': queuekey,
- 'time': qtime, }
+ queuestate = {
+ 'key': queuekey,
+ 'time': qtime,
+ }
return True
def _update_score(self, score_msg, queuekey, system):
@@ -302,11 +314,13 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
# We want to display available feedback in a particular order.
# This dictionary specifies which goes first--lower first.
- priorities = {# These go at the start of the feedback
- 'spelling': 0,
- 'grammar': 1,
- # needs to be after all the other feedback
- 'markup_text': 3}
+ priorities = {
+ # These go at the start of the feedback
+ 'spelling': 0,
+ 'grammar': 1,
+ # needs to be after all the other feedback
+ 'markup_text': 3
+ }
do_not_render = ['topicality', 'prompt-overlap']
default_priority = 2
@@ -393,7 +407,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
rubric_feedback = ""
feedback = self._convert_longform_feedback_to_html(response_items)
rubric_scores = []
- if response_items['rubric_scores_complete'] == True:
+ if response_items['rubric_scores_complete'] is True:
rubric_renderer = CombinedOpenEndedRubric(system, True)
rubric_dict = rubric_renderer.render_rubric(response_items['rubric_xml'])
success = rubric_dict['success']
@@ -401,8 +415,10 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
rubric_scores = rubric_dict['rubric_scores']
if not response_items['success']:
- return system.render_template("{0}/open_ended_error.html".format(self.TEMPLATE_DIR),
- {'errors': feedback})
+ return system.render_template(
+ "{0}/open_ended_error.html".format(self.TEMPLATE_DIR),
+ {'errors': feedback}
+ )
feedback_template = system.render_template("{0}/open_ended_feedback.html".format(self.TEMPLATE_DIR), {
'grader_type': response_items['grader_type'],
@@ -545,8 +561,11 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
if not self.child_history:
return ""
- feedback_dict = self._parse_score_msg(self.child_history[-1].get('post_assessment', ""), system,
- join_feedback=join_feedback)
+ feedback_dict = self._parse_score_msg(
+ self.child_history[-1].get('post_assessment', ""),
+ system,
+ join_feedback=join_feedback
+ )
if not short_feedback:
return feedback_dict['feedback'] if feedback_dict['valid'] else ''
if feedback_dict['valid']:
@@ -734,8 +753,9 @@ class OpenEndedDescriptor():
"""Assumes that xml_object has child k"""
return xml_object.xpath(k)[0]
- return {'oeparam': parse('openendedparam')}
-
+ return {
+ 'oeparam': parse('openendedparam')
+ }
def definition_to_xml(self, resource_fs):
'''Return an xml element representing this definition.'''
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py
index 2d8d3805f1..7dc8d99451 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py
@@ -101,8 +101,9 @@ class OpenEndedChild(object):
# completion (doesn't matter if you self-assessed correct/incorrect).
if system.open_ended_grading_interface:
self.peer_gs = PeerGradingService(system.open_ended_grading_interface, system)
- self.controller_qs = controller_query_service.ControllerQueryService(system.open_ended_grading_interface,
- system)
+ self.controller_qs = controller_query_service.ControllerQueryService(
+ system.open_ended_grading_interface,system
+ )
else:
self.peer_gs = MockPeerGradingService()
self.controller_qs = None
@@ -180,8 +181,8 @@ class OpenEndedChild(object):
try:
answer = autolink_html(answer)
cleaner = Cleaner(style=True, links=True, add_nofollow=False, page_structure=True, safe_attrs_only=True,
- host_whitelist=open_ended_image_submission.TRUSTED_IMAGE_DOMAINS,
- whitelist_tags=set(['embed', 'iframe', 'a', 'img']))
+ host_whitelist=open_ended_image_submission.TRUSTED_IMAGE_DOMAINS,
+ whitelist_tags=set(['embed', 'iframe', 'a', 'img']))
clean_html = cleaner.clean_html(answer)
clean_html = re.sub(r'
$', '', re.sub(r'^
', '', clean_html))
except:
@@ -282,7 +283,7 @@ class OpenEndedChild(object):
"""
#This is a dev_facing_error
log.warning("Open ended child state out sync. state: %r, get: %r. %s",
- self.child_state, get, msg)
+ self.child_state, get, msg)
#This is a student_facing_error
return {'success': False,
'error': 'The problem state got out-of-sync. Please try reloading the page.'}
@@ -343,7 +344,7 @@ class OpenEndedChild(object):
try:
image_data.seek(0)
success, s3_public_url = open_ended_image_submission.upload_to_s3(image_data, image_key,
- self.s3_interface)
+ self.s3_interface)
except:
log.exception("Could not upload image to S3.")
@@ -462,7 +463,7 @@ class OpenEndedChild(object):
allowed_to_submit = False
#This is a student_facing_error
error_message = error_string.format(count_required - count_graded, count_graded, count_required,
- student_sub_count)
+ student_sub_count)
return success, allowed_to_submit, error_message
def get_eta(self):
diff --git a/common/lib/xmodule/xmodule/peer_grading_module.py b/common/lib/xmodule/xmodule/peer_grading_module.py
index 1ad31922f5..eebfbe22e5 100644
--- a/common/lib/xmodule/xmodule/peer_grading_module.py
+++ b/common/lib/xmodule/xmodule/peer_grading_module.py
@@ -498,7 +498,6 @@ class PeerGradingModule(PeerGradingFields, XModule):
log.error("Problem {0} does not exist in this course".format(location))
raise
-
for problem in problem_list:
problem_location = problem['location']
descriptor = _find_corresponding_module_for_location(problem_location)
diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
index 48ea6e7911..d8f4fbbca1 100644
--- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
@@ -18,7 +18,7 @@ import logging
log = logging.getLogger(__name__)
-from .import test_system
+from . import test_system
ORG = 'edX'
COURSE = 'open_ended' # name of directory with course data
@@ -70,8 +70,7 @@ class OpenEndedChildTest(unittest.TestCase):
def setUp(self):
self.test_system = test_system()
self.openendedchild = OpenEndedChild(self.test_system, self.location,
- self.definition, self.descriptor, self.static_data, self.metadata)
-
+ self.definition, self.descriptor, self.static_data, self.metadata)
def test_latest_answer_empty(self):
answer = self.openendedchild.latest_answer()
@@ -117,7 +116,7 @@ class OpenEndedChildTest(unittest.TestCase):
post_assessment = "Post assessment"
self.openendedchild.record_latest_post_assessment(post_assessment)
self.assertEqual(post_assessment,
- self.openendedchild.latest_post_assessment(self.test_system))
+ self.openendedchild.latest_post_assessment(self.test_system))
def test_get_score(self):
new_answer = "New Answer"
@@ -144,12 +143,12 @@ class OpenEndedChildTest(unittest.TestCase):
self.openendedchild.new_history_entry(new_answer)
self.openendedchild.record_latest_score(self.static_data['max_score'])
self.assertEqual(self.openendedchild.is_last_response_correct(),
- 'correct')
+ 'correct')
self.openendedchild.new_history_entry(new_answer)
self.openendedchild.record_latest_score(0)
self.assertEqual(self.openendedchild.is_last_response_correct(),
- 'incorrect')
+ 'incorrect')
class OpenEndedModuleTest(unittest.TestCase):
@@ -207,7 +206,7 @@ class OpenEndedModuleTest(unittest.TestCase):
'default_queuename': 'testqueue',
'waittime': 1}
self.openendedmodule = OpenEndedModule(self.test_system, self.location,
- self.definition, self.descriptor, self.static_data, self.metadata)
+ self.definition, self.descriptor, self.static_data, self.metadata)
def test_message_post(self):
get = {'feedback': 'feedback text',
@@ -372,21 +371,20 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
descriptor = Mock(data=full_definition)
test_system = test_system()
combinedoe_container = CombinedOpenEndedModule(test_system,
- location,
- descriptor,
- model_data={'data': full_definition, 'weight': '1'})
-
+ location,
+ descriptor,
+ model_data={'data': full_definition, 'weight': '1'})
def setUp(self):
# TODO: this constructor call is definitely wrong, but neither branch
# of the merge matches the module constructor. Someone (Vik?) should fix this.
self.combinedoe = CombinedOpenEndedV1Module(self.test_system,
- self.location,
- self.definition,
- self.descriptor,
- static_data=self.static_data,
- metadata=self.metadata,
- instance_state=self.static_data)
+ self.location,
+ self.definition,
+ self.descriptor,
+ static_data=self.static_data,
+ metadata=self.metadata,
+ instance_state=self.static_data)
def test_get_tag_name(self):
name = self.combinedoe.get_tag_name("Tag")
@@ -441,12 +439,12 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
definition = {'prompt': etree.XML(self.prompt), 'rubric': etree.XML(self.rubric), 'task_xml': xml}
descriptor = Mock(data=definition)
combinedoe = CombinedOpenEndedV1Module(self.test_system,
- self.location,
- definition,
- descriptor,
- static_data=self.static_data,
- metadata=self.metadata,
- instance_state=self.static_data)
+ self.location,
+ definition,
+ descriptor,
+ static_data=self.static_data,
+ metadata=self.metadata,
+ instance_state=self.static_data)
changed = combinedoe.update_task_states()
self.assertFalse(changed)
@@ -471,12 +469,12 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
'task_xml': [self.task_xml1, self.task_xml2]}
descriptor = Mock(data=definition)
combinedoe = CombinedOpenEndedV1Module(self.test_system,
- self.location,
- definition,
- descriptor,
- static_data=self.static_data,
- metadata=self.metadata,
- instance_state=instance_state)
+ self.location,
+ definition,
+ descriptor,
+ static_data=self.static_data,
+ metadata=self.metadata,
+ instance_state=instance_state)
score_dict = combinedoe.get_score()
self.assertEqual(score_dict['score'], 15.0)
self.assertEqual(score_dict['total'], 15.0)
diff --git a/common/lib/xmodule/xmodule/tests/test_util_open_ended.py b/common/lib/xmodule/xmodule/tests/test_util_open_ended.py
index 3737586232..42d6410ebd 100644
--- a/common/lib/xmodule/xmodule/tests/test_util_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_util_open_ended.py
@@ -18,11 +18,11 @@ S3_INTERFACE = {
"aws_bucket_name": "",
}
+
class MockQueryDict(dict):
"""
Mock a query dict so that it can be used in test classes
"""
-
def getlist(self, key, default=None):
try:
return super(MockQueryDict, self).__getitem__(key)
@@ -51,4 +51,4 @@ class DummyModulestore(object):
if not isinstance(location, Location):
location = Location(location)
descriptor = self.modulestore.get_instance(course.id, location, depth=None)
- return descriptor.xmodule(self.test_system)
\ No newline at end of file
+ return descriptor.xmodule(self.test_system)
From f1f65c563ef06064e41853f3197d42cb8dc41f12 Mon Sep 17 00:00:00 2001
From: Arthur Barrett
Date: Thu, 9 May 2013 11:51:24 -0400
Subject: [PATCH 051/245] fix failing unit test in lms.envs.test env
---
lms/djangoapps/notes/api.py | 16 ++++++++++++++--
lms/djangoapps/notes/tests.py | 7 +++++--
2 files changed, 19 insertions(+), 4 deletions(-)
diff --git a/lms/djangoapps/notes/api.py b/lms/djangoapps/notes/api.py
index 9849c35d33..ea334faf10 100644
--- a/lms/djangoapps/notes/api.py
+++ b/lms/djangoapps/notes/api.py
@@ -28,6 +28,13 @@ API_SETTINGS = {
#----------------------------------------------------------------------#
# API requests are routed through api_request() using the resource map.
+def api_enabled(request, course_id):
+ '''
+ Returns True if the api is enabled for the course, otherwise False.
+ '''
+ course = _get_course(request, course_id)
+ return notes_enabled_for_course(course)
+
@login_required
def api_request(request, course_id, **kwargs):
'''
@@ -37,8 +44,7 @@ def api_request(request, course_id, **kwargs):
'''
# Verify that notes are enabled for the course
- course = get_course_with_access(request.user, course_id, 'load')
- if not notes_enabled_for_course(course):
+ if not api_enabled(request, course_id):
log.debug('Notes not enabled for course')
raise Http404
@@ -91,6 +97,12 @@ def api_format(request, response, data):
content = json.dumps(data)
return [content_type, content]
+def _get_course(request, course_id):
+ '''
+ Helper function to load and return a user's course.
+ '''
+ return get_course_with_access(request.user, course_id, 'load')
+
#----------------------------------------------------------------------#
# API actions exposed via the resource map.
diff --git a/lms/djangoapps/notes/tests.py b/lms/djangoapps/notes/tests.py
index 9861863561..c48202dcc2 100644
--- a/lms/djangoapps/notes/tests.py
+++ b/lms/djangoapps/notes/tests.py
@@ -14,8 +14,6 @@ import logging
from . import utils, api, models
-logging.disable(logging.CRITICAL) # remove debugging from the log output
-
class UtilsTest(TestCase):
def setUp(self):
'''
@@ -41,12 +39,17 @@ class UtilsTest(TestCase):
{'type': 'foo'},
{'name': 'My Notes', 'type': 'notes'},
{'type':'bar'}]
+
self.assertTrue(utils.notes_enabled_for_course(self.course))
class ApiTest(TestCase):
+
def setUp(self):
self.client = Client()
+ # Mocks
+ api.api_enabled = (lambda request, course_id: True)
+
# Create two accounts
self.password = 'abc'
self.student = User.objects.create_user('student', 'student@test.com', self.password)
From 244261f1cfdb7edb655c5555c5d90c1cd3900855 Mon Sep 17 00:00:00 2001
From: Arthur Barrett
Date: Thu, 9 May 2013 14:31:38 -0400
Subject: [PATCH 052/245] added more unit tests
---
lms/djangoapps/notes/api.py | 8 +--
lms/djangoapps/notes/tests.py | 128 ++++++++++++++++++++++++++++++----
2 files changed, 119 insertions(+), 17 deletions(-)
diff --git a/lms/djangoapps/notes/api.py b/lms/djangoapps/notes/api.py
index ea334faf10..43dc2805c8 100644
--- a/lms/djangoapps/notes/api.py
+++ b/lms/djangoapps/notes/api.py
@@ -13,7 +13,7 @@ API_SETTINGS = {
# Version
'META': {'name': 'Notes API', 'version': 1},
- # Maps resources to HTTP methods
+ # Maps resources to HTTP methods and actions
'RESOURCE_MAP': {
'root': {'GET': 'root'},
'notes': {'GET': 'index', 'POST': 'create'},
@@ -43,12 +43,12 @@ def api_request(request, course_id, **kwargs):
disabled for the course.
'''
- # Verify that notes are enabled for the course
+ # Verify that the api should be accessible to this course
if not api_enabled(request, course_id):
log.debug('Notes not enabled for course')
raise Http404
- # Locate and validate the requested resource
+ # Locate the requested resource
resource_map = API_SETTINGS.get('RESOURCE_MAP', {})
resource_name = kwargs.pop('resource')
resource_method = request.method
@@ -62,7 +62,7 @@ def api_request(request, course_id, **kwargs):
log.debug('Resource "{0}" does not support method "{1}"'.format(resource_name, resource_method))
raise Http404
- # Find the associated function definition and execute the request
+ # Execute the action associated with the resource
func = resource.get(resource_method)
module = globals()
if func not in module:
diff --git a/lms/djangoapps/notes/tests.py b/lms/djangoapps/notes/tests.py
index c48202dcc2..6417ad6965 100644
--- a/lms/djangoapps/notes/tests.py
+++ b/lms/djangoapps/notes/tests.py
@@ -1,5 +1,5 @@
"""
-Unit tests for the notes API and model.
+Unit tests for the notes app.
"""
from django.test import TestCase
@@ -7,8 +7,8 @@ from django.test.client import Client
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
-from collections import namedtuple
-from random import random
+import collections
+import unittest
import json
import logging
@@ -20,7 +20,7 @@ class UtilsTest(TestCase):
Setup a dummy course-like object with a tabs field that can be
accessed via attribute lookup.
'''
- self.course = namedtuple('DummyCourse', ['tabs'])
+ self.course = collections.namedtuple('DummyCourse', ['tabs'])
self.course.tabs = []
def test_notes_not_enabled(self):
@@ -48,11 +48,12 @@ class ApiTest(TestCase):
self.client = Client()
# Mocks
- api.api_enabled = (lambda request, course_id: True)
+ api.api_enabled = self.mock_api_enabled(True)
# Create two accounts
self.password = 'abc'
self.student = User.objects.create_user('student', 'student@test.com', self.password)
+ self.student2 = User.objects.create_user('student2', 'student2@test.com', self.password)
self.instructor = User.objects.create_user('instructor', 'instructor@test.com', self.password)
self.course_id = 'HarvardX/CB22x/The_Ancient_Greek_Hero'
self.note = {
@@ -68,15 +69,31 @@ class ApiTest(TestCase):
'tags':'a,b,c'
}
- def login(self):
- self.client.login(username=self.student.username, password=self.password)
+ def mock_api_enabled(self, is_enabled):
+ return (lambda request, course_id: is_enabled)
- def url(self, name):
- return reverse(name, kwargs={'course_id':self.course_id})
+ def login(self, as_student=None):
+ username = None
+ password = self.password
- def create_notes(self, num_notes):
- notes = [ models.Note(**self.note) for n in range(num_notes) ]
- models.Note.objects.bulk_create(notes)
+ if as_student is None:
+ username = self.student.username
+ else:
+ username = as_student.username
+
+ self.client.login(username=username, password=password)
+
+ def url(self, name, args={}):
+ args.update({'course_id':self.course_id})
+ return reverse(name, kwargs=args)
+
+ def create_notes(self, num_notes, create=True):
+ notes = []
+ for n in range(num_notes):
+ note = models.Note(**self.note)
+ if create:
+ note.save()
+ notes.append(note)
return notes
def test_root(self):
@@ -103,7 +120,7 @@ class ApiTest(TestCase):
self.assertEqual(len(content), 0)
def test_index_with_notes(self):
- num_notes = 7
+ num_notes = 3
self.login()
self.create_notes(num_notes)
@@ -112,6 +129,7 @@ class ApiTest(TestCase):
self.assertNotEqual(resp.content, '')
content = json.loads(resp.content)
+ self.assertIsInstance(content, list)
self.assertEqual(len(content), num_notes)
def test_index_max_notes(self):
@@ -126,4 +144,88 @@ class ApiTest(TestCase):
self.assertNotEqual(resp.content, '')
content = json.loads(resp.content)
+ self.assertIsInstance(content, list)
self.assertEqual(len(content), MAX_LIMIT)
+
+ def test_create_note(self):
+ self.login()
+
+ notes = self.create_notes(1)
+ self.assertEqual(len(notes), 1)
+
+ note_dict = notes[0].as_dict()
+ excluded_fields = ['id', 'user_id', 'created', 'updated']
+ note = dict([(k, v) for k,v in note_dict.items() if k not in excluded_fields])
+
+ resp = self.client.post(self.url('notes_api_notes'), json.dumps(note),
+ content_type='application/json',
+ HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+
+ self.assertEqual(resp.status_code, 303)
+ self.assertEqual(len(resp.content), 0)
+
+ def test_create_empty_notes(self):
+ self.login()
+
+ for empty_test in [None, [], '']:
+ resp = self.client.post(self.url('notes_api_notes'), json.dumps(empty_test),
+ content_type='application/json',
+ HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+ self.assertEqual(resp.status_code, 500)
+
+ def test_create_note_missing_ranges(self):
+ self.login()
+
+ notes = self.create_notes(1)
+ self.assertEqual(len(notes), 1)
+ note_dict = notes[0].as_dict()
+
+ excluded_fields = ['id', 'user_id', 'created', 'updated'] + ['ranges']
+ note = dict([(k, v) for k,v in note_dict.items() if k not in excluded_fields])
+
+ resp = self.client.post(self.url('notes_api_notes'), json.dumps(note),
+ content_type='application/json',
+ HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+ self.assertEqual(resp.status_code, 500)
+
+ def test_read_note(self):
+ self.login()
+
+ notes = self.create_notes(3)
+ self.assertEqual(len(notes), 3)
+
+ for note in notes:
+ resp = self.client.get(self.url('notes_api_note', {'note_id': note.id}))
+ self.assertEqual(resp.status_code, 200)
+ self.assertNotEqual(resp.content, '')
+
+ content = json.loads(resp.content)
+ self.assertEqual(content['id'], note.id)
+ self.assertEqual(content['user_id'], note.user_id)
+
+ def test_note_doesnt_exist_to_read(self):
+ NOTE_ID_DOES_NOT_EXIST = 99999
+
+ self.login()
+ resp = self.client.get(self.url('notes_api_note', {
+ 'note_id': NOTE_ID_DOES_NOT_EXIST
+ }))
+ self.assertEqual(resp.status_code, 404)
+ self.assertEqual(resp.content, '')
+
+ def test_student_doesnt_have_permission_to_read_note(self):
+ notes = self.create_notes(1)
+ self.assertEqual(len(notes), 1)
+ note = notes[0]
+
+ # set the student id to a different student (not the one that created the notes)
+ self.login(as_student=self.student2)
+ resp = self.client.get(self.url('notes_api_note', { 'note_id': note.id}))
+ self.assertEqual(resp.status_code, 403)
+ self.assertEqual(resp.content, '')
+
+ @unittest.skip("skipping update test stub")
+ def test_update_note(self): pass
+
+ @unittest.skip("skipping search test stub")
+ def test_search_note(self): pass
From 2838dba19918cb971f061e32faf88cddd5c093df Mon Sep 17 00:00:00 2001
From: Arthur Barrett
Date: Thu, 9 May 2013 15:12:22 -0400
Subject: [PATCH 053/245] fix pep8 violations
---
lms/djangoapps/notes/api.py | 55 +++++++++++++----------
lms/djangoapps/notes/models.py | 6 +--
lms/djangoapps/notes/tests.py | 80 ++++++++++++++++++----------------
lms/djangoapps/notes/urls.py | 11 ++---
lms/djangoapps/notes/utils.py | 22 +++++++---
lms/djangoapps/notes/views.py | 3 +-
6 files changed, 102 insertions(+), 75 deletions(-)
diff --git a/lms/djangoapps/notes/api.py b/lms/djangoapps/notes/api.py
index 43dc2805c8..68b144dd9a 100644
--- a/lms/djangoapps/notes/api.py
+++ b/lms/djangoapps/notes/api.py
@@ -28,6 +28,7 @@ API_SETTINGS = {
#----------------------------------------------------------------------#
# API requests are routed through api_request() using the resource map.
+
def api_enabled(request, course_id):
'''
Returns True if the api is enabled for the course, otherwise False.
@@ -35,9 +36,10 @@ def api_enabled(request, course_id):
course = _get_course(request, course_id)
return notes_enabled_for_course(course)
+
@login_required
def api_request(request, course_id, **kwargs):
- '''
+ '''
Routes API requests to the appropriate action method and returns JSON.
Raises a 404 if the requested resource does not exist or notes are
disabled for the course.
@@ -49,7 +51,7 @@ def api_request(request, course_id, **kwargs):
raise Http404
# Locate the requested resource
- resource_map = API_SETTINGS.get('RESOURCE_MAP', {})
+ resource_map = API_SETTINGS.get('RESOURCE_MAP', {})
resource_name = kwargs.pop('resource')
resource_method = request.method
resource = resource_map.get(resource_name)
@@ -59,7 +61,7 @@ def api_request(request, course_id, **kwargs):
raise Http404
if resource_method not in resource.keys():
- log.debug('Resource "{0}" does not support method "{1}"'.format(resource_name, resource_method))
+ log.debug('Resource "{0}" does not support method "{1}"'.format(resource_name, resource_method))
raise Http404
# Execute the action associated with the resource
@@ -75,7 +77,7 @@ def api_request(request, course_id, **kwargs):
# Format and output the results
data = None
response = result[0]
- if len(result) == 2:
+ if len(result) == 2:
data = result[1]
formatted = api_format(request, response, data)
@@ -86,10 +88,11 @@ def api_request(request, course_id, **kwargs):
return response
+
def api_format(request, response, data):
- '''
- Returns a two-element list containing the content type and content.
- '''
+ '''
+ Returns a two-element list containing the content type and content.
+ '''
content_type = 'application/json'
if data is None:
content = ''
@@ -97,6 +100,7 @@ def api_format(request, response, data):
content = json.dumps(data)
return [content_type, content]
+
def _get_course(request, course_id):
'''
Helper function to load and return a user's course.
@@ -106,20 +110,22 @@ def _get_course(request, course_id):
#----------------------------------------------------------------------#
# API actions exposed via the resource map.
+
def index(request, course_id):
- '''
- Returns a list of annotation objects.
+ '''
+ Returns a list of annotation objects.
'''
MAX_LIMIT = API_SETTINGS.get('MAX_NOTE_LIMIT')
notes = Note.objects.order_by('id').filter(course_id=course_id,
- user=request.user)[:MAX_LIMIT]
+ user=request.user)[:MAX_LIMIT]
return [HttpResponse(), [note.as_dict() for note in notes]]
+
def create(request, course_id):
- '''
- Receives an annotation object to create and returns a 303 with the read location.
+ '''
+ Receives an annotation object to create and returns a 303 with the read location.
'''
note = Note(course_id=course_id, user=request.user)
@@ -135,9 +141,10 @@ def create(request, course_id):
return [response, None]
+
def read(request, course_id, note_id):
- '''
- Returns a single annotation object.
+ '''
+ Returns a single annotation object.
'''
try:
note = Note.objects.get(id=note_id)
@@ -149,9 +156,10 @@ def read(request, course_id, note_id):
return [HttpResponse(), note.as_dict()]
+
def update(request, course_id, note_id):
- '''
- Updates an annotation object and returns a 303 with the read location.
+ '''
+ Updates an annotation object and returns a 303 with the read location.
'''
try:
note = Note.objects.get(id=note_id)
@@ -174,8 +182,9 @@ def update(request, course_id, note_id):
return [response, None]
+
def delete(request, course_id, note_id):
- '''
+ '''
Deletes the annotation object and returns a 204 with no content.
'''
try:
@@ -190,12 +199,13 @@ def delete(request, course_id, note_id):
return [HttpResponse('', status=204), None]
+
def search(request, course_id):
- '''
+ '''
Returns a subset of annotation objects based on a search query.
'''
MAX_LIMIT = API_SETTINGS.get('MAX_NOTE_LIMIT')
-
+
# search parameters
offset = request.GET.get('offset', '')
limit = request.GET.get('limit', '')
@@ -222,7 +232,7 @@ def search(request, course_id):
# retrieve notes
notes = Note.objects.order_by('id').filter(**filters)
total = notes.count()
- rows = notes[offset:offset+limit]
+ rows = notes[offset:offset + limit]
result = {
'total': total,
'rows': [note.as_dict() for note in rows]
@@ -230,8 +240,9 @@ def search(request, course_id):
return [HttpResponse(), result]
+
def root(request, course_id):
- '''
- Returns version information about the API.
+ '''
+ Returns version information about the API.
'''
return [HttpResponse(), API_SETTINGS.get('META')]
diff --git a/lms/djangoapps/notes/models.py b/lms/djangoapps/notes/models.py
index fe8a708391..c07995b8d7 100644
--- a/lms/djangoapps/notes/models.py
+++ b/lms/djangoapps/notes/models.py
@@ -2,12 +2,12 @@ from django.db import models
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.core.exceptions import ValidationError
-
import json
import logging
log = logging.getLogger(__name__)
+
class Note(models.Model):
user = models.ForeignKey(User, db_index=True)
course_id = models.CharField(max_length=255, db_index=True)
@@ -18,7 +18,7 @@ class Note(models.Model):
range_start_offset = models.IntegerField()
range_end = models.CharField(max_length=2048)
range_end_offset = models.IntegerField()
- tags = models.TextField(default="") # comma-separated string
+ tags = models.TextField(default="") # comma-separated string
created = models.DateTimeField(auto_now_add=True, null=True, db_index=True)
updated = models.DateTimeField(auto_now=True, db_index=True)
@@ -68,4 +68,4 @@ class Note(models.Model):
'tags': self.tags.split(","),
'created': str(self.created),
'updated': str(self.updated)
- }
\ No newline at end of file
+ }
diff --git a/lms/djangoapps/notes/tests.py b/lms/djangoapps/notes/tests.py
index 6417ad6965..d84b5d1026 100644
--- a/lms/djangoapps/notes/tests.py
+++ b/lms/djangoapps/notes/tests.py
@@ -14,11 +14,12 @@ import logging
from . import utils, api, models
+
class UtilsTest(TestCase):
- def setUp(self):
- '''
+ def setUp(self):
+ '''
Setup a dummy course-like object with a tabs field that can be
- accessed via attribute lookup.
+ accessed via attribute lookup.
'''
self.course = collections.namedtuple('DummyCourse', ['tabs'])
self.course.tabs = []
@@ -35,20 +36,20 @@ class UtilsTest(TestCase):
Tests that notes are enabled when the course tab configuration contains
a tab with type "notes."
'''
- self.course.tabs = [
- {'type': 'foo'},
- {'name': 'My Notes', 'type': 'notes'},
- {'type':'bar'}]
+ self.course.tabs = [{'type': 'foo'},
+ {'name': 'My Notes', 'type': 'notes'},
+ {'type': 'bar'}]
self.assertTrue(utils.notes_enabled_for_course(self.course))
+
class ApiTest(TestCase):
def setUp(self):
self.client = Client()
# Mocks
- api.api_enabled = self.mock_api_enabled(True)
+ api.api_enabled = self.mock_api_enabled(True)
# Create two accounts
self.password = 'abc'
@@ -57,16 +58,16 @@ class ApiTest(TestCase):
self.instructor = User.objects.create_user('instructor', 'instructor@test.com', self.password)
self.course_id = 'HarvardX/CB22x/The_Ancient_Greek_Hero'
self.note = {
- 'user':self.student,
- 'course_id':self.course_id,
- 'uri':'/',
- 'text':'foo',
- 'quote':'bar',
- 'range_start':0,
- 'range_start_offset':0,
- 'range_end':100,
- 'range_end_offset':0,
- 'tags':'a,b,c'
+ 'user': self.student,
+ 'course_id': self.course_id,
+ 'uri': '/',
+ 'text': 'foo',
+ 'quote': 'bar',
+ 'range_start': 0,
+ 'range_start_offset': 0,
+ 'range_end': 100,
+ 'range_end_offset': 0,
+ 'tags': 'a,b,c'
}
def mock_api_enabled(self, is_enabled):
@@ -84,7 +85,7 @@ class ApiTest(TestCase):
self.client.login(username=username, password=password)
def url(self, name, args={}):
- args.update({'course_id':self.course_id})
+ args.update({'course_id': self.course_id})
return reverse(name, kwargs=args)
def create_notes(self, num_notes, create=True):
@@ -100,12 +101,12 @@ class ApiTest(TestCase):
self.login()
resp = self.client.get(self.url('notes_api_root'))
- self.assertEqual(resp.status_code, 200)
+ self.assertEqual(resp.status_code, 200)
self.assertNotEqual(resp.content, '')
content = json.loads(resp.content)
- self.assertEqual(set(('name','version')), set(content.keys()))
+ self.assertEqual(set(('name', 'version')), set(content.keys()))
self.assertIsInstance(content['version'], int)
self.assertEqual(content['name'], 'Notes API')
@@ -135,7 +136,7 @@ class ApiTest(TestCase):
def test_index_max_notes(self):
self.login()
- MAX_LIMIT = api.API_SETTINGS.get('MAX_NOTE_LIMIT')
+ MAX_LIMIT = api.API_SETTINGS.get('MAX_NOTE_LIMIT')
num_notes = MAX_LIMIT + 1
self.create_notes(num_notes)
@@ -155,11 +156,12 @@ class ApiTest(TestCase):
note_dict = notes[0].as_dict()
excluded_fields = ['id', 'user_id', 'created', 'updated']
- note = dict([(k, v) for k,v in note_dict.items() if k not in excluded_fields])
+ note = dict([(k, v) for k, v in note_dict.items() if k not in excluded_fields])
- resp = self.client.post(self.url('notes_api_notes'), json.dumps(note),
- content_type='application/json',
- HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+ resp = self.client.post(self.url('notes_api_notes'),
+ json.dumps(note),
+ content_type='application/json',
+ HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(resp.status_code, 303)
self.assertEqual(len(resp.content), 0)
@@ -168,9 +170,10 @@ class ApiTest(TestCase):
self.login()
for empty_test in [None, [], '']:
- resp = self.client.post(self.url('notes_api_notes'), json.dumps(empty_test),
- content_type='application/json',
- HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+ resp = self.client.post(self.url('notes_api_notes'),
+ json.dumps(empty_test),
+ content_type='application/json',
+ HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(resp.status_code, 500)
def test_create_note_missing_ranges(self):
@@ -181,14 +184,15 @@ class ApiTest(TestCase):
note_dict = notes[0].as_dict()
excluded_fields = ['id', 'user_id', 'created', 'updated'] + ['ranges']
- note = dict([(k, v) for k,v in note_dict.items() if k not in excluded_fields])
+ note = dict([(k, v) for k, v in note_dict.items() if k not in excluded_fields])
- resp = self.client.post(self.url('notes_api_notes'), json.dumps(note),
- content_type='application/json',
- HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+ resp = self.client.post(self.url('notes_api_notes'),
+ json.dumps(note),
+ content_type='application/json',
+ HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(resp.status_code, 500)
- def test_read_note(self):
+ def test_read_note(self):
self.login()
notes = self.create_notes(3)
@@ -220,12 +224,14 @@ class ApiTest(TestCase):
# set the student id to a different student (not the one that created the notes)
self.login(as_student=self.student2)
- resp = self.client.get(self.url('notes_api_note', { 'note_id': note.id}))
+ resp = self.client.get(self.url('notes_api_note', {'note_id': note.id}))
self.assertEqual(resp.status_code, 403)
self.assertEqual(resp.content, '')
@unittest.skip("skipping update test stub")
- def test_update_note(self): pass
+ def test_update_note(self):
+ pass
@unittest.skip("skipping search test stub")
- def test_search_note(self): pass
+ def test_search_note(self):
+ pass
diff --git a/lms/djangoapps/notes/urls.py b/lms/djangoapps/notes/urls.py
index 7811a5f044..6abe92253a 100644
--- a/lms/djangoapps/notes/urls.py
+++ b/lms/djangoapps/notes/urls.py
@@ -1,9 +1,10 @@
from django.conf.urls import patterns, url
+
id_regex = r"(?P[0-9A-Fa-f]+)"
urlpatterns = patterns('notes.api',
- url(r'^api$', 'api_request', {'resource':'root'}, name='notes_api_root'),
- url(r'^api/annotations$', 'api_request', {'resource':'notes'}, name='notes_api_notes'),
- url(r'^api/annotations/' + id_regex + r'$', 'api_request', {'resource':'note'}, name='notes_api_note'),
- url(r'^api/search', 'api_request', {'resource':'search'}, name='notes_api_search')
-)
+ url(r'^api$', 'api_request', {'resource': 'root'}, name='notes_api_root'),
+ url(r'^api/annotations$', 'api_request', {'resource': 'notes'}, name='notes_api_notes'),
+ url(r'^api/annotations/' + id_regex + r'$', 'api_request', {'resource': 'note'}, name='notes_api_note'),
+ url(r'^api/search', 'api_request', {'resource': 'search'}, name='notes_api_search')
+ )
diff --git a/lms/djangoapps/notes/utils.py b/lms/djangoapps/notes/utils.py
index 80a872da4e..e6e784ce49 100644
--- a/lms/djangoapps/notes/utils.py
+++ b/lms/djangoapps/notes/utils.py
@@ -1,9 +1,17 @@
+from django.conf import settings
+
+
def notes_enabled_for_course(course):
- '''
- Returns True if the notes app is enabled for the course, False otherwise.
+
'''
- # TODO: create a separate policy setting to enable/disable notes
- tab_type = 'notes'
- tabs = course.tabs
- tab_found = next((True for t in tabs if t['type'] == tab_type), False)
- return tab_found
+ Returns True if the notes app is enabled for the course, False otherwise.
+
+ In order for the app to be enabled it must be:
+ 1) enabled globally via MITX_FEATURES.
+ 2) present in the course tab configuration.
+ '''
+
+ tab_found = next((True for t in course.tabs if t['type'] == 'notes'), False)
+ feature_enabled = settings.MITX_FEATURES.get('ENABLE_STUDENT_NOTES')
+
+ return feature_enabled and tab_found
diff --git a/lms/djangoapps/notes/views.py b/lms/djangoapps/notes/views.py
index ba47540071..cf125781d0 100644
--- a/lms/djangoapps/notes/views.py
+++ b/lms/djangoapps/notes/views.py
@@ -6,6 +6,7 @@ from notes.models import Note
from notes.utils import notes_enabled_for_course
import json
+
@login_required
def notes(request, course_id):
''' Displays the student's notes. '''
@@ -13,7 +14,7 @@ def notes(request, course_id):
course = get_course_with_access(request.user, course_id, 'load')
if not notes_enabled_for_course(course):
raise Http404
-
+
notes = Note.objects.filter(course_id=course_id, user=request.user).order_by('-created', 'uri')
json_notes = json.dumps([n.as_dict() for n in notes])
context = {
From aad5096ca12cf9028adefe63d7c073a5a3c7806f Mon Sep 17 00:00:00 2001
From: Arthur Barrett
Date: Thu, 9 May 2013 15:43:34 -0400
Subject: [PATCH 054/245] add more tests for update and delete actions
---
lms/djangoapps/notes/tests.py | 72 ++++++++++++++++++++++++++++++++---
1 file changed, 67 insertions(+), 5 deletions(-)
diff --git a/lms/djangoapps/notes/tests.py b/lms/djangoapps/notes/tests.py
index d84b5d1026..475c097df7 100644
--- a/lms/djangoapps/notes/tests.py
+++ b/lms/djangoapps/notes/tests.py
@@ -70,6 +70,9 @@ class ApiTest(TestCase):
'tags': 'a,b,c'
}
+ # Make sure no note with this ID ever exists for testing purposes
+ self.NOTE_ID_DOES_NOT_EXIST = 99999
+
def mock_api_enabled(self, is_enabled):
return (lambda request, course_id: is_enabled)
@@ -208,11 +211,9 @@ class ApiTest(TestCase):
self.assertEqual(content['user_id'], note.user_id)
def test_note_doesnt_exist_to_read(self):
- NOTE_ID_DOES_NOT_EXIST = 99999
-
self.login()
resp = self.client.get(self.url('notes_api_note', {
- 'note_id': NOTE_ID_DOES_NOT_EXIST
+ 'note_id': self.NOTE_ID_DOES_NOT_EXIST
}))
self.assertEqual(resp.status_code, 404)
self.assertEqual(resp.content, '')
@@ -228,9 +229,70 @@ class ApiTest(TestCase):
self.assertEqual(resp.status_code, 403)
self.assertEqual(resp.content, '')
- @unittest.skip("skipping update test stub")
+ def test_delete_note(self):
+ self.login()
+
+ notes = self.create_notes(1)
+ self.assertEqual(len(notes), 1)
+ note = notes[0]
+
+ resp = self.client.delete(self.url('notes_api_note', {
+ 'note_id': note.id
+ }))
+ self.assertEqual(resp.status_code, 204)
+ self.assertEqual(resp.content, '')
+
+ with self.assertRaises(models.Note.DoesNotExist):
+ models.Note.objects.get(pk=note.id)
+
+ def test_note_does_not_exist_to_delete(self):
+ self.login()
+
+ resp = self.client.delete(self.url('notes_api_note', {
+ 'note_id': self.NOTE_ID_DOES_NOT_EXIST
+ }))
+ self.assertEqual(resp.status_code, 404)
+ self.assertEqual(resp.content, '')
+
+ def test_student_doesnt_have_permission_to_delete_note(self):
+ notes = self.create_notes(1)
+ self.assertEqual(len(notes), 1)
+ note = notes[0]
+
+ self.login(as_student=self.student2)
+ resp = self.client.delete(self.url('notes_api_note', {
+ 'note_id': note.id
+ }))
+ self.assertEqual(resp.status_code, 403)
+ self.assertEqual(resp.content, '')
+
+ try:
+ models.Note.objects.get(pk=note.id)
+ except models.Note.DoesNotExist:
+ self.fail('note should exist and not be deleted because the student does not have permission to do so')
+
def test_update_note(self):
- pass
+ notes = self.create_notes(1)
+ note = notes[0]
+
+ updated_dict = note.as_dict()
+ updated_dict.update({
+ 'text': 'itchy and scratchy',
+ 'tags': ['simpsons', 'cartoons', 'animation']
+ })
+
+ self.login()
+ resp = self.client.put(self.url('notes_api_note', {'note_id': note.id}),
+ json.dumps(updated_dict),
+ content_type='application/json',
+ HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+ self.assertEqual(resp.status_code, 303)
+ self.assertEqual(resp.content, '')
+
+ actual = models.Note.objects.get(pk=note.id)
+ actual_dict = actual.as_dict()
+ for field in ['text', 'tags']:
+ self.assertEqual(actual_dict[field], updated_dict[field])
@unittest.skip("skipping search test stub")
def test_search_note(self):
From 9aed148dc120d91cac437c5e6aea541cefae5d62 Mon Sep 17 00:00:00 2001
From: Arthur Barrett
Date: Thu, 9 May 2013 16:08:29 -0400
Subject: [PATCH 055/245] add search test
---
lms/djangoapps/notes/tests.py | 47 ++++++++++++++++++++++++++++++++---
1 file changed, 44 insertions(+), 3 deletions(-)
diff --git a/lms/djangoapps/notes/tests.py b/lms/djangoapps/notes/tests.py
index 475c097df7..9b05512250 100644
--- a/lms/djangoapps/notes/tests.py
+++ b/lms/djangoapps/notes/tests.py
@@ -294,6 +294,47 @@ class ApiTest(TestCase):
for field in ['text', 'tags']:
self.assertEqual(actual_dict[field], updated_dict[field])
- @unittest.skip("skipping search test stub")
- def test_search_note(self):
- pass
+ def test_search_note_params(self):
+ self.login()
+
+ total = 3
+ notes = self.create_notes(total)
+ invalid_uri = ''.join([note.uri for note in notes])
+
+ tests = [{'limit': 0, 'offset': 0, 'expected_rows': total},
+ {'limit': 0, 'offset': 2, 'expected_rows': total - 2},
+ {'limit': 0, 'offset': total, 'expected_rows': 0},
+ {'limit': 1, 'offset': 0, 'expected_rows': 1},
+ {'limit': 2, 'offset': 0, 'expected_rows': 2},
+ {'limit': total, 'offset': 2, 'expected_rows': 1},
+ {'limit': total, 'offset': total, 'expected_rows': 0},
+ {'limit': total + 1, 'offset': total + 1, 'expected_rows': 0},
+ {'limit': total + 1, 'offset': 0, 'expected_rows': total},
+ {'limit': 0, 'offset': 0, 'uri': invalid_uri, 'expected_rows': 0, 'expected_total': 0}]
+
+ for test in tests:
+ params = dict([(k, str(test[k]))
+ for k in ('limit', 'offset', 'uri')
+ if k in test])
+ resp = self.client.get(self.url('notes_api_search'),
+ params,
+ content_type='application/json',
+ HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+
+ self.assertEqual(resp.status_code, 200)
+ self.assertNotEqual(resp.content, '')
+
+ content = json.loads(resp.content)
+
+ for expected_key in ('total', 'rows'):
+ self.assertTrue(expected_key in content)
+
+ if 'expected_total' in test:
+ self.assertEqual(content['total'], test['expected_total'])
+ else:
+ self.assertEqual(content['total'], total)
+
+ self.assertEqual(len(content['rows']), test['expected_rows'])
+
+ for row in content['rows']:
+ self.assertTrue('id' in row)
From 0bea50ede17a190a8f0a9ba61e71535be639090b Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Thu, 9 May 2013 16:24:21 -0400
Subject: [PATCH 056/245] start refactoring views.py
---
cms/djangoapps/contentstore/views.py | 1685 -----------------
cms/djangoapps/contentstore/views/__init__.py | 17 +
cms/djangoapps/contentstore/views/access.py | 39 +
cms/djangoapps/contentstore/views/assets.py | 118 ++
.../contentstore/views/checklist.py | 98 +
cms/djangoapps/contentstore/views/course.py | 202 ++
cms/djangoapps/contentstore/views/error.py | 21 +
cms/djangoapps/contentstore/views/item.py | 123 ++
cms/djangoapps/contentstore/views/preview.py | 156 ++
cms/djangoapps/contentstore/views/public.py | 50 +
cms/djangoapps/contentstore/views/requests.py | 91 +
.../contentstore/views/session_kv_store.py | 27 +
cms/djangoapps/contentstore/views/user.py | 107 ++
13 files changed, 1049 insertions(+), 1685 deletions(-)
delete mode 100644 cms/djangoapps/contentstore/views.py
create mode 100644 cms/djangoapps/contentstore/views/__init__.py
create mode 100644 cms/djangoapps/contentstore/views/access.py
create mode 100644 cms/djangoapps/contentstore/views/assets.py
create mode 100644 cms/djangoapps/contentstore/views/checklist.py
create mode 100644 cms/djangoapps/contentstore/views/course.py
create mode 100644 cms/djangoapps/contentstore/views/error.py
create mode 100644 cms/djangoapps/contentstore/views/item.py
create mode 100644 cms/djangoapps/contentstore/views/preview.py
create mode 100644 cms/djangoapps/contentstore/views/public.py
create mode 100644 cms/djangoapps/contentstore/views/requests.py
create mode 100644 cms/djangoapps/contentstore/views/session_kv_store.py
create mode 100644 cms/djangoapps/contentstore/views/user.py
diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py
deleted file mode 100644
index 824d2119f1..0000000000
--- a/cms/djangoapps/contentstore/views.py
+++ /dev/null
@@ -1,1685 +0,0 @@
-from util.json_request import expect_json
-import json
-import logging
-import os
-import sys
-import time
-import tarfile
-import shutil
-from collections import defaultdict
-from uuid import uuid4
-from path import path
-from xmodule.modulestore.xml_exporter import export_to_xml
-from tempfile import mkdtemp
-from django.core.servers.basehttp import FileWrapper
-from django.core.files.temp import NamedTemporaryFile
-
-from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseServerError
-from django.http import HttpResponseNotFound
-from django.contrib.auth.decorators import login_required
-from django.core.exceptions import PermissionDenied
-from django.core.context_processors import csrf
-from django_future.csrf import ensure_csrf_cookie
-from django.core.urlresolvers import reverse
-from django.conf import settings
-
-from xmodule.modulestore import Location
-from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
-from xmodule.modulestore.inheritance import own_metadata
-from xblock.core import Scope
-from xblock.runtime import KeyValueStore, DbModel, InvalidScopeError
-from xmodule.x_module import ModuleSystem
-from xmodule.error_module import ErrorDescriptor
-from xmodule.errortracker import exc_info_to_str
-import static_replace
-from external_auth.views import ssl_login_shortcut
-from xmodule.modulestore.mongo import MongoUsage
-
-from mitxmako.shortcuts import render_to_response, render_to_string
-from xmodule.modulestore.django import modulestore
-from xmodule_modifiers import replace_static_urls, wrap_xmodule
-from xmodule.exceptions import NotFoundError, ProcessingError
-from functools import partial
-
-from xmodule.contentstore.django import contentstore
-from xmodule.contentstore.content import StaticContent
-from xmodule.util.date_utils import get_default_time_display
-
-from auth.authz import is_user_in_course_group_role, get_users_in_course_group_by_role
-from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group
-from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME, create_all_course_groups
-from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state, \
- UnitState, get_course_for_item, get_url_reverse, add_open_ended_panel_tab, \
- remove_open_ended_panel_tab
-
-from xmodule.modulestore.xml_importer import import_from_xml
-from contentstore.course_info_model import get_course_updates, \
- update_course_updates, delete_course_update
-from cache_toolbox.core import del_cached_content
-from contentstore.module_info_model import get_module_info, set_module_info
-from models.settings.course_details import CourseDetails, \
- CourseSettingsEncoder
-from models.settings.course_grading import CourseGradingModel
-from contentstore.utils import get_modulestore
-from django.shortcuts import redirect
-from models.settings.course_metadata import CourseMetadata
-
-# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
-
-log = logging.getLogger(__name__)
-
-
-COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video']
-
-OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"]
-ADVANCED_COMPONENT_TYPES = ['annotatable'] + OPEN_ENDED_COMPONENT_TYPES
-ADVANCED_COMPONENT_CATEGORY = 'advanced'
-ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
-
-# cdodge: these are categories which should not be parented, they are detached from the hierarchy
-DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
-
-
-# ==== Public views ==================================================
-
-@ensure_csrf_cookie
-def signup(request):
- """
- Display the signup form.
- """
- csrf_token = csrf(request)['csrf_token']
- return render_to_response('signup.html', {'csrf': csrf_token})
-
-
-def old_login_redirect(request):
- '''
- Redirect to the active login url.
- '''
- return redirect('login', permanent=True)
-
-
-@ssl_login_shortcut
-@ensure_csrf_cookie
-def login_page(request):
- """
- Display the login form.
- """
- csrf_token = csrf(request)['csrf_token']
- return render_to_response('login.html', {
- 'csrf': csrf_token,
- 'forgot_password_link': "//{base}/#forgot-password-modal".format(base=settings.LMS_BASE),
- })
-
-
-def howitworks(request):
- if request.user.is_authenticated():
- return index(request)
- else:
- return render_to_response('howitworks.html', {})
-
-
-# static/proof-of-concept views
-def ux_alerts(request):
- return render_to_response('ux-alerts.html', {})
-
-
-# ==== Views for any logged-in user ==================================
-
-
-@login_required
-@ensure_csrf_cookie
-def index(request):
- """
- List all courses available to the logged in user
- """
- courses = modulestore('direct').get_items(['i4x', None, None, 'course', None])
-
- # filter out courses that we don't have access too
- def course_filter(course):
- return (has_access(request.user, course.location)
- and course.location.course != 'templates'
- and course.location.org != ''
- and course.location.course != ''
- and course.location.name != '')
- courses = filter(course_filter, courses)
-
- return render_to_response('index.html', {
- 'new_course_template': Location('i4x', 'edx', 'templates', 'course', 'Empty'),
- 'courses': [(course.display_name,
- get_url_reverse('CourseOutline', course),
- get_lms_link_for_item(course.location, course_id=course.location.course_id))
- for course in courses],
- 'user': request.user,
- 'disable_course_creation': settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff
- })
-
-
-# ==== Views with per-item permissions================================
-
-
-def has_access(user, location, role=STAFF_ROLE_NAME):
- '''
- Return True if user allowed to access this piece of data
- Note that the CMS permissions model is with respect to courses
- There is a super-admin permissions if user.is_staff is set
- Also, since we're unifying the user database between LMS and CAS,
- I'm presuming that the course instructor (formally known as admin)
- will not be in both INSTRUCTOR and STAFF groups, so we have to cascade our queries here as INSTRUCTOR
- has all the rights that STAFF do
- '''
- course_location = get_course_location_for_item(location)
- _has_access = is_user_in_course_group_role(user, course_location, role)
- # if we're not in STAFF, perhaps we're in INSTRUCTOR groups
- if not _has_access and role == STAFF_ROLE_NAME:
- _has_access = is_user_in_course_group_role(user, course_location, INSTRUCTOR_ROLE_NAME)
- return _has_access
-
-
-@login_required
-@ensure_csrf_cookie
-def course_index(request, org, course, name):
- """
- Display an editable course overview.
-
- org, course, name: Attributes of the Location for the item to edit
- """
- location = get_location_and_verify_access(request, org, course, name)
-
- lms_link = get_lms_link_for_item(location)
-
- upload_asset_callback_url = reverse('upload_asset', kwargs={
- 'org': org,
- 'course': course,
- 'coursename': name
- })
-
- course = modulestore().get_item(location, depth=3)
- sections = course.get_children()
-
- return render_to_response('overview.html', {
- 'active_tab': 'courseware',
- 'context_course': course,
- 'lms_link': lms_link,
- 'sections': sections,
- 'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
- 'parent_location': course.location,
- 'new_section_template': Location('i4x', 'edx', 'templates', 'chapter', 'Empty'),
- 'new_subsection_template': Location('i4x', 'edx', 'templates', 'sequential', 'Empty'), # for now they are the same, but the could be different at some point...
- 'upload_asset_callback_url': upload_asset_callback_url,
- 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty')
- })
-
-
-@login_required
-def edit_subsection(request, location):
- # check that we have permissions to edit this item
- course = get_course_for_item(location)
- if not has_access(request.user, course.location):
- raise PermissionDenied()
-
- item = modulestore().get_item(location, depth=1)
-
- lms_link = get_lms_link_for_item(location, course_id=course.location.course_id)
- preview_link = get_lms_link_for_item(location, course_id=course.location.course_id, preview=True)
-
- # make sure that location references a 'sequential', otherwise return BadRequest
- if item.location.category != 'sequential':
- return HttpResponseBadRequest()
-
- parent_locs = modulestore().get_parent_locations(location, None)
-
- # we're for now assuming a single parent
- if len(parent_locs) != 1:
- logging.error('Multiple (or none) parents have been found for {0}'.format(location))
-
- # this should blow up if we don't find any parents, which would be erroneous
- parent = modulestore().get_item(parent_locs[0])
-
- # remove all metadata from the generic dictionary that is presented in a more normalized UI
-
- policy_metadata = dict(
- (field.name, field.read_from(item))
- for field
- in item.fields
- if field.name not in ['display_name', 'start', 'due', 'format'] and field.scope == Scope.settings
- )
-
- can_view_live = False
- subsection_units = item.get_children()
- for unit in subsection_units:
- state = compute_unit_state(unit)
- if state == UnitState.public or state == UnitState.draft:
- can_view_live = True
- break
-
- return render_to_response('edit_subsection.html',
- {'subsection': item,
- 'context_course': course,
- 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
- 'lms_link': lms_link,
- 'preview_link': preview_link,
- 'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
- 'parent_location': course.location,
- 'parent_item': parent,
- 'policy_metadata': policy_metadata,
- 'subsection_units': subsection_units,
- 'can_view_live': can_view_live
- })
-
-
-@login_required
-def edit_unit(request, location):
- """
- Display an editing page for the specified module.
-
- Expects a GET request with the parameter 'id'.
-
- id: A Location URL
- """
- course = get_course_for_item(location)
- if not has_access(request.user, course.location):
- raise PermissionDenied()
-
- item = modulestore().get_item(location, depth=1)
-
- lms_link = get_lms_link_for_item(item.location, course_id=course.location.course_id)
-
- component_templates = defaultdict(list)
-
- # Check if there are any advanced modules specified in the course policy. These modules
- # should be specified as a list of strings, where the strings are the names of the modules
- # in ADVANCED_COMPONENT_TYPES that should be enabled for the course.
- course_advanced_keys = course.advanced_modules
-
- # Set component types according to course policy file
- component_types = list(COMPONENT_TYPES)
- if isinstance(course_advanced_keys, list):
- course_advanced_keys = [c for c in course_advanced_keys if c in ADVANCED_COMPONENT_TYPES]
- if len(course_advanced_keys) > 0:
- component_types.append(ADVANCED_COMPONENT_CATEGORY)
- else:
- log.error("Improper format for course advanced keys! {0}".format(course_advanced_keys))
-
- templates = modulestore().get_items(Location('i4x', 'edx', 'templates'))
- for template in templates:
- category = template.location.category
-
- if category in course_advanced_keys:
- category = ADVANCED_COMPONENT_CATEGORY
-
- if category in component_types:
- # This is a hack to create categories for different xmodules
- component_templates[category].append((
- template.display_name_with_default,
- template.location.url(),
- hasattr(template, 'markdown') and template.markdown is not None,
- template.cms.empty,
- ))
-
- components = [
- component.location.url()
- for component
- in item.get_children()
- ]
-
- # TODO (cpennington): If we share units between courses,
- # this will need to change to check permissions correctly so as
- # to pick the correct parent subsection
-
- containing_subsection_locs = modulestore().get_parent_locations(location, None)
- containing_subsection = modulestore().get_item(containing_subsection_locs[0])
-
- containing_section_locs = modulestore().get_parent_locations(containing_subsection.location, None)
- containing_section = modulestore().get_item(containing_section_locs[0])
-
- # cdodge hack. We're having trouble previewing drafts via jump_to redirect
- # so let's generate the link url here
-
- # need to figure out where this item is in the list of children as the preview will need this
- index = 1
- for child in containing_subsection.get_children():
- if child.location == item.location:
- break
- index = index + 1
-
- preview_lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE',
- 'preview.' + settings.LMS_BASE)
-
- preview_lms_link = '//{preview_lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}'.format(
- preview_lms_base=preview_lms_base,
- lms_base=settings.LMS_BASE,
- org=course.location.org,
- course=course.location.course,
- course_name=course.location.name,
- section=containing_section.location.name,
- subsection=containing_subsection.location.name,
- index=index)
-
- unit_state = compute_unit_state(item)
-
- return render_to_response('unit.html', {
- 'context_course': course,
- 'active_tab': 'courseware',
- 'unit': item,
- 'unit_location': location,
- 'components': components,
- 'component_templates': component_templates,
- 'draft_preview_link': preview_lms_link,
- 'published_preview_link': lms_link,
- 'subsection': containing_subsection,
- 'release_date': get_default_time_display(containing_subsection.lms.start) if containing_subsection.lms.start is not None else None,
- 'section': containing_section,
- 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
- 'unit_state': unit_state,
- 'published_date': item.cms.published_date.strftime('%B %d, %Y') if item.cms.published_date is not None else None,
- })
-
-
-@login_required
-def preview_component(request, location):
- # TODO (vshnayder): change name from id to location in coffee+html as well.
- if not has_access(request.user, location):
- raise HttpResponseForbidden()
-
- component = modulestore().get_item(location)
-
- return render_to_response('component.html', {
- 'preview': get_module_previews(request, component)[0],
- 'editor': wrap_xmodule(component.get_html, component, 'xmodule_edit.html')(),
- })
-
-
-@expect_json
-@login_required
-@ensure_csrf_cookie
-def assignment_type_update(request, org, course, category, name):
- '''
- CRUD operations on assignment types for sections and subsections and anything else gradable.
- '''
- location = Location(['i4x', org, course, category, name])
- if not has_access(request.user, location):
- raise HttpResponseForbidden()
-
- if request.method == 'GET':
- return HttpResponse(json.dumps(CourseGradingModel.get_section_grader_type(location)),
- mimetype="application/json")
- elif request.method == 'POST': # post or put, doesn't matter.
- return HttpResponse(json.dumps(CourseGradingModel.update_section_grader_type(location, request.POST)),
- mimetype="application/json")
-
-
-def user_author_string(user):
- '''Get an author string for commits by this user. Format:
- first last .
-
- If the first and last names are blank, uses the username instead.
- Assumes that the email is not blank.
- '''
- f = user.first_name
- l = user.last_name
- if f == '' and l == '':
- f = user.username
- return '{first} {last} <{email}>'.format(first=f,
- last=l,
- email=user.email)
-
-
-@login_required
-def preview_dispatch(request, preview_id, location, dispatch=None):
- """
- Dispatch an AJAX action to a preview XModule
-
- Expects a POST request, and passes the arguments to the module
-
- preview_id (str): An identifier specifying which preview this module is used for
- location: The Location of the module to dispatch to
- dispatch: The action to execute
- """
-
- descriptor = modulestore().get_item(location)
- instance = load_preview_module(request, preview_id, descriptor)
- # Let the module handle the AJAX
- try:
- ajax_return = instance.handle_ajax(dispatch, request.POST)
-
- except NotFoundError:
- log.exception("Module indicating to user that request doesn't exist")
- raise Http404
-
- except ProcessingError:
- log.warning("Module raised an error while processing AJAX request",
- exc_info=True)
- return HttpResponseBadRequest()
-
- except:
- log.exception("error processing ajax call")
- raise
-
- return HttpResponse(ajax_return)
-
-
-def render_from_lms(template_name, dictionary, context=None, namespace='main'):
- """
- Render a template using the LMS MAKO_TEMPLATES
- """
- return render_to_string(template_name, dictionary, context, namespace="lms." + namespace)
-
-
-class SessionKeyValueStore(KeyValueStore):
- def __init__(self, request, model_data):
- self._model_data = model_data
- self._session = request.session
-
- def get(self, key):
- try:
- return self._model_data[key.field_name]
- except (KeyError, InvalidScopeError):
- return self._session[tuple(key)]
-
- def set(self, key, value):
- try:
- self._model_data[key.field_name] = value
- except (KeyError, InvalidScopeError):
- self._session[tuple(key)] = value
-
- def delete(self, key):
- try:
- del self._model_data[key.field_name]
- except (KeyError, InvalidScopeError):
- del self._session[tuple(key)]
-
- def has(self, key):
- return key in self._model_data or key in self._session
-
-
-def preview_module_system(request, preview_id, descriptor):
- """
- Returns a ModuleSystem for the specified descriptor that is specialized for
- rendering module previews.
-
- request: The active django request
- preview_id (str): An identifier specifying which preview this module is used for
- descriptor: An XModuleDescriptor
- """
-
- def preview_model_data(descriptor):
- return DbModel(
- SessionKeyValueStore(request, descriptor._model_data),
- descriptor.module_class,
- preview_id,
- MongoUsage(preview_id, descriptor.location.url()),
- )
-
- return ModuleSystem(
- ajax_url=reverse('preview_dispatch', args=[preview_id, descriptor.location.url(), '']).rstrip('/'),
- # TODO (cpennington): Do we want to track how instructors are using the preview problems?
- track_function=lambda type, event: None,
- filestore=descriptor.system.resources_fs,
- get_module=partial(get_preview_module, request, preview_id),
- render_template=render_from_lms,
- debug=True,
- replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_namespace=descriptor.location),
- user=request.user,
- xblock_model_data=preview_model_data,
- )
-
-
-def get_preview_module(request, preview_id, descriptor):
- """
- Returns a preview XModule at the specified location. The preview_data is chosen arbitrarily
- from the set of preview data for the descriptor specified by Location
-
- request: The active django request
- preview_id (str): An identifier specifying which preview this module is used for
- location: A Location
- """
-
- return load_preview_module(request, preview_id, descriptor)
-
-
-def load_preview_module(request, preview_id, descriptor):
- """
- Return a preview XModule instantiated from the supplied descriptor, instance_state, and shared_state
-
- request: The active django request
- preview_id (str): An identifier specifying which preview this module is used for
- descriptor: An XModuleDescriptor
- instance_state: An instance state string
- shared_state: A shared state string
- """
- system = preview_module_system(request, preview_id, descriptor)
- try:
- module = descriptor.xmodule(system)
- except:
- log.debug("Unable to load preview module", exc_info=True)
- module = ErrorDescriptor.from_descriptor(
- descriptor,
- error_msg=exc_info_to_str(sys.exc_info())
- ).xmodule(system)
-
- # cdodge: Special case
- if module.location.category == 'static_tab':
- module.get_html = wrap_xmodule(
- module.get_html,
- module,
- "xmodule_tab_display.html",
- )
- else:
- module.get_html = wrap_xmodule(
- module.get_html,
- module,
- "xmodule_display.html",
- )
-
- module.get_html = replace_static_urls(
- module.get_html,
- getattr(module, 'data_dir', module.location.course),
- course_namespace=Location([module.location.tag, module.location.org, module.location.course, None, None])
- )
-
- return module
-
-
-def get_module_previews(request, descriptor):
- """
- Returns a list of preview XModule html contents. One preview is returned for each
- pair of states returned by get_sample_state() for the supplied descriptor.
-
- descriptor: An XModuleDescriptor
- """
- preview_html = []
- for idx, (instance_state, shared_state) in enumerate(descriptor.get_sample_state()):
- module = load_preview_module(request, str(idx), descriptor)
- preview_html.append(module.get_html())
- return preview_html
-
-
-def _xmodule_recurse(item, action):
- for child in item.get_children():
- _xmodule_recurse(child, action)
-
- action(item)
-
-
-@login_required
-@expect_json
-def delete_item(request):
- item_location = request.POST['id']
- item_loc = Location(item_location)
-
- # check permissions for this user within this course
- if not has_access(request.user, item_location):
- raise PermissionDenied()
-
- # optional parameter to delete all children (default False)
- delete_children = request.POST.get('delete_children', False)
- delete_all_versions = request.POST.get('delete_all_versions', False)
-
- store = modulestore()
-
- item = store.get_item(item_location)
-
- if delete_children:
- _xmodule_recurse(item, lambda i: store.delete_item(i.location, delete_all_versions))
- else:
- store.delete_item(item.location, delete_all_versions)
-
- # cdodge: we need to remove our parent's pointer to us so that it is no longer dangling
- if delete_all_versions:
- parent_locs = modulestore('direct').get_parent_locations(item_loc, None)
-
- for parent_loc in parent_locs:
- parent = modulestore('direct').get_item(parent_loc)
- item_url = item_loc.url()
- if item_url in parent.children:
- children = parent.children
- children.remove(item_url)
- parent.children = children
- modulestore('direct').update_children(parent.location, parent.children)
-
- return HttpResponse()
-
-
-@login_required
-@expect_json
-def save_item(request):
- item_location = request.POST['id']
-
- # check permissions for this user within this course
- if not has_access(request.user, item_location):
- raise PermissionDenied()
-
- store = get_modulestore(Location(item_location))
-
- if request.POST.get('data') is not None:
- data = request.POST['data']
- store.update_item(item_location, data)
-
- # cdodge: note calling request.POST.get('children') will return None if children is an empty array
- # so it lead to a bug whereby the last component to be deleted in the UI was not actually
- # deleting the children object from the children collection
- if 'children' in request.POST and request.POST['children'] is not None:
- children = request.POST['children']
- store.update_children(item_location, children)
-
- # cdodge: also commit any metadata which might have been passed along in the
- # POST from the client, if it is there
- # NOTE, that the postback is not the complete metadata, as there's system metadata which is
- # not presented to the end-user for editing. So let's fetch the original and
- # 'apply' the submitted metadata, so we don't end up deleting system metadata
- if request.POST.get('metadata') is not None:
- posted_metadata = request.POST['metadata']
- # fetch original
- existing_item = modulestore().get_item(item_location)
-
- # update existing metadata with submitted metadata (which can be partial)
- # 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():
-
- 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]
- del posted_metadata[metadata_key]
- else:
- existing_item._model_data[metadata_key] = value
-
- # commit to datastore
- # TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
- store.update_metadata(item_location, own_metadata(existing_item))
-
- return HttpResponse()
-
-
-@login_required
-@expect_json
-def create_draft(request):
- location = request.POST['id']
-
- # check permissions for this user within this course
- if not has_access(request.user, location):
- raise PermissionDenied()
-
- # This clones the existing item location to a draft location (the draft is implicit,
- # because modulestore is a Draft modulestore)
- modulestore().clone_item(location, location)
-
- return HttpResponse()
-
-
-@login_required
-@expect_json
-def publish_draft(request):
- location = request.POST['id']
-
- # check permissions for this user within this course
- if not has_access(request.user, location):
- raise PermissionDenied()
-
- item = modulestore().get_item(location)
- _xmodule_recurse(item, lambda i: modulestore().publish(i.location, request.user.id))
-
- return HttpResponse()
-
-
-@login_required
-@expect_json
-def unpublish_unit(request):
- location = request.POST['id']
-
- # check permissions for this user within this course
- if not has_access(request.user, location):
- raise PermissionDenied()
-
- item = modulestore().get_item(location)
- _xmodule_recurse(item, lambda i: modulestore().unpublish(i.location))
-
- return HttpResponse()
-
-
-@login_required
-@expect_json
-def clone_item(request):
- parent_location = Location(request.POST['parent_location'])
- template = Location(request.POST['template'])
-
- display_name = request.POST.get('display_name')
-
- if not has_access(request.user, parent_location):
- raise PermissionDenied()
-
- parent = get_modulestore(template).get_item(parent_location)
- dest_location = parent_location._replace(category=template.category, name=uuid4().hex)
-
- new_item = get_modulestore(template).clone_item(template, dest_location)
-
- # replace the display name with an optional parameter passed in from the caller
- if display_name is not None:
- new_item.display_name = display_name
-
- get_modulestore(template).update_metadata(new_item.location.url(), own_metadata(new_item))
-
- if new_item.location.category not in DETACHED_CATEGORIES:
- get_modulestore(parent.location).update_children(parent_location, parent.children + [new_item.location.url()])
-
- return HttpResponse(json.dumps({'id': dest_location.url()}))
-
-
-def upload_asset(request, org, course, coursename):
- '''
- cdodge: this method allows for POST uploading of files into the course asset library, which will
- be supported by GridFS in MongoDB.
- '''
- if request.method != 'POST':
- # (cdodge) @todo: Is there a way to do a - say - 'raise Http400'?
- return HttpResponseBadRequest()
-
- # construct a location from the passed in path
- location = get_location_and_verify_access(request, org, course, coursename)
-
- # Does the course actually exist?!? Get anything from it to prove its existance
-
- try:
- modulestore().get_item(location)
- except:
- # no return it as a Bad Request response
- logging.error('Could not find course' + location)
- return HttpResponseBadRequest()
-
- # compute a 'filename' which is similar to the location formatting, we're using the 'filename'
- # nomenclature since we're using a FileSystem paradigm here. We're just imposing
- # the Location string formatting expectations to keep things a bit more consistent
-
- filename = request.FILES['file'].name
- mime_type = request.FILES['file'].content_type
- filedata = request.FILES['file'].read()
-
- content_loc = StaticContent.compute_location(org, course, filename)
- content = StaticContent(content_loc, filename, mime_type, filedata)
-
- # first let's see if a thumbnail can be created
- (thumbnail_content, thumbnail_location) = contentstore().generate_thumbnail(content)
-
- # delete cached thumbnail even if one couldn't be created this time (else the old thumbnail will continue to show)
- del_cached_content(thumbnail_location)
- # now store thumbnail location only if we could create it
- if thumbnail_content is not None:
- content.thumbnail_location = thumbnail_location
-
- # then commit the content
- contentstore().save(content)
- del_cached_content(content.location)
-
- # readback the saved content - we need the database timestamp
- readback = contentstore().find(content.location)
-
- response_payload = {'displayname': content.name,
- 'uploadDate': get_default_time_display(readback.last_modified_at.timetuple()),
- 'url': StaticContent.get_url_path_from_location(content.location),
- 'thumb_url': StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None,
- 'msg': 'Upload completed'
- }
-
- response = HttpResponse(json.dumps(response_payload))
- response['asset_url'] = StaticContent.get_url_path_from_location(content.location)
- return response
-
-
-@login_required
-@ensure_csrf_cookie
-def manage_users(request, location):
- '''
- This view will return all CMS users who are editors for the specified course
- '''
- # check that logged in user has permissions to this item
- if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME) and not has_access(request.user, location, role=STAFF_ROLE_NAME):
- raise PermissionDenied()
-
- course_module = modulestore().get_item(location)
-
- return render_to_response('manage_users.html', {
- 'active_tab': 'users',
- 'context_course': course_module,
- 'staff': get_users_in_course_group_by_role(location, STAFF_ROLE_NAME),
- 'add_user_postback_url': reverse('add_user', args=[location]).rstrip('/'),
- 'remove_user_postback_url': reverse('remove_user', args=[location]).rstrip('/'),
- 'allow_actions': has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME),
- 'request_user_id': request.user.id
- })
-
-
-def create_json_response(errmsg=None):
- if errmsg is not None:
- resp = HttpResponse(json.dumps({'Status': 'Failed', 'ErrMsg': errmsg}))
- else:
- resp = HttpResponse(json.dumps({'Status': 'OK'}))
-
- return resp
-
-
-@expect_json
-@login_required
-@ensure_csrf_cookie
-def add_user(request, location):
- '''
- This POST-back view will add a user - specified by email - to the list of editors for
- the specified course
- '''
- email = request.POST["email"]
-
- if email == '':
- return create_json_response('Please specify an email address.')
-
- # check that logged in user has admin permissions to this course
- if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
- raise PermissionDenied()
-
- user = get_user_by_email(email)
-
- # user doesn't exist?!? Return error.
- if user is None:
- return create_json_response('Could not find user by email address \'{0}\'.'.format(email))
-
- # user exists, but hasn't activated account?!?
- if not user.is_active:
- return create_json_response('User {0} has registered but has not yet activated his/her account.'.format(email))
-
- # ok, we're cool to add to the course group
- add_user_to_course_group(request.user, user, location, STAFF_ROLE_NAME)
-
- return create_json_response()
-
-
-@expect_json
-@login_required
-@ensure_csrf_cookie
-def remove_user(request, location):
- '''
- This POST-back view will remove a user - specified by email - from the list of editors for
- the specified course
- '''
-
- email = request.POST["email"]
-
- # check that logged in user has admin permissions on this course
- if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
- raise PermissionDenied()
-
- user = get_user_by_email(email)
- if user is None:
- return create_json_response('Could not find user by email address \'{0}\'.'.format(email))
-
- # make sure we're not removing ourselves
- if user.id == request.user.id:
- raise PermissionDenied()
-
- remove_user_from_course_group(request.user, user, location, STAFF_ROLE_NAME)
-
- return create_json_response()
-
-
-# points to the temporary course landing page with log in and sign up
-def landing(request, org, course, coursename):
- return render_to_response('temp-course-landing.html', {})
-
-
-@login_required
-@ensure_csrf_cookie
-def static_pages(request, org, course, coursename):
-
- location = get_location_and_verify_access(request, org, course, coursename)
-
- course = modulestore().get_item(location)
-
- return render_to_response('static-pages.html', {
- 'active_tab': 'pages',
- 'context_course': course,
- })
-
-
-def edit_static(request, org, course, coursename):
- return render_to_response('edit-static-page.html', {})
-
-
-@login_required
-@expect_json
-def reorder_static_tabs(request):
- tabs = request.POST['tabs']
- course = get_course_for_item(tabs[0])
-
- if not has_access(request.user, course.location):
- raise PermissionDenied()
-
- # get list of existing static tabs in course
- # make sure they are the same lengths (i.e. the number of passed in tabs equals the number
- # that we know about) otherwise we can drop some!
-
- existing_static_tabs = [t for t in course.tabs if t['type'] == 'static_tab']
- if len(existing_static_tabs) != len(tabs):
- return HttpResponseBadRequest()
-
- # load all reference tabs, return BadRequest if we can't find any of them
- tab_items = []
- for tab in tabs:
- item = modulestore('direct').get_item(Location(tab))
- if item is None:
- return HttpResponseBadRequest()
-
- tab_items.append(item)
-
- # now just go through the existing course_tabs and re-order the static tabs
- reordered_tabs = []
- static_tab_idx = 0
- for tab in course.tabs:
- if tab['type'] == 'static_tab':
- reordered_tabs.append({'type': 'static_tab',
- 'name': tab_items[static_tab_idx].display_name,
- 'url_slug': tab_items[static_tab_idx].location.name})
- static_tab_idx += 1
- else:
- reordered_tabs.append(tab)
-
- # OK, re-assemble the static tabs in the new order
- course.tabs = reordered_tabs
- modulestore('direct').update_metadata(course.location, own_metadata(course))
- return HttpResponse()
-
-
-@login_required
-@ensure_csrf_cookie
-def edit_tabs(request, org, course, coursename):
- location = ['i4x', org, course, 'course', coursename]
- course_item = modulestore().get_item(location)
-
- # check that logged in user has permissions to this item
- if not has_access(request.user, location):
- raise PermissionDenied()
-
- # see tabs have been uninitialized (e.g. supporing courses created before tab support in studio)
- if course_item.tabs is None or len(course_item.tabs) == 0:
- initialize_course_tabs(course_item)
-
- # first get all static tabs from the tabs list
- # we do this because this is also the order in which items are displayed in the LMS
- static_tabs_refs = [t for t in course_item.tabs if t['type'] == 'static_tab']
-
- static_tabs = []
- for static_tab_ref in static_tabs_refs:
- static_tab_loc = Location(location)._replace(category='static_tab', name=static_tab_ref['url_slug'])
- static_tabs.append(modulestore('direct').get_item(static_tab_loc))
-
- components = [
- static_tab.location.url()
- for static_tab
- in static_tabs
- ]
-
- return render_to_response('edit-tabs.html', {
- 'active_tab': 'pages',
- 'context_course': course_item,
- 'components': components
- })
-
-
-def not_found(request):
- return render_to_response('error.html', {'error': '404'})
-
-
-def server_error(request):
- return render_to_response('error.html', {'error': '500'})
-
-
-@login_required
-@ensure_csrf_cookie
-def course_info(request, org, course, name, provided_id=None):
- """
- Send models and views as well as html for editing the course info to the client.
-
- org, course, name: Attributes of the Location for the item to edit
- """
- location = get_location_and_verify_access(request, org, course, name)
-
- course_module = modulestore().get_item(location)
-
- # get current updates
- location = ['i4x', org, course, 'course_info', "updates"]
-
- return render_to_response('course_info.html', {
- 'active_tab': 'courseinfo-tab',
- 'context_course': course_module,
- 'url_base': "/" + org + "/" + course + "/",
- 'course_updates': json.dumps(get_course_updates(location)),
- 'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url()
- })
-
-
-@expect_json
-@login_required
-@ensure_csrf_cookie
-def course_info_updates(request, org, course, provided_id=None):
- """
- restful CRUD operations on course_info updates.
-
- org, course: Attributes of the Location for the item to edit
- provided_id should be none if it's new (create) and a composite of the update db id + index otherwise.
- """
- # ??? No way to check for access permission afaik
- # get current updates
- location = ['i4x', org, course, 'course_info', "updates"]
-
- # Hmmm, provided_id is coming as empty string on create whereas I believe it used to be None :-(
- # Possibly due to my removing the seemingly redundant pattern in urls.py
- if provided_id == '':
- provided_id = None
-
- # check that logged in user has permissions to this item
- if not has_access(request.user, location):
- raise PermissionDenied()
-
- real_method = get_request_method(request)
-
- if request.method == 'GET':
- return HttpResponse(json.dumps(get_course_updates(location)),
- mimetype="application/json")
- elif real_method == 'DELETE':
- try:
- return HttpResponse(json.dumps(delete_course_update(location,
- request.POST, provided_id)), mimetype="application/json")
- except:
- return HttpResponseBadRequest("Failed to delete",
- content_type="text/plain")
- elif request.method == 'POST':
- try:
- return HttpResponse(json.dumps(update_course_updates(location,
- request.POST, provided_id)), mimetype="application/json")
- except:
- return HttpResponseBadRequest("Failed to save",
- content_type="text/plain")
-
-
-@expect_json
-@login_required
-@ensure_csrf_cookie
-def module_info(request, module_location):
- location = Location(module_location)
-
- # check that logged in user has permissions to this item
- if not has_access(request.user, location):
- raise PermissionDenied()
-
- real_method = get_request_method(request)
-
- rewrite_static_links = request.GET.get('rewrite_url_links', 'True') in ['True', 'true']
- logging.debug('rewrite_static_links = {0} {1}'.format(request.GET.get('rewrite_url_links', 'False'), rewrite_static_links))
-
- # check that logged in user has permissions to this item
- if not has_access(request.user, location):
- raise PermissionDenied()
-
- if real_method == 'GET':
- return HttpResponse(json.dumps(get_module_info(get_modulestore(location), location, rewrite_static_links=rewrite_static_links)), mimetype="application/json")
- elif real_method == 'POST' or real_method == 'PUT':
- return HttpResponse(json.dumps(set_module_info(get_modulestore(location), location, request.POST)), mimetype="application/json")
- else:
- return HttpResponseBadRequest()
-
-
-@login_required
-@ensure_csrf_cookie
-def get_course_settings(request, org, course, name):
- """
- Send models and views as well as html for editing the course settings to the client.
-
- org, course, name: Attributes of the Location for the item to edit
- """
- location = get_location_and_verify_access(request, org, course, name)
-
- course_module = modulestore().get_item(location)
-
- return render_to_response('settings.html', {
- 'context_course': course_module,
- 'course_location': location,
- 'details_url': reverse(course_settings_updates,
- kwargs={"org": org,
- "course": course,
- "name": name,
- "section": "details"})
- })
-
-
-@login_required
-@ensure_csrf_cookie
-def course_config_graders_page(request, org, course, name):
- """
- Send models and views as well as html for editing the course settings to the client.
-
- org, course, name: Attributes of the Location for the item to edit
- """
- location = get_location_and_verify_access(request, org, course, name)
-
- course_module = modulestore().get_item(location)
- course_details = CourseGradingModel.fetch(location)
-
- return render_to_response('settings_graders.html', {
- 'context_course': course_module,
- 'course_location': location,
- 'course_details': json.dumps(course_details, cls=CourseSettingsEncoder)
- })
-
-
-@login_required
-@ensure_csrf_cookie
-def course_config_advanced_page(request, org, course, name):
- """
- Send models and views as well as html for editing the advanced course settings to the client.
-
- org, course, name: Attributes of the Location for the item to edit
- """
- location = get_location_and_verify_access(request, org, course, name)
-
- course_module = modulestore().get_item(location)
-
- return render_to_response('settings_advanced.html', {
- 'context_course': course_module,
- 'course_location': location,
- 'advanced_dict': json.dumps(CourseMetadata.fetch(location)),
- })
-
-
-@expect_json
-@login_required
-@ensure_csrf_cookie
-def course_settings_updates(request, org, course, name, section):
- """
- restful CRUD operations on course settings. This differs from get_course_settings by communicating purely
- through json (not rendering any html) and handles section level operations rather than whole page.
-
- org, course: Attributes of the Location for the item to edit
- section: one of details, faculty, grading, problems, discussions
- """
- get_location_and_verify_access(request, org, course, name)
-
- if section == 'details':
- manager = CourseDetails
- elif section == 'grading':
- manager = CourseGradingModel
- else:
- return
-
- if request.method == 'GET':
- # Cannot just do a get w/o knowing the course name :-(
- return HttpResponse(json.dumps(manager.fetch(Location(['i4x', org, course, 'course', name])), cls=CourseSettingsEncoder),
- mimetype="application/json")
- elif request.method == 'POST': # post or put, doesn't matter.
- return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseSettingsEncoder),
- mimetype="application/json")
-
-
-@expect_json
-@login_required
-@ensure_csrf_cookie
-def course_grader_updates(request, org, course, name, grader_index=None):
- """
- restful CRUD operations on course_info updates. This differs from get_course_settings by communicating purely
- through json (not rendering any html) and handles section level operations rather than whole page.
-
- org, course: Attributes of the Location for the item to edit
- """
-
- location = get_location_and_verify_access(request, org, course, name)
-
- real_method = get_request_method(request)
-
- if real_method == 'GET':
- # Cannot just do a get w/o knowing the course name :-(
- return HttpResponse(json.dumps(CourseGradingModel.fetch_grader(Location(location), grader_index)),
- mimetype="application/json")
- elif real_method == "DELETE":
- # ??? Should this return anything? Perhaps success fail?
- CourseGradingModel.delete_grader(Location(location), grader_index)
- return HttpResponse()
- elif request.method == 'POST': # post or put, doesn't matter.
- return HttpResponse(json.dumps(CourseGradingModel.update_grader_from_json(Location(location), request.POST)),
- mimetype="application/json")
-
-
-# # NB: expect_json failed on ["key", "key2"] and json payload
-@login_required
-@ensure_csrf_cookie
-def course_advanced_updates(request, org, course, name):
- """
- restful CRUD operations on metadata. The payload is a json rep of the metadata dicts. For delete, otoh,
- the payload is either a key or a list of keys to delete.
-
- org, course: Attributes of the Location for the item to edit
- """
- location = get_location_and_verify_access(request, org, course, name)
-
- real_method = get_request_method(request)
-
- if real_method == 'GET':
- return HttpResponse(json.dumps(CourseMetadata.fetch(location)), mimetype="application/json")
- elif real_method == 'DELETE':
- return HttpResponse(json.dumps(CourseMetadata.delete_key(location, json.loads(request.body))),
- mimetype="application/json")
- elif real_method == 'POST' or real_method == 'PUT':
- # NOTE: request.POST is messed up because expect_json cloned_request.POST.copy() is creating a defective entry w/ the whole payload as the key
- request_body = json.loads(request.body)
- #Whether or not to filter the tabs key out of the settings metadata
- filter_tabs = True
- #Check to see if the user instantiated any advanced components. This is a hack to add the open ended panel tab
- #to a course automatically if the user has indicated that they want to edit the combinedopenended or peergrading
- #module, and to remove it if they have removed the open ended elements.
- if ADVANCED_COMPONENT_POLICY_KEY in request_body:
- #Check to see if the user instantiated any open ended components
- found_oe_type = False
- #Get the course so that we can scrape current tabs
- course_module = modulestore().get_item(location)
- for oe_type in OPEN_ENDED_COMPONENT_TYPES:
- if oe_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]:
- #Add an open ended tab to the course if needed
- changed, new_tabs = add_open_ended_panel_tab(course_module)
- #If a tab has been added to the course, then send the metadata along to CourseMetadata.update_from_json
- if changed:
- request_body.update({'tabs': new_tabs})
- #Indicate that tabs should not be filtered out of the metadata
- filter_tabs = False
- #Set this flag to avoid the open ended tab removal code below.
- found_oe_type = True
- break
- #If we did not find an open ended module type in the advanced settings,
- # we may need to remove the open ended tab from the course.
- if not found_oe_type:
- #Remove open ended tab to the course if needed
- changed, new_tabs = remove_open_ended_panel_tab(course_module)
- if changed:
- request_body.update({'tabs': new_tabs})
- #Indicate that tabs should not be filtered out of the metadata
- filter_tabs = False
- response_json = json.dumps(CourseMetadata.update_from_json(location, request_body, filter_tabs=filter_tabs))
- return HttpResponse(response_json, mimetype="application/json")
-
-
-@ensure_csrf_cookie
-@login_required
-def get_checklists(request, org, course, name):
- """
- Send models, views, and html for displaying the course checklists.
-
- org, course, name: Attributes of the Location for the item to edit
- """
- location = get_location_and_verify_access(request, org, course, name)
-
- modulestore = get_modulestore(location)
- course_module = modulestore.get_item(location)
- new_course_template = Location('i4x', 'edx', 'templates', 'course', 'Empty')
- template_module = modulestore.get_item(new_course_template)
-
- # If course was created before checklists were introduced, copy them over from the template.
- copied = False
- if not course_module.checklists:
- course_module.checklists = template_module.checklists
- copied = True
-
- checklists, modified = expand_checklist_action_urls(course_module)
- if copied or modified:
- modulestore.update_metadata(location, own_metadata(course_module))
- return render_to_response('checklists.html',
- {
- 'context_course': course_module,
- 'checklists': checklists
- })
-
-
-@ensure_csrf_cookie
-@login_required
-def update_checklist(request, org, course, name, checklist_index=None):
- """
- restful CRUD operations on course checklists. The payload is a json rep of
- the modified checklist. For PUT or POST requests, the index of the
- checklist being modified must be included; the returned payload will
- be just that one checklist. For GET requests, the returned payload
- is a json representation of the list of all checklists.
-
- org, course, name: Attributes of the Location for the item to edit
- """
- location = get_location_and_verify_access(request, org, course, name)
- modulestore = get_modulestore(location)
- course_module = modulestore.get_item(location)
-
- real_method = get_request_method(request)
- if real_method == 'POST' or real_method == 'PUT':
- if checklist_index is not None and 0 <= int(checklist_index) < len(course_module.checklists):
- index = int(checklist_index)
- course_module.checklists[index] = json.loads(request.body)
- checklists, modified = expand_checklist_action_urls(course_module)
- modulestore.update_metadata(location, own_metadata(course_module))
- return HttpResponse(json.dumps(checklists[index]), mimetype="application/json")
- else:
- return HttpResponseBadRequest(
- "Could not save checklist state because the checklist index was out of range or unspecified.",
- content_type="text/plain")
- elif request.method == 'GET':
- # In the JavaScript view initialize method, we do a fetch to get all the checklists.
- checklists, modified = expand_checklist_action_urls(course_module)
- if modified:
- modulestore.update_metadata(location, own_metadata(course_module))
- return HttpResponse(json.dumps(checklists), mimetype="application/json")
- else:
- return HttpResponseBadRequest("Unsupported request.", content_type="text/plain")
-
-
-def expand_checklist_action_urls(course_module):
- """
- Gets the checklists out of the course module and expands their action urls
- if they have not yet been expanded.
-
- Returns the checklists with modified urls, as well as a boolean
- indicating whether or not the checklists were modified.
- """
- checklists = course_module.checklists
- modified = False
- for checklist in checklists:
- if not checklist.get('action_urls_expanded', False):
- for item in checklist.get('items'):
- item['action_url'] = get_url_reverse(item.get('action_url'), course_module)
- checklist['action_urls_expanded'] = True
- modified = True
-
- return checklists, modified
-
-
-@login_required
-@ensure_csrf_cookie
-def asset_index(request, org, course, name):
- """
- Display an editable asset library
-
- org, course, name: Attributes of the Location for the item to edit
- """
- location = get_location_and_verify_access(request, org, course, name)
-
- upload_asset_callback_url = reverse('upload_asset', kwargs={
- 'org': org,
- 'course': course,
- 'coursename': name
- })
-
- course_module = modulestore().get_item(location)
-
- course_reference = StaticContent.compute_location(org, course, name)
- assets = contentstore().get_all_content_for_course(course_reference)
-
- # sort in reverse upload date order
- assets = sorted(assets, key=lambda asset: asset['uploadDate'], reverse=True)
-
- asset_display = []
- for asset in assets:
- id = asset['_id']
- display_info = {}
- display_info['displayname'] = asset['displayname']
- display_info['uploadDate'] = get_default_time_display(asset['uploadDate'].timetuple())
-
- asset_location = StaticContent.compute_location(id['org'], id['course'], id['name'])
- display_info['url'] = StaticContent.get_url_path_from_location(asset_location)
-
- # note, due to the schema change we may not have a 'thumbnail_location' in the result set
- _thumbnail_location = asset.get('thumbnail_location', None)
- thumbnail_location = Location(_thumbnail_location) if _thumbnail_location is not None else None
- display_info['thumb_url'] = StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_location is not None else None
-
- asset_display.append(display_info)
-
- return render_to_response('asset_index.html', {
- 'active_tab': 'assets',
- 'context_course': course_module,
- 'assets': asset_display,
- 'upload_asset_callback_url': upload_asset_callback_url
- })
-
-
-# points to the temporary edge page
-def edge(request):
- return render_to_response('university_profiles/edge.html', {})
-
-
-@login_required
-@expect_json
-def create_new_course(request):
-
- if settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff:
- raise PermissionDenied()
-
- # This logic is repeated in xmodule/modulestore/tests/factories.py
- # so if you change anything here, you need to also change it there.
- # TODO: write a test that creates two courses, one with the factory and
- # the other with this method, then compare them to make sure they are
- # equivalent.
- template = Location(request.POST['template'])
- org = request.POST.get('org')
- number = request.POST.get('number')
- display_name = request.POST.get('display_name')
-
- try:
- dest_location = Location('i4x', org, number, 'course', Location.clean(display_name))
- except InvalidLocationError as e:
- return HttpResponse(json.dumps({'ErrMsg': "Unable to create course '" + display_name + "'.\n\n" + e.message}))
-
- # see if the course already exists
- existing_course = None
- try:
- existing_course = modulestore('direct').get_item(dest_location)
- except ItemNotFoundError:
- pass
-
- if existing_course is not None:
- return HttpResponse(json.dumps({'ErrMsg': 'There is already a course defined with this name.'}))
-
- course_search_location = ['i4x', dest_location.org, dest_location.course, 'course', None]
- courses = modulestore().get_items(course_search_location)
-
- if len(courses) > 0:
- return HttpResponse(json.dumps({'ErrMsg': 'There is already a course defined with the same organization and course number.'}))
-
- new_course = modulestore('direct').clone_item(template, dest_location)
-
- # clone a default 'about' module as well
-
- about_template_location = Location(['i4x', 'edx', 'templates', 'about', 'overview'])
- dest_about_location = dest_location._replace(category='about', name='overview')
- modulestore('direct').clone_item(about_template_location, dest_about_location)
-
- if display_name is not None:
- new_course.display_name = display_name
-
- # set a default start date to now
- new_course.start = time.gmtime()
-
- initialize_course_tabs(new_course)
-
- create_all_course_groups(request.user, new_course.location)
-
- return HttpResponse(json.dumps({'id': new_course.location.url()}))
-
-
-def initialize_course_tabs(course):
- # set up the default tabs
- # I've added this because when we add static tabs, the LMS either expects a None for the tabs list or
- # at least a list populated with the minimal times
- # @TODO: I don't like the fact that the presentation tier is away of these data related constraints, let's find a better
- # place for this. Also rather than using a simple list of dictionaries a nice class model would be helpful here
-
- # This logic is repeated in xmodule/modulestore/tests/factories.py
- # so if you change anything here, you need to also change it there.
- course.tabs = [{"type": "courseware"},
- {"type": "course_info", "name": "Course Info"},
- {"type": "discussion", "name": "Discussion"},
- {"type": "wiki", "name": "Wiki"},
- {"type": "progress", "name": "Progress"}]
-
- modulestore('direct').update_metadata(course.location.url(), own_metadata(course))
-
-
-@ensure_csrf_cookie
-@login_required
-def import_course(request, org, course, name):
-
- location = get_location_and_verify_access(request, org, course, name)
-
- if request.method == 'POST':
- filename = request.FILES['course-data'].name
-
- if not filename.endswith('.tar.gz'):
- return HttpResponse(json.dumps({'ErrMsg': 'We only support uploading a .tar.gz file.'}))
-
- data_root = path(settings.GITHUB_REPO_ROOT)
-
- course_subdir = "{0}-{1}-{2}".format(org, course, name)
- course_dir = data_root / course_subdir
- if not course_dir.isdir():
- os.mkdir(course_dir)
-
- temp_filepath = course_dir / filename
-
- logging.debug('importing course to {0}'.format(temp_filepath))
-
- # stream out the uploaded files in chunks to disk
- temp_file = open(temp_filepath, 'wb+')
- for chunk in request.FILES['course-data'].chunks():
- temp_file.write(chunk)
- temp_file.close()
-
- tf = tarfile.open(temp_filepath)
- tf.extractall(course_dir + '/')
-
- # find the 'course.xml' file
-
- for r, d, f in os.walk(course_dir):
- for files in f:
- if files == 'course.xml':
- break
- if files == 'course.xml':
- break
-
- if files != 'course.xml':
- return HttpResponse(json.dumps({'ErrMsg': 'Could not find the course.xml file in the package.'}))
-
- logging.debug('found course.xml at {0}'.format(r))
-
- if r != course_dir:
- for fname in os.listdir(r):
- shutil.move(r / fname, course_dir)
-
- module_store, course_items = import_from_xml(modulestore('direct'), settings.GITHUB_REPO_ROOT,
- [course_subdir], load_error_modules=False,
- static_content_store=contentstore(),
- target_location_namespace=Location(location),
- draft_store=modulestore())
-
- # we can blow this away when we're done importing.
- shutil.rmtree(course_dir)
-
- logging.debug('new course at {0}'.format(course_items[0].location))
-
- create_all_course_groups(request.user, course_items[0].location)
-
- return HttpResponse(json.dumps({'Status': 'OK'}))
- else:
- course_module = modulestore().get_item(location)
-
- return render_to_response('import.html', {
- 'context_course': course_module,
- 'active_tab': 'import',
- 'successful_import_redirect_url': get_url_reverse('CourseOutline', course_module)
- })
-
-
-@ensure_csrf_cookie
-@login_required
-def generate_export_course(request, org, course, name):
- location = get_location_and_verify_access(request, org, course, name)
-
- loc = Location(location)
- export_file = NamedTemporaryFile(prefix=name + '.', suffix=".tar.gz")
-
- root_dir = path(mkdtemp())
-
- # export out to a tempdir
-
- logging.debug('root = {0}'.format(root_dir))
-
- export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name, modulestore())
- #filename = root_dir / name + '.tar.gz'
-
- logging.debug('tar file being generated at {0}'.format(export_file.name))
- tf = tarfile.open(name=export_file.name, mode='w:gz')
- tf.add(root_dir / name, arcname=name)
- tf.close()
-
- # remove temp dir
- shutil.rmtree(root_dir / name)
-
- wrapper = FileWrapper(export_file)
- response = HttpResponse(wrapper, content_type='application/x-tgz')
- response['Content-Disposition'] = 'attachment; filename=%s' % os.path.basename(export_file.name)
- response['Content-Length'] = os.path.getsize(export_file.name)
- return response
-
-
-@ensure_csrf_cookie
-@login_required
-def export_course(request, org, course, name):
-
- location = get_location_and_verify_access(request, org, course, name)
-
- course_module = modulestore().get_item(location)
-
- return render_to_response('export.html', {
- 'context_course': course_module,
- 'active_tab': 'export',
- 'successful_import_redirect_url': ''
- })
-
-
-def event(request):
- '''
- A noop to swallow the analytics call so that cms methods don't spook and poor developers looking at
- console logs don't get distracted :-)
- '''
- return HttpResponse(True)
-
-
-def render_404(request):
- return HttpResponseNotFound(render_to_string('404.html', {}))
-
-
-def render_500(request):
- return HttpResponseServerError(render_to_string('500.html', {}))
-
-
-def get_location_and_verify_access(request, org, course, name):
- """
- Create the location tuple verify that the user has permissions
- to view the location. Returns the location.
- """
- location = ['i4x', org, course, 'course', name]
-
- # check that logged in user has permissions to this item
- if not has_access(request.user, location):
- raise PermissionDenied()
-
- return location
-
-
-def get_request_method(request):
- """
- Using HTTP_X_HTTP_METHOD_OVERRIDE, in the request metadata, determine
- what type of request came from the client, and return it.
- """
- # NB: we're setting Backbone.emulateHTTP to true on the client so everything comes as a post!!!
- if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
- real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
- else:
- real_method = request.method
-
- return real_method
diff --git a/cms/djangoapps/contentstore/views/__init__.py b/cms/djangoapps/contentstore/views/__init__.py
new file mode 100644
index 0000000000..0b7c271b1e
--- /dev/null
+++ b/cms/djangoapps/contentstore/views/__init__.py
@@ -0,0 +1,17 @@
+from new import *
+from error import *
+from course import *
+from item import *
+from public import *
+from user import *
+from preview import *
+from assets import *
+from checklist import *
+from requests import landing
+
+
+"""
+
+from main import *
+
+"""
diff --git a/cms/djangoapps/contentstore/views/access.py b/cms/djangoapps/contentstore/views/access.py
new file mode 100644
index 0000000000..dd3add1099
--- /dev/null
+++ b/cms/djangoapps/contentstore/views/access.py
@@ -0,0 +1,39 @@
+#from auth.authz import is_user_in_course_group_role, get_users_in_course_group_by_role
+#from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group
+#from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME, create_all_course_groups
+
+from auth.authz import STAFF_ROLE_NAME, INSTRUCTOR_ROLE_NAME
+from auth.authz import is_user_in_course_group_role
+from contentstore.utils import get_course_location_for_item
+
+
+def get_location_and_verify_access(request, org, course, name):
+ """
+ Create the location tuple verify that the user has permissions
+ to view the location. Returns the location.
+ """
+ location = ['i4x', org, course, 'course', name]
+
+ # check that logged in user has permissions to this item
+ if not has_access(request.user, location):
+ raise PermissionDenied()
+
+ return location
+
+
+def has_access(user, location, role=STAFF_ROLE_NAME):
+ '''
+ Return True if user allowed to access this piece of data
+ Note that the CMS permissions model is with respect to courses
+ There is a super-admin permissions if user.is_staff is set
+ Also, since we're unifying the user database between LMS and CAS,
+ I'm presuming that the course instructor (formally known as admin)
+ will not be in both INSTRUCTOR and STAFF groups, so we have to cascade our queries here as INSTRUCTOR
+ has all the rights that STAFF do
+ '''
+ course_location = get_course_location_for_item(location)
+ _has_access = is_user_in_course_group_role(user, course_location, role)
+ # if we're not in STAFF, perhaps we're in INSTRUCTOR groups
+ if not _has_access and role == STAFF_ROLE_NAME:
+ _has_access = is_user_in_course_group_role(user, course_location, INSTRUCTOR_ROLE_NAME)
+ return _has_access
diff --git a/cms/djangoapps/contentstore/views/assets.py b/cms/djangoapps/contentstore/views/assets.py
new file mode 100644
index 0000000000..616b04342d
--- /dev/null
+++ b/cms/djangoapps/contentstore/views/assets.py
@@ -0,0 +1,118 @@
+from django.http import HttpResponse, HttpResponseBadRequest
+from django.contrib.auth.decorators import login_required
+from django_future.csrf import ensure_csrf_cookie
+
+from xmodule.contentstore.content import StaticContent
+from access import get_location_and_verify_access
+from xmodule.util.date_utils import get_default_time_display
+from mitxmako.shortcuts import render_to_response
+
+
+@login_required
+@ensure_csrf_cookie
+def asset_index(request, org, course, name):
+ """
+ Display an editable asset library
+
+ org, course, name: Attributes of the Location for the item to edit
+ """
+ location = get_location_and_verify_access(request, org, course, name)
+
+ upload_asset_callback_url = reverse('upload_asset', kwargs={
+ 'org': org,
+ 'course': course,
+ 'coursename': name
+ })
+
+ course_module = modulestore().get_item(location)
+
+ course_reference = StaticContent.compute_location(org, course, name)
+ assets = contentstore().get_all_content_for_course(course_reference)
+
+ # sort in reverse upload date order
+ assets = sorted(assets, key=lambda asset: asset['uploadDate'], reverse=True)
+
+ asset_display = []
+ for asset in assets:
+ id = asset['_id']
+ display_info = {}
+ display_info['displayname'] = asset['displayname']
+ display_info['uploadDate'] = get_default_time_display(asset['uploadDate'].timetuple())
+
+ asset_location = StaticContent.compute_location(id['org'], id['course'], id['name'])
+ display_info['url'] = StaticContent.get_url_path_from_location(asset_location)
+
+ # note, due to the schema change we may not have a 'thumbnail_location' in the result set
+ _thumbnail_location = asset.get('thumbnail_location', None)
+ thumbnail_location = Location(_thumbnail_location) if _thumbnail_location is not None else None
+ display_info['thumb_url'] = StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_location is not None else None
+
+ asset_display.append(display_info)
+
+ return render_to_response('asset_index.html', {
+ 'active_tab': 'assets',
+ 'context_course': course_module,
+ 'assets': asset_display,
+ 'upload_asset_callback_url': upload_asset_callback_url
+ })
+
+
+def upload_asset(request, org, course, coursename):
+ '''
+ cdodge: this method allows for POST uploading of files into the course asset library, which will
+ be supported by GridFS in MongoDB.
+ '''
+ if request.method != 'POST':
+ # (cdodge) @todo: Is there a way to do a - say - 'raise Http400'?
+ return HttpResponseBadRequest()
+
+ # construct a location from the passed in path
+ location = get_location_and_verify_access(request, org, course, coursename)
+
+ # Does the course actually exist?!? Get anything from it to prove its existance
+
+ try:
+ modulestore().get_item(location)
+ except:
+ # no return it as a Bad Request response
+ logging.error('Could not find course' + location)
+ return HttpResponseBadRequest()
+
+ # compute a 'filename' which is similar to the location formatting, we're using the 'filename'
+ # nomenclature since we're using a FileSystem paradigm here. We're just imposing
+ # the Location string formatting expectations to keep things a bit more consistent
+
+ filename = request.FILES['file'].name
+ mime_type = request.FILES['file'].content_type
+ filedata = request.FILES['file'].read()
+
+ content_loc = StaticContent.compute_location(org, course, filename)
+ content = StaticContent(content_loc, filename, mime_type, filedata)
+
+ # first let's see if a thumbnail can be created
+ (thumbnail_content, thumbnail_location) = contentstore().generate_thumbnail(content)
+
+ # delete cached thumbnail even if one couldn't be created this time (else the old thumbnail will continue to show)
+ del_cached_content(thumbnail_location)
+ # now store thumbnail location only if we could create it
+ if thumbnail_content is not None:
+ content.thumbnail_location = thumbnail_location
+
+ # then commit the content
+ contentstore().save(content)
+ del_cached_content(content.location)
+
+ # readback the saved content - we need the database timestamp
+ readback = contentstore().find(content.location)
+
+ response_payload = {'displayname': content.name,
+ 'uploadDate': get_default_time_display(readback.last_modified_at.timetuple()),
+ 'url': StaticContent.get_url_path_from_location(content.location),
+ 'thumb_url': StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None,
+ 'msg': 'Upload completed'
+ }
+
+ response = HttpResponse(json.dumps(response_payload))
+ response['asset_url'] = StaticContent.get_url_path_from_location(content.location)
+ return response
+
diff --git a/cms/djangoapps/contentstore/views/checklist.py b/cms/djangoapps/contentstore/views/checklist.py
new file mode 100644
index 0000000000..376e041523
--- /dev/null
+++ b/cms/djangoapps/contentstore/views/checklist.py
@@ -0,0 +1,98 @@
+from django.http import HttpResponse, HttpResponseBadRequest
+from django.contrib.auth.decorators import login_required
+from django_future.csrf import ensure_csrf_cookie
+from access import get_location_and_verify_access
+from mitxmako.shortcuts import render_to_response
+from contentstore.utils import get_modulestore, get_url_reverse
+
+@ensure_csrf_cookie
+@login_required
+def get_checklists(request, org, course, name):
+ """
+ Send models, views, and html for displaying the course checklists.
+
+ org, course, name: Attributes of the Location for the item to edit
+ """
+ location = get_location_and_verify_access(request, org, course, name)
+
+ modulestore = get_modulestore(location)
+ course_module = modulestore.get_item(location)
+ new_course_template = Location('i4x', 'edx', 'templates', 'course', 'Empty')
+ template_module = modulestore.get_item(new_course_template)
+
+ # If course was created before checklists were introduced, copy them over from the template.
+ copied = False
+ if not course_module.checklists:
+ course_module.checklists = template_module.checklists
+ copied = True
+
+ checklists, modified = expand_checklist_action_urls(course_module)
+ if copied or modified:
+ modulestore.update_metadata(location, own_metadata(course_module))
+ return render_to_response('checklists.html',
+ {
+ 'context_course': course_module,
+ 'checklists': checklists
+ })
+
+
+@ensure_csrf_cookie
+@login_required
+def update_checklist(request, org, course, name, checklist_index=None):
+ """
+ restful CRUD operations on course checklists. The payload is a json rep of
+ the modified checklist. For PUT or POST requests, the index of the
+ checklist being modified must be included; the returned payload will
+ be just that one checklist. For GET requests, the returned payload
+ is a json representation of the list of all checklists.
+
+ org, course, name: Attributes of the Location for the item to edit
+ """
+ location = get_location_and_verify_access(request, org, course, name)
+ modulestore = get_modulestore(location)
+ course_module = modulestore.get_item(location)
+
+ real_method = get_request_method(request)
+ if real_method == 'POST' or real_method == 'PUT':
+ if checklist_index is not None and 0 <= int(checklist_index) < len(course_module.checklists):
+ index = int(checklist_index)
+ course_module.checklists[index] = json.loads(request.body)
+ checklists, modified = expand_checklist_action_urls(course_module)
+ modulestore.update_metadata(location, own_metadata(course_module))
+ return HttpResponse(json.dumps(checklists[index]), mimetype="application/json")
+ else:
+ return HttpResponseBadRequest(
+ "Could not save checklist state because the checklist index was out of range or unspecified.",
+ content_type="text/plain")
+ elif request.method == 'GET':
+ # In the JavaScript view initialize method, we do a fetch to get all the checklists.
+ checklists, modified = expand_checklist_action_urls(course_module)
+ if modified:
+ modulestore.update_metadata(location, own_metadata(course_module))
+ return HttpResponse(json.dumps(checklists), mimetype="application/json")
+ else:
+ return HttpResponseBadRequest("Unsupported request.", content_type="text/plain")
+
+
+def expand_checklist_action_urls(course_module):
+ """
+ Gets the checklists out of the course module and expands their action urls
+ if they have not yet been expanded.
+
+ Returns the checklists with modified urls, as well as a boolean
+ indicating whether or not the checklists were modified.
+ """
+ checklists = course_module.checklists
+ modified = False
+ for checklist in checklists:
+ if not checklist.get('action_urls_expanded', False):
+ for item in checklist.get('items'):
+ item['action_url'] = get_url_reverse(item.get('action_url'), course_module)
+ checklist['action_urls_expanded'] = True
+ modified = True
+
+ return checklists, modified
+
+
+
+
diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py
new file mode 100644
index 0000000000..fc2214f970
--- /dev/null
+++ b/cms/djangoapps/contentstore/views/course.py
@@ -0,0 +1,202 @@
+from django.contrib.auth.decorators import login_required
+from django_future.csrf import ensure_csrf_cookie
+
+from util.json_request import expect_json
+from mitxmako.shortcuts import render_to_response
+
+@login_required
+@expect_json
+def create_new_course(request):
+
+ if settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff:
+ raise PermissionDenied()
+
+ # This logic is repeated in xmodule/modulestore/tests/factories.py
+ # so if you change anything here, you need to also change it there.
+ # TODO: write a test that creates two courses, one with the factory and
+ # the other with this method, then compare them to make sure they are
+ # equivalent.
+ template = Location(request.POST['template'])
+ org = request.POST.get('org')
+ number = request.POST.get('number')
+ display_name = request.POST.get('display_name')
+
+ try:
+ dest_location = Location('i4x', org, number, 'course', Location.clean(display_name))
+ except InvalidLocationError as e:
+ return HttpResponse(json.dumps({'ErrMsg': "Unable to create course '" + display_name + "'.\n\n" + e.message}))
+
+ # see if the course already exists
+ existing_course = None
+ try:
+ existing_course = modulestore('direct').get_item(dest_location)
+ except ItemNotFoundError:
+ pass
+
+ if existing_course is not None:
+ return HttpResponse(json.dumps({'ErrMsg': 'There is already a course defined with this name.'}))
+
+ course_search_location = ['i4x', dest_location.org, dest_location.course, 'course', None]
+ courses = modulestore().get_items(course_search_location)
+
+ if len(courses) > 0:
+ return HttpResponse(json.dumps({'ErrMsg': 'There is already a course defined with the same organization and course number.'}))
+
+ new_course = modulestore('direct').clone_item(template, dest_location)
+
+ # clone a default 'about' module as well
+
+ about_template_location = Location(['i4x', 'edx', 'templates', 'about', 'overview'])
+ dest_about_location = dest_location._replace(category='about', name='overview')
+ modulestore('direct').clone_item(about_template_location, dest_about_location)
+
+ if display_name is not None:
+ new_course.display_name = display_name
+
+ # set a default start date to now
+ new_course.start = time.gmtime()
+
+ initialize_course_tabs(new_course)
+
+ create_all_course_groups(request.user, new_course.location)
+
+ return HttpResponse(json.dumps({'id': new_course.location.url()}))
+
+
+def initialize_course_tabs(course):
+ # set up the default tabs
+ # I've added this because when we add static tabs, the LMS either expects a None for the tabs list or
+ # at least a list populated with the minimal times
+ # @TODO: I don't like the fact that the presentation tier is away of these data related constraints, let's find a better
+ # place for this. Also rather than using a simple list of dictionaries a nice class model would be helpful here
+
+ # This logic is repeated in xmodule/modulestore/tests/factories.py
+ # so if you change anything here, you need to also change it there.
+ course.tabs = [{"type": "courseware"},
+ {"type": "course_info", "name": "Course Info"},
+ {"type": "discussion", "name": "Discussion"},
+ {"type": "wiki", "name": "Wiki"},
+ {"type": "progress", "name": "Progress"}]
+
+ modulestore('direct').update_metadata(course.location.url(), own_metadata(course))
+
+
+@ensure_csrf_cookie
+@login_required
+def import_course(request, org, course, name):
+
+ location = get_location_and_verify_access(request, org, course, name)
+
+ if request.method == 'POST':
+ filename = request.FILES['course-data'].name
+
+ if not filename.endswith('.tar.gz'):
+ return HttpResponse(json.dumps({'ErrMsg': 'We only support uploading a .tar.gz file.'}))
+
+ data_root = path(settings.GITHUB_REPO_ROOT)
+
+ course_subdir = "{0}-{1}-{2}".format(org, course, name)
+ course_dir = data_root / course_subdir
+ if not course_dir.isdir():
+ os.mkdir(course_dir)
+
+ temp_filepath = course_dir / filename
+
+ logging.debug('importing course to {0}'.format(temp_filepath))
+
+ # stream out the uploaded files in chunks to disk
+ temp_file = open(temp_filepath, 'wb+')
+ for chunk in request.FILES['course-data'].chunks():
+ temp_file.write(chunk)
+ temp_file.close()
+
+ tf = tarfile.open(temp_filepath)
+ tf.extractall(course_dir + '/')
+
+ # find the 'course.xml' file
+
+ for r, d, f in os.walk(course_dir):
+ for files in f:
+ if files == 'course.xml':
+ break
+ if files == 'course.xml':
+ break
+
+ if files != 'course.xml':
+ return HttpResponse(json.dumps({'ErrMsg': 'Could not find the course.xml file in the package.'}))
+
+ logging.debug('found course.xml at {0}'.format(r))
+
+ if r != course_dir:
+ for fname in os.listdir(r):
+ shutil.move(r / fname, course_dir)
+
+ module_store, course_items = import_from_xml(modulestore('direct'), settings.GITHUB_REPO_ROOT,
+ [course_subdir], load_error_modules=False,
+ static_content_store=contentstore(),
+ target_location_namespace=Location(location),
+ draft_store=modulestore())
+
+ # we can blow this away when we're done importing.
+ shutil.rmtree(course_dir)
+
+ logging.debug('new course at {0}'.format(course_items[0].location))
+
+ create_all_course_groups(request.user, course_items[0].location)
+
+ return HttpResponse(json.dumps({'Status': 'OK'}))
+ else:
+ course_module = modulestore().get_item(location)
+
+ return render_to_response('import.html', {
+ 'context_course': course_module,
+ 'active_tab': 'import',
+ 'successful_import_redirect_url': get_url_reverse('CourseOutline', course_module)
+ })
+
+
+@ensure_csrf_cookie
+@login_required
+def generate_export_course(request, org, course, name):
+ location = get_location_and_verify_access(request, org, course, name)
+
+ loc = Location(location)
+ export_file = NamedTemporaryFile(prefix=name + '.', suffix=".tar.gz")
+
+ root_dir = path(mkdtemp())
+
+ # export out to a tempdir
+
+ logging.debug('root = {0}'.format(root_dir))
+
+ export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name, modulestore())
+ #filename = root_dir / name + '.tar.gz'
+
+ logging.debug('tar file being generated at {0}'.format(export_file.name))
+ tf = tarfile.open(name=export_file.name, mode='w:gz')
+ tf.add(root_dir / name, arcname=name)
+ tf.close()
+
+ # remove temp dir
+ shutil.rmtree(root_dir / name)
+
+ wrapper = FileWrapper(export_file)
+ response = HttpResponse(wrapper, content_type='application/x-tgz')
+ response['Content-Disposition'] = 'attachment; filename=%s' % os.path.basename(export_file.name)
+ response['Content-Length'] = os.path.getsize(export_file.name)
+ return response
+
+@ensure_csrf_cookie
+@login_required
+def export_course(request, org, course, name):
+
+ location = get_location_and_verify_access(request, org, course, name)
+
+ course_module = modulestore().get_item(location)
+
+ return render_to_response('export.html', {
+ 'context_course': course_module,
+ 'active_tab': 'export',
+ 'successful_import_redirect_url': ''
+ })
+
diff --git a/cms/djangoapps/contentstore/views/error.py b/cms/djangoapps/contentstore/views/error.py
new file mode 100644
index 0000000000..527f137b9e
--- /dev/null
+++ b/cms/djangoapps/contentstore/views/error.py
@@ -0,0 +1,21 @@
+from django.http import HttpResponse, HttpResponseServerError, HttpResponseNotFound
+
+from mitxmako.shortcuts import render_to_string, render_to_response
+
+
+def not_found(request):
+ return render_to_response('error.html', {'error': '404'})
+
+
+def server_error(request):
+ return render_to_response('error.html', {'error': '500'})
+
+
+def render_404(request):
+ return HttpResponseNotFound(render_to_string('404.html', {}))
+
+
+def render_500(request):
+ return HttpResponseServerError(render_to_string('500.html', {}))
+
+
diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py
new file mode 100644
index 0000000000..876251203e
--- /dev/null
+++ b/cms/djangoapps/contentstore/views/item.py
@@ -0,0 +1,123 @@
+from django.http import HttpResponse
+from django.contrib.auth.decorators import login_required
+from util.json_request import expect_json
+from mitxmako.shortcuts import render_to_response
+
+
+@login_required
+@expect_json
+def save_item(request):
+ item_location = request.POST['id']
+
+ # check permissions for this user within this course
+ if not has_access(request.user, item_location):
+ raise PermissionDenied()
+
+ store = get_modulestore(Location(item_location))
+
+ if request.POST.get('data') is not None:
+ data = request.POST['data']
+ store.update_item(item_location, data)
+
+ # cdodge: note calling request.POST.get('children') will return None if children is an empty array
+ # so it lead to a bug whereby the last component to be deleted in the UI was not actually
+ # deleting the children object from the children collection
+ if 'children' in request.POST and request.POST['children'] is not None:
+ children = request.POST['children']
+ store.update_children(item_location, children)
+
+ # cdodge: also commit any metadata which might have been passed along in the
+ # POST from the client, if it is there
+ # NOTE, that the postback is not the complete metadata, as there's system metadata which is
+ # not presented to the end-user for editing. So let's fetch the original and
+ # 'apply' the submitted metadata, so we don't end up deleting system metadata
+ if request.POST.get('metadata') is not None:
+ posted_metadata = request.POST['metadata']
+ # fetch original
+ existing_item = modulestore().get_item(item_location)
+
+ # update existing metadata with submitted metadata (which can be partial)
+ # 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():
+
+ 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]
+ del posted_metadata[metadata_key]
+ else:
+ existing_item._model_data[metadata_key] = value
+
+ # commit to datastore
+ # TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
+ store.update_metadata(item_location, own_metadata(existing_item))
+
+ return HttpResponse()
+
+
+@login_required
+@expect_json
+def clone_item(request):
+ parent_location = Location(request.POST['parent_location'])
+ template = Location(request.POST['template'])
+
+ display_name = request.POST.get('display_name')
+
+ if not has_access(request.user, parent_location):
+ raise PermissionDenied()
+
+ parent = get_modulestore(template).get_item(parent_location)
+ dest_location = parent_location._replace(category=template.category, name=uuid4().hex)
+
+ new_item = get_modulestore(template).clone_item(template, dest_location)
+
+ # replace the display name with an optional parameter passed in from the caller
+ if display_name is not None:
+ new_item.display_name = display_name
+
+ get_modulestore(template).update_metadata(new_item.location.url(), own_metadata(new_item))
+
+ if new_item.location.category not in DETACHED_CATEGORIES:
+ get_modulestore(parent.location).update_children(parent_location, parent.children + [new_item.location.url()])
+
+ return HttpResponse(json.dumps({'id': dest_location.url()}))
+
+
+@login_required
+@expect_json
+def delete_item(request):
+ item_location = request.POST['id']
+ item_loc = Location(item_location)
+
+ # check permissions for this user within this course
+ if not has_access(request.user, item_location):
+ raise PermissionDenied()
+
+ # optional parameter to delete all children (default False)
+ delete_children = request.POST.get('delete_children', False)
+ delete_all_versions = request.POST.get('delete_all_versions', False)
+
+ store = modulestore()
+
+ item = store.get_item(item_location)
+
+ if delete_children:
+ _xmodule_recurse(item, lambda i: store.delete_item(i.location, delete_all_versions))
+ else:
+ store.delete_item(item.location, delete_all_versions)
+
+ # cdodge: we need to remove our parent's pointer to us so that it is no longer dangling
+ if delete_all_versions:
+ parent_locs = modulestore('direct').get_parent_locations(item_loc, None)
+
+ for parent_loc in parent_locs:
+ parent = modulestore('direct').get_item(parent_loc)
+ item_url = item_loc.url()
+ if item_url in parent.children:
+ children = parent.children
+ children.remove(item_url)
+ parent.children = children
+ modulestore('direct').update_children(parent.location, parent.children)
+
+ return HttpResponse()
+
diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py
new file mode 100644
index 0000000000..f473b962c5
--- /dev/null
+++ b/cms/djangoapps/contentstore/views/preview.py
@@ -0,0 +1,156 @@
+from django.contrib.auth.decorators import login_required
+from xblock.runtime import DbModel
+from xmodule.x_module import ModuleSystem
+from xmodule.modulestore.mongo import MongoUsage
+from xmodule_modifiers import wrap_xmodule
+from session_kv_store import SessionKeyValueStore
+from requests import render_from_lms
+from functools import partial
+
+@login_required
+def preview_dispatch(request, preview_id, location, dispatch=None):
+ """
+ Dispatch an AJAX action to a preview XModule
+
+ Expects a POST request, and passes the arguments to the module
+
+ preview_id (str): An identifier specifying which preview this module is used for
+ location: The Location of the module to dispatch to
+ dispatch: The action to execute
+ """
+
+ descriptor = modulestore().get_item(location)
+ instance = load_preview_module(request, preview_id, descriptor)
+ # Let the module handle the AJAX
+ try:
+ ajax_return = instance.handle_ajax(dispatch, request.POST)
+
+ except NotFoundError:
+ log.exception("Module indicating to user that request doesn't exist")
+ raise Http404
+
+ except ProcessingError:
+ log.warning("Module raised an error while processing AJAX request",
+ exc_info=True)
+ return HttpResponseBadRequest()
+
+ except:
+ log.exception("error processing ajax call")
+ raise
+
+ return HttpResponse(ajax_return)
+
+@login_required
+def preview_component(request, location):
+ # TODO (vshnayder): change name from id to location in coffee+html as well.
+ if not has_access(request.user, location):
+ raise HttpResponseForbidden()
+
+ component = modulestore().get_item(location)
+
+ return render_to_response('component.html', {
+ 'preview': get_module_previews(request, component)[0],
+ 'editor': wrap_xmodule(component.get_html, component, 'xmodule_edit.html')(),
+ })
+
+
+
+def preview_module_system(request, preview_id, descriptor):
+ """
+ Returns a ModuleSystem for the specified descriptor that is specialized for
+ rendering module previews.
+
+ request: The active django request
+ preview_id (str): An identifier specifying which preview this module is used for
+ descriptor: An XModuleDescriptor
+ """
+
+ def preview_model_data(descriptor):
+ return DbModel(
+ SessionKeyValueStore(request, descriptor._model_data),
+ descriptor.module_class,
+ preview_id,
+ MongoUsage(preview_id, descriptor.location.url()),
+ )
+
+ return ModuleSystem(
+ ajax_url=reverse('preview_dispatch', args=[preview_id, descriptor.location.url(), '']).rstrip('/'),
+ # TODO (cpennington): Do we want to track how instructors are using the preview problems?
+ track_function=lambda type, event: None,
+ filestore=descriptor.system.resources_fs,
+ get_module=partial(get_preview_module, request, preview_id),
+ render_template=render_from_lms,
+ debug=True,
+ replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_namespace=descriptor.location),
+ user=request.user,
+ xblock_model_data=preview_model_data,
+ )
+
+def get_preview_module(request, preview_id, descriptor):
+ """
+ Returns a preview XModule at the specified location. The preview_data is chosen arbitrarily
+ from the set of preview data for the descriptor specified by Location
+
+ request: The active django request
+ preview_id (str): An identifier specifying which preview this module is used for
+ location: A Location
+ """
+
+ return load_preview_module(request, preview_id, descriptor)
+
+
+def load_preview_module(request, preview_id, descriptor):
+ """
+ Return a preview XModule instantiated from the supplied descriptor, instance_state, and shared_state
+
+ request: The active django request
+ preview_id (str): An identifier specifying which preview this module is used for
+ descriptor: An XModuleDescriptor
+ instance_state: An instance state string
+ shared_state: A shared state string
+ """
+ system = preview_module_system(request, preview_id, descriptor)
+ try:
+ module = descriptor.xmodule(system)
+ except:
+ log.debug("Unable to load preview module", exc_info=True)
+ module = ErrorDescriptor.from_descriptor(
+ descriptor,
+ error_msg=exc_info_to_str(sys.exc_info())
+ ).xmodule(system)
+
+ # cdodge: Special case
+ if module.location.category == 'static_tab':
+ module.get_html = wrap_xmodule(
+ module.get_html,
+ module,
+ "xmodule_tab_display.html",
+ )
+ else:
+ module.get_html = wrap_xmodule(
+ module.get_html,
+ module,
+ "xmodule_display.html",
+ )
+
+ module.get_html = replace_static_urls(
+ module.get_html,
+ getattr(module, 'data_dir', module.location.course),
+ course_namespace=Location([module.location.tag, module.location.org, module.location.course, None, None])
+ )
+
+ return module
+
+def get_module_previews(request, descriptor):
+ """
+ Returns a list of preview XModule html contents. One preview is returned for each
+ pair of states returned by get_sample_state() for the supplied descriptor.
+
+ descriptor: An XModuleDescriptor
+ """
+ preview_html = []
+ for idx, (instance_state, shared_state) in enumerate(descriptor.get_sample_state()):
+ module = load_preview_module(request, str(idx), descriptor)
+ preview_html.append(module.get_html())
+ return preview_html
+
diff --git a/cms/djangoapps/contentstore/views/public.py b/cms/djangoapps/contentstore/views/public.py
new file mode 100644
index 0000000000..7c207b7893
--- /dev/null
+++ b/cms/djangoapps/contentstore/views/public.py
@@ -0,0 +1,50 @@
+from external_auth.views import ssl_login_shortcut
+from mitxmako.shortcuts import render_to_response
+from django_future.csrf import ensure_csrf_cookie
+from requests import index
+
+"""
+Public views
+"""
+
+@ensure_csrf_cookie
+def signup(request):
+ """
+ Display the signup form.
+ """
+ csrf_token = csrf(request)['csrf_token']
+ return render_to_response('signup.html', {'csrf': csrf_token})
+
+
+def old_login_redirect(request):
+ '''
+ Redirect to the active login url.
+ '''
+ return redirect('login', permanent=True)
+
+
+@ssl_login_shortcut
+@ensure_csrf_cookie
+def login_page(request):
+ """
+ Display the login form.
+ """
+ csrf_token = csrf(request)['csrf_token']
+ return render_to_response('login.html', {
+ 'csrf': csrf_token,
+ 'forgot_password_link': "//{base}/#forgot-password-modal".format(base=settings.LMS_BASE),
+ })
+
+
+def howitworks(request):
+ if request.user.is_authenticated():
+ return index(request)
+ else:
+ return render_to_response('howitworks.html', {})
+
+def ux_alerts(request):
+ """
+ static/proof-of-concept views
+ """
+ return render_to_response('ux-alerts.html', {})
+
diff --git a/cms/djangoapps/contentstore/views/requests.py b/cms/djangoapps/contentstore/views/requests.py
new file mode 100644
index 0000000000..131068768a
--- /dev/null
+++ b/cms/djangoapps/contentstore/views/requests.py
@@ -0,0 +1,91 @@
+from django.contrib.auth.decorators import login_required
+from django_future.csrf import ensure_csrf_cookie
+from mitxmako.shortcuts import render_to_response
+from xmodule.modulestore import Location
+from xmodule.modulestore.django import modulestore
+from access import has_access
+from contentstore.utils import get_url_reverse, get_lms_link_for_item
+from django.conf import settings
+
+@login_required
+@ensure_csrf_cookie
+def index(request):
+ """
+ List all courses available to the logged in user
+ """
+ courses = modulestore('direct').get_items(['i4x', None, None, 'course', None])
+
+ # filter out courses that we don't have access too
+ def course_filter(course):
+ return (has_access(request.user, course.location)
+ and course.location.course != 'templates'
+ and course.location.org != ''
+ and course.location.course != ''
+ and course.location.name != '')
+ courses = filter(course_filter, courses)
+
+ return render_to_response('index.html', {
+ 'new_course_template': Location('i4x', 'edx', 'templates', 'course', 'Empty'),
+ 'courses': [(course.display_name,
+ get_url_reverse('CourseOutline', course),
+ get_lms_link_for_item(course.location, course_id=course.location.course_id))
+ for course in courses],
+ 'user': request.user,
+ 'disable_course_creation': settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff
+ })
+
+
+# ==== Views with per-item permissions================================
+
+
+# points to the temporary course landing page with log in and sign up
+def landing(request, org, course, coursename):
+ return render_to_response('temp-course-landing.html', {})
+
+# points to the temporary edge page
+def edge(request):
+ return render_to_response('university_profiles/edge.html', {})
+
+
+def event(request):
+ '''
+ A noop to swallow the analytics call so that cms methods don't spook and poor developers looking at
+ console logs don't get distracted :-)
+ '''
+ return HttpResponse(True)
+
+
+def get_request_method(request):
+ """
+ Using HTTP_X_HTTP_METHOD_OVERRIDE, in the request metadata, determine
+ what type of request came from the client, and return it.
+ """
+ # NB: we're setting Backbone.emulateHTTP to true on the client so everything comes as a post!!!
+ if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
+ real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
+ else:
+ real_method = request.method
+
+ return real_method
+
+def create_json_response(errmsg=None):
+ if errmsg is not None:
+ resp = HttpResponse(json.dumps({'Status': 'Failed', 'ErrMsg': errmsg}))
+ else:
+ resp = HttpResponse(json.dumps({'Status': 'OK'}))
+
+ return resp
+
+def render_from_lms(template_name, dictionary, context=None, namespace='main'):
+ """
+ Render a template using the LMS MAKO_TEMPLATES
+ """
+ return render_to_string(template_name, dictionary, context, namespace="lms." + namespace)
+
+
+def _xmodule_recurse(item, action):
+ for child in item.get_children():
+ _xmodule_recurse(child, action)
+
+ action(item)
+
diff --git a/cms/djangoapps/contentstore/views/session_kv_store.py b/cms/djangoapps/contentstore/views/session_kv_store.py
new file mode 100644
index 0000000000..2f6868ee81
--- /dev/null
+++ b/cms/djangoapps/contentstore/views/session_kv_store.py
@@ -0,0 +1,27 @@
+from xblock.runtime import KeyValueStore
+
+class SessionKeyValueStore(KeyValueStore):
+ def __init__(self, request, model_data):
+ self._model_data = model_data
+ self._session = request.session
+
+ def get(self, key):
+ try:
+ return self._model_data[key.field_name]
+ except (KeyError, InvalidScopeError):
+ return self._session[tuple(key)]
+
+ def set(self, key, value):
+ try:
+ self._model_data[key.field_name] = value
+ except (KeyError, InvalidScopeError):
+ self._session[tuple(key)] = value
+
+ def delete(self, key):
+ try:
+ del self._model_data[key.field_name]
+ except (KeyError, InvalidScopeError):
+ del self._session[tuple(key)]
+
+ def has(self, key):
+ return key in self._model_data or key in self._session
diff --git a/cms/djangoapps/contentstore/views/user.py b/cms/djangoapps/contentstore/views/user.py
new file mode 100644
index 0000000000..5be78a0c37
--- /dev/null
+++ b/cms/djangoapps/contentstore/views/user.py
@@ -0,0 +1,107 @@
+from django.contrib.auth.decorators import login_required
+from django_future.csrf import ensure_csrf_cookie
+
+from util.json_request import expect_json
+from mitxmako.shortcuts import render_to_response
+
+def user_author_string(user):
+ '''Get an author string for commits by this user. Format:
+ first last .
+
+ If the first and last names are blank, uses the username instead.
+ Assumes that the email is not blank.
+ '''
+ f = user.first_name
+ l = user.last_name
+ if f == '' and l == '':
+ f = user.username
+ return '{first} {last} <{email}>'.format(first=f,
+ last=l,
+ email=user.email)
+
+
+@login_required
+@ensure_csrf_cookie
+def manage_users(request, location):
+ '''
+ This view will return all CMS users who are editors for the specified course
+ '''
+ # check that logged in user has permissions to this item
+ if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME) and not has_access(request.user, location, role=STAFF_ROLE_NAME):
+ raise PermissionDenied()
+
+ course_module = modulestore().get_item(location)
+
+ return render_to_response('manage_users.html', {
+ 'active_tab': 'users',
+ 'context_course': course_module,
+ 'staff': get_users_in_course_group_by_role(location, STAFF_ROLE_NAME),
+ 'add_user_postback_url': reverse('add_user', args=[location]).rstrip('/'),
+ 'remove_user_postback_url': reverse('remove_user', args=[location]).rstrip('/'),
+ 'allow_actions': has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME),
+ 'request_user_id': request.user.id
+ })
+
+
+
+
+@expect_json
+@login_required
+@ensure_csrf_cookie
+def add_user(request, location):
+ '''
+ This POST-back view will add a user - specified by email - to the list of editors for
+ the specified course
+ '''
+ email = request.POST["email"]
+
+ if email == '':
+ return create_json_response('Please specify an email address.')
+
+ # check that logged in user has admin permissions to this course
+ if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
+ raise PermissionDenied()
+
+ user = get_user_by_email(email)
+
+ # user doesn't exist?!? Return error.
+ if user is None:
+ return create_json_response('Could not find user by email address \'{0}\'.'.format(email))
+
+ # user exists, but hasn't activated account?!?
+ if not user.is_active:
+ return create_json_response('User {0} has registered but has not yet activated his/her account.'.format(email))
+
+ # ok, we're cool to add to the course group
+ add_user_to_course_group(request.user, user, location, STAFF_ROLE_NAME)
+
+ return create_json_response()
+
+
+@expect_json
+@login_required
+@ensure_csrf_cookie
+def remove_user(request, location):
+ '''
+ This POST-back view will remove a user - specified by email - from the list of editors for
+ the specified course
+ '''
+
+ email = request.POST["email"]
+
+ # check that logged in user has admin permissions on this course
+ if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
+ raise PermissionDenied()
+
+ user = get_user_by_email(email)
+ if user is None:
+ return create_json_response('Could not find user by email address \'{0}\'.'.format(email))
+
+ # make sure we're not removing ourselves
+ if user.id == request.user.id:
+ raise PermissionDenied()
+
+ remove_user_from_course_group(request.user, user, location, STAFF_ROLE_NAME)
+
+ return create_json_response()
+
From a407c84bdf9c2a13e869fbc163093c30304502f5 Mon Sep 17 00:00:00 2001
From: Arthur Barrett
Date: Thu, 9 May 2013 16:34:02 -0400
Subject: [PATCH 057/245] add model unit tests
---
lms/djangoapps/notes/tests.py | 59 +++++++++++++++++++++++++++++++++++
1 file changed, 59 insertions(+)
diff --git a/lms/djangoapps/notes/tests.py b/lms/djangoapps/notes/tests.py
index 9b05512250..b10edd3232 100644
--- a/lms/djangoapps/notes/tests.py
+++ b/lms/djangoapps/notes/tests.py
@@ -6,6 +6,7 @@ from django.test import TestCase
from django.test.client import Client
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
+from django.core.exceptions import ValidationError
import collections
import unittest
@@ -13,6 +14,7 @@ import json
import logging
from . import utils, api, models
+from .models import Note
class UtilsTest(TestCase):
@@ -338,3 +340,60 @@ class ApiTest(TestCase):
for row in content['rows']:
self.assertTrue('id' in row)
+
+
+class NoteTest(TestCase):
+ def setUp(self):
+ self.password = 'abc'
+ self.student = User.objects.create_user('student', 'student@test.com', self.password)
+ self.course_id = 'HarvardX/CB22x/The_Ancient_Greek_Hero'
+ self.note = {
+ 'user': self.student,
+ 'course_id': self.course_id,
+ 'uri': '/',
+ 'text': 'foo',
+ 'quote': 'bar',
+ 'range_start': 0,
+ 'range_start_offset': 0,
+ 'range_end': 100,
+ 'range_end_offset': 0,
+ 'tags': 'a,b,c'
+ }
+
+ def test_clean_valid_note(self):
+ reference_note = Note(**self.note)
+ body = reference_note.as_dict()
+
+ note = Note(course_id=self.course_id, user=self.student)
+ try:
+ note.clean(json.dumps(body))
+ self.assertEqual(note.uri, body['uri'])
+ self.assertEqual(note.text, body['text'])
+ self.assertEqual(note.quote, body['quote'])
+ self.assertEqual(note.range_start, body['ranges'][0]['start'])
+ self.assertEqual(note.range_start_offset, body['ranges'][0]['startOffset'])
+ self.assertEqual(note.range_end, body['ranges'][0]['end'])
+ self.assertEqual(note.range_end_offset, body['ranges'][0]['endOffset'])
+ self.assertEqual(note.tags, ','.join(body['tags']))
+ except ValidationError:
+ self.fail('a valid note should not raise an exception')
+
+ def test_clean_invalid_note(self):
+ note = Note(course_id=self.course_id, user=self.student)
+ for empty_type in (None, '', 0, []):
+ with self.assertRaises(ValidationError):
+ note.clean(None)
+
+ with self.assertRaises(ValidationError):
+ note.clean(json.dumps({
+ 'text': 'foo',
+ 'quote': 'bar',
+ 'ranges': [{} for i in range(10)] # too many ranges
+ }))
+
+ def test_as_dict(self):
+ note = Note(course_id=self.course_id, user=self.student)
+ d = note.as_dict()
+ self.assertNotIsInstance(d, basestring)
+ self.assertEqual(d['user_id'], self.student.id)
+ self.assertTrue('course_id' not in d)
From a6ad65cb9846655158adeae7de5fbfe3ebe42edd Mon Sep 17 00:00:00 2001
From: Arthur Barrett
Date: Thu, 9 May 2013 16:57:19 -0400
Subject: [PATCH 058/245] trying to fix pylint errors
---
lms/djangoapps/notes/api.py | 2 +-
lms/djangoapps/notes/models.py | 15 ++++++++++++---
lms/djangoapps/notes/tests.py | 27 +++++++++++++--------------
3 files changed, 26 insertions(+), 18 deletions(-)
diff --git a/lms/djangoapps/notes/api.py b/lms/djangoapps/notes/api.py
index 68b144dd9a..43ba1bc92c 100644
--- a/lms/djangoapps/notes/api.py
+++ b/lms/djangoapps/notes/api.py
@@ -68,7 +68,7 @@ def api_request(request, course_id, **kwargs):
func = resource.get(resource_method)
module = globals()
if func not in module:
- log.debug('Function "{0}" does not exist for request {1} {2}'.format(action, resource_method, resource_name))
+ log.debug('Function "{0}" does not exist for request {1} {2}'.format(func, resource_method, resource_name))
raise Http404
log.debug('API request: {0} {1}'.format(resource_method, resource_name))
diff --git a/lms/djangoapps/notes/models.py b/lms/djangoapps/notes/models.py
index c07995b8d7..035705b3c5 100644
--- a/lms/djangoapps/notes/models.py
+++ b/lms/djangoapps/notes/models.py
@@ -23,6 +23,9 @@ class Note(models.Model):
updated = models.DateTimeField(auto_now=True, db_index=True)
def clean(self, json_body):
+ '''
+ Cleans the note object or raises a ValidationError.
+ '''
if json_body is None:
raise ValidationError('Note must have a body.')
@@ -49,13 +52,19 @@ class Note(models.Model):
self.tags = ",".join(tags)
def get_absolute_url(self):
- kwargs = {'course_id': self.course_id, 'note_id': str(self.id)}
+ '''
+ Returns the aboslute url for the note object.
+ '''
+ kwargs = {'course_id': self.course_id, 'note_id': str(self.pk)}
return reverse('notes_api_note', kwargs=kwargs)
def as_dict(self):
+ '''
+ Returns the note object as a dictionary.
+ '''
return {
- 'id': self.id,
- 'user_id': self.user.id,
+ 'id': self.pk,
+ 'user_id': self.user.pk,
'uri': self.uri,
'text': self.text,
'quote': self.quote,
diff --git a/lms/djangoapps/notes/tests.py b/lms/djangoapps/notes/tests.py
index b10edd3232..f06dc20e14 100644
--- a/lms/djangoapps/notes/tests.py
+++ b/lms/djangoapps/notes/tests.py
@@ -14,7 +14,6 @@ import json
import logging
from . import utils, api, models
-from .models import Note
class UtilsTest(TestCase):
@@ -204,12 +203,12 @@ class ApiTest(TestCase):
self.assertEqual(len(notes), 3)
for note in notes:
- resp = self.client.get(self.url('notes_api_note', {'note_id': note.id}))
+ resp = self.client.get(self.url('notes_api_note', {'note_id': note.pk}))
self.assertEqual(resp.status_code, 200)
self.assertNotEqual(resp.content, '')
content = json.loads(resp.content)
- self.assertEqual(content['id'], note.id)
+ self.assertEqual(content['id'], note.pk)
self.assertEqual(content['user_id'], note.user_id)
def test_note_doesnt_exist_to_read(self):
@@ -227,7 +226,7 @@ class ApiTest(TestCase):
# set the student id to a different student (not the one that created the notes)
self.login(as_student=self.student2)
- resp = self.client.get(self.url('notes_api_note', {'note_id': note.id}))
+ resp = self.client.get(self.url('notes_api_note', {'note_id': note.pk}))
self.assertEqual(resp.status_code, 403)
self.assertEqual(resp.content, '')
@@ -239,13 +238,13 @@ class ApiTest(TestCase):
note = notes[0]
resp = self.client.delete(self.url('notes_api_note', {
- 'note_id': note.id
+ 'note_id': note.pk
}))
self.assertEqual(resp.status_code, 204)
self.assertEqual(resp.content, '')
with self.assertRaises(models.Note.DoesNotExist):
- models.Note.objects.get(pk=note.id)
+ models.Note.objects.get(pk=note.pk)
def test_note_does_not_exist_to_delete(self):
self.login()
@@ -263,13 +262,13 @@ class ApiTest(TestCase):
self.login(as_student=self.student2)
resp = self.client.delete(self.url('notes_api_note', {
- 'note_id': note.id
+ 'note_id': note.pk
}))
self.assertEqual(resp.status_code, 403)
self.assertEqual(resp.content, '')
try:
- models.Note.objects.get(pk=note.id)
+ models.Note.objects.get(pk=note.pk)
except models.Note.DoesNotExist:
self.fail('note should exist and not be deleted because the student does not have permission to do so')
@@ -284,14 +283,14 @@ class ApiTest(TestCase):
})
self.login()
- resp = self.client.put(self.url('notes_api_note', {'note_id': note.id}),
+ resp = self.client.put(self.url('notes_api_note', {'note_id': note.pk}),
json.dumps(updated_dict),
content_type='application/json',
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(resp.status_code, 303)
self.assertEqual(resp.content, '')
- actual = models.Note.objects.get(pk=note.id)
+ actual = models.Note.objects.get(pk=note.pk)
actual_dict = actual.as_dict()
for field in ['text', 'tags']:
self.assertEqual(actual_dict[field], updated_dict[field])
@@ -361,10 +360,10 @@ class NoteTest(TestCase):
}
def test_clean_valid_note(self):
- reference_note = Note(**self.note)
+ reference_note = models.Note(**self.note)
body = reference_note.as_dict()
- note = Note(course_id=self.course_id, user=self.student)
+ note = models.Note(course_id=self.course_id, user=self.student)
try:
note.clean(json.dumps(body))
self.assertEqual(note.uri, body['uri'])
@@ -379,7 +378,7 @@ class NoteTest(TestCase):
self.fail('a valid note should not raise an exception')
def test_clean_invalid_note(self):
- note = Note(course_id=self.course_id, user=self.student)
+ note = models.Note(course_id=self.course_id, user=self.student)
for empty_type in (None, '', 0, []):
with self.assertRaises(ValidationError):
note.clean(None)
@@ -392,7 +391,7 @@ class NoteTest(TestCase):
}))
def test_as_dict(self):
- note = Note(course_id=self.course_id, user=self.student)
+ note = models.Note(course_id=self.course_id, user=self.student)
d = note.as_dict()
self.assertNotIsInstance(d, basestring)
self.assertEqual(d['user_id'], self.student.id)
From 3352ae7f752202fee7e94c94957f34e84dd4e838 Mon Sep 17 00:00:00 2001
From: John Jarvis
Date: Thu, 9 May 2013 18:29:45 -0400
Subject: [PATCH 059/245] repo rename to edx-platform
---
rakefile | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/rakefile b/rakefile
index 32d92a0349..4d70547a51 100644
--- a/rakefile
+++ b/rakefile
@@ -14,8 +14,8 @@ LMS_REPORT_DIR = File.join(REPORT_DIR, "lms")
# Packaging constants
DEPLOY_DIR = "/opt/wwc"
-PACKAGE_NAME = "mitx"
-LINK_PATH = "/opt/wwc/mitx"
+PACKAGE_NAME = "edx-platform"
+LINK_PATH = "/opt/wwc/edx-platform"
PKG_VERSION = "0.1"
COMMIT = (ENV["GIT_COMMIT"] || `git rev-parse HEAD`).chomp()[0, 10]
BRANCH = (ENV["GIT_BRANCH"] || `git symbolic-ref -q HEAD`).chomp().gsub('refs/heads/', '').gsub('origin/', '')
From c730d43dbf150a3926cf58ddafe7500940876e43 Mon Sep 17 00:00:00 2001
From: Will Daly
Date: Fri, 10 May 2013 09:18:37 -0400
Subject: [PATCH 060/245] Resolved import conflict involving django-staticfiles
(lettuce assumes we are using django.contrib.staticfiles). This allows us to
run the django-admin.py harvest test server, instead of launching the server
ourselves.
---
common/djangoapps/terrain/browser.py | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py
index 1d371a3242..b52ae2b712 100644
--- a/common/djangoapps/terrain/browser.py
+++ b/common/djangoapps/terrain/browser.py
@@ -9,6 +9,21 @@ from django.conf import settings
from lms import one_time_startup
from cms import one_time_startup
+# There is an import issue when using django-staticfiles with lettuce
+# Lettuce assumes that we are using django.contrib.staticfiles,
+# but the rest of the app assumes we are using django-staticfiles
+# (in particular, django-pipeline and our mako implementation)
+# To resolve this, we check whether staticfiles is installed,
+# then redirect imports for django.contrib.staticfiles
+# to use staticfiles.
+try:
+ import staticfiles
+except ImportError:
+ pass
+else:
+ import sys
+ sys.modules['django.contrib.staticfiles'] = staticfiles
+
logger = getLogger(__name__)
logger.info("Loading the lettuce acceptance testing terrain file...")
From 1b0eff52edbf229d55eaa7f82a3e0de33a7bd0fd Mon Sep 17 00:00:00 2001
From: Chris Dodge
Date: Fri, 10 May 2013 10:01:10 -0400
Subject: [PATCH 061/245] create test cases to simulate error
exporting/importing textbooks
---
cms/djangoapps/contentstore/tests/test_contentstore.py | 8 ++++++++
common/test/data/full/course.xml | 2 +-
common/test/data/full/course/6.002_Spring_2012.xml | 5 +++--
lms/djangoapps/courseware/tests/tests.py | 8 ++++++++
4 files changed, 20 insertions(+), 3 deletions(-)
diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index 07b7032e60..7879ae9222 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -220,6 +220,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
num_drafts = self._get_draft_counts(course)
self.assertEqual(num_drafts, 1)
+ def test_import_textbook_as_content_element(self):
+ import_from_xml(modulestore(), 'common/test/data/', ['full'])
+
+ module_store = modulestore('direct')
+ course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
+
+ self.assertGreater(len(course.textbooks), 0)
+
def test_static_tab_reordering(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
diff --git a/common/test/data/full/course.xml b/common/test/data/full/course.xml
index 4f093a1128..7a05db42f2 100644
--- a/common/test/data/full/course.xml
+++ b/common/test/data/full/course.xml
@@ -1 +1 @@
-
+
diff --git a/common/test/data/full/course/6.002_Spring_2012.xml b/common/test/data/full/course/6.002_Spring_2012.xml
index 0d22e96beb..e0beb5a3ee 100644
--- a/common/test/data/full/course/6.002_Spring_2012.xml
+++ b/common/test/data/full/course/6.002_Spring_2012.xml
@@ -1,4 +1,5 @@
-
+
+
@@ -9,4 +10,4 @@
-
+
diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py
index d5064ec5e5..235f7d60bb 100644
--- a/lms/djangoapps/courseware/tests/tests.py
+++ b/lms/djangoapps/courseware/tests/tests.py
@@ -399,6 +399,14 @@ class TestCoursesLoadTestCase_MongoModulestore(PageLoaderTestCase):
import_from_xml(module_store, TEST_DATA_DIR, ['toy'])
self.check_random_page_loads(module_store)
+ def test_full_textbooks_loads(self):
+ module_store = modulestore()
+ import_from_xml(module_store, TEST_DATA_DIR, ['full'])
+
+ course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
+
+ self.assertGreater(len(course.textbooks), 0)
+
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestNavigation(LoginEnrollmentTestCase):
From 2546d6227f91b4006abe2831e136623d048c901d Mon Sep 17 00:00:00 2001
From: Will Daly
Date: Fri, 10 May 2013 10:03:44 -0400
Subject: [PATCH 062/245] Added test_acceptance command to rake file that runs
all acceptance tests.
---
rakefile | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/rakefile b/rakefile
index 32d92a0349..9fd5ea7279 100644
--- a/rakefile
+++ b/rakefile
@@ -243,6 +243,12 @@ def run_tests(system, report_dir, stop_on_failure=true)
end
end
+def run_acceptance_tests(system, report_dir)
+ ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, 'acceptance_tests.xml')
+ sh(django_admin(system, 'acceptance', 'harvest', '--debug-mode'))
+end
+
+
TEST_TASK_DIRS = []
task :fastlms do
@@ -265,6 +271,12 @@ end
run_tests(system, report_dir, args.stop_on_failure)
end
+ # Run acceptance tests
+ desc "Run acceptance tests"
+ task "test_acceptance" => ["clean_test_files", "#{system}:gather_assets:acceptance", :predjango, report_dir] do
+ run_acceptance_tests(system, report_dir)
+ end
+
task :fasttest => "fasttest_#{system}"
TEST_TASK_DIRS << system
From a1e6c194c6455e1895328842d39d48858356439d Mon Sep 17 00:00:00 2001
From: John Jarvis
Date: Fri, 10 May 2013 10:24:32 -0400
Subject: [PATCH 063/245] removing LINK_PATH
---
rakefile | 1 -
1 file changed, 1 deletion(-)
diff --git a/rakefile b/rakefile
index 4d70547a51..fb6eb1c6e5 100644
--- a/rakefile
+++ b/rakefile
@@ -15,7 +15,6 @@ LMS_REPORT_DIR = File.join(REPORT_DIR, "lms")
# Packaging constants
DEPLOY_DIR = "/opt/wwc"
PACKAGE_NAME = "edx-platform"
-LINK_PATH = "/opt/wwc/edx-platform"
PKG_VERSION = "0.1"
COMMIT = (ENV["GIT_COMMIT"] || `git rev-parse HEAD`).chomp()[0, 10]
BRANCH = (ENV["GIT_BRANCH"] || `git symbolic-ref -q HEAD`).chomp().gsub('refs/heads/', '').gsub('origin/', '')
From 881b72a6d11d62799fe410198e13ec8041cde24c Mon Sep 17 00:00:00 2001
From: Will Daly
Date: Fri, 10 May 2013 10:35:51 -0400
Subject: [PATCH 064/245] Added option to run a specific feature
---
rakefile | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/rakefile b/rakefile
index 9fd5ea7279..55ac9ce64d 100644
--- a/rakefile
+++ b/rakefile
@@ -243,9 +243,9 @@ def run_tests(system, report_dir, stop_on_failure=true)
end
end
-def run_acceptance_tests(system, report_dir)
+def run_acceptance_tests(system, report_dir, feature_path)
ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, 'acceptance_tests.xml')
- sh(django_admin(system, 'acceptance', 'harvest', '--debug-mode'))
+ sh(django_admin(system, 'acceptance', 'harvest', '--debug-mode', feature_path))
end
@@ -273,8 +273,9 @@ end
# Run acceptance tests
desc "Run acceptance tests"
- task "test_acceptance" => ["clean_test_files", "#{system}:gather_assets:acceptance", :predjango, report_dir] do
- run_acceptance_tests(system, report_dir)
+ task "test_acceptance", [:feature_path] => ["clean_test_files", :predjango, report_dir] do |t, args|
+ args.with_defaults(:feature_path => '')
+ run_acceptance_tests(system, report_dir, args.feature_path)
end
task :fasttest => "fasttest_#{system}"
From 43244538277d63e2763aaacd087f2a38e0885482 Mon Sep 17 00:00:00 2001
From: Will Daly
Date: Fri, 10 May 2013 10:43:46 -0400
Subject: [PATCH 065/245] Added option to run acceptance tests without
collectstatic
---
rakefile | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/rakefile b/rakefile
index 55ac9ce64d..8da061a4db 100644
--- a/rakefile
+++ b/rakefile
@@ -271,14 +271,18 @@ end
run_tests(system, report_dir, args.stop_on_failure)
end
+ task :fasttest => "fasttest_#{system}"
+
# Run acceptance tests
desc "Run acceptance tests"
- task "test_acceptance", [:feature_path] => ["clean_test_files", :predjango, report_dir] do |t, args|
+ task "test_acceptance", [:feature_path] => ["#{system}:gather_assets:acceptance", "fasttest_acceptance"]
+
+ desc "Run acceptance tests without collectstatic"
+ task "fasttest_acceptance", [:feature_path] => ["clean_test_files", :predjango, report_dir] do |t, args|
args.with_defaults(:feature_path => '')
run_acceptance_tests(system, report_dir, args.feature_path)
end
- task :fasttest => "fasttest_#{system}"
TEST_TASK_DIRS << system
From f7655576cd24686585118a33d100378e483834f7 Mon Sep 17 00:00:00 2001
From: "Mark L. Chang"
Date: Fri, 10 May 2013 13:21:09 -0400
Subject: [PATCH 066/245] copy change
---
cms/templates/500.html | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/cms/templates/500.html b/cms/templates/500.html
index 2645b0067b..6e381c0844 100644
--- a/cms/templates/500.html
+++ b/cms/templates/500.html
@@ -1,12 +1,17 @@
<%inherit file="base.html" />
-<%block name="title">Server Error%block>
+
+<%block name="title">Studio Server Error%block>
<%block name="content">
-
Currently the edX servers are down
-
Our staff is currently working to get the site back up as soon as possible. Please email us at technical@edx.org to report any problems or downtime.
+
The Studio servers encountered an error
+
+ An error occurred in Studio and the page could not be loaded. Please try again in a few moments.
+ We've logged the error and our staff is currently working to get Studio back up as soon as possible.
+ If the problem persists, please email us at technical@edx.org.
+
An error occurred in Studio and the page could not be loaded. Please try again in a few moments.
- We've logged the error and our staff is currently working to get Studio back up as soon as possible.
+ We've logged the error and our staff is currently working to resolve this error as soon as possible.
If the problem persists, please email us at technical@edx.org.
From 9350a2c0674eee804ba7aae96c6d358a5156b291 Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Fri, 10 May 2013 13:56:30 -0400
Subject: [PATCH 068/245] refactoring views
---
cms/djangoapps/contentstore/views/__init__.py | 28 +-
cms/djangoapps/contentstore/views/access.py | 6 +-
cms/djangoapps/contentstore/views/assets.py | 142 +++++-
.../contentstore/views/checklist.py | 9 +-
.../contentstore/views/component.py | 313 +++++++++++++
cms/djangoapps/contentstore/views/course.py | 420 ++++++++++++------
cms/djangoapps/contentstore/views/error.py | 2 +-
cms/djangoapps/contentstore/views/item.py | 16 +-
cms/djangoapps/contentstore/views/preview.py | 15 +
cms/djangoapps/contentstore/views/public.py | 6 +-
cms/djangoapps/contentstore/views/requests.py | 41 +-
.../contentstore/views/session_kv_store.py | 2 +-
cms/djangoapps/contentstore/views/tabs.py | 110 +++++
cms/djangoapps/contentstore/views/user.py | 43 +-
14 files changed, 956 insertions(+), 197 deletions(-)
create mode 100644 cms/djangoapps/contentstore/views/component.py
create mode 100644 cms/djangoapps/contentstore/views/tabs.py
diff --git a/cms/djangoapps/contentstore/views/__init__.py b/cms/djangoapps/contentstore/views/__init__.py
index 0b7c271b1e..37f786ac3c 100644
--- a/cms/djangoapps/contentstore/views/__init__.py
+++ b/cms/djangoapps/contentstore/views/__init__.py
@@ -1,17 +1,13 @@
-from new import *
-from error import *
+# TODO: replace asterisks, should explicitly enumerate imports instead
+
+from assets import asset_index, upload_asset, import_course, generate_export_course, export_course
+from checklist import get_checklists, update_checklist
+from component import *
from course import *
-from item import *
-from public import *
-from user import *
-from preview import *
-from assets import *
-from checklist import *
-from requests import landing
-
-
-"""
-
-from main import *
-
-"""
+from error import not_found, server_error, render_404, render_500
+from item import save_item, clone_item, delete_item
+from preview import preview_dispatch, preview_component
+from public import signup, old_login_redirect, login_page, howitworks, ux_alerts
+from user import index, add_user, remove_user, manage_users
+from tabs import edit_tabs, reorder_static_tabs
+from requests import edge, event, landing
diff --git a/cms/djangoapps/contentstore/views/access.py b/cms/djangoapps/contentstore/views/access.py
index dd3add1099..37f6fcb767 100644
--- a/cms/djangoapps/contentstore/views/access.py
+++ b/cms/djangoapps/contentstore/views/access.py
@@ -1,11 +1,7 @@
-#from auth.authz import is_user_in_course_group_role, get_users_in_course_group_by_role
-#from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group
-#from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME, create_all_course_groups
-
from auth.authz import STAFF_ROLE_NAME, INSTRUCTOR_ROLE_NAME
from auth.authz import is_user_in_course_group_role
from contentstore.utils import get_course_location_for_item
-
+from django.core.exceptions import PermissionDenied
def get_location_and_verify_access(request, org, course, name):
"""
diff --git a/cms/djangoapps/contentstore/views/assets.py b/cms/djangoapps/contentstore/views/assets.py
index 616b04342d..c2aa52b1e1 100644
--- a/cms/djangoapps/contentstore/views/assets.py
+++ b/cms/djangoapps/contentstore/views/assets.py
@@ -1,12 +1,29 @@
+import logging, json, os, tarfile, shutil
+from tempfile import mkdtemp
+from path import path
+
+from django.conf import settings
from django.http import HttpResponse, HttpResponseBadRequest
from django.contrib.auth.decorators import login_required
from django_future.csrf import ensure_csrf_cookie
+from django.core.urlresolvers import reverse
+from django.core.servers.basehttp import FileWrapper
+from django.core.files.temp import NamedTemporaryFile
-from xmodule.contentstore.content import StaticContent
-from access import get_location_and_verify_access
-from xmodule.util.date_utils import get_default_time_display
from mitxmako.shortcuts import render_to_response
+from cache_toolbox.core import del_cached_content
+from contentstore.utils import get_url_reverse
+from xmodule.modulestore.xml_importer import import_from_xml
+from xmodule.contentstore.django import contentstore
+from xmodule.modulestore.xml_exporter import export_to_xml
+from xmodule.modulestore.django import modulestore
+from xmodule.modulestore import Location
+from xmodule.contentstore.content import StaticContent
+from xmodule.util.date_utils import get_default_time_display
+
+from access import get_location_and_verify_access
+from auth.authz import create_all_course_groups
@login_required
@ensure_csrf_cookie
@@ -116,3 +133,122 @@ def upload_asset(request, org, course, coursename):
response['asset_url'] = StaticContent.get_url_path_from_location(content.location)
return response
+@ensure_csrf_cookie
+@login_required
+def import_course(request, org, course, name):
+
+ location = get_location_and_verify_access(request, org, course, name)
+
+ if request.method == 'POST':
+ filename = request.FILES['course-data'].name
+
+ if not filename.endswith('.tar.gz'):
+ return HttpResponse(json.dumps({'ErrMsg': 'We only support uploading a .tar.gz file.'}))
+
+ data_root = path(settings.GITHUB_REPO_ROOT)
+
+ course_subdir = "{0}-{1}-{2}".format(org, course, name)
+ course_dir = data_root / course_subdir
+ if not course_dir.isdir():
+ os.mkdir(course_dir)
+
+ temp_filepath = course_dir / filename
+
+ logging.debug('importing course to {0}'.format(temp_filepath))
+
+ # stream out the uploaded files in chunks to disk
+ temp_file = open(temp_filepath, 'wb+')
+ for chunk in request.FILES['course-data'].chunks():
+ temp_file.write(chunk)
+ temp_file.close()
+
+ tf = tarfile.open(temp_filepath)
+ tf.extractall(course_dir + '/')
+
+ # find the 'course.xml' file
+
+ for r, d, f in os.walk(course_dir):
+ for files in f:
+ if files == 'course.xml':
+ break
+ if files == 'course.xml':
+ break
+
+ if files != 'course.xml':
+ return HttpResponse(json.dumps({'ErrMsg': 'Could not find the course.xml file in the package.'}))
+
+ logging.debug('found course.xml at {0}'.format(r))
+
+ if r != course_dir:
+ for fname in os.listdir(r):
+ shutil.move(r / fname, course_dir)
+
+ module_store, course_items = import_from_xml(modulestore('direct'), settings.GITHUB_REPO_ROOT,
+ [course_subdir], load_error_modules=False,
+ static_content_store=contentstore(),
+ target_location_namespace=Location(location),
+ draft_store=modulestore())
+
+ # we can blow this away when we're done importing.
+ shutil.rmtree(course_dir)
+
+ logging.debug('new course at {0}'.format(course_items[0].location))
+
+ create_all_course_groups(request.user, course_items[0].location)
+
+ return HttpResponse(json.dumps({'Status': 'OK'}))
+ else:
+ course_module = modulestore().get_item(location)
+
+ return render_to_response('import.html', {
+ 'context_course': course_module,
+ 'active_tab': 'import',
+ 'successful_import_redirect_url': get_url_reverse('CourseOutline', course_module)
+ })
+
+
+@ensure_csrf_cookie
+@login_required
+def generate_export_course(request, org, course, name):
+ location = get_location_and_verify_access(request, org, course, name)
+
+ loc = Location(location)
+ export_file = NamedTemporaryFile(prefix=name + '.', suffix=".tar.gz")
+
+ root_dir = path(mkdtemp())
+
+ # export out to a tempdir
+
+ logging.debug('root = {0}'.format(root_dir))
+
+ export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name, modulestore())
+ #filename = root_dir / name + '.tar.gz'
+
+ logging.debug('tar file being generated at {0}'.format(export_file.name))
+ tf = tarfile.open(name=export_file.name, mode='w:gz')
+ tf.add(root_dir / name, arcname=name)
+ tf.close()
+
+ # remove temp dir
+ shutil.rmtree(root_dir / name)
+
+ wrapper = FileWrapper(export_file)
+ response = HttpResponse(wrapper, content_type='application/x-tgz')
+ response['Content-Disposition'] = 'attachment; filename=%s' % os.path.basename(export_file.name)
+ response['Content-Length'] = os.path.getsize(export_file.name)
+ return response
+
+@ensure_csrf_cookie
+@login_required
+def export_course(request, org, course, name):
+
+ location = get_location_and_verify_access(request, org, course, name)
+
+ course_module = modulestore().get_item(location)
+
+ return render_to_response('export.html', {
+ 'context_course': course_module,
+ 'active_tab': 'export',
+ 'successful_import_redirect_url': ''
+ })
+
diff --git a/cms/djangoapps/contentstore/views/checklist.py b/cms/djangoapps/contentstore/views/checklist.py
index 376e041523..4a97ddc1df 100644
--- a/cms/djangoapps/contentstore/views/checklist.py
+++ b/cms/djangoapps/contentstore/views/checklist.py
@@ -1,9 +1,16 @@
+import json
+
from django.http import HttpResponse, HttpResponseBadRequest
from django.contrib.auth.decorators import login_required
from django_future.csrf import ensure_csrf_cookie
-from access import get_location_and_verify_access
from mitxmako.shortcuts import render_to_response
+
+from xmodule.modulestore import Location
+from xmodule.modulestore.inheritance import own_metadata
+
from contentstore.utils import get_modulestore, get_url_reverse
+from requests import get_request_method
+from access import get_location_and_verify_access
@ensure_csrf_cookie
@login_required
diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py
new file mode 100644
index 0000000000..2dd7307976
--- /dev/null
+++ b/cms/djangoapps/contentstore/views/component.py
@@ -0,0 +1,313 @@
+import json, logging
+from collections import defaultdict
+
+from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
+from django.contrib.auth.decorators import login_required
+from django.core.exceptions import PermissionDenied
+from django_future.csrf import ensure_csrf_cookie
+from django.conf import settings
+
+from mitxmako.shortcuts import render_to_response
+
+from xmodule.modulestore import Location
+from xmodule.modulestore.django import modulestore
+from xmodule.util.date_utils import get_default_time_display
+
+from xblock.core import Scope
+from util.json_request import expect_json
+
+from contentstore.module_info_model import get_module_info, set_module_info
+from contentstore.utils import get_modulestore, get_lms_link_for_item, \
+ compute_unit_state, UnitState, get_course_for_item
+
+from models.settings.course_grading import CourseGradingModel
+
+from requests import get_request_method, _xmodule_recurse
+from access import has_access, get_location_and_verify_access
+
+# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
+
+log = logging.getLogger(__name__)
+
+COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video']
+
+OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"]
+ADVANCED_COMPONENT_TYPES = ['annotatable'] + OPEN_ENDED_COMPONENT_TYPES
+ADVANCED_COMPONENT_CATEGORY = 'advanced'
+ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
+
+
+@login_required
+def edit_subsection(request, location):
+ # check that we have permissions to edit this item
+ course = get_course_for_item(location)
+ if not has_access(request.user, course.location):
+ raise PermissionDenied()
+
+ item = modulestore().get_item(location, depth=1)
+
+ lms_link = get_lms_link_for_item(location, course_id=course.location.course_id)
+ preview_link = get_lms_link_for_item(location, course_id=course.location.course_id, preview=True)
+
+ # make sure that location references a 'sequential', otherwise return BadRequest
+ if item.location.category != 'sequential':
+ return HttpResponseBadRequest()
+
+ parent_locs = modulestore().get_parent_locations(location, None)
+
+ # we're for now assuming a single parent
+ if len(parent_locs) != 1:
+ logging.error('Multiple (or none) parents have been found for {0}'.format(location))
+
+ # this should blow up if we don't find any parents, which would be erroneous
+ parent = modulestore().get_item(parent_locs[0])
+
+ # remove all metadata from the generic dictionary that is presented in a more normalized UI
+
+ policy_metadata = dict(
+ (field.name, field.read_from(item))
+ for field
+ in item.fields
+ if field.name not in ['display_name', 'start', 'due', 'format'] and field.scope == Scope.settings
+ )
+
+ can_view_live = False
+ subsection_units = item.get_children()
+ for unit in subsection_units:
+ state = compute_unit_state(unit)
+ if state == UnitState.public or state == UnitState.draft:
+ can_view_live = True
+ break
+
+ return render_to_response('edit_subsection.html',
+ {'subsection': item,
+ 'context_course': course,
+ 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
+ 'lms_link': lms_link,
+ 'preview_link': preview_link,
+ 'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
+ 'parent_location': course.location,
+ 'parent_item': parent,
+ 'policy_metadata': policy_metadata,
+ 'subsection_units': subsection_units,
+ 'can_view_live': can_view_live
+ })
+
+
+@login_required
+def edit_unit(request, location):
+ """
+ Display an editing page for the specified module.
+
+ Expects a GET request with the parameter 'id'.
+
+ id: A Location URL
+ """
+ course = get_course_for_item(location)
+ if not has_access(request.user, course.location):
+ raise PermissionDenied()
+
+ item = modulestore().get_item(location, depth=1)
+
+ lms_link = get_lms_link_for_item(item.location, course_id=course.location.course_id)
+
+ component_templates = defaultdict(list)
+
+ # Check if there are any advanced modules specified in the course policy. These modules
+ # should be specified as a list of strings, where the strings are the names of the modules
+ # in ADVANCED_COMPONENT_TYPES that should be enabled for the course.
+ course_advanced_keys = course.advanced_modules
+
+ # Set component types according to course policy file
+ component_types = list(COMPONENT_TYPES)
+ if isinstance(course_advanced_keys, list):
+ course_advanced_keys = [c for c in course_advanced_keys if c in ADVANCED_COMPONENT_TYPES]
+ if len(course_advanced_keys) > 0:
+ component_types.append(ADVANCED_COMPONENT_CATEGORY)
+ else:
+ log.error("Improper format for course advanced keys! {0}".format(course_advanced_keys))
+
+ templates = modulestore().get_items(Location('i4x', 'edx', 'templates'))
+ for template in templates:
+ category = template.location.category
+
+ if category in course_advanced_keys:
+ category = ADVANCED_COMPONENT_CATEGORY
+
+ if category in component_types:
+ # This is a hack to create categories for different xmodules
+ component_templates[category].append((
+ template.display_name_with_default,
+ template.location.url(),
+ hasattr(template, 'markdown') and template.markdown is not None,
+ template.cms.empty,
+ ))
+
+ components = [
+ component.location.url()
+ for component
+ in item.get_children()
+ ]
+
+ # TODO (cpennington): If we share units between courses,
+ # this will need to change to check permissions correctly so as
+ # to pick the correct parent subsection
+
+ containing_subsection_locs = modulestore().get_parent_locations(location, None)
+ containing_subsection = modulestore().get_item(containing_subsection_locs[0])
+
+ containing_section_locs = modulestore().get_parent_locations(containing_subsection.location, None)
+ containing_section = modulestore().get_item(containing_section_locs[0])
+
+ # cdodge hack. We're having trouble previewing drafts via jump_to redirect
+ # so let's generate the link url here
+
+ # need to figure out where this item is in the list of children as the preview will need this
+ index = 1
+ for child in containing_subsection.get_children():
+ if child.location == item.location:
+ break
+ index = index + 1
+
+ preview_lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE',
+ 'preview.' + settings.LMS_BASE)
+
+ preview_lms_link = '//{preview_lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}'.format(
+ preview_lms_base=preview_lms_base,
+ lms_base=settings.LMS_BASE,
+ org=course.location.org,
+ course=course.location.course,
+ course_name=course.location.name,
+ section=containing_section.location.name,
+ subsection=containing_subsection.location.name,
+ index=index)
+
+ unit_state = compute_unit_state(item)
+
+ return render_to_response('unit.html', {
+ 'context_course': course,
+ 'active_tab': 'courseware',
+ 'unit': item,
+ 'unit_location': location,
+ 'components': components,
+ 'component_templates': component_templates,
+ 'draft_preview_link': preview_lms_link,
+ 'published_preview_link': lms_link,
+ 'subsection': containing_subsection,
+ 'release_date': get_default_time_display(containing_subsection.lms.start) if containing_subsection.lms.start is not None else None,
+ 'section': containing_section,
+ 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
+ 'unit_state': unit_state,
+ 'published_date': item.cms.published_date.strftime('%B %d, %Y') if item.cms.published_date is not None else None,
+ })
+
+
+@expect_json
+@login_required
+@ensure_csrf_cookie
+def assignment_type_update(request, org, course, category, name):
+ '''
+ CRUD operations on assignment types for sections and subsections and anything else gradable.
+ '''
+ location = Location(['i4x', org, course, category, name])
+ if not has_access(request.user, location):
+ raise HttpResponseForbidden()
+
+ if request.method == 'GET':
+ return HttpResponse(json.dumps(CourseGradingModel.get_section_grader_type(location)),
+ mimetype="application/json")
+ elif request.method == 'POST': # post or put, doesn't matter.
+ return HttpResponse(json.dumps(CourseGradingModel.update_section_grader_type(location, request.POST)),
+ mimetype="application/json")
+
+
+@login_required
+@expect_json
+def create_draft(request):
+ location = request.POST['id']
+
+ # check permissions for this user within this course
+ if not has_access(request.user, location):
+ raise PermissionDenied()
+
+ # This clones the existing item location to a draft location (the draft is implicit,
+ # because modulestore is a Draft modulestore)
+ modulestore().clone_item(location, location)
+
+ return HttpResponse()
+
+
+@login_required
+@expect_json
+def publish_draft(request):
+ location = request.POST['id']
+
+ # check permissions for this user within this course
+ if not has_access(request.user, location):
+ raise PermissionDenied()
+
+ item = modulestore().get_item(location)
+ _xmodule_recurse(item, lambda i: modulestore().publish(i.location, request.user.id))
+
+ return HttpResponse()
+
+
+@login_required
+@expect_json
+def unpublish_unit(request):
+ location = request.POST['id']
+
+ # check permissions for this user within this course
+ if not has_access(request.user, location):
+ raise PermissionDenied()
+
+ item = modulestore().get_item(location)
+ _xmodule_recurse(item, lambda i: modulestore().unpublish(i.location))
+
+ return HttpResponse()
+
+
+@login_required
+@ensure_csrf_cookie
+def static_pages(request, org, course, coursename):
+
+ location = get_location_and_verify_access(request, org, course, coursename)
+
+ course = modulestore().get_item(location)
+
+ return render_to_response('static-pages.html', {
+ 'active_tab': 'pages',
+ 'context_course': course,
+ })
+
+
+def edit_static(request, org, course, coursename):
+ return render_to_response('edit-static-page.html', {})
+
+@expect_json
+@login_required
+@ensure_csrf_cookie
+def module_info(request, module_location):
+ location = Location(module_location)
+
+ # check that logged in user has permissions to this item
+ if not has_access(request.user, location):
+ raise PermissionDenied()
+
+ real_method = get_request_method(request)
+
+ rewrite_static_links = request.GET.get('rewrite_url_links', 'True') in ['True', 'true']
+ logging.debug('rewrite_static_links = {0} {1}'.format(request.GET.get('rewrite_url_links', 'False'), rewrite_static_links))
+
+ # check that logged in user has permissions to this item
+ if not has_access(request.user, location):
+ raise PermissionDenied()
+
+ if real_method == 'GET':
+ return HttpResponse(json.dumps(get_module_info(get_modulestore(location), location, rewrite_static_links=rewrite_static_links)), mimetype="application/json")
+ elif real_method == 'POST' or real_method == 'PUT':
+ return HttpResponse(json.dumps(set_module_info(get_modulestore(location), location, request.POST)), mimetype="application/json")
+ else:
+ return HttpResponseBadRequest()
+
+
diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py
index fc2214f970..c0f6acc808 100644
--- a/cms/djangoapps/contentstore/views/course.py
+++ b/cms/djangoapps/contentstore/views/course.py
@@ -1,9 +1,65 @@
+import json, time
+
from django.contrib.auth.decorators import login_required
from django_future.csrf import ensure_csrf_cookie
-
-from util.json_request import expect_json
+from django.conf import settings
+from django.core.exceptions import PermissionDenied
+from django.http import HttpResponse, HttpResponseBadRequest
+from django.core.urlresolvers import reverse
from mitxmako.shortcuts import render_to_response
+from xmodule.modulestore.django import modulestore
+from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
+from xmodule.modulestore import Location
+
+from contentstore.course_info_model import get_course_updates, update_course_updates, delete_course_update
+from contentstore.utils import get_lms_link_for_item, add_open_ended_panel_tab, remove_open_ended_panel_tab
+from models.settings.course_details import CourseDetails, CourseSettingsEncoder
+from models.settings.course_grading import CourseGradingModel
+from models.settings.course_metadata import CourseMetadata
+from component import OPEN_ENDED_COMPONENT_TYPES, ADVANCED_COMPONENT_POLICY_KEY
+from auth.authz import create_all_course_groups
+from util.json_request import expect_json
+from access import has_access, get_location_and_verify_access
+from requests import get_request_method
+from tabs import initialize_course_tabs
+
+
+@login_required
+@ensure_csrf_cookie
+def course_index(request, org, course, name):
+ """
+ Display an editable course overview.
+
+ org, course, name: Attributes of the Location for the item to edit
+ """
+ location = get_location_and_verify_access(request, org, course, name)
+
+ lms_link = get_lms_link_for_item(location)
+
+ upload_asset_callback_url = reverse('upload_asset', kwargs={
+ 'org': org,
+ 'course': course,
+ 'coursename': name
+ })
+
+ course = modulestore().get_item(location, depth=3)
+ sections = course.get_children()
+
+ return render_to_response('overview.html', {
+ 'active_tab': 'courseware',
+ 'context_course': course,
+ 'lms_link': lms_link,
+ 'sections': sections,
+ 'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
+ 'parent_location': course.location,
+ 'new_section_template': Location('i4x', 'edx', 'templates', 'chapter', 'Empty'),
+ 'new_subsection_template': Location('i4x', 'edx', 'templates', 'sequential', 'Empty'), # for now they are the same, but the could be different at some point...
+ 'upload_asset_callback_url': upload_asset_callback_url,
+ 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty')
+ })
+
+
@login_required
@expect_json
def create_new_course(request):
@@ -63,140 +119,246 @@ def create_new_course(request):
return HttpResponse(json.dumps({'id': new_course.location.url()}))
-def initialize_course_tabs(course):
- # set up the default tabs
- # I've added this because when we add static tabs, the LMS either expects a None for the tabs list or
- # at least a list populated with the minimal times
- # @TODO: I don't like the fact that the presentation tier is away of these data related constraints, let's find a better
- # place for this. Also rather than using a simple list of dictionaries a nice class model would be helpful here
-
- # This logic is repeated in xmodule/modulestore/tests/factories.py
- # so if you change anything here, you need to also change it there.
- course.tabs = [{"type": "courseware"},
- {"type": "course_info", "name": "Course Info"},
- {"type": "discussion", "name": "Discussion"},
- {"type": "wiki", "name": "Wiki"},
- {"type": "progress", "name": "Progress"}]
-
- modulestore('direct').update_metadata(course.location.url(), own_metadata(course))
-
-
-@ensure_csrf_cookie
@login_required
-def import_course(request, org, course, name):
-
- location = get_location_and_verify_access(request, org, course, name)
-
- if request.method == 'POST':
- filename = request.FILES['course-data'].name
-
- if not filename.endswith('.tar.gz'):
- return HttpResponse(json.dumps({'ErrMsg': 'We only support uploading a .tar.gz file.'}))
-
- data_root = path(settings.GITHUB_REPO_ROOT)
-
- course_subdir = "{0}-{1}-{2}".format(org, course, name)
- course_dir = data_root / course_subdir
- if not course_dir.isdir():
- os.mkdir(course_dir)
-
- temp_filepath = course_dir / filename
-
- logging.debug('importing course to {0}'.format(temp_filepath))
-
- # stream out the uploaded files in chunks to disk
- temp_file = open(temp_filepath, 'wb+')
- for chunk in request.FILES['course-data'].chunks():
- temp_file.write(chunk)
- temp_file.close()
-
- tf = tarfile.open(temp_filepath)
- tf.extractall(course_dir + '/')
-
- # find the 'course.xml' file
-
- for r, d, f in os.walk(course_dir):
- for files in f:
- if files == 'course.xml':
- break
- if files == 'course.xml':
- break
-
- if files != 'course.xml':
- return HttpResponse(json.dumps({'ErrMsg': 'Could not find the course.xml file in the package.'}))
-
- logging.debug('found course.xml at {0}'.format(r))
-
- if r != course_dir:
- for fname in os.listdir(r):
- shutil.move(r / fname, course_dir)
-
- module_store, course_items = import_from_xml(modulestore('direct'), settings.GITHUB_REPO_ROOT,
- [course_subdir], load_error_modules=False,
- static_content_store=contentstore(),
- target_location_namespace=Location(location),
- draft_store=modulestore())
-
- # we can blow this away when we're done importing.
- shutil.rmtree(course_dir)
-
- logging.debug('new course at {0}'.format(course_items[0].location))
-
- create_all_course_groups(request.user, course_items[0].location)
-
- return HttpResponse(json.dumps({'Status': 'OK'}))
- else:
- course_module = modulestore().get_item(location)
-
- return render_to_response('import.html', {
- 'context_course': course_module,
- 'active_tab': 'import',
- 'successful_import_redirect_url': get_url_reverse('CourseOutline', course_module)
- })
-
-
@ensure_csrf_cookie
-@login_required
-def generate_export_course(request, org, course, name):
- location = get_location_and_verify_access(request, org, course, name)
-
- loc = Location(location)
- export_file = NamedTemporaryFile(prefix=name + '.', suffix=".tar.gz")
-
- root_dir = path(mkdtemp())
-
- # export out to a tempdir
-
- logging.debug('root = {0}'.format(root_dir))
-
- export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name, modulestore())
- #filename = root_dir / name + '.tar.gz'
-
- logging.debug('tar file being generated at {0}'.format(export_file.name))
- tf = tarfile.open(name=export_file.name, mode='w:gz')
- tf.add(root_dir / name, arcname=name)
- tf.close()
-
- # remove temp dir
- shutil.rmtree(root_dir / name)
-
- wrapper = FileWrapper(export_file)
- response = HttpResponse(wrapper, content_type='application/x-tgz')
- response['Content-Disposition'] = 'attachment; filename=%s' % os.path.basename(export_file.name)
- response['Content-Length'] = os.path.getsize(export_file.name)
- return response
-
-@ensure_csrf_cookie
-@login_required
-def export_course(request, org, course, name):
+def course_info(request, org, course, name, provided_id=None):
+ """
+ Send models and views as well as html for editing the course info to the client.
+ org, course, name: Attributes of the Location for the item to edit
+ """
location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
- return render_to_response('export.html', {
+ # get current updates
+ location = ['i4x', org, course, 'course_info', "updates"]
+
+ return render_to_response('course_info.html', {
+ 'active_tab': 'courseinfo-tab',
'context_course': course_module,
- 'active_tab': 'export',
- 'successful_import_redirect_url': ''
+ 'url_base': "/" + org + "/" + course + "/",
+ 'course_updates': json.dumps(get_course_updates(location)),
+ 'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url()
})
+
+@expect_json
+@login_required
+@ensure_csrf_cookie
+def course_info_updates(request, org, course, provided_id=None):
+ """
+ restful CRUD operations on course_info updates.
+
+ org, course: Attributes of the Location for the item to edit
+ provided_id should be none if it's new (create) and a composite of the update db id + index otherwise.
+ """
+ # ??? No way to check for access permission afaik
+ # get current updates
+ location = ['i4x', org, course, 'course_info', "updates"]
+
+ # Hmmm, provided_id is coming as empty string on create whereas I believe it used to be None :-(
+ # Possibly due to my removing the seemingly redundant pattern in urls.py
+ if provided_id == '':
+ provided_id = None
+
+ # check that logged in user has permissions to this item
+ if not has_access(request.user, location):
+ raise PermissionDenied()
+
+ real_method = get_request_method(request)
+
+ if request.method == 'GET':
+ return HttpResponse(json.dumps(get_course_updates(location)),
+ mimetype="application/json")
+ elif real_method == 'DELETE':
+ try:
+ return HttpResponse(json.dumps(delete_course_update(location,
+ request.POST, provided_id)), mimetype="application/json")
+ except:
+ return HttpResponseBadRequest("Failed to delete",
+ content_type="text/plain")
+ elif request.method == 'POST':
+ try:
+ return HttpResponse(json.dumps(update_course_updates(location,
+ request.POST, provided_id)), mimetype="application/json")
+ except:
+ return HttpResponseBadRequest("Failed to save",
+ content_type="text/plain")
+
+@login_required
+@ensure_csrf_cookie
+def get_course_settings(request, org, course, name):
+ """
+ Send models and views as well as html for editing the course settings to the client.
+
+ org, course, name: Attributes of the Location for the item to edit
+ """
+ location = get_location_and_verify_access(request, org, course, name)
+
+ course_module = modulestore().get_item(location)
+
+ return render_to_response('settings.html', {
+ 'context_course': course_module,
+ 'course_location': location,
+ 'details_url': reverse(course_settings_updates,
+ kwargs={"org": org,
+ "course": course,
+ "name": name,
+ "section": "details"})
+ })
+
+
+@login_required
+@ensure_csrf_cookie
+def course_config_graders_page(request, org, course, name):
+ """
+ Send models and views as well as html for editing the course settings to the client.
+
+ org, course, name: Attributes of the Location for the item to edit
+ """
+ location = get_location_and_verify_access(request, org, course, name)
+
+ course_module = modulestore().get_item(location)
+ course_details = CourseGradingModel.fetch(location)
+
+ return render_to_response('settings_graders.html', {
+ 'context_course': course_module,
+ 'course_location': location,
+ 'course_details': json.dumps(course_details, cls=CourseSettingsEncoder)
+ })
+
+
+@login_required
+@ensure_csrf_cookie
+def course_config_advanced_page(request, org, course, name):
+ """
+ Send models and views as well as html for editing the advanced course settings to the client.
+
+ org, course, name: Attributes of the Location for the item to edit
+ """
+ location = get_location_and_verify_access(request, org, course, name)
+
+ course_module = modulestore().get_item(location)
+
+ return render_to_response('settings_advanced.html', {
+ 'context_course': course_module,
+ 'course_location': location,
+ 'advanced_dict': json.dumps(CourseMetadata.fetch(location)),
+ })
+
+
+@expect_json
+@login_required
+@ensure_csrf_cookie
+def course_settings_updates(request, org, course, name, section):
+ """
+ restful CRUD operations on course settings. This differs from get_course_settings by communicating purely
+ through json (not rendering any html) and handles section level operations rather than whole page.
+
+ org, course: Attributes of the Location for the item to edit
+ section: one of details, faculty, grading, problems, discussions
+ """
+ get_location_and_verify_access(request, org, course, name)
+
+ if section == 'details':
+ manager = CourseDetails
+ elif section == 'grading':
+ manager = CourseGradingModel
+ else:
+ return
+
+ if request.method == 'GET':
+ # Cannot just do a get w/o knowing the course name :-(
+ return HttpResponse(json.dumps(manager.fetch(Location(['i4x', org, course, 'course', name])), cls=CourseSettingsEncoder),
+ mimetype="application/json")
+ elif request.method == 'POST': # post or put, doesn't matter.
+ return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseSettingsEncoder),
+ mimetype="application/json")
+
+
+@expect_json
+@login_required
+@ensure_csrf_cookie
+def course_grader_updates(request, org, course, name, grader_index=None):
+ """
+ restful CRUD operations on course_info updates. This differs from get_course_settings by communicating purely
+ through json (not rendering any html) and handles section level operations rather than whole page.
+
+ org, course: Attributes of the Location for the item to edit
+ """
+
+ location = get_location_and_verify_access(request, org, course, name)
+
+ real_method = get_request_method(request)
+
+ if real_method == 'GET':
+ # Cannot just do a get w/o knowing the course name :-(
+ return HttpResponse(json.dumps(CourseGradingModel.fetch_grader(Location(location), grader_index)),
+ mimetype="application/json")
+ elif real_method == "DELETE":
+ # ??? Should this return anything? Perhaps success fail?
+ CourseGradingModel.delete_grader(Location(location), grader_index)
+ return HttpResponse()
+ elif request.method == 'POST': # post or put, doesn't matter.
+ return HttpResponse(json.dumps(CourseGradingModel.update_grader_from_json(Location(location), request.POST)),
+ mimetype="application/json")
+
+
+# # NB: expect_json failed on ["key", "key2"] and json payload
+@login_required
+@ensure_csrf_cookie
+def course_advanced_updates(request, org, course, name):
+ """
+ restful CRUD operations on metadata. The payload is a json rep of the metadata dicts. For delete, otoh,
+ the payload is either a key or a list of keys to delete.
+
+ org, course: Attributes of the Location for the item to edit
+ """
+ location = get_location_and_verify_access(request, org, course, name)
+
+ real_method = get_request_method(request)
+
+ if real_method == 'GET':
+ return HttpResponse(json.dumps(CourseMetadata.fetch(location)), mimetype="application/json")
+ elif real_method == 'DELETE':
+ return HttpResponse(json.dumps(CourseMetadata.delete_key(location, json.loads(request.body))),
+ mimetype="application/json")
+ elif real_method == 'POST' or real_method == 'PUT':
+ # NOTE: request.POST is messed up because expect_json cloned_request.POST.copy() is creating a defective entry w/ the whole payload as the key
+ request_body = json.loads(request.body)
+ #Whether or not to filter the tabs key out of the settings metadata
+ filter_tabs = True
+ #Check to see if the user instantiated any advanced components. This is a hack to add the open ended panel tab
+ #to a course automatically if the user has indicated that they want to edit the combinedopenended or peergrading
+ #module, and to remove it if they have removed the open ended elements.
+ if ADVANCED_COMPONENT_POLICY_KEY in request_body:
+ #Check to see if the user instantiated any open ended components
+ found_oe_type = False
+ #Get the course so that we can scrape current tabs
+ course_module = modulestore().get_item(location)
+ for oe_type in OPEN_ENDED_COMPONENT_TYPES:
+ if oe_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]:
+ #Add an open ended tab to the course if needed
+ changed, new_tabs = add_open_ended_panel_tab(course_module)
+ #If a tab has been added to the course, then send the metadata along to CourseMetadata.update_from_json
+ if changed:
+ request_body.update({'tabs': new_tabs})
+ #Indicate that tabs should not be filtered out of the metadata
+ filter_tabs = False
+ #Set this flag to avoid the open ended tab removal code below.
+ found_oe_type = True
+ break
+ #If we did not find an open ended module type in the advanced settings,
+ # we may need to remove the open ended tab from the course.
+ if not found_oe_type:
+ #Remove open ended tab to the course if needed
+ changed, new_tabs = remove_open_ended_panel_tab(course_module)
+ if changed:
+ request_body.update({'tabs': new_tabs})
+ #Indicate that tabs should not be filtered out of the metadata
+ filter_tabs = False
+ response_json = json.dumps(CourseMetadata.update_from_json(location, request_body, filter_tabs=filter_tabs))
+ return HttpResponse(response_json, mimetype="application/json")
+
+
diff --git a/cms/djangoapps/contentstore/views/error.py b/cms/djangoapps/contentstore/views/error.py
index 527f137b9e..814af96104 100644
--- a/cms/djangoapps/contentstore/views/error.py
+++ b/cms/djangoapps/contentstore/views/error.py
@@ -1,4 +1,4 @@
-from django.http import HttpResponse, HttpResponseServerError, HttpResponseNotFound
+from django.http import HttpResponseServerError, HttpResponseNotFound
from mitxmako.shortcuts import render_to_string, render_to_response
diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py
index 876251203e..b6d03e3f81 100644
--- a/cms/djangoapps/contentstore/views/item.py
+++ b/cms/djangoapps/contentstore/views/item.py
@@ -1,7 +1,21 @@
+import json
+from uuid import uuid4
+
+from django.core.exceptions import PermissionDenied
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required
+
+from xmodule.modulestore import Location
+from xmodule.modulestore.django import modulestore
+from xmodule.modulestore.inheritance import own_metadata
+
from util.json_request import expect_json
-from mitxmako.shortcuts import render_to_response
+from contentstore.utils import get_modulestore
+from access import has_access
+from requests import _xmodule_recurse
+
+# cdodge: these are categories which should not be parented, they are detached from the hierarchy
+DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
@login_required
diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py
index f473b962c5..a3fc816730 100644
--- a/cms/djangoapps/contentstore/views/preview.py
+++ b/cms/djangoapps/contentstore/views/preview.py
@@ -1,3 +1,10 @@
+import logging, sys
+import static_replace
+from xmodule_modifiers import replace_static_urls
+from xmodule.error_module import ErrorDescriptor
+from xmodule.errortracker import exc_info_to_str
+from django.core.urlresolvers import reverse
+from mitxmako.shortcuts import render_to_response
from django.contrib.auth.decorators import login_required
from xblock.runtime import DbModel
from xmodule.x_module import ModuleSystem
@@ -6,6 +13,14 @@ from xmodule_modifiers import wrap_xmodule
from session_kv_store import SessionKeyValueStore
from requests import render_from_lms
from functools import partial
+from xmodule.modulestore import Location
+from access import has_access
+from xmodule.modulestore.django import modulestore
+from xmodule.exceptions import NotFoundError, ProcessingError
+from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden
+
+
+log = logging.getLogger(__name__)
@login_required
def preview_dispatch(request, preview_id, location, dispatch=None):
diff --git a/cms/djangoapps/contentstore/views/public.py b/cms/djangoapps/contentstore/views/public.py
index 7c207b7893..fe26fbec7c 100644
--- a/cms/djangoapps/contentstore/views/public.py
+++ b/cms/djangoapps/contentstore/views/public.py
@@ -1,7 +1,11 @@
from external_auth.views import ssl_login_shortcut
from mitxmako.shortcuts import render_to_response
from django_future.csrf import ensure_csrf_cookie
-from requests import index
+from django.core.context_processors import csrf
+from django.shortcuts import redirect
+from django.conf import settings
+
+from user import index
"""
Public views
diff --git a/cms/djangoapps/contentstore/views/requests.py b/cms/djangoapps/contentstore/views/requests.py
index 131068768a..58a3275527 100644
--- a/cms/djangoapps/contentstore/views/requests.py
+++ b/cms/djangoapps/contentstore/views/requests.py
@@ -1,42 +1,7 @@
-from django.contrib.auth.decorators import login_required
-from django_future.csrf import ensure_csrf_cookie
-from mitxmako.shortcuts import render_to_response
-from xmodule.modulestore import Location
-from xmodule.modulestore.django import modulestore
-from access import has_access
-from contentstore.utils import get_url_reverse, get_lms_link_for_item
-from django.conf import settings
-
-@login_required
-@ensure_csrf_cookie
-def index(request):
- """
- List all courses available to the logged in user
- """
- courses = modulestore('direct').get_items(['i4x', None, None, 'course', None])
-
- # filter out courses that we don't have access too
- def course_filter(course):
- return (has_access(request.user, course.location)
- and course.location.course != 'templates'
- and course.location.org != ''
- and course.location.course != ''
- and course.location.name != '')
- courses = filter(course_filter, courses)
-
- return render_to_response('index.html', {
- 'new_course_template': Location('i4x', 'edx', 'templates', 'course', 'Empty'),
- 'courses': [(course.display_name,
- get_url_reverse('CourseOutline', course),
- get_lms_link_for_item(course.location, course_id=course.location.course_id))
- for course in courses],
- 'user': request.user,
- 'disable_course_creation': settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff
- })
-
-
-# ==== Views with per-item permissions================================
+import json
+from django.http import HttpResponse
+from mitxmako.shortcuts import render_to_string, render_to_response
# points to the temporary course landing page with log in and sign up
def landing(request, org, course, coursename):
diff --git a/cms/djangoapps/contentstore/views/session_kv_store.py b/cms/djangoapps/contentstore/views/session_kv_store.py
index 2f6868ee81..7bfb14351d 100644
--- a/cms/djangoapps/contentstore/views/session_kv_store.py
+++ b/cms/djangoapps/contentstore/views/session_kv_store.py
@@ -1,4 +1,4 @@
-from xblock.runtime import KeyValueStore
+from xblock.runtime import KeyValueStore, InvalidScopeError
class SessionKeyValueStore(KeyValueStore):
def __init__(self, request, model_data):
diff --git a/cms/djangoapps/contentstore/views/tabs.py b/cms/djangoapps/contentstore/views/tabs.py
new file mode 100644
index 0000000000..9a6d8736bf
--- /dev/null
+++ b/cms/djangoapps/contentstore/views/tabs.py
@@ -0,0 +1,110 @@
+from access import has_access
+from util.json_request import expect_json
+
+from django.http import HttpResponse, HttpResponseBadRequest
+from django.contrib.auth.decorators import login_required
+from django.core.exceptions import PermissionDenied
+from django_future.csrf import ensure_csrf_cookie
+from mitxmako.shortcuts import render_to_response
+
+from xmodule.modulestore import Location
+from xmodule.modulestore.inheritance import own_metadata
+from xmodule.modulestore.django import modulestore
+from contentstore.utils import get_course_for_item
+
+
+def initialize_course_tabs(course):
+ # set up the default tabs
+ # I've added this because when we add static tabs, the LMS either expects a None for the tabs list or
+ # at least a list populated with the minimal times
+ # @TODO: I don't like the fact that the presentation tier is away of these data related constraints, let's find a better
+ # place for this. Also rather than using a simple list of dictionaries a nice class model would be helpful here
+
+ # This logic is repeated in xmodule/modulestore/tests/factories.py
+ # so if you change anything here, you need to also change it there.
+ course.tabs = [{"type": "courseware"},
+ {"type": "course_info", "name": "Course Info"},
+ {"type": "discussion", "name": "Discussion"},
+ {"type": "wiki", "name": "Wiki"},
+ {"type": "progress", "name": "Progress"}]
+
+ modulestore('direct').update_metadata(course.location.url(), own_metadata(course))
+
+
+@login_required
+@expect_json
+def reorder_static_tabs(request):
+ tabs = request.POST['tabs']
+ course = get_course_for_item(tabs[0])
+
+ if not has_access(request.user, course.location):
+ raise PermissionDenied()
+
+ # get list of existing static tabs in course
+ # make sure they are the same lengths (i.e. the number of passed in tabs equals the number
+ # that we know about) otherwise we can drop some!
+
+ existing_static_tabs = [t for t in course.tabs if t['type'] == 'static_tab']
+ if len(existing_static_tabs) != len(tabs):
+ return HttpResponseBadRequest()
+
+ # load all reference tabs, return BadRequest if we can't find any of them
+ tab_items = []
+ for tab in tabs:
+ item = modulestore('direct').get_item(Location(tab))
+ if item is None:
+ return HttpResponseBadRequest()
+
+ tab_items.append(item)
+
+ # now just go through the existing course_tabs and re-order the static tabs
+ reordered_tabs = []
+ static_tab_idx = 0
+ for tab in course.tabs:
+ if tab['type'] == 'static_tab':
+ reordered_tabs.append({'type': 'static_tab',
+ 'name': tab_items[static_tab_idx].display_name,
+ 'url_slug': tab_items[static_tab_idx].location.name})
+ static_tab_idx += 1
+ else:
+ reordered_tabs.append(tab)
+
+ # OK, re-assemble the static tabs in the new order
+ course.tabs = reordered_tabs
+ modulestore('direct').update_metadata(course.location, own_metadata(course))
+ return HttpResponse()
+
+@login_required
+@ensure_csrf_cookie
+def edit_tabs(request, org, course, coursename):
+ location = ['i4x', org, course, 'course', coursename]
+ course_item = modulestore().get_item(location)
+
+ # check that logged in user has permissions to this item
+ if not has_access(request.user, location):
+ raise PermissionDenied()
+
+ # see tabs have been uninitialized (e.g. supporing courses created before tab support in studio)
+ if course_item.tabs is None or len(course_item.tabs) == 0:
+ initialize_course_tabs(course_item)
+
+ # first get all static tabs from the tabs list
+ # we do this because this is also the order in which items are displayed in the LMS
+ static_tabs_refs = [t for t in course_item.tabs if t['type'] == 'static_tab']
+
+ static_tabs = []
+ for static_tab_ref in static_tabs_refs:
+ static_tab_loc = Location(location)._replace(category='static_tab', name=static_tab_ref['url_slug'])
+ static_tabs.append(modulestore('direct').get_item(static_tab_loc))
+
+ components = [
+ static_tab.location.url()
+ for static_tab
+ in static_tabs
+ ]
+
+ return render_to_response('edit-tabs.html', {
+ 'active_tab': 'pages',
+ 'context_course': course_item,
+ 'components': components
+ })
diff --git a/cms/djangoapps/contentstore/views/user.py b/cms/djangoapps/contentstore/views/user.py
index 5be78a0c37..0ead03257b 100644
--- a/cms/djangoapps/contentstore/views/user.py
+++ b/cms/djangoapps/contentstore/views/user.py
@@ -1,9 +1,23 @@
+from django.conf import settings
+from django.core.exceptions import PermissionDenied
+from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import login_required
from django_future.csrf import ensure_csrf_cookie
-from util.json_request import expect_json
from mitxmako.shortcuts import render_to_response
+from xmodule.modulestore import Location
+from xmodule.modulestore.django import modulestore
+from contentstore.utils import get_url_reverse, get_lms_link_for_item
+
+from access import has_access
+from requests import create_json_response
+from util.json_request import expect_json
+
+from auth.authz import STAFF_ROLE_NAME, INSTRUCTOR_ROLE_NAME, get_users_in_course_group_by_role
+from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group
+
+
def user_author_string(user):
'''Get an author string for commits by this user. Format:
first last .
@@ -20,6 +34,33 @@ def user_author_string(user):
email=user.email)
+@login_required
+@ensure_csrf_cookie
+def index(request):
+ """
+ List all courses available to the logged in user
+ """
+ courses = modulestore('direct').get_items(['i4x', None, None, 'course', None])
+
+ # filter out courses that we don't have access too
+ def course_filter(course):
+ return (has_access(request.user, course.location)
+ and course.location.course != 'templates'
+ and course.location.org != ''
+ and course.location.course != ''
+ and course.location.name != '')
+ courses = filter(course_filter, courses)
+
+ return render_to_response('index.html', {
+ 'new_course_template': Location('i4x', 'edx', 'templates', 'course', 'Empty'),
+ 'courses': [(course.display_name,
+ get_url_reverse('CourseOutline', course),
+ get_lms_link_for_item(course.location, course_id=course.location.course_id))
+ for course in courses],
+ 'user': request.user,
+ 'disable_course_creation': settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff
+ })
+
@login_required
@ensure_csrf_cookie
def manage_users(request, location):
From 01d4fbeb7c9915d7ad3c31dbdb36594d48d49ff3 Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Fri, 10 May 2013 14:14:36 -0400
Subject: [PATCH 069/245] reorder imports
---
cms/djangoapps/contentstore/views/preview.py | 27 ++++++++++----------
cms/djangoapps/contentstore/views/public.py | 5 ++--
2 files changed, 17 insertions(+), 15 deletions(-)
diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py
index a3fc816730..36ca01ec86 100644
--- a/cms/djangoapps/contentstore/views/preview.py
+++ b/cms/djangoapps/contentstore/views/preview.py
@@ -1,24 +1,25 @@
import logging, sys
import static_replace
-from xmodule_modifiers import replace_static_urls
+from functools import partial
+
+from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden
+from django.core.urlresolvers import reverse
+from django.contrib.auth.decorators import login_required
+from mitxmako.shortcuts import render_to_response
+
+from xmodule_modifiers import replace_static_urls, wrap_xmodule
from xmodule.error_module import ErrorDescriptor
from xmodule.errortracker import exc_info_to_str
-from django.core.urlresolvers import reverse
-from mitxmako.shortcuts import render_to_response
-from django.contrib.auth.decorators import login_required
-from xblock.runtime import DbModel
-from xmodule.x_module import ModuleSystem
+from xmodule.exceptions import NotFoundError, ProcessingError
+from xmodule.modulestore import Location
+from xmodule.modulestore.django import modulestore
from xmodule.modulestore.mongo import MongoUsage
-from xmodule_modifiers import wrap_xmodule
+from xmodule.x_module import ModuleSystem
+from xblock.runtime import DbModel
+
from session_kv_store import SessionKeyValueStore
from requests import render_from_lms
-from functools import partial
-from xmodule.modulestore import Location
from access import has_access
-from xmodule.modulestore.django import modulestore
-from xmodule.exceptions import NotFoundError, ProcessingError
-from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden
-
log = logging.getLogger(__name__)
diff --git a/cms/djangoapps/contentstore/views/public.py b/cms/djangoapps/contentstore/views/public.py
index fe26fbec7c..3ab9e4e5a0 100644
--- a/cms/djangoapps/contentstore/views/public.py
+++ b/cms/djangoapps/contentstore/views/public.py
@@ -1,10 +1,11 @@
-from external_auth.views import ssl_login_shortcut
-from mitxmako.shortcuts import render_to_response
from django_future.csrf import ensure_csrf_cookie
from django.core.context_processors import csrf
from django.shortcuts import redirect
from django.conf import settings
+from mitxmako.shortcuts import render_to_response
+
+from external_auth.views import ssl_login_shortcut
from user import index
"""
From 35d72f30b905dc2132d7d4bda428c48b97b782af Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Fri, 10 May 2013 14:40:30 -0400
Subject: [PATCH 070/245] update .gitignore; fix logger import in execute.py
---
.gitignore | 3 +++
i18n/execute.py | 5 ++---
2 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/.gitignore b/.gitignore
index d01baf055a..f1784a48f3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@
*.swp
*.orig
*.DS_Store
+*.mo
:2e_*
:2e#
.AppleDouble
@@ -22,6 +23,8 @@ reports/
*.egg-info
Gemfile.lock
.env/
+conf/locale/en/LC_MESSAGES/*.po
+!messages.po
lms/static/sass/*.css
cms/static/sass/*.css
lms/lib/comment_client/python
diff --git a/i18n/execute.py b/i18n/execute.py
index e3f3478d12..e55e653ea7 100644
--- a/i18n/execute.py
+++ b/i18n/execute.py
@@ -1,9 +1,8 @@
-import os, subprocess
+import os, subprocess, logging
-from logger import get_logger
from config import CONFIGURATION, BASE_DIR
-LOG = get_logger(__name__)
+LOG = logging.getLogger(__name__)
def execute(command, working_directory=BASE_DIR, log=LOG):
"""
From f30f6207d5e46938daf674d8685b1236c63be138 Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Fri, 10 May 2013 14:47:47 -0400
Subject: [PATCH 071/245] clean up rakefile syntax for task dependencies
---
rakefile | 9 +++------
1 file changed, 3 insertions(+), 6 deletions(-)
diff --git a/rakefile b/rakefile
index 3edbc39067..cf9363d47b 100644
--- a/rakefile
+++ b/rakefile
@@ -513,14 +513,12 @@ end
namespace :i18n do
desc "Extract localizable strings from sources"
- task :extract do
- Rake::Task["i18n:validate:gettext"].execute
+ task :extract => "i18n:validate:gettext" do
sh(File.join(REPO_ROOT, "i18n", "extract.py"))
end
desc "Compile localizable strings from sources. With optional flag 'extract', will extract strings first."
- task :generate do
- Rake::Task["i18n:validate:gettext"].execute
+ task :generate => "i18n:validate:gettext" do
if ARGV.last.downcase == 'extract'
Rake::Task["i18n:extract"].execute
end
@@ -579,8 +577,7 @@ namespace :i18n do
end
desc "Run tests for the internationalization library"
- task :test do
- Rake::Task["i18n:validate:gettext"].execute
+ task :test => "i18n:validate:gettext" do
test = File.join(REPO_ROOT, "i18n", "tests")
sh("nosetests #{test}")
end
From a52cf85c0811d8bfd77b96676873550d0910da30 Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Fri, 10 May 2013 14:50:49 -0400
Subject: [PATCH 072/245] rake task dependency for transifex
---
rakefile | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/rakefile b/rakefile
index cf9363d47b..04a7db4904 100644
--- a/rakefile
+++ b/rakefile
@@ -562,15 +562,13 @@ namespace :i18n do
namespace :transifex do
desc "Push source strings to Transifex for translation"
- task :push do
- Rake::Task["i18n:validate:transifex_config"].execute
+ task :push => "i18n:validate:transifex_config" do
cmd = File.join(REPO_ROOT, "i18n", "transifex.py")
sh("#{cmd} push")
end
desc "Pull translated strings from Transifex"
- task :pull do
- Rake::Task["i18n:validate:transifex_config"].execute
+ task :pull => "i18n:validate:transifex_config" do
cmd = File.join(REPO_ROOT, "i18n", "transifex.py")
sh("#{cmd} pull")
end
From 1c5815a8444a1f8e8fa406fbced565468fb3d731 Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Fri, 10 May 2013 15:11:10 -0400
Subject: [PATCH 073/245] per-file log objects
---
i18n/execute.py | 14 +++++++++-----
i18n/tests/test_validate.py | 2 +-
2 files changed, 10 insertions(+), 6 deletions(-)
diff --git a/i18n/execute.py b/i18n/execute.py
index e55e653ea7..1ff439ef38 100644
--- a/i18n/execute.py
+++ b/i18n/execute.py
@@ -4,25 +4,28 @@ from config import CONFIGURATION, BASE_DIR
LOG = logging.getLogger(__name__)
-def execute(command, working_directory=BASE_DIR, log=LOG):
+def execute(command, working_directory=BASE_DIR, log=True):
"""
Executes shell command in a given working_directory.
Command is a string to pass to the shell.
- The command is logged to log, output is ignored.
+ log is boolean. If true, the command's invocation string is logged.
+ Output is ignored.
"""
if log:
- log.info(command)
+ LOG.info(command)
subprocess.call(command.split(' '), cwd=working_directory)
-def call(command, working_directory=BASE_DIR, log=LOG):
+def call(command, working_directory=BASE_DIR, log=True):
"""
Executes shell command in a given working_directory.
Command is a string to pass to the shell.
Returns a tuple of two strings: (stdout, stderr)
+ log is boolean. If true, the command's invocation string is logged.
+
"""
if log:
- log.info(command)
+ LOG.info(command)
p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=working_directory)
out, err = p.communicate()
return (out, err)
@@ -36,6 +39,7 @@ def create_dir_if_necessary(pathname):
def remove_file(filename, log=LOG, verbose=True):
"""
Attempt to delete filename.
+ log is boolean. If true, removal is logged.
Log a warning if file does not exist.
Logging filenames are releative to BASE_DIR to cut down on noise in output.
"""
diff --git a/i18n/tests/test_validate.py b/i18n/tests/test_validate.py
index 67057a30e7..7d970c8de2 100644
--- a/i18n/tests/test_validate.py
+++ b/i18n/tests/test_validate.py
@@ -28,7 +28,7 @@ def validate_po_file(filename, log):
raise SkipTest()
# Use relative paths to make output less noisy.
rfile = os.path.relpath(filename, LOCALE_DIR)
- (out, err) = call(['msgfmt','-c', rfile], log=None, working_directory=LOCALE_DIR)
+ (out, err) = call(['msgfmt','-c', rfile], log=False, working_directory=LOCALE_DIR)
if err != '':
log.warn('\n'+err)
From f970bbd121da44f6a28a4d29170ec7acc1ea49a3 Mon Sep 17 00:00:00 2001
From: Peter Baratta
Date: Fri, 10 May 2013 15:21:29 -0400
Subject: [PATCH 074/245] Add tests on the problem level that show the infinite
answer bug
---
.../lib/capa/capa/tests/test_responsetypes.py | 52 +++++++++++++++++++
1 file changed, 52 insertions(+)
diff --git a/common/lib/capa/capa/tests/test_responsetypes.py b/common/lib/capa/capa/tests/test_responsetypes.py
index 7a43fff4c9..f7848ca094 100644
--- a/common/lib/capa/capa/tests/test_responsetypes.py
+++ b/common/lib/capa/capa/tests/test_responsetypes.py
@@ -438,6 +438,36 @@ class FormulaResponseTest(ResponseTest):
self.assert_grade(problem, incorrect, 'incorrect',
msg="Failed on function {0}; the given, incorrect answer was {1} but graded 'correct'".format(func, incorrect))
+ def test_grade_infinity(self):
+ # This resolves a bug where a problem with relative tolerance would
+ # pass with any arbitrarily large student answer.
+
+ sample_dict = {'x' : (1,2)}
+
+ # Test problem
+ problem = self.build_problem(sample_dict=sample_dict,
+ num_samples=10,
+ tolerance="1%",
+ answer="x")
+ # Expect such a large answer to be marked incorrect
+ input_formula = "x*1e999"
+ self.assert_grade(problem, input_formula, "incorrect")
+
+ def test_grade_nan(self):
+ # attempt to produce a value which causes the student's answer to be
+ # evaluated to nan. See if this is resolved correctly.
+
+ sample_dict = {'x' : (1,2)}
+
+ # Test problem
+ problem = self.build_problem(sample_dict=sample_dict,
+ num_samples=10,
+ tolerance="1%",
+ answer="x")
+ # Expect an incorrect answer (+ nan) to be marked incorrect
+ input_formula = "10*x + 0*1e999" # right now this evaluates to 'nan' for a given x
+ self.assert_grade(problem, input_formula, "incorrect")
+
class StringResponseTest(ResponseTest):
from response_xml_factory import StringResponseXMLFactory
@@ -714,6 +744,28 @@ class NumericalResponseTest(ResponseTest):
incorrect_responses = ["", "4.5", "3.5", "0"]
self.assert_multiple_grade(problem, correct_responses, incorrect_responses)
+ def test_grade_infinity(self):
+ # This resolves a bug where a problem with relative tolerance would
+ # pass with any arbitrarily large student answer.
+ problem = self.build_problem(question_text="What is 2 + 2 approximately?",
+ explanation="The answer is 4",
+ answer=4,
+ tolerance="10%")
+ correct_responses = []
+ incorrect_responses = ["1e999"]
+ self.assert_multiple_grade(problem, correct_responses, incorrect_responses)
+
+ def test_grade_nan(self):
+ # attempt to produce a value which causes the student's answer to be
+ # evaluated to nan. See if this is resolved correctly.
+ problem = self.build_problem(question_text="What is 2 + 2 approximately?",
+ explanation="The answer is 4",
+ answer=4,
+ tolerance="10%")
+ correct_responses = []
+ incorrect_responses = ["0*1e999"] # right now this evaluates to 'nan' for a given x
+ self.assert_multiple_grade(problem, correct_responses, incorrect_responses)
+
def test_grade_with_script(self):
script_text = "computed_response = math.sqrt(4)"
problem = self.build_problem(question_text="What is sqrt(4)?",
From a1db394bcd4c841ea48fb3b13ac00737d26db28a Mon Sep 17 00:00:00 2001
From: Peter Baratta
Date: Fri, 10 May 2013 15:22:23 -0400
Subject: [PATCH 075/245] Test for infinity in numerical and formula responses
---
common/lib/capa/capa/responsetypes.py | 2 --
common/lib/capa/capa/util.py | 6 +++++-
2 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py
index fda70c6a55..f4f5d854a9 100644
--- a/common/lib/capa/capa/responsetypes.py
+++ b/common/lib/capa/capa/responsetypes.py
@@ -1869,8 +1869,6 @@ class FormulaResponse(LoncapaResponse):
log.debug('formularesponse: error %s in formula' % err)
raise StudentInputError("Invalid input: Could not parse '%s' as a formula" %
cgi.escape(given))
- if numpy.isnan(student_result) or numpy.isinf(student_result):
- return "incorrect"
if not compare_with_tolerance(student_result, instructor_result, self.tolerance):
return "incorrect"
return "correct"
diff --git a/common/lib/capa/capa/util.py b/common/lib/capa/capa/util.py
index 9f3e8bd3a0..c219a7b5f6 100644
--- a/common/lib/capa/capa/util.py
+++ b/common/lib/capa/capa/util.py
@@ -1,4 +1,5 @@
from .calc import evaluator, UndefinedVariable
+from cmath import isinf
#-----------------------------------------------------------------------------
#
@@ -20,8 +21,11 @@ def compare_with_tolerance(v1, v2, tol):
tolerance = tolerance_rel * max(abs(v1), abs(v2))
else:
tolerance = evaluator(dict(), dict(), tol)
- return abs(v1 - v2) <= tolerance
+ if isinf(v1) or isinf(v2):
+ return v1 == v2 # because the other numerical comparison does not work with infinities
+ else:
+ return abs(v1 - v2) <= tolerance
def contextualize_text(text, context): # private
''' Takes a string with variables. E.g. $a+$b.
From 70cca0540dbccf086a121d973c1dd7ba30f46b5d Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Fri, 10 May 2013 15:23:28 -0400
Subject: [PATCH 076/245] reorder imports for pep8
---
cms/djangoapps/contentstore/views/preview.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py
index 36ca01ec86..bbee9f621c 100644
--- a/cms/djangoapps/contentstore/views/preview.py
+++ b/cms/djangoapps/contentstore/views/preview.py
@@ -1,5 +1,4 @@
import logging, sys
-import static_replace
from functools import partial
from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden
@@ -17,6 +16,7 @@ from xmodule.modulestore.mongo import MongoUsage
from xmodule.x_module import ModuleSystem
from xblock.runtime import DbModel
+import static_replace
from session_kv_store import SessionKeyValueStore
from requests import render_from_lms
from access import has_access
From 571a9af0c087eae413001db91ea5fdc25d01eb46 Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Fri, 10 May 2013 16:37:28 -0400
Subject: [PATCH 077/245] declared exports
---
cms/djangoapps/contentstore/views/__init__.py | 19 ++++++++++---------
cms/djangoapps/contentstore/views/assets.py | 2 ++
.../contentstore/views/checklist.py | 2 ++
cms/djangoapps/contentstore/views/error.py | 2 ++
cms/djangoapps/contentstore/views/item.py | 2 ++
cms/djangoapps/contentstore/views/preview.py | 2 ++
cms/djangoapps/contentstore/views/public.py | 2 ++
cms/djangoapps/contentstore/views/requests.py | 2 ++
cms/djangoapps/contentstore/views/tabs.py | 1 +
9 files changed, 25 insertions(+), 9 deletions(-)
diff --git a/cms/djangoapps/contentstore/views/__init__.py b/cms/djangoapps/contentstore/views/__init__.py
index 37f786ac3c..e37e4bae37 100644
--- a/cms/djangoapps/contentstore/views/__init__.py
+++ b/cms/djangoapps/contentstore/views/__init__.py
@@ -1,13 +1,14 @@
# TODO: replace asterisks, should explicitly enumerate imports instead
-from assets import asset_index, upload_asset, import_course, generate_export_course, export_course
-from checklist import get_checklists, update_checklist
+from assets import *
+from checklist import *
from component import *
from course import *
-from error import not_found, server_error, render_404, render_500
-from item import save_item, clone_item, delete_item
-from preview import preview_dispatch, preview_component
-from public import signup, old_login_redirect, login_page, howitworks, ux_alerts
-from user import index, add_user, remove_user, manage_users
-from tabs import edit_tabs, reorder_static_tabs
-from requests import edge, event, landing
+from error import *
+from item import *
+from preview import *
+from public import *
+from user import *
+from tabs import *
+from requests import *
+
diff --git a/cms/djangoapps/contentstore/views/assets.py b/cms/djangoapps/contentstore/views/assets.py
index c2aa52b1e1..04eb0c0ed6 100644
--- a/cms/djangoapps/contentstore/views/assets.py
+++ b/cms/djangoapps/contentstore/views/assets.py
@@ -25,6 +25,8 @@ from xmodule.util.date_utils import get_default_time_display
from access import get_location_and_verify_access
from auth.authz import create_all_course_groups
+__all__ = ['asset_index', 'upload_asset', 'import_course', 'generate_export_course', 'export_course']
+
@login_required
@ensure_csrf_cookie
def asset_index(request, org, course, name):
diff --git a/cms/djangoapps/contentstore/views/checklist.py b/cms/djangoapps/contentstore/views/checklist.py
index 4a97ddc1df..a86c751c14 100644
--- a/cms/djangoapps/contentstore/views/checklist.py
+++ b/cms/djangoapps/contentstore/views/checklist.py
@@ -12,6 +12,8 @@ from contentstore.utils import get_modulestore, get_url_reverse
from requests import get_request_method
from access import get_location_and_verify_access
+__all__ = ['get_checklists', 'update_checklist']
+
@ensure_csrf_cookie
@login_required
def get_checklists(request, org, course, name):
diff --git a/cms/djangoapps/contentstore/views/error.py b/cms/djangoapps/contentstore/views/error.py
index 814af96104..64d1538d5d 100644
--- a/cms/djangoapps/contentstore/views/error.py
+++ b/cms/djangoapps/contentstore/views/error.py
@@ -2,6 +2,8 @@ from django.http import HttpResponseServerError, HttpResponseNotFound
from mitxmako.shortcuts import render_to_string, render_to_response
+__all__ = ['not_found', 'server_error', 'render_404', 'render_500']
+
def not_found(request):
return render_to_response('error.html', {'error': '404'})
diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py
index b6d03e3f81..ac82e38577 100644
--- a/cms/djangoapps/contentstore/views/item.py
+++ b/cms/djangoapps/contentstore/views/item.py
@@ -14,6 +14,8 @@ from contentstore.utils import get_modulestore
from access import has_access
from requests import _xmodule_recurse
+__all__ = ['save_item', 'clone_item', 'delete_item']
+
# cdodge: these are categories which should not be parented, they are detached from the hierarchy
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py
index bbee9f621c..5e8abb238c 100644
--- a/cms/djangoapps/contentstore/views/preview.py
+++ b/cms/djangoapps/contentstore/views/preview.py
@@ -21,6 +21,8 @@ from session_kv_store import SessionKeyValueStore
from requests import render_from_lms
from access import has_access
+__all__ = ['preview_dispatch', 'preview_component']
+
log = logging.getLogger(__name__)
@login_required
diff --git a/cms/djangoapps/contentstore/views/public.py b/cms/djangoapps/contentstore/views/public.py
index 3ab9e4e5a0..fe8a2b7a91 100644
--- a/cms/djangoapps/contentstore/views/public.py
+++ b/cms/djangoapps/contentstore/views/public.py
@@ -8,6 +8,8 @@ from mitxmako.shortcuts import render_to_response
from external_auth.views import ssl_login_shortcut
from user import index
+__all__ = ['signup', 'old_login_redirect', 'login_page', 'howitworks', 'ux_alerts']
+
"""
Public views
"""
diff --git a/cms/djangoapps/contentstore/views/requests.py b/cms/djangoapps/contentstore/views/requests.py
index 58a3275527..46b42fad7a 100644
--- a/cms/djangoapps/contentstore/views/requests.py
+++ b/cms/djangoapps/contentstore/views/requests.py
@@ -3,6 +3,8 @@ import json
from django.http import HttpResponse
from mitxmako.shortcuts import render_to_string, render_to_response
+__all__ = ['edge', 'event', 'landing']
+
# points to the temporary course landing page with log in and sign up
def landing(request, org, course, coursename):
return render_to_response('temp-course-landing.html', {})
diff --git a/cms/djangoapps/contentstore/views/tabs.py b/cms/djangoapps/contentstore/views/tabs.py
index 9a6d8736bf..b947c163eb 100644
--- a/cms/djangoapps/contentstore/views/tabs.py
+++ b/cms/djangoapps/contentstore/views/tabs.py
@@ -12,6 +12,7 @@ from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.django import modulestore
from contentstore.utils import get_course_for_item
+__all__ = ['edit_tabs', 'reorder_static_tabs']
def initialize_course_tabs(course):
# set up the default tabs
From 6b0394172d413e07619002f9f74159357b5b5c44 Mon Sep 17 00:00:00 2001
From: Will Daly
Date: Fri, 10 May 2013 10:47:48 -0400
Subject: [PATCH 078/245] Changed test_acceptance to test_acceptance_lms and
test_acceptance_cms
Updated testing docs to reflect changes to the rake file
Added option to pass arguments (including --pdb) to rake test_acceptance
Updated readme
---
doc/testing.md | 22 ++++++++++++++--------
rakefile | 12 ++++++------
2 files changed, 20 insertions(+), 14 deletions(-)
diff --git a/doc/testing.md b/doc/testing.md
index 84175fee3d..d41a2cb3eb 100644
--- a/doc/testing.md
+++ b/doc/testing.md
@@ -171,20 +171,26 @@ Before running the tests, you need to set up the test database:
rm ../db/test_mitx.db
rake django-admin[syncdb,lms,acceptance,--noinput]
rake django-admin[migrate,lms,acceptance,--noinput]
+ rake django-admin[syncdb,cms,acceptance,--noinput]
+ rake django-admin[migrate,cms,acceptance,--noinput]
-To run the acceptance tests:
+To run all the acceptance tests:
-1. Start the Django server locally using the settings in **acceptance.py**:
+ rake test_acceptance_lms
+ rake test_acceptance_cms
- rake lms[acceptance]
-
-2. In another shell, run the tests:
-
- django-admin.py harvest --no-server --settings=lms.envs.acceptance --pythonpath=. lms/djangoapps/portal/features/
To test only a specific feature:
- django-admin.py harvest --no-server --settings=lms.envs.acceptance --pythonpath=. lms/djangoapps/courseware/features/high-level-tabs.feature
+ rake test_acceptance_lms[lms/djangoapps/courseware/features/problems.feature]
+
+To start the debugger on failure, add the `--pdb` option:
+
+ rake test_acceptance_lms["lms/djangoapps/courseware/features/problems.feature --pdb"]
+
+To run tests faster by not collecting static files, you can use
+`rake fasttest_acceptance_lms` and `rake fasttest_acceptance_cms`.
+
**Troubleshooting**: If you get an error message that says something about harvest not being a command, you probably are missing a requirement.
Try running:
diff --git a/rakefile b/rakefile
index 8da061a4db..a805fad3e5 100644
--- a/rakefile
+++ b/rakefile
@@ -243,9 +243,9 @@ def run_tests(system, report_dir, stop_on_failure=true)
end
end
-def run_acceptance_tests(system, report_dir, feature_path)
+def run_acceptance_tests(system, report_dir, harvest_args)
ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, 'acceptance_tests.xml')
- sh(django_admin(system, 'acceptance', 'harvest', '--debug-mode', feature_path))
+ sh(django_admin(system, 'acceptance', 'harvest', '--debug-mode', harvest_args))
end
@@ -275,12 +275,12 @@ end
# Run acceptance tests
desc "Run acceptance tests"
- task "test_acceptance", [:feature_path] => ["#{system}:gather_assets:acceptance", "fasttest_acceptance"]
+ task "test_acceptance_#{system}", [:harvest_args] => ["#{system}:gather_assets:acceptance", "fasttest_acceptance_#{system}"]
desc "Run acceptance tests without collectstatic"
- task "fasttest_acceptance", [:feature_path] => ["clean_test_files", :predjango, report_dir] do |t, args|
- args.with_defaults(:feature_path => '')
- run_acceptance_tests(system, report_dir, args.feature_path)
+ task "fasttest_acceptance_#{system}", [:harvest_args] => ["clean_test_files", :predjango, report_dir] do |t, args|
+ args.with_defaults(:harvest_args => '')
+ run_acceptance_tests(system, report_dir, args.harvest_args)
end
From c071ee448f69aa409f3898ea19f842d05cc60881 Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Fri, 10 May 2013 16:51:26 -0400
Subject: [PATCH 079/245] pep8 compliance
---
cms/djangoapps/contentstore/views/__init__.py | 2 --
cms/djangoapps/contentstore/views/component.py | 2 ++
cms/djangoapps/contentstore/views/course.py | 4 +++-
cms/djangoapps/contentstore/views/preview.py | 9 ++++++---
cms/djangoapps/contentstore/views/requests.py | 1 -
5 files changed, 11 insertions(+), 7 deletions(-)
diff --git a/cms/djangoapps/contentstore/views/__init__.py b/cms/djangoapps/contentstore/views/__init__.py
index e37e4bae37..e17b27e9b1 100644
--- a/cms/djangoapps/contentstore/views/__init__.py
+++ b/cms/djangoapps/contentstore/views/__init__.py
@@ -1,5 +1,3 @@
-# TODO: replace asterisks, should explicitly enumerate imports instead
-
from assets import *
from checklist import *
from component import *
diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py
index 2dd7307976..f2a63c9b2c 100644
--- a/cms/djangoapps/contentstore/views/component.py
+++ b/cms/djangoapps/contentstore/views/component.py
@@ -25,6 +25,8 @@ from models.settings.course_grading import CourseGradingModel
from requests import get_request_method, _xmodule_recurse
from access import has_access, get_location_and_verify_access
+# TODO: should explicitly enumerate exports with __all__
+
# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
log = logging.getLogger(__name__)
diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py
index c0f6acc808..336a4ad0fe 100644
--- a/cms/djangoapps/contentstore/views/course.py
+++ b/cms/djangoapps/contentstore/views/course.py
@@ -17,13 +17,15 @@ from contentstore.utils import get_lms_link_for_item, add_open_ended_panel_tab,
from models.settings.course_details import CourseDetails, CourseSettingsEncoder
from models.settings.course_grading import CourseGradingModel
from models.settings.course_metadata import CourseMetadata
-from component import OPEN_ENDED_COMPONENT_TYPES, ADVANCED_COMPONENT_POLICY_KEY
from auth.authz import create_all_course_groups
from util.json_request import expect_json
+
from access import has_access, get_location_and_verify_access
from requests import get_request_method
from tabs import initialize_course_tabs
+from component import OPEN_ENDED_COMPONENT_TYPES, ADVANCED_COMPONENT_POLICY_KEY
+# TODO: should explicitly enumerate exports with __all__
@login_required
@ensure_csrf_cookie
diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py
index 5e8abb238c..0b839a6754 100644
--- a/cms/djangoapps/contentstore/views/preview.py
+++ b/cms/djangoapps/contentstore/views/preview.py
@@ -1,4 +1,5 @@
-import logging, sys
+import logging
+import sys
from functools import partial
from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden
@@ -25,6 +26,7 @@ __all__ = ['preview_dispatch', 'preview_component']
log = logging.getLogger(__name__)
+
@login_required
def preview_dispatch(request, preview_id, location, dispatch=None):
"""
@@ -58,6 +60,7 @@ def preview_dispatch(request, preview_id, location, dispatch=None):
return HttpResponse(ajax_return)
+
@login_required
def preview_component(request, location):
# TODO (vshnayder): change name from id to location in coffee+html as well.
@@ -72,7 +75,6 @@ def preview_component(request, location):
})
-
def preview_module_system(request, preview_id, descriptor):
"""
Returns a ModuleSystem for the specified descriptor that is specialized for
@@ -104,6 +106,7 @@ def preview_module_system(request, preview_id, descriptor):
xblock_model_data=preview_model_data,
)
+
def get_preview_module(request, preview_id, descriptor):
"""
Returns a preview XModule at the specified location. The preview_data is chosen arbitrarily
@@ -159,6 +162,7 @@ def load_preview_module(request, preview_id, descriptor):
return module
+
def get_module_previews(request, descriptor):
"""
Returns a list of preview XModule html contents. One preview is returned for each
@@ -171,4 +175,3 @@ def get_module_previews(request, descriptor):
module = load_preview_module(request, str(idx), descriptor)
preview_html.append(module.get_html())
return preview_html
-
diff --git a/cms/djangoapps/contentstore/views/requests.py b/cms/djangoapps/contentstore/views/requests.py
index 46b42fad7a..07903637d3 100644
--- a/cms/djangoapps/contentstore/views/requests.py
+++ b/cms/djangoapps/contentstore/views/requests.py
@@ -40,7 +40,6 @@ def create_json_response(errmsg=None):
resp = HttpResponse(json.dumps({'Status': 'Failed', 'ErrMsg': errmsg}))
else:
resp = HttpResponse(json.dumps({'Status': 'OK'}))
-
return resp
def render_from_lms(template_name, dictionary, context=None, namespace='main'):
From d95e87cf6734ae0eede773bdd61ccf4e5f24db65 Mon Sep 17 00:00:00 2001
From: Chris Dodge
Date: Sat, 11 May 2013 10:34:36 -0400
Subject: [PATCH 080/245] insert the textbook XML element when writing
definition to xml
---
common/lib/xmodule/xmodule/course_module.py | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index 5efd7b4005..2a34b75a1f 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -382,6 +382,19 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
return definition, children
+ def definition_to_xml(self, resource_fs):
+ xml_object = super(CourseDescriptor, self).definition_to_xml(resource_fs)
+
+ if len(self.textbooks) > 0:
+ textbook_xml_object = etree.Element('textbook')
+ for textbook in self.textbooks:
+ textbook_xml_object.set('title', textbook.title)
+ textbook_xml_object.set('book_url', textbook.book_url)
+
+ xml_object.append(textbook_xml_object)
+
+ return xml_object
+
def has_ended(self):
"""
Returns True if the current time is after the specified course end date.
From ee1ee26c4974fa7adc0af67736aeb8fbf4e0bfbe Mon Sep 17 00:00:00 2001
From: Arthur Barrett
Date: Sat, 11 May 2013 13:57:07 -0400
Subject: [PATCH 081/245] log the course_id when notes are disabled
---
lms/djangoapps/notes/api.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lms/djangoapps/notes/api.py b/lms/djangoapps/notes/api.py
index 43ba1bc92c..fd749b6cae 100644
--- a/lms/djangoapps/notes/api.py
+++ b/lms/djangoapps/notes/api.py
@@ -47,7 +47,7 @@ def api_request(request, course_id, **kwargs):
# Verify that the api should be accessible to this course
if not api_enabled(request, course_id):
- log.debug('Notes not enabled for course')
+ log.debug('Notes are disabled for course: {0}'.format(course_id))
raise Http404
# Locate the requested resource
From 11a48dc962b24439fce77632bdb4ae8b3d73489a Mon Sep 17 00:00:00 2001
From: Arthur Barrett
Date: Sat, 11 May 2013 14:50:35 -0400
Subject: [PATCH 082/245] replacing lists with namedtuples to improve
readability
---
lms/djangoapps/notes/api.py | 74 +++++++++++++++++++------------------
1 file changed, 38 insertions(+), 36 deletions(-)
diff --git a/lms/djangoapps/notes/api.py b/lms/djangoapps/notes/api.py
index fd749b6cae..4df16094db 100644
--- a/lms/djangoapps/notes/api.py
+++ b/lms/djangoapps/notes/api.py
@@ -1,16 +1,18 @@
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse, Http404
from django.core.exceptions import ValidationError
+
from notes.models import Note
from notes.utils import notes_enabled_for_course
from courseware.courses import get_course_with_access
+
import json
import logging
+import collections
log = logging.getLogger(__name__)
API_SETTINGS = {
- # Version
'META': {'name': 'Notes API', 'version': 1},
# Maps resources to HTTP methods and actions
@@ -25,6 +27,9 @@ API_SETTINGS = {
'MAX_NOTE_LIMIT': 1000,
}
+# Wrapper class for HTTP response and data. All API actions are expected to return this.
+ApiResponse = collections.namedtuple('ApiResponse', ['http_response', 'data'])
+
#----------------------------------------------------------------------#
# API requests are routed through api_request() using the resource map.
@@ -72,33 +77,30 @@ def api_request(request, course_id, **kwargs):
raise Http404
log.debug('API request: {0} {1}'.format(resource_method, resource_name))
- result = module[func](request, course_id, **kwargs)
- # Format and output the results
- data = None
- response = result[0]
- if len(result) == 2:
- data = result[1]
+ api_response = module[func](request, course_id, **kwargs)
+ http_response = api_format(api_response)
- formatted = api_format(request, response, data)
- response['Content-type'] = formatted[0]
- response.content = formatted[1]
-
- log.debug('API response: {0}'.format(formatted))
-
- return response
+ return http_response
-def api_format(request, response, data):
+def api_format(api_response):
'''
- Returns a two-element list containing the content type and content.
+ Takes an ApiResponse and returns an HttpResponse.
'''
+ http_response = api_response.http_response
content_type = 'application/json'
- if data is None:
- content = ''
- else:
- content = json.dumps(data)
- return [content_type, content]
+ content = ''
+
+ if api_response.data is not None and api_response.data != '':
+ content = json.dumps(api_response.data)
+
+ http_response['Content-type'] = content_type
+ http_response.content = content
+
+ log.debug('API response type: {0} content: {1}'.format(content_type, content))
+
+ return http_response
def _get_course(request, course_id):
@@ -120,7 +122,7 @@ def index(request, course_id):
notes = Note.objects.order_by('id').filter(course_id=course_id,
user=request.user)[:MAX_LIMIT]
- return [HttpResponse(), [note.as_dict() for note in notes]]
+ return ApiResponse(http_response=HttpResponse(), data=[note.as_dict() for note in notes])
def create(request, course_id):
@@ -133,13 +135,13 @@ def create(request, course_id):
note.clean(request.body)
except ValidationError as e:
log.debug(e)
- return [HttpResponse('', status=500), None]
+ return ApiResponse(http_response=HttpResponse('', status=500), data=None)
note.save()
response = HttpResponse('', status=303)
response['Location'] = note.get_absolute_url()
- return [response, None]
+ return ApiResponse(http_response=response, data=None)
def read(request, course_id, note_id):
@@ -149,12 +151,12 @@ def read(request, course_id, note_id):
try:
note = Note.objects.get(id=note_id)
except:
- return [HttpResponse('', status=404), None]
+ return ApiResponse(http_response=HttpResponse('', status=404), data=None)
if not note.user.id == request.user.id:
- return [HttpResponse('', status=403)]
+ return ApiResponse(http_response=HttpResponse('', status=403), data=None)
- return [HttpResponse(), note.as_dict()]
+ return ApiResponse(http_response=HttpResponse(), data=note.as_dict())
def update(request, course_id, note_id):
@@ -164,23 +166,23 @@ def update(request, course_id, note_id):
try:
note = Note.objects.get(id=note_id)
except:
- return [HttpResponse('', status=404), None]
+ return ApiResponse(http_response=HttpResponse('', status=404), data=None)
if not note.user.id == request.user.id:
- return [HttpResponse('', status=403)]
+ return ApiResponse(http_response=HttpResponse('', status=403), data=None)
try:
note.clean(request.body)
except ValidationError as e:
log.debug(e)
- return [HttpResponse('', status=500), None]
+ return ApiResponse(http_response=HttpResponse('', status=500), data=None)
note.save()
response = HttpResponse('', status=303)
response['Location'] = note.get_absolute_url()
- return [response, None]
+ return ApiResponse(http_response=response, data=None)
def delete(request, course_id, note_id):
@@ -190,14 +192,14 @@ def delete(request, course_id, note_id):
try:
note = Note.objects.get(id=note_id)
except:
- return [HttpResponse('', status=404), None]
+ return ApiResponse(http_response=HttpResponse('', status=404), data=None)
if not note.user.id == request.user.id:
- return [HttpResponse('', status=403)]
+ return ApiResponse(http_response=HttpResponse('', status=403), data=None)
note.delete()
- return [HttpResponse('', status=204), None]
+ return ApiResponse(http_response=HttpResponse('', status=204), data=None)
def search(request, course_id):
@@ -238,11 +240,11 @@ def search(request, course_id):
'rows': [note.as_dict() for note in rows]
}
- return [HttpResponse(), result]
+ return ApiResponse(http_response=HttpResponse(), data=result)
def root(request, course_id):
'''
Returns version information about the API.
'''
- return [HttpResponse(), API_SETTINGS.get('META')]
+ return ApiResponse(http_response=HttpResponse(), data=API_SETTINGS.get('META'))
From 317831a8132e432723898324cd119e61fe4a01d0 Mon Sep 17 00:00:00 2001
From: "Mark L. Chang"
Date: Fri, 10 May 2013 13:21:09 -0400
Subject: [PATCH 083/245] copy change
---
cms/templates/500.html | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/cms/templates/500.html b/cms/templates/500.html
index 2645b0067b..6e381c0844 100644
--- a/cms/templates/500.html
+++ b/cms/templates/500.html
@@ -1,12 +1,17 @@
<%inherit file="base.html" />
-<%block name="title">Server Error%block>
+
+<%block name="title">Studio Server Error%block>
<%block name="content">
-
Currently the edX servers are down
-
Our staff is currently working to get the site back up as soon as possible. Please email us at technical@edx.org to report any problems or downtime.
+
The Studio servers encountered an error
+
+ An error occurred in Studio and the page could not be loaded. Please try again in a few moments.
+ We've logged the error and our staff is currently working to get Studio back up as soon as possible.
+ If the problem persists, please email us at technical@edx.org.
+
An error occurred in Studio and the page could not be loaded. Please try again in a few moments.
- We've logged the error and our staff is currently working to get Studio back up as soon as possible.
+ We've logged the error and our staff is currently working to resolve this error as soon as possible.
If the problem persists, please email us at technical@edx.org.
From 1f7bf1f00a43ccf142b1f4b1517cbeb2acbcafd3 Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Sun, 12 May 2013 21:52:07 -0400
Subject: [PATCH 085/245] use global LOG instead of local log
---
i18n/execute.py | 18 +++++++-----------
i18n/extract.py | 5 +++--
i18n/generate.py | 12 ++++++------
i18n/tests/test_validate.py | 2 +-
4 files changed, 17 insertions(+), 20 deletions(-)
diff --git a/i18n/execute.py b/i18n/execute.py
index 1ff439ef38..8e7f0f52de 100644
--- a/i18n/execute.py
+++ b/i18n/execute.py
@@ -4,28 +4,24 @@ from config import CONFIGURATION, BASE_DIR
LOG = logging.getLogger(__name__)
-def execute(command, working_directory=BASE_DIR, log=True):
+def execute(command, working_directory=BASE_DIR):
"""
Executes shell command in a given working_directory.
Command is a string to pass to the shell.
- log is boolean. If true, the command's invocation string is logged.
Output is ignored.
"""
- if log:
- LOG.info(command)
+ LOG.info(command)
subprocess.call(command.split(' '), cwd=working_directory)
-def call(command, working_directory=BASE_DIR, log=True):
+def call(command, working_directory=BASE_DIR):
"""
Executes shell command in a given working_directory.
Command is a string to pass to the shell.
Returns a tuple of two strings: (stdout, stderr)
- log is boolean. If true, the command's invocation string is logged.
"""
- if log:
- LOG.info(command)
+ LOG.info(command)
p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=working_directory)
out, err = p.communicate()
return (out, err)
@@ -36,7 +32,7 @@ def create_dir_if_necessary(pathname):
os.makedirs(dirname)
-def remove_file(filename, log=LOG, verbose=True):
+def remove_file(filename, verbose=True):
"""
Attempt to delete filename.
log is boolean. If true, removal is logged.
@@ -44,8 +40,8 @@ def remove_file(filename, log=LOG, verbose=True):
Logging filenames are releative to BASE_DIR to cut down on noise in output.
"""
if verbose:
- log.info('Deleting file %s' % os.path.relpath(filename, BASE_DIR))
+ LOG.info('Deleting file %s' % os.path.relpath(filename, BASE_DIR))
if not os.path.exists(filename):
- log.warn("File does not exist: %s" % os.path.relpath(filename, BASE_DIR))
+ LOG.warn("File does not exist: %s" % os.path.relpath(filename, BASE_DIR))
else:
os.remove(filename)
diff --git a/i18n/extract.py b/i18n/extract.py
index c517de3b51..c28c3868e2 100755
--- a/i18n/extract.py
+++ b/i18n/extract.py
@@ -32,8 +32,9 @@ BABEL_OUT = BASE_DIR.relpathto(CONFIGURATION.source_messages_dir.joinpath('mako.
SOURCE_WARN = 'This English source file is machine-generated. Do not check it into github'
+LOG = logging.getLogger(__name__)
+
def main ():
- log = logging.getLogger(__name__)
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
create_dir_if_necessary(LOCALE_DIR)
source_msgs_dir = CONFIGURATION.source_messages_dir
@@ -63,7 +64,7 @@ def main ():
execute(make_djangojs_cmd, working_directory=BASE_DIR)
for filename in generated_files:
- log.info('Cleaning %s' % filename)
+ LOG.info('Cleaning %s' % filename)
po = pofile(source_msgs_dir.joinpath(filename))
# replace default headers with edX headers
fix_header(po)
diff --git a/i18n/generate.py b/i18n/generate.py
index 48470796a2..65c65c00d6 100755
--- a/i18n/generate.py
+++ b/i18n/generate.py
@@ -17,9 +17,11 @@ import os, sys, logging
from polib import pofile
from config import BASE_DIR, CONFIGURATION
-from execute import execute, remove_file
+from execute import execute
-def merge(locale, target='django.po', fail_if_missing=True, log=None):
+LOG = logging.getLogger(__name__)
+
+def merge(locale, target='django.po', fail_if_missing=True):
"""
For the given locale, merge django-partial.po, messages.po, mako.po -> django.po
target is the resulting filename
@@ -28,8 +30,7 @@ def merge(locale, target='django.po', fail_if_missing=True, log=None):
If fail_if_missing is False, and the files to be merged are missing,
just return silently.
"""
- if log:
- log.info('Merging locale={0}'.format(locale))
+ LOG.info('Merging locale={0}'.format(locale))
locale_directory = CONFIGURATION.get_messages_dir(locale)
files_to_merge = ('django-partial.po', 'messages.po', 'mako.po')
try:
@@ -71,13 +72,12 @@ def validate_files(dir, files_to_merge):
raise Exception("I18N: Cannot generate because file not found: {0}".format(pathname))
def main ():
- log = logging.getLogger(__name__)
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
for locale in CONFIGURATION.locales:
merge(locale)
# Dummy text is not required. Don't raise exception if files are missing.
- merge(CONFIGURATION.dummy_locale, fail_if_missing=False, log=log)
+ merge(CONFIGURATION.dummy_locale, fail_if_missing=False)
compile_cmd = 'django-admin.py compilemessages'
execute(compile_cmd, working_directory=BASE_DIR)
diff --git a/i18n/tests/test_validate.py b/i18n/tests/test_validate.py
index 7d970c8de2..bef563faea 100644
--- a/i18n/tests/test_validate.py
+++ b/i18n/tests/test_validate.py
@@ -28,7 +28,7 @@ def validate_po_file(filename, log):
raise SkipTest()
# Use relative paths to make output less noisy.
rfile = os.path.relpath(filename, LOCALE_DIR)
- (out, err) = call(['msgfmt','-c', rfile], log=False, working_directory=LOCALE_DIR)
+ (out, err) = call(['msgfmt','-c', rfile], working_directory=LOCALE_DIR)
if err != '':
log.warn('\n'+err)
From e5daeb41fb7daaf03169a8fd05e7221d76537df8 Mon Sep 17 00:00:00 2001
From: Alexander Kryklia
Date: Thu, 4 Apr 2013 12:33:22 +0300
Subject: [PATCH 086/245] update docstring
---
common/lib/xmodule/xmodule/poll_module.py | 2 --
1 file changed, 2 deletions(-)
diff --git a/common/lib/xmodule/xmodule/poll_module.py b/common/lib/xmodule/xmodule/poll_module.py
index c8ad44a918..dafcef9835 100644
--- a/common/lib/xmodule/xmodule/poll_module.py
+++ b/common/lib/xmodule/xmodule/poll_module.py
@@ -4,8 +4,6 @@ to do set of polls.
On the client side we show:
If student does not yet anwered - Question with set of choices.
If student have answered - Question with statistics for each answers.
-
-Student can't change his answer.
"""
import cgi
From bb9bc421713b8f91e807fe156b0fe2afea327ea6 Mon Sep 17 00:00:00 2001
From: Alexander Kryklia
Date: Thu, 4 Apr 2013 12:34:03 +0300
Subject: [PATCH 087/245] adds initial word_cloud_module files
---
common/lib/xmodule/setup.py | 1 +
.../xmodule/js/src/word_cloud/logme.js | 54 +++
.../xmodule/js/src/word_cloud/word_cloud.js | 323 ++++++++++++++++++
.../js/src/word_cloud/word_cloud_main.js | 5 +
.../lib/xmodule/xmodule/word_cloud_module.py | 166 +++++++++
lms/templates/word_cloud.html | 8 +
6 files changed, 557 insertions(+)
create mode 100644 common/lib/xmodule/xmodule/js/src/word_cloud/logme.js
create mode 100644 common/lib/xmodule/xmodule/js/src/word_cloud/word_cloud.js
create mode 100644 common/lib/xmodule/xmodule/js/src/word_cloud/word_cloud_main.js
create mode 100644 common/lib/xmodule/xmodule/word_cloud_module.py
create mode 100644 lms/templates/word_cloud.html
diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py
index 982a77631d..43d970d898 100644
--- a/common/lib/xmodule/setup.py
+++ b/common/lib/xmodule/setup.py
@@ -52,6 +52,7 @@ setup(
"graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor",
"annotatable = xmodule.annotatable_module:AnnotatableDescriptor",
"foldit = xmodule.foldit_module:FolditDescriptor",
+ "word_cloud = xmodule.word_cloud_module:WordCloudDescriptor",
"hidden = xmodule.hidden_module:HiddenDescriptor",
"raw = xmodule.raw_module:RawDescriptor",
],
diff --git a/common/lib/xmodule/xmodule/js/src/word_cloud/logme.js b/common/lib/xmodule/xmodule/js/src/word_cloud/logme.js
new file mode 100644
index 0000000000..c045757044
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/src/word_cloud/logme.js
@@ -0,0 +1,54 @@
+// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
+// define() functions from Require JS available inside the anonymous function.
+(function (requirejs, require, define) {
+
+define('logme', [], function () {
+ var debugMode;
+
+ // debugMode can be one of the following:
+ //
+ // true - All messages passed to logme will be written to the internal
+ // browser console.
+ // false - Suppress all output to the internal browser console.
+ //
+ // Obviously, if anywhere there is a direct console.log() call, we can't do
+ // anything about it. That's why use logme() - it will allow to turn off
+ // the output of debug information with a single change to a variable.
+ debugMode = true;
+
+ return logme;
+
+ /*
+ * function: logme
+ *
+ * A helper function that provides logging facilities. We don't want
+ * to call console.log() directly, because sometimes it is not supported
+ * by the browser. Also when everything is routed through this function.
+ * the logging output can be easily turned off.
+ *
+ * logme() supports multiple parameters. Each parameter will be passed to
+ * console.log() function separately.
+ *
+ */
+ function logme() {
+ var i;
+
+ if (
+ (typeof debugMode === 'undefined') ||
+ (debugMode !== true) ||
+ (typeof window.console === 'undefined')
+ ) {
+ return;
+ }
+
+ for (i = 0; i < arguments.length; i++) {
+ window.console.log(arguments[i]);
+ }
+ } // End-of: function logme
+});
+
+// End of wrapper for RequireJS. As you can see, we are passing
+// namespaced Require JS variables to an anonymous function. Within
+// it, you can use the standard requirejs(), require(), and define()
+// functions as if they were in the global namespace.
+}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)
diff --git a/common/lib/xmodule/xmodule/js/src/word_cloud/word_cloud.js b/common/lib/xmodule/xmodule/js/src/word_cloud/word_cloud.js
new file mode 100644
index 0000000000..74f2a488d7
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/src/word_cloud/word_cloud.js
@@ -0,0 +1,323 @@
+(function (requirejs, require, define) {
+define('PollMain', ['logme'], function (logme) {
+
+PollMain.prototype = {
+
+'showAnswerGraph': function (poll_answers, total) {
+ var _this, totalValue;
+
+ totalValue = parseFloat(total);
+ if (isFinite(totalValue) === false) {
+ return;
+ }
+
+ _this = this;
+
+ $.each(poll_answers, function (index, value) {
+ var numValue, percentValue;
+
+ numValue = parseFloat(value);
+ if (isFinite(numValue) === false) {
+ return;
+ }
+
+ percentValue = (numValue / totalValue) * 100.0;
+
+ _this.answersObj[index].statsEl.show();
+ _this.answersObj[index].numberEl.html('' + value + ' (' + percentValue.toFixed(1) + '%)');
+ _this.answersObj[index].percentEl.css({
+ 'width': '' + percentValue.toFixed(1) + '%'
+ });
+ });
+},
+
+'submitAnswer': function (answer, answerObj) {
+ var _this;
+
+ // Make sure that the user can answer a question only once.
+ if (this.questionAnswered === true) {
+ return;
+ }
+ this.questionAnswered = true;
+
+ _this = this;
+
+ console.log('submit answer');
+
+ answerObj.buttonEl.addClass('answered');
+
+ // Send the data to the server as an AJAX request. Attach a callback that will
+ // be fired on server's response.
+ $.postWithPrefix(
+ _this.ajax_url + '/' + answer, {},
+ function (response) {
+ console.log('success! response = ');
+ console.log(response);
+
+ _this.showAnswerGraph(response.poll_answers, response.total);
+
+ if (_this.canReset === true) {
+ _this.resetButton.show();
+ }
+
+ // Initialize Conditional constructors.
+ if (_this.wrapperSectionEl !== null) {
+ $(_this.wrapperSectionEl).find('.xmodule_ConditionalModule').each(function (index, value) {
+ new window.Conditional(value, _this.id.replace(/^poll_/, ''));
+ });
+ }
+ }
+ );
+
+}, // End-of: 'submitAnswer': function (answer, answerEl) {
+
+
+'submitReset': function () {
+ var _this;
+
+ _this = this;
+
+ console.log('submit reset');
+
+ // Send the data to the server as an AJAX request. Attach a callback that will
+ // be fired on server's response.
+ $.postWithPrefix(
+ this.ajax_url + '/' + 'reset_poll',
+ {},
+ function (response) {
+ console.log('success! response = ');
+ console.log(response);
+
+ if (
+ (response.hasOwnProperty('status') !== true) ||
+ (typeof response.status !== 'string') ||
+ (response.status.toLowerCase() !== 'success')) {
+ return;
+ }
+
+ _this.questionAnswered = false;
+ _this.questionEl.find('.button.answered').removeClass('answered');
+ _this.questionEl.find('.stats').hide();
+ _this.resetButton.hide();
+
+ // Initialize Conditional constructors. We will specify the third parameter as 'true'
+ // notifying the constructor that this is a reset operation.
+ if (_this.wrapperSectionEl !== null) {
+ $(_this.wrapperSectionEl).find('.xmodule_ConditionalModule').each(function (index, value) {
+ new window.Conditional(value, _this.id.replace(/^poll_/, ''));
+ });
+ }
+ }
+ );
+}, // End-of: 'submitAnswer': function (answer, answerEl) {
+
+'postInit': function () {
+ var _this;
+
+ // Access this object inside inner functions.
+ _this = this;
+
+ if (
+ (this.jsonConfig.poll_answer.length > 0) &&
+ (this.jsonConfig.answers.hasOwnProperty(this.jsonConfig.poll_answer) === false)
+ ) {
+ this.questionEl.append(
+ '
Error!
' +
+ '
XML data format changed. List of answers was modified, but poll data was not updated.
'
+ );
+
+ return;
+ }
+
+ // Get the DOM id of the question.
+ this.id = this.questionEl.attr('id');
+
+ // Get the URL to which we will post the users answer to the question.
+ this.ajax_url = this.questionEl.data('ajax-url');
+
+ this.questionHtmlMarkup = $('').html(this.jsonConfig.question).text();
+ this.questionEl.append(this.questionHtmlMarkup);
+
+ // When the user selects and answer, we will set this flag to true.
+ this.questionAnswered = false;
+
+ this.answersObj = {};
+ this.shortVersion = true;
+
+ $.each(this.jsonConfig.answers, function (index, value) {
+ if (value.length >= 18) {
+ _this.shortVersion = false;
+ }
+ });
+
+ $.each(this.jsonConfig.answers, function (index, value) {
+ var answer;
+
+ answer = {};
+
+ _this.answersObj[index] = answer;
+
+ answer.el = $('');
+
+ answer.questionEl = $('');
+ answer.buttonEl = $('');
+ answer.textEl = $('');
+ answer.questionEl.append(answer.buttonEl);
+ answer.questionEl.append(answer.textEl);
+
+ answer.el.append(answer.questionEl);
+
+ answer.statsEl = $('');
+ answer.barEl = $('');
+ answer.percentEl = $('');
+ answer.barEl.append(answer.percentEl);
+ answer.numberEl = $('');
+ answer.statsEl.append(answer.barEl);
+ answer.statsEl.append(answer.numberEl);
+
+ answer.statsEl.hide();
+
+ answer.el.append(answer.statsEl);
+
+ answer.textEl.html(value);
+
+ if (_this.shortVersion === true) {
+ $.each(answer, function (index, value) {
+ if (value instanceof jQuery) {
+ value.addClass('short');
+ }
+ });
+ }
+
+ answer.el.appendTo(_this.questionEl);
+
+ answer.textEl.on('click', function () {
+ _this.submitAnswer(index, answer);
+ });
+
+ answer.buttonEl.on('click', function () {
+ _this.submitAnswer(index, answer);
+ });
+
+ if (index === _this.jsonConfig.poll_answer) {
+ answer.buttonEl.addClass('answered');
+ _this.questionAnswered = true;
+ }
+ });
+
+ console.log(this.jsonConfig.reset);
+
+ if ((typeof this.jsonConfig.reset === 'string') && (this.jsonConfig.reset.toLowerCase() === 'true')) {
+ this.canReset = true;
+
+ this.resetButton = $('
Change your vote
');
+
+ if (this.questionAnswered === false) {
+ this.resetButton.hide();
+ }
+
+ this.resetButton.appendTo(this.questionEl);
+
+ this.resetButton.on('click', function () {
+ _this.submitReset();
+ });
+ } else {
+ this.canReset = false;
+ }
+
+ // If it turns out that the user already answered the question, show the answers graph.
+ if (this.questionAnswered === true) {
+ this.showAnswerGraph(this.jsonConfig.poll_answers, this.jsonConfig.total);
+ }
+} // End-of: 'postInit': function () {
+}; // End-of: PollMain.prototype = {
+
+return PollMain;
+
+function PollMain(el) {
+ var _this;
+
+ this.questionEl = $(el).find('.poll_question');
+ if (this.questionEl.length !== 1) {
+ // We require one question DOM element.
+ logme('ERROR: PollMain constructor requires one question DOM element.');
+
+ return;
+ }
+
+ // Just a safety precussion. If we run this code more than once, multiple 'click' callback handlers will be
+ // attached to the same DOM elements. We don't want this to happen.
+ if (this.questionEl.attr('poll_main_processed') === 'true') {
+ logme(
+ 'ERROR: PolMain JS constructor was called on a DOM element that has already been processed once.'
+ );
+
+ return;
+ }
+
+ // This element was not processed earlier.
+ // Make sure that next time we will not process this element a second time.
+ this.questionEl.attr('poll_main_processed', 'true');
+
+ // Access this object inside inner functions.
+ _this = this;
+
+ // DOM element which contains the current poll along with any conditionals. By default we assume that such
+ // element is not present. We will try to find it.
+ this.wrapperSectionEl = null;
+
+ (function (tempEl, c1) {
+ while (tempEl.tagName.toLowerCase() !== 'body') {
+ tempEl = $(tempEl).parent()[0];
+ c1 += 1;
+
+ if (
+ (tempEl.tagName.toLowerCase() === 'section') &&
+ ($(tempEl).hasClass('xmodule_WrapperModule') === true)
+ ) {
+ _this.wrapperSectionEl = tempEl;
+
+ break;
+ } else if (c1 > 50) {
+ // In case something breaks, and we enter an endless loop, a sane
+ // limit for loop iterations.
+
+ break;
+ }
+ }
+ }($(el)[0], 0));
+
+ try {
+ this.jsonConfig = JSON.parse(this.questionEl.children('.poll_question_div').html());
+
+ $.postWithPrefix(
+ '' + this.questionEl.data('ajax-url') + '/' + 'get_state', {},
+ function (response) {
+ _this.jsonConfig.poll_answer = response.poll_answer;
+ _this.jsonConfig.total = response.total;
+
+ $.each(response.poll_answers, function (index, value) {
+ _this.jsonConfig.poll_answers[index] = value;
+ });
+
+ _this.questionEl.children('.poll_question_div').html(JSON.stringify(_this.jsonConfig));
+
+ _this.postInit();
+ }
+ );
+
+ return;
+ } catch (err) {
+ logme(
+ 'ERROR: Invalid JSON config for poll ID "' + this.id + '".',
+ 'Error messsage: "' + err.message + '".'
+ );
+
+ return;
+ }
+} // End-of: function PollMain(el) {
+
+}); // End-of: define('PollMain', ['logme'], function (logme) {
+
+// End-of: (function (requirejs, require, define) {
+}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
diff --git a/common/lib/xmodule/xmodule/js/src/word_cloud/word_cloud_main.js b/common/lib/xmodule/xmodule/js/src/word_cloud/word_cloud_main.js
new file mode 100644
index 0000000000..a2ccbc7c03
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/src/word_cloud/word_cloud_main.js
@@ -0,0 +1,5 @@
+window.Poll = function (el) {
+ RequireJS.require(['PollMain'], function (PollMain) {
+ new PollMain(el);
+ });
+};
diff --git a/common/lib/xmodule/xmodule/word_cloud_module.py b/common/lib/xmodule/xmodule/word_cloud_module.py
new file mode 100644
index 0000000000..7c2dfc6ad2
--- /dev/null
+++ b/common/lib/xmodule/xmodule/word_cloud_module.py
@@ -0,0 +1,166 @@
+"""Word cloud is ungraded xblock used by students to
+generate and view word cloud..
+
+On the client side we show:
+If student does not yet anwered - five text inputs.
+If student have answered - words he entered and cloud.
+
+Stunent can change his answer.
+"""
+
+import cgi
+import json
+import logging
+from copy import deepcopy
+from collections import OrderedDict
+
+from lxml import etree
+from pkg_resources import resource_string
+
+from xmodule.x_module import XModule
+from xmodule.stringify import stringify_children
+from xmodule.mako_module import MakoModuleDescriptor
+from xmodule.xml_module import XmlDescriptor
+from xblock.core import Scope, String, Object, Boolean, List
+
+log = logging.getLogger(__name__)
+
+
+class WordCloudFields(object):
+ # Name of poll to use in links to this poll
+ display_name = String(help="Display name for this module", scope=Scope.settings)
+
+ submitted = Boolean(help="Whether this student has voted on the poll", scope=Scope.student_state, default=False)
+ student_words= List(help="Student answer", scope=Scope.student_state, default=[])
+ all_words = Object(help="All possible words from other students", scope=Scope.content)
+ top_words = Object(help="Top N words for word cloud", scope=Scope.content)
+ top_low_border = Int(help="Number to distinguish top from all words", scope=Scope.content)
+
+class WordCloudModule(WordCloudFields, XModule):
+ """WordCloud Module"""
+ js = {
+ 'coffee': [resource_string(__name__, 'js/src/javascript_loader.coffee')],
+ 'js': [resource_string(__name__, 'js/src/word_cloud/logme.js'),
+ resource_string(__name__, 'js/src/word_cloud/word_cloud.js'),
+ resource_string(__name__, 'js/src/word_cloud/word_cloud_main.js')]
+ }
+ css = {'scss': [resource_string(__name__, 'css/word_cloud/display.scss')]}
+ js_module_name = "Word_Cloud"
+
+ Number_of_top_words = 250
+
+ def handle_ajax(self, dispatch, get):
+ """Ajax handler.
+
+ Args:
+ dispatch: string request slug
+ get: dict request get parameters
+
+ Returns:
+ json string
+ """
+ if dispatch == 'submit':
+
+ # self.all_words[word] -= 1
+ # FIXME: fix this, when xblock will support mutable types.
+ # Now we use this hack.
+ # speed issues
+ temp_all_words = self.all_words
+ temp_top_words = self.top_words
+
+ if self.submitted:
+
+ for word in self.student_words:
+ temp_all_words[word] -= 1
+
+ if word in temp_top_words:
+ temp_top_words -= 1
+
+ else:
+ self.submitted = True
+
+ self.student_words = get['student_words']
+
+ question_words = {}
+
+ for word in self.student_words:
+ temp_all_words[word] += 1
+
+ if word in temp_top_words:
+ temp_top_words += 1
+ else:
+ if temp_all_words[word] > top_low_border:
+ question_words[word] = temp_all_words[word]
+
+
+ self.all_words = temp_all_words
+
+ self.top_words = self.update_top_words(question_words, temp_top_words)
+
+
+ return json.dumps({'student_words': self.student_words,
+ 'top_words': self.top_words,
+ })
+ elif dispatch == 'get_state':
+ return json.dumps({'student_answers': self.student_answers,
+ 'top_words': self.top_words)
+ })
+ else: # return error message
+ return json.dumps({'error': 'Unknown Command!'})
+
+
+ def update_top_words(question_words, top_words):
+
+ for word, number in question_words:
+ for top_word, top_number in top_words[:]:
+ if top_number < number:
+ del top_words[top_word]
+ top_words[word] - number
+ break
+
+ return top_words
+
+ def get_html(self):
+ """Renders parameters to template."""
+ params = {
+ 'element_id': self.location.html_id(),
+ 'element_class': self.location.category,
+ 'ajax_url': self.system.ajax_url,
+ 'configuration_json': json.dumps({}),
+ }
+ self.content = self.system.render_template('word_cloud.html', params)
+ return self.content
+
+
+
+class WordCloudDescriptor(WordCloudFields, MakoModuleDescriptor, XmlDescriptor):
+ _tag_name = 'word_cloud'
+
+ module_class = WordCloudModule
+ template_dir_name = 'word_cloud'
+ stores_state = True
+
+ @classmethod
+ def definition_from_xml(cls, xml_object, system):
+ """Pull out the data into dictionary.
+
+ Args:
+ xml_object: xml from file.
+ system: `system` object.
+
+ Returns:
+ (definition, children) - tuple
+
+ """
+ definition = {}
+ children = []
+
+ return (definition, children)
+
+ def definition_to_xml(self, resource_fs):
+ """Return an xml element representing to this definition."""
+ poll_str = '<{tag_name}/>'.format(tag_name=self._tag_name)
+ xml_object = etree.fromstring(poll_str)
+ xml_object.set('display_name', self.display_name)
+
+ return xml_object
diff --git a/lms/templates/word_cloud.html b/lms/templates/word_cloud.html
new file mode 100644
index 0000000000..091d2b0317
--- /dev/null
+++ b/lms/templates/word_cloud.html
@@ -0,0 +1,8 @@
+
+
+
${configuration_json}
+
From 143d2c86367f99ec82342f68c8071714b29e4477 Mon Sep 17 00:00:00 2001
From: Vasyl Nakvasiuk
Date: Fri, 5 Apr 2013 16:13:05 +0300
Subject: [PATCH 088/245] fix WordCloudDescriptor
---
common/lib/xmodule/xmodule/word_cloud_module.py | 17 +++++++++--------
1 file changed, 9 insertions(+), 8 deletions(-)
diff --git a/common/lib/xmodule/xmodule/word_cloud_module.py b/common/lib/xmodule/xmodule/word_cloud_module.py
index 7c2dfc6ad2..63c8d40f4a 100644
--- a/common/lib/xmodule/xmodule/word_cloud_module.py
+++ b/common/lib/xmodule/xmodule/word_cloud_module.py
@@ -21,7 +21,7 @@ from xmodule.x_module import XModule
from xmodule.stringify import stringify_children
from xmodule.mako_module import MakoModuleDescriptor
from xmodule.xml_module import XmlDescriptor
-from xblock.core import Scope, String, Object, Boolean, List
+from xblock.core import Scope, String, Object, Boolean, List, Integer
log = logging.getLogger(__name__)
@@ -29,12 +29,13 @@ log = logging.getLogger(__name__)
class WordCloudFields(object):
# Name of poll to use in links to this poll
display_name = String(help="Display name for this module", scope=Scope.settings)
+ num_inputs = Integer(help="Number of inputs", scope=Scope.settings)
submitted = Boolean(help="Whether this student has voted on the poll", scope=Scope.student_state, default=False)
student_words= List(help="Student answer", scope=Scope.student_state, default=[])
all_words = Object(help="All possible words from other students", scope=Scope.content)
top_words = Object(help="Top N words for word cloud", scope=Scope.content)
- top_low_border = Int(help="Number to distinguish top from all words", scope=Scope.content)
+ top_low_border = Integer(help="Number to distinguish top from all words", scope=Scope.content)
class WordCloudModule(WordCloudFields, XModule):
"""WordCloud Module"""
@@ -44,8 +45,8 @@ class WordCloudModule(WordCloudFields, XModule):
resource_string(__name__, 'js/src/word_cloud/word_cloud.js'),
resource_string(__name__, 'js/src/word_cloud/word_cloud_main.js')]
}
- css = {'scss': [resource_string(__name__, 'css/word_cloud/display.scss')]}
- js_module_name = "Word_Cloud"
+ # css = {'scss': [resource_string(__name__, 'css/word_cloud/display.scss')]}
+ js_module_name = "WordCloud"
Number_of_top_words = 250
@@ -103,7 +104,7 @@ class WordCloudModule(WordCloudFields, XModule):
})
elif dispatch == 'get_state':
return json.dumps({'student_answers': self.student_answers,
- 'top_words': self.top_words)
+ 'top_words': self.top_words
})
else: # return error message
return json.dumps({'error': 'Unknown Command!'})
@@ -132,7 +133,6 @@ class WordCloudModule(WordCloudFields, XModule):
return self.content
-
class WordCloudDescriptor(WordCloudFields, MakoModuleDescriptor, XmlDescriptor):
_tag_name = 'word_cloud'
@@ -159,8 +159,9 @@ class WordCloudDescriptor(WordCloudFields, MakoModuleDescriptor, XmlDescriptor):
def definition_to_xml(self, resource_fs):
"""Return an xml element representing to this definition."""
- poll_str = '<{tag_name}/>'.format(tag_name=self._tag_name)
- xml_object = etree.fromstring(poll_str)
+ xml_str = '<{tag_name} />'.format(tag_name=self._tag_name)
+ xml_object = etree.fromstring(xml_str)
xml_object.set('display_name', self.display_name)
+ xml_object.set('num_inputs', self.num_inputs)
return xml_object
From 4b3fe54d48fc6febc3dcb7139623a904ada3ab65 Mon Sep 17 00:00:00 2001
From: Vasyl Nakvasiuk
Date: Fri, 5 Apr 2013 18:01:35 +0300
Subject: [PATCH 089/245] add test data xml for word_cloud
---
.../lib/xmodule/xmodule/tests/test_export.py | 3 +
common/test/data/word_cloud/README | 50 +++++++++++
common/test/data/word_cloud/README.md | 2 +
.../about/2013_Spring/overview.html | 79 ++++++++++++++++++
.../about/2013_Spring/prerequisites.html | 1 +
.../about/2013_Spring/short_description.html | 1 +
.../word_cloud/about/2013_Spring/video.html | 1 +
common/test/data/word_cloud/chapter/Staff.xml | 3 +
.../data/word_cloud/conditional/condone.xml | 3 +
common/test/data/word_cloud/course.xml | 2 +
.../data/word_cloud/course/2013_Spring.xml | 6 ++
.../test/data/word_cloud/creating_course.xml | 8 ++
.../word_cloud/info/2013_Spring/handouts.html | 3 +
.../word_cloud/info/2013_Spring/updates.html | 10 +++
.../policies/2013_Spring/policy.json | 8 ++
.../data/word_cloud/roots/2013_Spring.xml | 2 +
.../word_cloud/sequential/Problem_Demos.xml | 9 ++
common/test/data/word_cloud/static/README | 5 ++
.../word_cloud/static/images/course_image.jpg | Bin 0 -> 12626 bytes
.../static/images/professor-sandel.jpg | Bin 0 -> 453715 bytes
20 files changed, 196 insertions(+)
create mode 100644 common/test/data/word_cloud/README
create mode 100644 common/test/data/word_cloud/README.md
create mode 100644 common/test/data/word_cloud/about/2013_Spring/overview.html
create mode 100644 common/test/data/word_cloud/about/2013_Spring/prerequisites.html
create mode 100644 common/test/data/word_cloud/about/2013_Spring/short_description.html
create mode 100644 common/test/data/word_cloud/about/2013_Spring/video.html
create mode 100644 common/test/data/word_cloud/chapter/Staff.xml
create mode 100644 common/test/data/word_cloud/conditional/condone.xml
create mode 100644 common/test/data/word_cloud/course.xml
create mode 100644 common/test/data/word_cloud/course/2013_Spring.xml
create mode 100644 common/test/data/word_cloud/creating_course.xml
create mode 100644 common/test/data/word_cloud/info/2013_Spring/handouts.html
create mode 100644 common/test/data/word_cloud/info/2013_Spring/updates.html
create mode 100644 common/test/data/word_cloud/policies/2013_Spring/policy.json
create mode 100644 common/test/data/word_cloud/roots/2013_Spring.xml
create mode 100644 common/test/data/word_cloud/sequential/Problem_Demos.xml
create mode 100644 common/test/data/word_cloud/static/README
create mode 100644 common/test/data/word_cloud/static/images/course_image.jpg
create mode 100644 common/test/data/word_cloud/static/images/professor-sandel.jpg
diff --git a/common/lib/xmodule/xmodule/tests/test_export.py b/common/lib/xmodule/xmodule/tests/test_export.py
index 170a89d783..e912ce8a0d 100644
--- a/common/lib/xmodule/xmodule/tests/test_export.py
+++ b/common/lib/xmodule/xmodule/tests/test_export.py
@@ -123,3 +123,6 @@ class RoundTripTestCase(unittest.TestCase):
def test_exam_registration_roundtrip(self):
# Test exam_registration xmodule to see if it exports correctly
self.check_export_roundtrip(DATA_DIR, "test_exam_registration")
+
+ def test_word_cloud_roundtrip(self):
+ self.check_export_roundtrip(DATA_DIR, "word_cloud")
diff --git a/common/test/data/word_cloud/README b/common/test/data/word_cloud/README
new file mode 100644
index 0000000000..fc95a7c0c9
--- /dev/null
+++ b/common/test/data/word_cloud/README
@@ -0,0 +1,50 @@
+Any place that says "YEAR_SEMESTER" needs to be replaced with something
+in the form "2013_Spring". Take note of this name exactly, you'll need to
+use it everywhere, precisely - capitalization is very important.
+
+See https://github.com/MITx/mitx/blob/master/doc/xml-format.md for more on all this.
+-----------------------
+
+about/: Files that live here will be visible OUTSIDE OF COURSEWARE.
+ YEAR_SEMESTER/
+ end_date.html: Specifies in plain-text the end date of the course
+ overview.html: Text of the overview of the course
+ short_description.html: 10-15 words about the course
+ prerequisites.html: Any prerequisites for the course, or None if there are none.
+
+course/
+ YEAR_SEMESTER.xml: This is your top-level xml page that points at chapters.
+ Can just be for now.
+
+course.xml: This top level file points at a file in roots/. See creating_course.xml.
+
+creating_course.xml: Explains how to create course.xml
+
+info/: Files that live here will be visible on the COURSE LANDING PAGE
+ (Course Info) WITHIN THE COURSEWARE.
+ YEAR_SEMESTER/
+ handouts.html: A list of handouts, or an empty file if there are none
+ (if this file doesn't exist, it displays an error)
+ updates.html: Course updates.
+
+policies/
+ YEAR_SEMESTER/
+ policy.json: See https://github.com/MITx/mitx/blob/master/doc/xml-format.md
+ for more on the fields specified by this file.
+ grading_policy.json: Optional -- you don't need it to get a course off the
+ ground but will eventually. For more info see
+ https://github.com/MITx/mitx/blob/master/doc/course_grading.md
+
+roots/
+ YEAR_SEMESTER.xml: Looks something like
+
+ where ORG in {"MITx", "HarvardX", "BerkeleyX"}
+
+static/
+ See README.
+
+ images/
+ course_image.jpg: You MUST have an image named this to be the background
+ banner image on edx.org
+
+-----------------------
\ No newline at end of file
diff --git a/common/test/data/word_cloud/README.md b/common/test/data/word_cloud/README.md
new file mode 100644
index 0000000000..7dbfa46a26
--- /dev/null
+++ b/common/test/data/word_cloud/README.md
@@ -0,0 +1,2 @@
+content-harvard-justicex
+========================
\ No newline at end of file
diff --git a/common/test/data/word_cloud/about/2013_Spring/overview.html b/common/test/data/word_cloud/about/2013_Spring/overview.html
new file mode 100644
index 0000000000..9c49899948
--- /dev/null
+++ b/common/test/data/word_cloud/about/2013_Spring/overview.html
@@ -0,0 +1,79 @@
+
+
+
+
About ER22x
+
+
Justice is a critical analysis of classical and contemporary theories of justice, including discussion of present-day applications. Topics include affirmative action, income distribution, same-sex marriage, the role of markets, debates about rights (human rights and property rights), arguments for and against equality, dilemmas of loyalty in public and private life. The course invites students to subject their own views on these controversies to critical examination.
+
+
The principle readings for the course are texts by Aristotle, John Locke, Immanuel Kant, John Stuart Mill, and John Rawls. Other assigned readings include writings by contemporary philosophers, court cases, and articles about political controversies that raise philosophical questions.
+
+
+
+
+
+
+
+
Course instructor
+
+
+
+
+
Michael J. Sandel
+
Michael J. Sandel is the Anne T. and Robert M. Bass Professor of Government at Harvard University, where he teaches political philosophy. His course "Justice" has enrolled more than 15,000 Harvard students. Sandel's writings have been published in 21 languages. His books include What Money Can't Buy: The Moral Limits of Markets (2012); Justice: What's the Right Thing to Do? (2009); The Case against Perfection: Ethics in the Age of Genetic Engineering (2007); Public Philosophy: Essays on Morality in Politics (2005); Democracy's Discontent (1996); and Liberalism and the Limits of Justice(1982; 2nd ed., 1998).
+
+
+
+
+
+
+
Frequently Asked Questions
+
+
How much does it cost to take the course?
+
Nothing! The course is free.
+
+
+
+
Does the course have any prerequisites?
+
No. Only an interest in thinking through some of the big ethical and civic questions we face in our everyday lives.
+
+
+
+
Do I need any other materials to take the course?
+
No. As long as you’ve got a computer to access the website, you are ready to take the course.
+
+
+
+
Is there a textbook for the course?
+
All of the course readings that are in the public domain are freely available online, at links provided on the course website. The course can be taken using these free resources alone. For those who wish to purchase a printed version of the assigned readings, an edited volume entitled, Justice: A Reader (ed., Michael Sandel) is available in paperback from Oxford University Press (in bookstores and from online booksellers). Those who would like supplementary readings on the themes of the lectures can find them in Michael Sandel's book Justice: What's the Right Thing to Do?, which is available in various languages throughout the world. This book is not required, and the course can be taken using the free online resources alone.
+
+
+
+
Do I need to watch the lectures at a specific time?
+
No. You can watch the lectures at your leisure.
+
+
+
+
Will I be able to participate in class discussions?
+
Yes, in several ways:
+
+
+
Each lecture invites you to respond to a poll question related to the themes of the lecture. If you respond to the question, you will be presented with a challenge to the opinion you have expressed, and invited to reply to the challenge. You can also, if you wish, comment on the opinions and responses posted by other students in the course, continuing the discussion.
+
+
In addition to the poll question, each class contains a discussion prompt that invites you to offer your view on a controversial question related to the lecture. If you wish, you can respond to this question, and then see what other students have to say about the argument you present. You can also comment on the opinions posted by other students. One aim of the course is to promote reasoned public dialogue about hard moral and political questions.
+
+
Each week, there will be an optional live dialogue enabling students to interact with instructors and participants from around the world.
+
+
+
+
+
Will certificates be awarded?
+
Yes. Online learners who achieve a passing grade in a course can earn a certificate of mastery. These certificates will indicate you have successfully completed the course, but will not include a specific grade. Certificates will be issued by edX under the name of HarvardX, designating the institution from which the course originated.
+
+
+
+
+
+
\ No newline at end of file
diff --git a/common/test/data/word_cloud/about/2013_Spring/prerequisites.html b/common/test/data/word_cloud/about/2013_Spring/prerequisites.html
new file mode 100644
index 0000000000..b0047fa49f
--- /dev/null
+++ b/common/test/data/word_cloud/about/2013_Spring/prerequisites.html
@@ -0,0 +1 @@
+None
diff --git a/common/test/data/word_cloud/about/2013_Spring/short_description.html b/common/test/data/word_cloud/about/2013_Spring/short_description.html
new file mode 100644
index 0000000000..208880c842
--- /dev/null
+++ b/common/test/data/word_cloud/about/2013_Spring/short_description.html
@@ -0,0 +1 @@
+JusticeX is an introduction to moral and political philosophy, including discussion of contemporary dilemmas and controversies.
\ No newline at end of file
diff --git a/common/test/data/word_cloud/about/2013_Spring/video.html b/common/test/data/word_cloud/about/2013_Spring/video.html
new file mode 100644
index 0000000000..0cf427b16c
--- /dev/null
+++ b/common/test/data/word_cloud/about/2013_Spring/video.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/common/test/data/word_cloud/chapter/Staff.xml b/common/test/data/word_cloud/chapter/Staff.xml
new file mode 100644
index 0000000000..e1d5216f6d
--- /dev/null
+++ b/common/test/data/word_cloud/chapter/Staff.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/common/test/data/word_cloud/conditional/condone.xml b/common/test/data/word_cloud/conditional/condone.xml
new file mode 100644
index 0000000000..80b061e244
--- /dev/null
+++ b/common/test/data/word_cloud/conditional/condone.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/common/test/data/word_cloud/course.xml b/common/test/data/word_cloud/course.xml
new file mode 100644
index 0000000000..1b97a5a714
--- /dev/null
+++ b/common/test/data/word_cloud/course.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/common/test/data/word_cloud/course/2013_Spring.xml b/common/test/data/word_cloud/course/2013_Spring.xml
new file mode 100644
index 0000000000..cb6e7c1217
--- /dev/null
+++ b/common/test/data/word_cloud/course/2013_Spring.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/common/test/data/word_cloud/creating_course.xml b/common/test/data/word_cloud/creating_course.xml
new file mode 100644
index 0000000000..4c90f1c2ec
--- /dev/null
+++ b/common/test/data/word_cloud/creating_course.xml
@@ -0,0 +1,8 @@
+
diff --git a/common/test/data/word_cloud/info/2013_Spring/handouts.html b/common/test/data/word_cloud/info/2013_Spring/handouts.html
new file mode 100644
index 0000000000..35f2c89474
--- /dev/null
+++ b/common/test/data/word_cloud/info/2013_Spring/handouts.html
@@ -0,0 +1,3 @@
+
+
A list of course handouts, or an empty file if there are none.