Merge branch 'master' into ux/marco/studio-componentsettings
1
.gitignore
vendored
@@ -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:
|
||||
@@ -1498,6 +1487,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
|
||||
|
||||
53
common/lib/xmodule/xmodule/templates/about/overview.yaml
Normal file
@@ -0,0 +1,53 @@
|
||||
---
|
||||
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>
|
||||
|
||||
BIN
common/static/images/firecode.jpg
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
common/static/images/high_pass_filter.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
common/static/images/mit_dome.jpg
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
common/static/images/pl-course.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
common/static/images/pl-faculty.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
common/static/images/simple_graph.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
common/static/images/voltage_divider.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
@@ -1 +1 @@
|
||||
{"locales" : ["en", "fr", "de"]}
|
||||
{"locales" : ["en"]}
|
||||
|
||||
1
conf/locale/en/LC_MESSAGES/messages.po
Normal file
@@ -0,0 +1 @@
|
||||
# empty
|
||||
@@ -1,5 +1,10 @@
|
||||
# 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:
|
||||
|
||||
86
i18n/execute.py
Normal file
@@ -0,0 +1,86 @@
|
||||
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)
|
||||
|
||||
145
i18n/extract.py
Executable file
@@ -0,0 +1,145 @@
|
||||
#!/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()
|
||||
64
i18n/generate.py
Executable file
@@ -0,0 +1,64 @@
|
||||
#!/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)
|
||||
|
||||
4
i18n/tests/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from test_extract import TestExtract
|
||||
from test_generate import TestGenerate
|
||||
from test_converter import TestConverter
|
||||
from test_dummy import TestDummy
|
||||
42
i18n/tests/test_converter.py
Normal file
@@ -0,0 +1,42 @@
|
||||
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)
|
||||
50
i18n/tests/test_dummy.py
Normal file
@@ -0,0 +1,50 @@
|
||||
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)
|
||||
85
i18n/tests/test_extract.py
Normal file
@@ -0,0 +1,85 @@
|
||||
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)
|
||||
61
i18n/tests/test_generate.py
Normal file
@@ -0,0 +1,61 @@
|
||||
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))
|
||||
110
i18n/update.py
@@ -1,110 +0,0 @@
|
||||
#!/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@96d8f5f4#egg=XBlock
|
||||
|
||||
35
rakefile
@@ -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."
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
-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
|
||||
|
||||