diff --git a/.gitignore b/.gitignore
index 87a0778a6f..d01baf055a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,7 @@
:2e#
.AppleDouble
database.sqlite
+private-requirements.txt
courseware/static/js/mathjax/*
flushdb.sh
build
diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index ed95d81d67..07b7032e60 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -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
diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py
index 603010f5b4..caf3901e03 100644
--- a/cms/djangoapps/contentstore/views.py
+++ b/cms/djangoapps/contentstore/views.py
@@ -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
diff --git a/cms/templates/base.html b/cms/templates/base.html
index 65e08b3cc5..3f286c2582 100644
--- a/cms/templates/base.html
+++ b/cms/templates/base.html
@@ -30,7 +30,7 @@
diff --git a/common/lib/capa/capa/calc.py b/common/lib/capa/capa/calc.py
index c3fe6b656b..bb1fb97153 100644
--- a/common/lib/capa/capa/calc.py
+++ b/common/lib/capa/capa/calc.py
@@ -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"
diff --git a/common/lib/capa/capa/tests/test_responsetypes.py b/common/lib/capa/capa/tests/test_responsetypes.py
index bf64d3cc69..7a43fff4c9 100644
--- a/common/lib/capa/capa/tests/test_responsetypes.py
+++ b/common/lib/capa/capa/tests/test_responsetypes.py
@@ -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):
diff --git a/common/lib/xmodule/xmodule/modulestore/draft.py b/common/lib/xmodule/xmodule/modulestore/draft.py
index 43eb050129..c3f1b23688 100644
--- a/common/lib/xmodule/xmodule/modulestore/draft.py
+++ b/common/lib/xmodule/xmodule/modulestore/draft.py
@@ -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
diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py
index 28ea1f2659..c8256422f8 100644
--- a/common/lib/xmodule/xmodule/modulestore/mongo.py
+++ b/common/lib/xmodule/xmodule/modulestore/mongo.py
@@ -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)
diff --git a/common/lib/xmodule/xmodule/modulestore/store_utilities.py b/common/lib/xmodule/xmodule/modulestore/store_utilities.py
index e90613d0da..6beffcb71d 100644
--- a/common/lib/xmodule/xmodule/modulestore/store_utilities.py
+++ b/common/lib/xmodule/xmodule/modulestore/store_utilities.py
@@ -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
diff --git a/common/lib/xmodule/xmodule/templates/about/overview.yaml b/common/lib/xmodule/xmodule/templates/about/overview.yaml
new file mode 100644
index 0000000000..0031ebffaf
--- /dev/null
+++ b/common/lib/xmodule/xmodule/templates/about/overview.yaml
@@ -0,0 +1,53 @@
+---
+metadata:
+ display_name: overview
+
+data: |
+
+
About This Course
+
Include your long course description here. The long course description should contain 150-400 words.
+
+
This is paragraph 2 of the long course description. Add more paragraphs as needed. Make sure to enclose them in paragraph tags.
+
+
+
+
Prerequisites
+
Add information about course prerequisites here.
+
+
+
+
Course Staff
+
+
+
+
+
+
Staff Member #1
+
Biography of instructor/staff member #1
+
+
+
+
+
+
+
+
Staff Member #2
+
Biography of instructor/staff member #2
+
+
+
+
+
+
Frequently Asked Questions
+
+
Do I need to buy a textbook?
+
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.
diff --git a/common/static/images/firecode.jpg b/common/static/images/firecode.jpg
new file mode 100644
index 0000000000..a20206559b
Binary files /dev/null and b/common/static/images/firecode.jpg differ
diff --git a/common/static/images/high_pass_filter.png b/common/static/images/high_pass_filter.png
new file mode 100644
index 0000000000..5f425d4671
Binary files /dev/null and b/common/static/images/high_pass_filter.png differ
diff --git a/common/static/images/mit_dome.jpg b/common/static/images/mit_dome.jpg
new file mode 100644
index 0000000000..75a5bd949f
Binary files /dev/null and b/common/static/images/mit_dome.jpg differ
diff --git a/common/static/images/pl-course.png b/common/static/images/pl-course.png
new file mode 100644
index 0000000000..1a3da9e631
Binary files /dev/null and b/common/static/images/pl-course.png differ
diff --git a/common/static/images/pl-faculty.png b/common/static/images/pl-faculty.png
new file mode 100644
index 0000000000..b55ba44542
Binary files /dev/null and b/common/static/images/pl-faculty.png differ
diff --git a/common/static/images/simple_graph.png b/common/static/images/simple_graph.png
new file mode 100644
index 0000000000..f255795025
Binary files /dev/null and b/common/static/images/simple_graph.png differ
diff --git a/common/static/images/voltage_divider.png b/common/static/images/voltage_divider.png
new file mode 100644
index 0000000000..1fa695b1f3
Binary files /dev/null and b/common/static/images/voltage_divider.png differ
diff --git a/conf/locale/config b/conf/locale/config
index fe811ee02e..2d01e1ea43 100644
--- a/conf/locale/config
+++ b/conf/locale/config
@@ -1 +1 @@
-{"locales" : ["en", "fr", "de"]}
+{"locales" : ["en"]}
diff --git a/conf/locale/en/LC_MESSAGES/messages.po b/conf/locale/en/LC_MESSAGES/messages.po
new file mode 100644
index 0000000000..1bb8bf6d7f
--- /dev/null
+++ b/conf/locale/en/LC_MESSAGES/messages.po
@@ -0,0 +1 @@
+# empty
diff --git a/github-requirements.txt b/github-requirements.txt
index 468d55ce65..0d7b75b89b 100644
--- a/github-requirements.txt
+++ b/github-requirements.txt
@@ -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
diff --git a/i18n/dummy.py b/i18n/dummy.py
index 798ee525b5..78bfdc3b58 100644
--- a/i18n/dummy.py
+++ b/i18n/dummy.py
@@ -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:
diff --git a/i18n/execute.py b/i18n/execute.py
new file mode 100644
index 0000000000..3c3416b65d
--- /dev/null
+++ b/i18n/execute.py
@@ -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)
+
diff --git a/i18n/extract.py b/i18n/extract.py
new file mode 100755
index 0000000000..c6fedd3bfa
--- /dev/null
+++ b/i18n/extract.py
@@ -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 , 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 ',
+ 'EdX Team ')
+ )
+ 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 ',
+ u'Language-Team': u'LANGUAGE ',
+ 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 ',
+ }
+ 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()
diff --git a/i18n/generate.py b/i18n/generate.py
new file mode 100755
index 0000000000..ddbaadfa70
--- /dev/null
+++ b/i18n/generate.py
@@ -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()
diff --git a/i18n/make_dummy.py b/i18n/make_dummy.py
index 4ccfb0d5f1..c8dcde861a 100755
--- a/i18n/make_dummy.py
+++ b/i18n/make_dummy.py
@@ -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)
diff --git a/i18n/tests/__init__.py b/i18n/tests/__init__.py
new file mode 100644
index 0000000000..d60515c712
--- /dev/null
+++ b/i18n/tests/__init__.py
@@ -0,0 +1,4 @@
+from test_extract import TestExtract
+from test_generate import TestGenerate
+from test_converter import TestConverter
+from test_dummy import TestDummy
diff --git a/i18n/tests/test_converter.py b/i18n/tests/test_converter.py
new file mode 100644
index 0000000000..4dd5f02e3f
--- /dev/null
+++ b/i18n/tests/test_converter.py
@@ -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 bad wolf', 'BIG BAD WOLF'),
+ # two html tags
+ ('big badwolf', 'BIG BADWOLF'),
+ # 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
+ ('big %(adjective)s %(noun)s',
+ 'BIG %(adjective)s %(noun)s'),
+ )
+ for (source, expected) in test_cases:
+ result = c.convert(source)
+ self.assertEquals(result, expected)
diff --git a/i18n/tests/test_dummy.py b/i18n/tests/test_dummy.py
new file mode 100644
index 0000000000..88addb5a95
--- /dev/null
+++ b/i18n/tests/test_dummy.py
@@ -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 tag ids',
+ u'd\xf6n\'t \xe7\xf6nv\xe9rt t\xe4g \xefds 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)
diff --git a/i18n/tests/test_extract.py b/i18n/tests/test_extract.py
new file mode 100644
index 0000000000..b14ae9872d
--- /dev/null
+++ b/i18n/tests/test_extract.py
@@ -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)
diff --git a/i18n/tests/test_generate.py b/i18n/tests/test_generate.py
new file mode 100644
index 0000000000..fc22988251
--- /dev/null
+++ b/i18n/tests/test_generate.py
@@ -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))
diff --git a/i18n/update.py b/i18n/update.py
deleted file mode 100755
index 447dcf71d5..0000000000
--- a/i18n/update.py
+++ /dev/null
@@ -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()
diff --git a/lms/static/sass/multicourse/_course_about.scss b/lms/static/sass/multicourse/_course_about.scss
index d23917fe27..0982577f42 100644
--- a/lms/static/sass/multicourse/_course_about.scss
+++ b/lms/static/sass/multicourse/_course_about.scss
@@ -272,7 +272,9 @@
}
.course-staff {
+
.teacher {
+ @include clearfix;
margin-bottom: 40px;
h3 {
@@ -312,7 +314,7 @@
}
}
}
-
+
.faq {
@include clearfix;
diff --git a/lms/templates/static_templates/contact.html b/lms/templates/static_templates/contact.html
index d848164720..79e2743dbc 100644
--- a/lms/templates/static_templates/contact.html
+++ b/lms/templates/static_templates/contact.html
@@ -33,6 +33,9 @@
Universities
If you are a university wishing to collaborate or with questions about edX, please email university@edx.org.
+
Accessibility
+
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 accessibility@edx.org.
+
diff --git a/local-requirements.txt b/local-requirements.txt
index 177897f53d..201467d11e 100644
--- a/local-requirements.txt
+++ b/local-requirements.txt
@@ -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
diff --git a/rakefile b/rakefile
index 766dd8a914..798e1c28bf 100644
--- a/rakefile
+++ b/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."
diff --git a/requirements.txt b/requirements.txt
index 77239a4d50..d3fdd46b81 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -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