From 55dd0fc8bcb40efa2557653261d9ac6a29c3683f Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Fri, 3 May 2013 12:45:55 -0400
Subject: [PATCH 01/65] 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 02/65] 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 03/65] 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 04/65] 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 05/65] 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 4620f50fb151e7a235bc119a92d4adec794a871d Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Tue, 7 May 2013 13:24:38 -0400
Subject: [PATCH 06/65] 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 07/65] 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 08/65] 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 5c55595e8bae9f77731e82a5063f1612895d90d8 Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Wed, 8 May 2013 12:10:49 -0400
Subject: [PATCH 09/65] 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 10/65] /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 11/65] 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 beb4b39b73f84dbd839f9802e1508c3e0abc2fbe Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Wed, 8 May 2013 14:25:31 -0400
Subject: [PATCH 12/65] 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 13/65] 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 165e7059c81c10005f6412594dc3167eaef13da6 Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Wed, 8 May 2013 15:34:35 -0400
Subject: [PATCH 14/65] 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 15/65] 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 16/65] 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 17/65] 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 f26a2585982c796e6aa1be0835882c070e1f155c Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Wed, 8 May 2013 18:07:17 -0400
Subject: [PATCH 18/65] 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 19/65] 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 20/65] 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 21/65] 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 22/65] 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 23/65] 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 24/65] 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 25/65] 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 26/65] 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 27/65] 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 28/65] 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 29/65] 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 30/65] 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 0bea50ede17a190a8f0a9ba61e71535be639090b Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Thu, 9 May 2013 16:24:21 -0400
Subject: [PATCH 31/65] 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 3352ae7f752202fee7e94c94957f34e84dd4e838 Mon Sep 17 00:00:00 2001
From: John Jarvis
Date: Thu, 9 May 2013 18:29:45 -0400
Subject: [PATCH 32/65] 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 1b0eff52edbf229d55eaa7f82a3e0de33a7bd0fd Mon Sep 17 00:00:00 2001
From: Chris Dodge
Date: Fri, 10 May 2013 10:01:10 -0400
Subject: [PATCH 33/65] 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 a1e6c194c6455e1895328842d39d48858356439d Mon Sep 17 00:00:00 2001
From: John Jarvis
Date: Fri, 10 May 2013 10:24:32 -0400
Subject: [PATCH 34/65] 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 f7655576cd24686585118a33d100378e483834f7 Mon Sep 17 00:00:00 2001
From: "Mark L. Chang"
Date: Fri, 10 May 2013 13:21:09 -0400
Subject: [PATCH 35/65] 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 37/65] 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 38/65] 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 39/65] 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 40/65] 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 41/65] 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 42/65] 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 43/65] 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 44/65] 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 45/65] 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 46/65] 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 c071ee448f69aa409f3898ea19f842d05cc60881 Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Fri, 10 May 2013 16:51:26 -0400
Subject: [PATCH 47/65] 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 48/65] 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 317831a8132e432723898324cd119e61fe4a01d0 Mon Sep 17 00:00:00 2001
From: "Mark L. Chang"
Date: Fri, 10 May 2013 13:21:09 -0400
Subject: [PATCH 49/65] 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 51/65] 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 979e3be1746180eccecf076c7a970f4a9187af71 Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Mon, 13 May 2013 09:58:30 -0400
Subject: [PATCH 52/65] fix pep8: imports and blank lines
---
cms/djangoapps/contentstore/views/__init__.py | 1 -
cms/djangoapps/contentstore/views/access.py | 1 +
cms/djangoapps/contentstore/views/assets.py | 10 ++++++--
.../contentstore/views/checklist.py | 5 +---
.../contentstore/views/component.py | 24 +++----------------
cms/djangoapps/contentstore/views/course.py | 7 +++---
cms/djangoapps/contentstore/views/error.py | 2 --
cms/djangoapps/contentstore/views/item.py | 1 -
cms/djangoapps/contentstore/views/public.py | 3 ++-
cms/djangoapps/contentstore/views/requests.py | 5 +++-
.../contentstore/views/session_kv_store.py | 1 +
cms/djangoapps/contentstore/views/tabs.py | 21 ++++++++++++++++
cms/djangoapps/contentstore/views/user.py | 4 +---
13 files changed, 46 insertions(+), 39 deletions(-)
diff --git a/cms/djangoapps/contentstore/views/__init__.py b/cms/djangoapps/contentstore/views/__init__.py
index e17b27e9b1..d31102c58e 100644
--- a/cms/djangoapps/contentstore/views/__init__.py
+++ b/cms/djangoapps/contentstore/views/__init__.py
@@ -9,4 +9,3 @@ from public import *
from user import *
from tabs import *
from requests import *
-
diff --git a/cms/djangoapps/contentstore/views/access.py b/cms/djangoapps/contentstore/views/access.py
index 37f6fcb767..0e4985ff8a 100644
--- a/cms/djangoapps/contentstore/views/access.py
+++ b/cms/djangoapps/contentstore/views/access.py
@@ -3,6 +3,7 @@ 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):
"""
Create the location tuple verify that the user has permissions
diff --git a/cms/djangoapps/contentstore/views/assets.py b/cms/djangoapps/contentstore/views/assets.py
index 04eb0c0ed6..edc1c977ab 100644
--- a/cms/djangoapps/contentstore/views/assets.py
+++ b/cms/djangoapps/contentstore/views/assets.py
@@ -1,4 +1,8 @@
-import logging, json, os, tarfile, shutil
+import logging
+import json
+import os
+import tarfile
+import shutil
from tempfile import mkdtemp
from path import path
@@ -27,6 +31,7 @@ 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):
@@ -135,6 +140,7 @@ 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):
@@ -240,6 +246,7 @@ def generate_export_course(request, org, course, name):
response['Content-Length'] = os.path.getsize(export_file.name)
return response
+
@ensure_csrf_cookie
@login_required
def export_course(request, org, course, name):
@@ -253,4 +260,3 @@ def export_course(request, org, course, name):
'active_tab': 'export',
'successful_import_redirect_url': ''
})
-
diff --git a/cms/djangoapps/contentstore/views/checklist.py b/cms/djangoapps/contentstore/views/checklist.py
index a86c751c14..d6efdefdc3 100644
--- a/cms/djangoapps/contentstore/views/checklist.py
+++ b/cms/djangoapps/contentstore/views/checklist.py
@@ -14,6 +14,7 @@ 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):
@@ -101,7 +102,3 @@ def expand_checklist_action_urls(course_module):
modified = True
return checklists, modified
-
-
-
-
diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py
index f2a63c9b2c..00fcce9f2b 100644
--- a/cms/djangoapps/contentstore/views/component.py
+++ b/cms/djangoapps/contentstore/views/component.py
@@ -1,4 +1,5 @@
-import json, logging
+import json
+import logging
from collections import defaultdict
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
@@ -23,7 +24,7 @@ from contentstore.utils import get_modulestore, get_lms_link_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
+from access import has_access
# TODO: should explicitly enumerate exports with __all__
@@ -269,23 +270,6 @@ def unpublish_unit(request):
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
@@ -311,5 +295,3 @@ def module_info(request, module_location):
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 336a4ad0fe..f1414faf4e 100644
--- a/cms/djangoapps/contentstore/views/course.py
+++ b/cms/djangoapps/contentstore/views/course.py
@@ -1,4 +1,5 @@
-import json, time
+import json
+import time
from django.contrib.auth.decorators import login_required
from django_future.csrf import ensure_csrf_cookie
@@ -27,6 +28,7 @@ from component import OPEN_ENDED_COMPONENT_TYPES, ADVANCED_COMPONENT_POLICY_KEY
# TODO: should explicitly enumerate exports with __all__
+
@login_required
@ensure_csrf_cookie
def course_index(request, org, course, name):
@@ -188,6 +190,7 @@ def course_info_updates(request, org, course, provided_id=None):
return HttpResponseBadRequest("Failed to save",
content_type="text/plain")
+
@login_required
@ensure_csrf_cookie
def get_course_settings(request, org, course, name):
@@ -362,5 +365,3 @@ def course_advanced_updates(request, org, course, name):
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 64d1538d5d..0422452c47 100644
--- a/cms/djangoapps/contentstore/views/error.py
+++ b/cms/djangoapps/contentstore/views/error.py
@@ -19,5 +19,3 @@ def render_404(request):
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
index ac82e38577..67f1da2710 100644
--- a/cms/djangoapps/contentstore/views/item.py
+++ b/cms/djangoapps/contentstore/views/item.py
@@ -136,4 +136,3 @@ def delete_item(request):
modulestore('direct').update_children(parent.location, parent.children)
return HttpResponse()
-
diff --git a/cms/djangoapps/contentstore/views/public.py b/cms/djangoapps/contentstore/views/public.py
index fe8a2b7a91..1049b29a28 100644
--- a/cms/djangoapps/contentstore/views/public.py
+++ b/cms/djangoapps/contentstore/views/public.py
@@ -14,6 +14,7 @@ __all__ = ['signup', 'old_login_redirect', 'login_page', 'howitworks', 'ux_alert
Public views
"""
+
@ensure_csrf_cookie
def signup(request):
"""
@@ -49,9 +50,9 @@ def howitworks(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
index 07903637d3..b02a13fe3f 100644
--- a/cms/djangoapps/contentstore/views/requests.py
+++ b/cms/djangoapps/contentstore/views/requests.py
@@ -5,10 +5,12 @@ 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', {})
+
# points to the temporary edge page
def edge(request):
return render_to_response('university_profiles/edge.html', {})
@@ -35,6 +37,7 @@ def get_request_method(request):
return real_method
+
def create_json_response(errmsg=None):
if errmsg is not None:
resp = HttpResponse(json.dumps({'Status': 'Failed', 'ErrMsg': errmsg}))
@@ -42,6 +45,7 @@ def create_json_response(errmsg=None):
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
@@ -54,4 +58,3 @@ def _xmodule_recurse(item, action):
_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
index 7bfb14351d..309518c27d 100644
--- a/cms/djangoapps/contentstore/views/session_kv_store.py
+++ b/cms/djangoapps/contentstore/views/session_kv_store.py
@@ -1,5 +1,6 @@
from xblock.runtime import KeyValueStore, InvalidScopeError
+
class SessionKeyValueStore(KeyValueStore):
def __init__(self, request, model_data):
self._model_data = model_data
diff --git a/cms/djangoapps/contentstore/views/tabs.py b/cms/djangoapps/contentstore/views/tabs.py
index b947c163eb..8ecfc31602 100644
--- a/cms/djangoapps/contentstore/views/tabs.py
+++ b/cms/djangoapps/contentstore/views/tabs.py
@@ -11,9 +11,11 @@ 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
+from access import get_location_and_verify_access
__all__ = ['edit_tabs', 'reorder_static_tabs']
+
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
@@ -75,6 +77,7 @@ def reorder_static_tabs(request):
modulestore('direct').update_metadata(course.location, own_metadata(course))
return HttpResponse()
+
@login_required
@ensure_csrf_cookie
def edit_tabs(request, org, course, coursename):
@@ -109,3 +112,21 @@ def edit_tabs(request, org, course, coursename):
'context_course': course_item,
'components': components
})
+
+
+@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', {})
diff --git a/cms/djangoapps/contentstore/views/user.py b/cms/djangoapps/contentstore/views/user.py
index 0ead03257b..6b1926bbae 100644
--- a/cms/djangoapps/contentstore/views/user.py
+++ b/cms/djangoapps/contentstore/views/user.py
@@ -61,6 +61,7 @@ def index(request):
'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):
@@ -84,8 +85,6 @@ def manage_users(request, location):
})
-
-
@expect_json
@login_required
@ensure_csrf_cookie
@@ -145,4 +144,3 @@ def remove_user(request, location):
remove_user_from_course_group(request.user, user, location, STAFF_ROLE_NAME)
return create_json_response()
-
From b288a8e3bda5224724fe1cc989219894947cee9f Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Mon, 13 May 2013 10:03:20 -0400
Subject: [PATCH 53/65] fix export from tabs.__all__
---
cms/djangoapps/contentstore/views/tabs.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cms/djangoapps/contentstore/views/tabs.py b/cms/djangoapps/contentstore/views/tabs.py
index 8ecfc31602..672723a161 100644
--- a/cms/djangoapps/contentstore/views/tabs.py
+++ b/cms/djangoapps/contentstore/views/tabs.py
@@ -13,7 +13,7 @@ from xmodule.modulestore.django import modulestore
from contentstore.utils import get_course_for_item
from access import get_location_and_verify_access
-__all__ = ['edit_tabs', 'reorder_static_tabs']
+__all__ = ['edit_tabs', 'reorder_static_tabs', 'static_pages', 'edit_static']
def initialize_course_tabs(course):
From 3cf865a88f9a41022f7446a089bcc5627a89d489 Mon Sep 17 00:00:00 2001
From: Chris Dodge
Date: Mon, 13 May 2013 10:16:18 -0400
Subject: [PATCH 54/65] added another unit test check point for export/import
of classic textbooks
---
cms/djangoapps/contentstore/tests/test_contentstore.py | 7 ++++++-
common/lib/xmodule/xmodule/course_module.py | 2 +-
2 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index 273a276c00..927df009bf 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -226,7 +226,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
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)
+ self.assertGreater(len(course.textbooks), 0)
def test_static_tab_reordering(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
@@ -498,6 +498,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertTrue(getattr(test_private_vertical, 'is_draft', False))
+ # make sure the textbook survived the export/import
+ course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
+
+ self.assertGreater(len(course.textbooks), 0)
+
shutil.rmtree(root_dir)
def test_course_handouts_rewrites(self):
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index 2a34b75a1f..063e53aef4 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -392,7 +392,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
textbook_xml_object.set('book_url', textbook.book_url)
xml_object.append(textbook_xml_object)
-
+
return xml_object
def has_ended(self):
From 2b9d78dfd5d939687c5310252734b3f6feda94be Mon Sep 17 00:00:00 2001
From: Peter Baratta
Date: Mon, 13 May 2013 10:18:19 -0400
Subject: [PATCH 55/65] Pep8 fixes and changes to NaN tests
---
common/lib/capa/capa/responsetypes.py | 3 ++-
common/lib/capa/capa/tests/test_responsetypes.py | 13 +++++++++----
common/lib/capa/capa/util.py | 7 +++++--
3 files changed, 16 insertions(+), 7 deletions(-)
diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py
index f4f5d854a9..65e903b576 100644
--- a/common/lib/capa/capa/responsetypes.py
+++ b/common/lib/capa/capa/responsetypes.py
@@ -2,7 +2,8 @@
# File: courseware/capa/responsetypes.py
#
'''
-Problem response evaluation. Handles checking of student responses, of a variety of types.
+Problem response evaluation. Handles checking of student responses,
+of a variety of types.
Used by capa_problem.py
'''
diff --git a/common/lib/capa/capa/tests/test_responsetypes.py b/common/lib/capa/capa/tests/test_responsetypes.py
index f7848ca094..554df1cde6 100644
--- a/common/lib/capa/capa/tests/test_responsetypes.py
+++ b/common/lib/capa/capa/tests/test_responsetypes.py
@@ -442,7 +442,7 @@ class FormulaResponseTest(ResponseTest):
# This resolves a bug where a problem with relative tolerance would
# pass with any arbitrarily large student answer.
- sample_dict = {'x' : (1,2)}
+ sample_dict = {'x': (1, 2)}
# Test problem
problem = self.build_problem(sample_dict=sample_dict,
@@ -457,7 +457,7 @@ class FormulaResponseTest(ResponseTest):
# 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)}
+ sample_dict = {'x': (1, 2)}
# Test problem
problem = self.build_problem(sample_dict=sample_dict,
@@ -465,7 +465,11 @@ class FormulaResponseTest(ResponseTest):
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
+ # right now this evaluates to 'nan' for a given x (Python implementation-dependent)
+ input_formula = "10*x + 0*1e999"
+ self.assert_grade(problem, input_formula, "incorrect")
+ # Expect an correct answer (+ nan) to be marked incorrect
+ input_formula = "x + 0*1e999"
self.assert_grade(problem, input_formula, "incorrect")
@@ -763,7 +767,8 @@ class NumericalResponseTest(ResponseTest):
answer=4,
tolerance="10%")
correct_responses = []
- incorrect_responses = ["0*1e999"] # right now this evaluates to 'nan' for a given x
+ # right now these evaluate to 'nan'
+ incorrect_responses = ["0*1e999", "4 + 0*1e999"]
self.assert_multiple_grade(problem, correct_responses, incorrect_responses)
def test_grade_with_script(self):
diff --git a/common/lib/capa/capa/util.py b/common/lib/capa/capa/util.py
index c219a7b5f6..4663b388c2 100644
--- a/common/lib/capa/capa/util.py
+++ b/common/lib/capa/capa/util.py
@@ -23,10 +23,12 @@ def compare_with_tolerance(v1, v2, tol):
tolerance = evaluator(dict(), dict(), tol)
if isinf(v1) or isinf(v2):
- return v1 == v2 # because the other numerical comparison does not work with infinities
+ # because the other numerical comparison does not work with infinities
+ return v1 == v2
else:
return abs(v1 - v2) <= tolerance
+
def contextualize_text(text, context): # private
''' Takes a string with variables. E.g. $a+$b.
Does a substitution of those variables from the context '''
@@ -55,7 +57,8 @@ def convert_files_to_filenames(answers):
new_answers = dict()
for answer_id in answers.keys():
answer = answers[answer_id]
- if is_list_of_files(answer): # Files are stored as a list, even if one file
+ # Files are stored as a list, even if one file
+ if is_list_of_files(answer):
new_answers[answer_id] = [f.name for f in answer]
else:
new_answers[answer_id] = answers[answer_id]
From e03585d85c463cafbe1fcb6daa4fd3495230de8d Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Mon, 13 May 2013 11:07:08 -0400
Subject: [PATCH 56/65] pylint: refactor imports
---
cms/djangoapps/contentstore/views/__init__.py | 30 ++++++++-----
cms/djangoapps/contentstore/views/course.py | 45 ++++++++++++-------
2 files changed, 48 insertions(+), 27 deletions(-)
diff --git a/cms/djangoapps/contentstore/views/__init__.py b/cms/djangoapps/contentstore/views/__init__.py
index d31102c58e..0da8a15707 100644
--- a/cms/djangoapps/contentstore/views/__init__.py
+++ b/cms/djangoapps/contentstore/views/__init__.py
@@ -1,11 +1,19 @@
-from assets import *
-from checklist import *
-from component import *
-from course import *
-from error import *
-from item import *
-from preview import *
-from public import *
-from user import *
-from tabs import *
-from requests import *
+# pylint: disable=W0401, W0511
+
+# TODO: component.py should explicitly enumerate exports with __all__
+from .component import *
+
+# TODO: course.py should explicitly enumerate exports with __all__
+from .course import *
+
+# Disable warnings about import from wildcard
+# All files below declare exports with __all__
+from .assets import *
+from .checklist import *
+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/course.py b/cms/djangoapps/contentstore/views/course.py
index f1414faf4e..f71759e410 100644
--- a/cms/djangoapps/contentstore/views/course.py
+++ b/cms/djangoapps/contentstore/views/course.py
@@ -10,12 +10,16 @@ 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.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 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 auth.authz import create_all_course_groups
@@ -24,7 +28,8 @@ 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
+from component import OPEN_ENDED_COMPONENT_TYPES, \
+ ADVANCED_COMPONENT_POLICY_KEY
# TODO: should explicitly enumerate exports with __all__
@@ -325,18 +330,23 @@ def course_advanced_updates(request, org, course, name):
real_method = get_request_method(request)
if real_method == 'GET':
- return HttpResponse(json.dumps(CourseMetadata.fetch(location)), mimetype="application/json")
+ 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))),
+ 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
+ # 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
+ # 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.
+ # 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
@@ -346,7 +356,8 @@ def course_advanced_updates(request, org, course, name):
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 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
@@ -357,11 +368,13 @@ def course_advanced_updates(request, org, course, name):
#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
+ # 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
+ # 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))
+ response_json = json.dumps(CourseMetadata.update_from_json(location,
+ request_body,
+ filter_tabs=filter_tabs))
return HttpResponse(response_json, mimetype="application/json")
From 4d197e10ec3f25225e4b8b02e78d52c5b2d27d56 Mon Sep 17 00:00:00 2001
From: Chris Dodge
Date: Mon, 13 May 2013 11:10:11 -0400
Subject: [PATCH 57/65] fix some violations. Build up karma
---
.../contentstore/tests/test_contentstore.py | 3 +-
.../contentstore/tests/test_course_updates.py | 75 +++++++++----------
.../contentstore/tests/test_i18n.py | 29 ++++---
3 files changed, 52 insertions(+), 55 deletions(-)
diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index 927df009bf..c11b350349 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -226,7 +226,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
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)
+ self.assertGreater(len(course.textbooks), 0)
def test_static_tab_reordering(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
@@ -301,7 +301,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# make sure the parent no longer points to the child object which was deleted
self.assertFalse(sequential.location.url() in chapter.children)
-
def test_about_overrides(self):
'''
This test case verifies that a course can use specialized override for about data, e.g. /about/Fall_2012/effort.html
diff --git a/cms/djangoapps/contentstore/tests/test_course_updates.py b/cms/djangoapps/contentstore/tests/test_course_updates.py
index 80d4f0bbc2..ae14555b32 100644
--- a/cms/djangoapps/contentstore/tests/test_course_updates.py
+++ b/cms/djangoapps/contentstore/tests/test_course_updates.py
@@ -10,9 +10,9 @@ class CourseUpdateTest(CourseTestCase):
'''Go through each interface and ensure it works.'''
# first get the update to force the creation
url = reverse('course_info',
- kwargs={'org': self.course_location.org,
- 'course': self.course_location.course,
- 'name': self.course_location.name})
+ kwargs={'org': self.course_location.org,
+ 'course': self.course_location.course,
+ 'name': self.course_location.name})
self.client.get(url)
init_content = '