Commit 13fe9c11 by cahrens

Merged with master.

parent 1c8cef32
......@@ -8,6 +8,7 @@
:2e#
.AppleDouble
database.sqlite
private-requirements.txt
courseware/static/js/mathjax/*
flushdb.sh
build
......
......@@ -264,13 +264,13 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
def test_delete(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct')
direct_store = modulestore('direct')
sequential = module_store.get_item(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None]))
sequential = direct_store.get_item(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None]))
chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter', 'Week_1', None]))
chapter = direct_store.get_item(Location(['i4x', 'edX', 'full', 'chapter', 'Week_1', None]))
# make sure the parent no longer points to the child object which was deleted
# make sure the parent points to the child object which is to be deleted
self.assertTrue(sequential.location.url() in chapter.children)
self.client.post(
......@@ -281,18 +281,19 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
found = False
try:
module_store.get_item(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None]))
direct_store.get_item(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None]))
found = True
except ItemNotFoundError:
pass
self.assertFalse(found)
chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter', 'Week_1', None]))
chapter = direct_store.get_item(Location(['i4x', 'edX', 'full', 'chapter', 'Week_1', None]))
# 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
......
......@@ -615,25 +615,14 @@ def delete_item(request):
delete_children = request.POST.get('delete_children', False)
delete_all_versions = request.POST.get('delete_all_versions', False)
item = modulestore().get_item(item_location)
store = modulestore()
store = get_modulestore(item_loc)
# @TODO: this probably leaves draft items dangling. My preferance would be for the semantic to be
# if item.location.revision=None, then delete both draft and published version
# if caller wants to only delete the draft than the caller should put item.location.revision='draft'
item = store.get_item(item_location)
if delete_children:
_xmodule_recurse(item, lambda i: store.delete_item(i.location))
_xmodule_recurse(item, lambda i: store.delete_item(i.location, delete_all_versions))
else:
store.delete_item(item.location)
# cdodge: this is a bit of a hack until I can talk with Cale about the
# semantics of delete_item whereby the store is draft aware. Right now calling
# delete_item on a vertical tries to delete the draft version leaving the
# requested delete to never occur
if item.location.revision is None and item.location.category == 'vertical' and delete_all_versions:
modulestore('direct').delete_item(item.location)
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:
......@@ -1494,6 +1483,12 @@ def create_new_course(request):
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
......
......@@ -30,7 +30,7 @@
<body class="<%block name='bodyclass'></%block> hide-wip">
<%include file="courseware_vendor_js.html"/>
<script type="text/javascript" src="${static.url('jsi18n/')}"></script>
<script type="text/javascript" src="jsi18n/"></script>
<script type="text/javascript" src="${static.url('js/vendor/json2.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/backbone-min.js')}"></script>
......
......@@ -179,7 +179,7 @@ from contentstore import utils
<li class="field text" id="field-course-overview">
<label for="course-overview">Course Overview</label>
<textarea class="tinymce text-editor" id="course-overview"></textarea>
<span class="tip tip-stacked">Introductions, prerequisites, FAQs that are used on <a class="link-courseURL" rel="external" href="${utils.get_lms_link_for_about_page(course_location)}">your course summary page</a></span>
<span class="tip tip-stacked">Introductions, prerequisites, FAQs that are used on <a class="link-courseURL" rel="external" href="${utils.get_lms_link_for_about_page(course_location)}">your course summary page</a> (formatted in HTML)</span>
</li>
<li class="field video" id="field-course-introduction-video">
......
......@@ -24,7 +24,9 @@ default_functions = {'sin': numpy.sin,
'arccos': numpy.arccos,
'arcsin': numpy.arcsin,
'arctan': numpy.arctan,
'abs': numpy.abs
'abs': numpy.abs,
'fact': math.factorial,
'factorial': math.factorial
}
default_variables = {'j': numpy.complex(0, 1),
'e': numpy.e,
......@@ -112,18 +114,18 @@ def evaluator(variables, functions, string, cs=False):
return float('nan')
ops = {"^": operator.pow,
"*": operator.mul,
"/": operator.truediv,
"+": operator.add,
"-": operator.sub,
}
"*": operator.mul,
"/": operator.truediv,
"+": operator.add,
"-": operator.sub,
}
# We eliminated extreme ones, since they're rarely used, and potentially
# confusing. They may also conflict with variables if we ever allow e.g.
# 5R instead of 5*R
suffixes = {'%': 0.01, 'k': 1e3, 'M': 1e6, 'G': 1e9,
'T': 1e12, # 'P':1e15,'E':1e18,'Z':1e21,'Y':1e24,
'c': 1e-2, 'm': 1e-3, 'u': 1e-6,
'n': 1e-9, 'p': 1e-12} # ,'f':1e-15,'a':1e-18,'z':1e-21,'y':1e-24}
'T': 1e12, # 'P':1e15,'E':1e18,'Z':1e21,'Y':1e24,
'c': 1e-2, 'm': 1e-3, 'u': 1e-6,
'n': 1e-9, 'p': 1e-12} # ,'f':1e-15,'a':1e-18,'z':1e-21,'y':1e-24}
def super_float(text):
''' Like float, but with si extensions. 1k goes to 1000'''
......@@ -246,4 +248,9 @@ if __name__ == '__main__':
print evaluator({}, {}, "5+1*j")
print evaluator({}, {}, "j||1")
print evaluator({}, {}, "e^(j*pi)")
print evaluator({}, {}, "5+7 QWSEKO")
print evaluator({}, {}, "fact(5)")
print evaluator({}, {}, "factorial(5)")
try:
print evaluator({}, {}, "5+7 QWSEKO")
except UndefinedVariable:
print "Successfully caught undefined variable"
......@@ -7,6 +7,7 @@ from datetime import datetime
import json
from nose.plugins.skip import SkipTest
import os
import random
import unittest
import textwrap
......@@ -14,7 +15,7 @@ from . import test_system
import capa.capa_problem as lcp
from capa.responsetypes import LoncapaProblemError, \
StudentInputError, ResponseError
StudentInputError, ResponseError
from capa.correctmap import CorrectMap
from capa.util import convert_files_to_filenames
from capa.xqueue_interface import dateformat
......@@ -33,10 +34,13 @@ class ResponseTest(unittest.TestCase):
xml = self.xml_factory.build_xml(**kwargs)
return lcp.LoncapaProblem(xml, '1', system=test_system)
def assert_grade(self, problem, submission, expected_correctness):
def assert_grade(self, problem, submission, expected_correctness, msg=None):
input_dict = {'1_2_1': submission}
correct_map = problem.grade_answers(input_dict)
self.assertEquals(correct_map.get_correctness('1_2_1'), expected_correctness)
if msg is None:
self.assertEquals(correct_map.get_correctness('1_2_1'), expected_correctness)
else:
self.assertEquals(correct_map.get_correctness('1_2_1'), expected_correctness, msg)
def assert_answer_format(self, problem):
answers = problem.get_question_answers()
......@@ -357,6 +361,83 @@ class FormulaResponseTest(ResponseTest):
self.assert_grade(problem, '2*x', 'correct')
self.assert_grade(problem, '3*x', 'incorrect')
def test_parallel_resistors(self):
"""Test parallel resistors"""
sample_dict = {'R1': (10, 10), 'R2': (2, 2), 'R3': (5, 5), 'R4': (1, 1)}
# Test problem
problem = self.build_problem(sample_dict=sample_dict,
num_samples=10,
tolerance=0.01,
answer="R1||R2")
# Expect answer to be marked correct
input_formula = "R1||R2"
self.assert_grade(problem, input_formula, "correct")
# Expect random number to be marked incorrect
input_formula = "13"
self.assert_grade(problem, input_formula, "incorrect")
# Expect incorrect answer marked incorrect
input_formula = "R3||R4"
self.assert_grade(problem, input_formula, "incorrect")
def test_default_variables(self):
"""Test the default variables provided in common/lib/capa/capa/calc.py"""
# which are: j (complex number), e, pi, k, c, T, q
# Sample x in the range [-10,10]
sample_dict = {'x': (-10, 10)}
default_variables = [('j', 2, 3), ('e', 2, 3), ('pi', 2, 3), ('c', 2, 3), ('T', 2, 3),
('k', 2 * 10 ** 23, 3 * 10 ** 23), # note k = scipy.constants.k = 1.3806488e-23
('q', 2 * 10 ** 19, 3 * 10 ** 19)] # note k = scipy.constants.e = 1.602176565e-19
for (var, cscalar, iscalar) in default_variables:
# The expected solution is numerically equivalent to cscalar*var
correct = '{0}*x*{1}'.format(cscalar, var)
incorrect = '{0}*x*{1}'.format(iscalar, var)
problem = self.build_problem(sample_dict=sample_dict,
num_samples=10,
tolerance=0.01,
answer=correct)
# Expect that the inputs are graded correctly
self.assert_grade(problem, correct, 'correct',
msg="Failed on variable {0}; the given, correct answer was {1} but graded 'incorrect'".format(var, correct))
self.assert_grade(problem, incorrect, 'incorrect',
msg="Failed on variable {0}; the given, incorrect answer was {1} but graded 'correct'".format(var, incorrect))
def test_default_functions(self):
"""Test the default functions provided in common/lib/capa/capa/calc.py"""
# which are: sin, cos, tan, sqrt, log10, log2, ln,
# arccos, arcsin, arctan, abs,
# fact, factorial
w = random.randint(3, 10)
sample_dict = {'x': (-10, 10), # Sample x in the range [-10,10]
'y': (1, 10), # Sample y in the range [1,10] - logs, arccos need positive inputs
'z': (-1, 1), # Sample z in the range [1,10] - for arcsin, arctan
'w': (w, w)} # Sample w is a random, positive integer - factorial needs a positive, integer input,
# and the way formularesponse is defined, we can only specify a float range
default_functions = [('sin', 2, 3, 'x'), ('cos', 2, 3, 'x'), ('tan', 2, 3, 'x'), ('sqrt', 2, 3, 'y'), ('log10', 2, 3, 'y'),
('log2', 2, 3, 'y'), ('ln', 2, 3, 'y'), ('arccos', 2, 3, 'z'), ('arcsin', 2, 3, 'z'), ('arctan', 2, 3, 'x'),
('abs', 2, 3, 'x'), ('fact', 2, 3, 'w'), ('factorial', 2, 3, 'w')]
for (func, cscalar, iscalar, var) in default_functions:
print 'func is: {0}'.format(func)
# The expected solution is numerically equivalent to cscalar*func(var)
correct = '{0}*{1}({2})'.format(cscalar, func, var)
incorrect = '{0}*{1}({2})'.format(iscalar, func, var)
problem = self.build_problem(sample_dict=sample_dict,
num_samples=10,
tolerance=0.01,
answer=correct)
# Expect that the inputs are graded correctly
self.assert_grade(problem, correct, 'correct',
msg="Failed on function {0}; the given, correct answer was {1} but graded 'incorrect'".format(func, correct))
self.assert_grade(problem, incorrect, 'incorrect',
msg="Failed on function {0}; the given, incorrect answer was {1} but graded 'correct'".format(func, incorrect))
class StringResponseTest(ResponseTest):
from response_xml_factory import StringResponseXMLFactory
......@@ -904,14 +985,13 @@ class CustomResponseTest(ResponseTest):
with self.assertRaises(ResponseError):
problem.grade_answers({'1_2_1': '42'})
def test_module_imports_inline(self):
'''
Check that the correct modules are available to custom
response scripts
'''
for module_name in ['random', 'numpy', 'math', 'scipy',
for module_name in ['random', 'numpy', 'math', 'scipy',
'calc', 'eia', 'chemcalc', 'chemtools',
'miller', 'draganddrop']:
......@@ -921,26 +1001,25 @@ class CustomResponseTest(ResponseTest):
script = textwrap.dedent('''
correct[0] = 'correct'
assert('%s' in globals())''' % module_name)
# Create the problem
problem = self.build_problem(answer=script)
# Expect that we can grade an answer without
# Expect that we can grade an answer without
# getting an exception
try:
problem.grade_answers({'1_2_1': '42'})
except ResponseError:
self.fail("Could not use name '%s' in custom response"
% module_name)
self.fail("Could not use name '{0}s' in custom response".format(module_name))
def test_module_imports_function(self):
'''
Check that the correct modules are available to custom
response scripts
'''
for module_name in ['random', 'numpy', 'math', 'scipy',
for module_name in ['random', 'numpy', 'math', 'scipy',
'calc', 'eia', 'chemcalc', 'chemtools',
'miller', 'draganddrop']:
......@@ -951,18 +1030,17 @@ class CustomResponseTest(ResponseTest):
def check_func(expect, answer_given):
assert('%s' in globals())
return True''' % module_name)
# Create the problem
problem = self.build_problem(script=script, cfn="check_func")
# Expect that we can grade an answer without
# Expect that we can grade an answer without
# getting an exception
try:
problem.grade_answers({'1_2_1': '42'})
except ResponseError:
self.fail("Could not use name '%s' in custom response"
% module_name)
self.fail("Could not use name '{0}s' in custom response".format(module_name))
class SchematicResponseTest(ResponseTest):
......
......@@ -13,6 +13,12 @@ def as_draft(location):
"""
return Location(location)._replace(revision=DRAFT)
def as_published(location):
"""
Returns the Location that is the published version for `location`
"""
return Location(location)._replace(revision=None)
def wrap_draft(item):
"""
......@@ -159,13 +165,17 @@ class DraftModuleStore(ModuleStoreBase):
return super(DraftModuleStore, self).update_metadata(draft_loc, metadata)
def delete_item(self, location):
def delete_item(self, location, delete_all_versions=False):
"""
Delete an item from this modulestore
location: Something that can be passed to Location
"""
return super(DraftModuleStore, self).delete_item(as_draft(location))
super(DraftModuleStore, self).delete_item(as_draft(location))
if delete_all_versions:
super(DraftModuleStore, self).delete_item(as_published(location))
return
def get_parent_locations(self, location, course_id):
'''Find all locations that are the parents of this location. Needed
......
......@@ -694,11 +694,12 @@ class MongoModuleStore(ModuleStoreBase):
self.refresh_cached_metadata_inheritance_tree(loc)
self.fire_updated_modulestore_signal(get_course_id_no_run(Location(location)), Location(location))
def delete_item(self, location):
def delete_item(self, location, delete_all_versions=False):
"""
Delete an item from this modulestore
location: Something that can be passed to Location
delete_all_versions: is here because the DraftMongoModuleStore needs it and we need to keep the interface the same. It is unused.
"""
# VS[compat] cdodge: This is a hack because static_tabs also have references from the course module, so
# if we add one then we need to also add it to the policy information (i.e. metadata)
......
......@@ -13,10 +13,19 @@ def clone_course(modulestore, contentstore, source_location, dest_location, dele
if not modulestore.has_item(dest_location):
raise Exception("An empty course at {0} must have already been created. Aborting...".format(dest_location))
# verify that the dest_location really is an empty course, which means only one
# verify that the dest_location really is an empty course, which means only one with an optional 'overview'
dest_modules = modulestore.get_items([dest_location.tag, dest_location.org, dest_location.course, None, None, None])
if len(dest_modules) != 1:
basically_empty = True
for module in dest_modules:
if module.location.category == 'course' or (module.location.category == 'about'
and module.location.name == 'overview'):
continue
basically_empty = False
break
if not basically_empty:
raise Exception("Course at destination {0} is not an empty course. You can only clone into an empty course. Aborting...".format(dest_location))
# check to see if the source course is actually there
......
---
metadata:
display_name: overview
data: |
<section class="about">
<h2>About This Course</h2>
<p>Include your long course description here. The long course description should contain 150-400 words.</p>
<p>This is paragraph 2 of the long course description. Add more paragraphs as needed. Make sure to enclose them in paragraph tags.</p>
</section>
<section class="prerequisites">
<h2>Prerequisites</h2>
<p>Add information about course prerequisites here.</p>
</section>
<section class="course-staff">
<h2>Course Staff</h2>
<article class="teacher">
<div class="teacher-image">
<img src="/static/images/pl-faculty.png" align="left" style="margin:0 20 px 0">
</div>
<h3>Staff Member #1</h3>
<p>Biography of instructor/staff member #1</p>
</article>
<article class="teacher">
<div class="teacher-image">
<img src="/static/images/pl-faculty.png" align="left" style="margin:0 20 px 0">
</div>
<h3>Staff Member #2</h3>
<p>Biography of instructor/staff member #2</p>
</article>
</section>
<section class="faq">
<section class="responses">
<h2>Frequently Asked Questions</h2>
<article class="response">
<h3>Do I need to buy a textbook?</h3>
<p>No, a free online version of Chemistry: Principles, Patterns, and Applications, First Edition by Bruce Averill and Patricia Eldredge will be available, though you can purchase a printed version (published by FlatWorld Knowledge) if you’d like.</p>
</article>
<article class="response">
<h3>Question #2</h3>
<p>Your answer would be displayed here.</p>
</article>
</section>
</section>
children: []
......@@ -188,7 +188,7 @@ data: |
<p>
Include image by using the edXxml macro: </p>
<p>
<img src="http://autoid.mit.edu/images/mit_dome.jpg"/>
<img src="/static/images/mit_dome.jpg"/>
</p>
<p>
<h4>Example show/hide explanation</h4>
......
{"locales" : ["en", "fr", "de"]}
{"locales" : ["en"]}
# Python libraries to install directly from github
# Third-party:
-e git://github.com/MITx/django-staticfiles.git@6d2504e5c8#egg=django-staticfiles
-e git://github.com/MITx/django-pipeline.git#egg=django-pipeline
-e git://github.com/MITx/django-wiki.git@e2e84558#egg=django-wiki
-e git://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev
# Our libraries:
-e git+https://github.com/edx/XBlock.git@5ce6f70a#egg=XBlock
......@@ -86,7 +86,7 @@ class Dummy (Converter):
def init_msgs(self, msgs):
"""
Make sure the first msg in msgs has a plural property.
msgs is list of instances of pofile.Msg
msgs is list of instances of polib.POEntry
"""
if len(msgs)==0:
return
......@@ -100,8 +100,8 @@ class Dummy (Converter):
def convert_msg(self, msg):
"""
Takes one Msg object and converts it (adds a dummy translation to it)
msg is an instance of pofile.Msg
Takes one POEntry object and converts it (adds a dummy translation to it)
msg is an instance of polib.POEntry
"""
source = msg.msgid
if len(source)==0:
......
import os, subprocess, logging, json
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():
"""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
# Run this after defining messages_dir and get_logger, because it depends on these.
init_module()
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.
"""
log.info(command)
subprocess.call(command.split(' '), cwd=working_directory)
def get_config():
"""Returns data found in config file, or returns None if file not found"""
config_path = os.path.abspath(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):
os.makedirs(dirname)
def remove_file(filename, log=LOG, verbose=True):
"""
Attempt to delete filename.
Log a warning if file does not exist.
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))
if not os.path.exists(filename):
log.warn("File does not exist: %s" % os.path.relpath(filename, BASE_DIR))
else:
os.remove(filename)
#!/usr/bin/python
"""
See https://edx-wiki.atlassian.net/wiki/display/ENG/PO+File+workflow
This task extracts all English strings from all source code
and produces three human-readable files:
conf/locale/en/LC_MESSAGES/django-partial.po
conf/locale/en/LC_MESSAGES/djangojs.po
conf/locale/en/LC_MESSAGES/mako.po
This task will clobber any existing django.po file.
This is because django-admin.py makemessages hardcodes this filename
and it cannot be overridden.
"""
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
# 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)
# 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)
def main ():
create_dir_if_necessary(LOCALE_DIR)
generated_files = ('django-partial.po', 'djangojs.po', 'mako.po')
for filename in generated_files:
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)
# Extract strings from django source files
make_django_cmd = 'django-admin.py makemessages -l en --ignore=src/* --ignore=i18n/* ' \
+ '--extension html'
# Extract strings from javascript source files
make_djangojs_cmd = 'django-admin.py makemessages -l en -d djangojs --ignore=src/* ' \
+ '--ignore=i18n/* --extension js'
execute(babel_mako_cmd, working_directory=BASE_DIR)
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'))
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))
# replace default headers with edX headers
fix_header(po)
# replace default metadata with edX metadata
fix_metadata(po)
# remove key strings which belong in messages.po
strip_key_strings(po)
po.save()
# By default, django-admin.py makemessages creates this header:
"""
SOME DESCRIPTIVE TITLE.
Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
This file is distributed under the same license as the PACKAGE package.
FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
"""
def fix_header(po):
"""
Replace default headers with edX headers
"""
header = po.header
fixes = (
('SOME DESCRIPTIVE TITLE', 'edX translation file'),
('Translations template for PROJECT.', 'edX translation file'),
('YEAR', '%s' % datetime.utcnow().year),
('ORGANIZATION', 'edX'),
("THE PACKAGE'S COPYRIGHT HOLDER", "EdX"),
('This file is distributed under the same license as the PROJECT project.',
'This file is distributed under the GNU AFFERO GENERAL PUBLIC LICENSE.'),
('This file is distributed under the same license as the PACKAGE package.',
'This file is distributed under the GNU AFFERO GENERAL PUBLIC LICENSE.'),
('FIRST AUTHOR <EMAIL@ADDRESS>',
'EdX Team <info@edx.org>')
)
for (src, dest) in fixes:
header = header.replace(src, dest)
po.header = header
# By default, django-admin.py makemessages creates this metadata:
"""
{u'PO-Revision-Date': u'YEAR-MO-DA HO:MI+ZONE',
u'Language': u'',
u'Content-Transfer-Encoding': u'8bit',
u'Project-Id-Version': u'PACKAGE VERSION',
u'Report-Msgid-Bugs-To': u'',
u'Last-Translator': u'FULL NAME <EMAIL@ADDRESS>',
u'Language-Team': u'LANGUAGE <LL@li.org>',
u'POT-Creation-Date': u'2013-04-25 14:14-0400',
u'Content-Type': u'text/plain; charset=UTF-8',
u'MIME-Version': u'1.0'}
"""
def fix_metadata(po):
"""
Replace default metadata with edX metadata
"""
fixes = {'PO-Revision-Date': datetime.utcnow(),
'Report-Msgid-Bugs-To': 'translation_team@edx.org',
'Project-Id-Version': '0.1a',
'Language' : 'en',
'Language-Team': 'translation team <translation_team@edx.org>',
}
if po.metadata.has_key('Last-Translator'):
del po.metadata['Last-Translator']
po.metadata.update(fixes)
def strip_key_strings(po):
"""
Removes all entries in PO which are key strings.
These entries should appear only in messages.po, not in any other po files.
"""
newlist = [entry for entry in po if not is_key_string(entry.msgid)]
del po[:]
po += newlist
def is_key_string(string):
"""
returns True if string is a key string.
Key strings begin with underscore.
"""
return len(string)>1 and string[0]=='_'
if __name__ == '__main__':
main()
#!/usr/bin/python
"""
See https://edx-wiki.atlassian.net/wiki/display/ENG/PO+File+workflow
This task merges and compiles the human-readable .pofiles on the
local filesystem into machine-readable .mofiles. This is typically
necessary as part of the build process since these .mofiles are
needed by Django when serving the web app.
The configuration file (in mitx/conf/locale/config) specifies which
languages to generate.
"""
import os
from execute import execute, get_config, messages_dir, remove_file, \
BASE_DIR, LOG, SOURCE_LOCALE
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)
files_to_merge = ('django-partial.po', 'messages.po', 'mako.po')
validate_files(locale_directory, files_to_merge)
# merged file is merged.po
merge_cmd = 'msgcat -o merged.po ' + ' '.join(files_to_merge)
execute(merge_cmd, working_directory=locale_directory)
# rename merged.po -> django.po (default)
merged_filename = os.path.join(locale_directory, 'merged.po')
django_filename = os.path.join(locale_directory, target)
os.rename(merged_filename, django_filename) # can't overwrite file on Windows
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.
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))
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:
merge(locale)
compile_cmd = 'django-admin.py compilemessages'
execute(compile_cmd, working_directory=BASE_DIR)
if __name__ == '__main__':
main()
......@@ -18,20 +18,12 @@
import os, sys
import polib
from dummy import Dummy
from execute import create_dir_if_necessary
# 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'.
OUT_LANG = 'fr'
def main(file):
def main(file, locale):
"""
Takes a source po file, reads it, and writes out a new po file
containing a dummy translation.
in :param locale: containing a dummy translation.
"""
if not os.path.exists(file):
raise IOError('File does not exist: %s' % file)
......@@ -40,29 +32,36 @@ def main(file):
converter.init_msgs(pofile.translated_entries())
for msg in pofile:
converter.convert_msg(msg)
new_file = new_filename(file, OUT_LANG)
new_file = new_filename(file, locale)
create_dir_if_necessary(new_file)
pofile.save(new_file)
def new_filename(original_filename, new_lang):
"""Returns a filename derived from original_filename, using new_lang as the locale"""
def new_filename(original_filename, new_locale):
"""Returns a filename derived from original_filename, using new_locale as the locale"""
orig_dir = os.path.dirname(original_filename)
msgs_dir = os.path.basename(orig_dir)
orig_file = os.path.basename(original_filename)
return '%s/%s/%s/%s' % (os.path.abspath(orig_dir + '/../..'),
new_lang,
msgs_dir,
orig_file)
return os.path.join(orig_dir,
'/../..',
new_locale,
msgs_dir,
orig_file)
def create_dir_if_necessary(pathname):
dirname = os.path.dirname(pathname)
if not os.path.exists(dirname):
os.makedirs(dirname)
# 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__':
if len(sys.argv)<2:
raise Exception("missing file argument")
main(sys.argv[1])
if len(sys.argv)<2:
locale = DEFAULT_LOCALE
else:
locale = sys.argv[2]
main(sys.argv[1], locale)
from test_extract import TestExtract
from test_generate import TestGenerate
from test_converter import TestConverter
from test_dummy import TestDummy
import os
from unittest import TestCase
import converter
class UpcaseConverter (converter.Converter):
"""
Converts a string to uppercase. Just used for testing.
"""
def inner_convert_string(self, string):
return string.upper()
class TestConverter(TestCase):
"""
Tests functionality of i18n/converter.py
"""
def test_converter(self):
"""
Tests with a simple converter (converts strings to uppercase).
Assert that embedded HTML and python tags are not converted.
"""
c = UpcaseConverter()
test_cases = (
# no tags
('big bad wolf', 'BIG BAD WOLF'),
# one html tag
('big <strong>bad</strong> wolf', 'BIG <strong>BAD</strong> WOLF'),
# two html tags
('big <b>bad</b> <i>wolf</i>', 'BIG <b>BAD</b> <i>WOLF</i>'),
# one python tag
('big %(adjective)s wolf', 'BIG %(adjective)s WOLF'),
# two python tags
('big %(adjective)s %(noun)s', 'BIG %(adjective)s %(noun)s'),
# both kinds of tags
('<strong>big</strong> %(adjective)s %(noun)s',
'<strong>BIG</strong> %(adjective)s %(noun)s'),
)
for (source, expected) in test_cases:
result = c.convert(source)
self.assertEquals(result, expected)
import os, string, random
from unittest import TestCase
from polib import POEntry
import dummy
class TestDummy(TestCase):
"""
Tests functionality of i18n/dummy.py
"""
def setUp(self):
self.converter = dummy.Dummy()
def test_dummy(self):
"""
Tests with a dummy converter (adds spurious accents to strings).
Assert that embedded HTML and python tags are not converted.
"""
test_cases = (("hello my name is Bond, James Bond",
u'h\xe9ll\xf6 my n\xe4m\xe9 \xefs B\xf6nd, J\xe4m\xe9s B\xf6nd Lorem i#'),
('don\'t convert <a href="href">tag ids</a>',
u'd\xf6n\'t \xe7\xf6nv\xe9rt <a href="href">t\xe4g \xefds</a> Lorem ipsu#'),
('don\'t convert %(name)s tags on %(date)s',
u"d\xf6n't \xe7\xf6nv\xe9rt %(name)s t\xe4gs \xf6n %(date)s Lorem ips#")
)
for (source, expected) in test_cases:
result = self.converter.convert(source)
self.assertEquals(result, expected)
def test_singular(self):
entry = POEntry()
entry.msgid = 'A lovely day for a cup of tea.'
expected = u'\xc0 l\xf6v\xe9ly d\xe4y f\xf6r \xe4 \xe7\xfcp \xf6f t\xe9\xe4. Lorem i#'
self.converter.convert_msg(entry)
self.assertEquals(entry.msgstr, expected)
def test_plural(self):
entry = POEntry()
entry.msgid = 'A lovely day for a cup of tea.'
entry.msgid_plural = 'A lovely day for some cups of tea.'
expected_s = u'\xc0 l\xf6v\xe9ly d\xe4y f\xf6r \xe4 \xe7\xfcp \xf6f t\xe9\xe4. Lorem i#'
expected_p = u'\xc0 l\xf6v\xe9ly d\xe4y f\xf6r s\xf6m\xe9 \xe7\xfcps \xf6f t\xe9\xe4. Lorem ip#'
self.converter.convert_msg(entry)
result = entry.msgstr_plural
self.assertEquals(result['0'], expected_s)
self.assertEquals(result['1'], expected_p)
import os, polib
from unittest import TestCase
from nose.plugins.skip import SkipTest
from datetime import datetime, timedelta
import extract
from execute import SOURCE_MSGS_DIR
# Make sure setup runs only once
SETUP_HAS_RUN = False
class TestExtract(TestCase):
"""
Tests functionality of i18n/extract.py
"""
generated_files = ('django-partial.po', 'djangojs.po', 'mako.po')
def setUp(self):
# Skip this test because it takes too long (>1 minute)
# TODO: figure out how to declare a "long-running" test suite
# and add this test to it.
raise SkipTest()
global SETUP_HAS_RUN
# 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)
super(TestExtract, self).setUp()
if not SETUP_HAS_RUN:
# Run extraction script. Warning, this takes 1 minute or more
extract.main()
SETUP_HAS_RUN = True
def get_files (self):
"""
This is a generator.
Returns the fully expanded filenames for all extracted files
Fails assertion if one of the files doesn't exist.
"""
for filename in self.generated_files:
path = os.path.join(SOURCE_MSGS_DIR, filename)
exists = os.path.exists(path)
self.assertTrue(exists, msg='Missing file: %s' % filename)
if exists:
yield path
def test_files(self):
"""
Asserts that each auto-generated file has been modified since 'extract' was launched.
Intended to show that the file has been touched by 'extract'.
"""
for path in self.get_files():
self.assertTrue(datetime.fromtimestamp(os.path.getmtime(path)) > self.start_time,
msg='File not recently modified: %s' % os.path.basename(path))
def test_is_keystring(self):
"""
Verifies is_keystring predicate
"""
entry1 = polib.POEntry()
entry2 = polib.POEntry()
entry1.msgid = "_.lms.admin.warning.keystring"
entry2.msgid = "This is not a keystring"
self.assertTrue(extract.is_key_string(entry1.msgid))
self.assertFalse(extract.is_key_string(entry2.msgid))
def test_headers(self):
"""Verify all headers have been modified"""
for path in self.get_files():
po = polib.pofile(path)
header = po.header
self.assertEqual(header.find('edX translation file'), 0,
msg='Missing header in %s:\n"%s"' % \
(os.path.basename(path), header))
def test_metadata(self):
"""Verify all metadata has been modified"""
for path in self.get_files():
po = polib.pofile(path)
metadata = po.metadata
value = metadata['Report-Msgid-Bugs-To']
expected = 'translation_team@edx.org'
self.assertEquals(expected, value)
import os, string, random
from unittest import TestCase
from datetime import datetime, timedelta
import generate
from execute import get_config, messages_dir, SOURCE_MSGS_DIR, SOURCE_LOCALE
class TestGenerate(TestCase):
"""
Tests functionality of i18n/generate.py
"""
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)
self.assertTrue(os.path.exists(filename))
os.remove(filename)
def test_main(self):
"""
Runs generate.main() which should merge source files,
then compile all sources in all configured languages.
Validates output by checking all .mo files in all configured languages.
.mo files should exist, and be recently created (modified
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)
exists = os.path.exists(path)
self.assertTrue(exists, msg='Missing file in locale %s: %s' % (locale, filename))
self.assertTrue(datetime.fromtimestamp(os.path.getmtime(path)) >= self.start_time,
msg='File not recently modified: %s' % path)
def random_name(size=6):
"""Returns random filename as string, like test-4BZ81W"""
chars = string.ascii_uppercase + string.digits
return 'test-' + ''.join(random.choice(chars) for x in range(size))
#!/usr/bin/python
import os, subprocess, logging, json
from make_dummy import create_dir_if_necessary, main as dummy_main
'''
Generate or update all translation files
Usage:
$ update.py
1. extracts files from mako templates
2. extracts files from django templates and python source files
3. extracts files from django javascript files
4. generates dummy text translations
5. compiles po files to mo files
Configuration (e.g. known languages) declared in mitx/conf/locale/config
'''
# -----------------------------------
# BASE_DIR is the working directory to execute django-admin commands from.
# Typically this should be the 'mitx' directory.
BASE_DIR = os.path.abspath(os.path.dirname(os.path.abspath(__file__))+'/..')
# LOCALE_DIR contains the locale files.
# Typically this should be 'mitx/conf/locale'
LOCALE_DIR = BASE_DIR + '/conf/locale'
# MSGS_DIR contains the English po files
MSGS_DIR = LOCALE_DIR + '/en/LC_MESSAGES'
# CONFIG_FILENAME contains localization configuration in json format
CONFIG_FILENAME = LOCALE_DIR + '/config'
# BABEL_CONFIG contains declarations for Babel to extract strings from mako template files
BABEL_CONFIG = LOCALE_DIR + '/babel.cfg'
# Strings from mako template files are written to BABEL_OUT
BABEL_OUT = MSGS_DIR + '/mako.po'
# These are the shell commands invoked by main()
COMMANDS = {
'babel_mako': 'pybabel extract -F %s -c "TRANSLATORS:" . -o %s' % (BABEL_CONFIG, BABEL_OUT),
'make_django': 'django-admin.py makemessages --all --ignore=src/* --extension html -l en',
'make_djangojs': 'django-admin.py makemessages --all -d djangojs --ignore=src/* --extension js -l en',
'msgcat' : 'msgcat -o merged.po django.po %s' % BABEL_OUT,
'rename_django' : 'mv django.po django_old.po',
'rename_merged' : 'mv merged.po django.po',
'compile': 'django-admin.py compilemessages'
}
def execute (command_kwd, log, working_directory=BASE_DIR):
'''
Executes command_kwd, which references a shell command in COMMANDS.
'''
full_cmd = COMMANDS[command_kwd]
log.info('%s' % full_cmd)
subprocess.call(full_cmd.split(' '), cwd=working_directory)
def make_log ():
'''returns a 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
def get_config ():
'''Returns data found in config file, or returns None if file not found'''
config_path = os.path.abspath(CONFIG_FILENAME)
if not os.path.exists(config_path):
return None
with open(config_path) as stream:
return json.load(stream)
def main ():
log = make_log()
create_dir_if_necessary(LOCALE_DIR)
log.info('Executing all commands from %s' % BASE_DIR)
remove_files = ['django.po', 'djangojs.po', 'nonesuch']
for filename in remove_files:
path = MSGS_DIR + '/' + filename
log.info('Deleting file %s' % path)
if not os.path.exists(path):
log.warn("File does not exist: %s" % path)
else:
os.remove(path)
# Generate or update human-readable .po files from all source code.
execute('babel_mako', log=log)
execute('make_django', log=log)
execute('make_djangojs', log=log)
execute('msgcat', log=log, working_directory=MSGS_DIR)
execute('rename_django', log=log, working_directory=MSGS_DIR)
execute('rename_merged', log=log, working_directory=MSGS_DIR)
# Generate dummy text files from the English .po files
log.info('Generating dummy text.')
dummy_main(LOCALE_DIR + '/en/LC_MESSAGES/django.po')
dummy_main(LOCALE_DIR + '/en/LC_MESSAGES/djangojs.po')
# Generate machine-readable .mo files
execute('compile', log)
if __name__ == '__main__':
main()
......@@ -272,7 +272,9 @@
}
.course-staff {
.teacher {
@include clearfix;
margin-bottom: 40px;
h3 {
......@@ -312,7 +314,7 @@
}
}
}
.faq {
@include clearfix;
......
......@@ -33,6 +33,9 @@
<h2>Universities</h2>
<p>If you are a university wishing to collaborate or with questions about edX, please email <a href="mailto:university@edx.org">university@edx.org</a>.</p>
<h2>Accessibility</h2>
<p>EdX strives to create an innovative online-learning platform that promotes accessibility for everyone, including students with disabilities. We are dedicated to improving the accessibility of the platform and welcome your comments or questions at <a href="mailto:accessibility@edx.org">accessibility@edx.org.</a></p>
</div>
</section>
</section>
......
......@@ -2,8 +2,3 @@
-e common/lib/capa
-e common/lib/xmodule
-e .
# XBlock:
# Might change frequently, so put it in local-requirements.txt,
# but conceptually is an external package, so it is in a separate repo.
-e git+https://github.com/edx/XBlock.git@5ce6f70a#egg=XBlock
......@@ -174,6 +174,11 @@ end
desc "Install all python prerequisites for the lms and cms"
task :install_python_prereqs do
sh('pip install -r requirements.txt')
# Check for private-requirements.txt: used to install our libs as working dirs,
# or personal-use tools.
if File.file?("private-requirements.txt")
sh('pip install -r private-requirements.txt')
end
end
task :predjango do
......@@ -330,6 +335,12 @@ 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}"
......@@ -501,6 +512,30 @@ task :autodeploy_properties do
end
end
# --- Internationalization tasks
desc "Extract localizable strings from sources"
task :extract_dev_strings 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_i18n do
if ARGV.last.downcase == 'extract'
Rake::Task["extract_dev_strings"].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_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}")
end
end
# --- Develop and public documentation ---
desc "Invoke sphinx 'make build' to generate docs."
......
-r repo-requirements.txt
Babel==0.9.6
beautifulsoup4==4.1.3
beautifulsoup==3.2.1
boto==2.6.0
......@@ -62,6 +61,10 @@ newrelic==1.8.0.13
# Used for documentation gathering
sphinx==1.1.3
# Used for Internationalization and localization
Babel==0.9.6
transifex-client==0.8
# Used for testing
coverage==3.6
factory_boy==2.0.2
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment