diff options
| -rw-r--r-- | src/mailman/utilities/i18n.py | 128 | ||||
| -rw-r--r-- | src/mailman/utilities/tests/test_templates.py | 141 |
2 files changed, 254 insertions, 15 deletions
diff --git a/src/mailman/utilities/i18n.py b/src/mailman/utilities/i18n.py index 45262997b..a0f49a436 100644 --- a/src/mailman/utilities/i18n.py +++ b/src/mailman/utilities/i18n.py @@ -21,14 +21,138 @@ from __future__ import absolute_import, unicode_literals __metaclass__ = type __all__ = [ + 'TemplateNotFoundError', 'find', 'make', ] +import os +import errno + +from itertools import product + +from mailman.config import config +from mailman.core.constants import system_preferences + + -def find(): - pass +class TemplateNotFoundError(Exception): + """The named template was not found.""" + + def __init__(self, template_file): + self.template_file = template_file + + + +def _search(template_file, mailing_list=None, language=None): + """Generator that provides file system search order.""" + + languages = ['en', system_preferences.preferred_language.code] + if mailing_list is not None: + languages.append(mailing_list.preferred_language.code) + if language is not None: + languages.append(language) + languages.reverse() + # File system locations to search. + paths = [config.TEMPLATE_DIR, + os.path.join(config.TEMPLATE_DIR, 'site')] + if mailing_list is not None: + paths.append(os.path.join(config.TEMPLATE_DIR, + mailing_list.host_name)) + paths.append(os.path.join(config.LIST_DATA_DIR, + mailing_list.fqdn_listname)) + paths.reverse() + for language, path in product(languages, paths): + yield os.path.join(path, language, template_file) + + + +def find(template_file, mailing_list=None, language=None): + """Locate an i18n template file. + + When something in Mailman needs a template file, it always asks for the + file through this interface. The results of the search is path to the + 'matching' template, with the search order depending on whether + `mailing_list` and `language` are provided. + + When looking for a template in a specific language, there are 4 locations + that are searched, in this order: + + * The list-specific language directory + <var_dir>/lists/<fqdn_listname>/<language> + + * The domain-specific language directory + <template_dir>/<list-host-name>/<language> + + * The site-wide language directory + <template_dir>/site/<language> + + * The global default language directory + <template_dir>/<language> + + The first match stops the search. In this way, you can specialize + templates at the desired level, or if you only use the default templates, + you don't need to change anything. NEVER modify files in + <template_dir>/<language> since Mailman will overwrite these when you + upgrade. Instead you can use <template_dir>/site. + + The <language> path component is calculated as follows, in this order: + + * The `language` parameter if given + * `mailing_list.preferred_language` if given + * The server's default language + * English ('en') + + Languages are iterated after each of the four locations are searched. So + for example, when searching for the 'foo.txt' template, where the server's + default language is 'fr', the mailing list's (test@example.com) language + is 'de' and the `language` parameter is 'it', these locations are searched + in order: + + * <var_dir>/lists/test@example.com/it/foo.txt + * <template_dir>/example.com/it/foo.txt + * <template_dir>/site/it/foo.txt + * <template_dir>/it/foo.txt + + * <var_dir>/lists/test@example.com/de/foo.txt + * <template_dir>/example.com/de/foo.txt + * <template_dir>/site/de/foo.txt + * <template_dir>/de/foo.txt + + * <var_dir>/lists/test@example.com/fr/foo.txt + * <template_dir>/example.com/fr/foo.txt + * <template_dir>/site/fr/foo.txt + * <template_dir>/fr/foo.txt + + * <var_dir>/lists/test@example.com/en/foo.txt + * <template_dir>/example.com/en/foo.txt + * <template_dir>/site/en/foo.txt + * <template_dir>/en/foo.txt + + :param template_file: The name of the template file to search for. + :type template_file: string + :param mailing_list: Optional mailing list used as the context for + searching for the template file. The list's preferred language will + influence the search, as will the list's data directory. + :type mailing_list: `IMailingList` + :param language: Optional language code, which influences the search. + :type language: string + :return: A tuple of the file system path to the first matching template, + and an open file object allowing reading of the file. + :rtype: (string, file) + :raises TemplateNotFoundError: when the template could not be found. + """ + raw_search_order = _search(template_file, mailing_list, language) + for path in raw_search_order: + try: + fp = open(path) + except IOError as error: + if error.errno != errno.ENOENT: + raise + else: + return path, fp + raise TemplateNotFoundError(template_file) def make(): diff --git a/src/mailman/utilities/tests/test_templates.py b/src/mailman/utilities/tests/test_templates.py index 446595108..23f99b60e 100644 --- a/src/mailman/utilities/tests/test_templates.py +++ b/src/mailman/utilities/tests/test_templates.py @@ -36,13 +36,116 @@ from mailman.app.lifecycle import create_list from mailman.config import config from mailman.interfaces.languages import ILanguageManager from mailman.testing.layers import ConfigLayer -#from mailman.utilities.i18n import find, make +from mailman.utilities.i18n import TemplateNotFoundError, _search, find, make from mailman.Utils import findtext +class TestSearchOrder(unittest.TestCase): + """Test internal search order for language templates.""" + + layer = ConfigLayer + + def setUp(self): + self.template_dir = tempfile.mkdtemp() + config.push('no template dir', """\ + [mailman] + default_language: fr + [paths.testing] + template_dir: {0}/t + var_dir: {0}/v + """.format(self.template_dir)) + language_manager = getUtility(ILanguageManager) + language_manager.add('de', 'utf-8', 'German') + language_manager.add('it', 'utf-8', 'Italian') + self.mlist = create_list('l@example.com') + self.mlist.preferred_language = 'de' + + def tearDown(self): + config.pop('no template dir') + shutil.rmtree(self.template_dir) + + def _stripped_search_order(self, template_file, + mailing_list=None, language=None): + raw_search_order = _search(template_file, mailing_list, language) + for path in raw_search_order: + yield path[len(self.template_dir):] + + def test_fully_specified_search_order(self): + search_order = self._stripped_search_order('foo.txt', self.mlist, 'it') + # language argument + self.assertEqual(next(search_order), + '/v/lists/l@example.com/it/foo.txt') + self.assertEqual(next(search_order), '/t/example.com/it/foo.txt') + self.assertEqual(next(search_order), '/t/site/it/foo.txt') + self.assertEqual(next(search_order), '/t/it/foo.txt') + # mlist.preferred_language + self.assertEqual(next(search_order), + '/v/lists/l@example.com/de/foo.txt') + self.assertEqual(next(search_order), '/t/example.com/de/foo.txt') + self.assertEqual(next(search_order), '/t/site/de/foo.txt') + self.assertEqual(next(search_order), '/t/de/foo.txt') + # site's default language + self.assertEqual(next(search_order), + '/v/lists/l@example.com/fr/foo.txt') + self.assertEqual(next(search_order), '/t/example.com/fr/foo.txt') + self.assertEqual(next(search_order), '/t/site/fr/foo.txt') + self.assertEqual(next(search_order), '/t/fr/foo.txt') + # English + self.assertEqual(next(search_order), + '/v/lists/l@example.com/en/foo.txt') + self.assertEqual(next(search_order), '/t/example.com/en/foo.txt') + self.assertEqual(next(search_order), '/t/site/en/foo.txt') + self.assertEqual(next(search_order), '/t/en/foo.txt') + + def test_no_language_argument_search_order(self): + search_order = self._stripped_search_order('foo.txt', self.mlist) + # mlist.preferred_language + self.assertEqual(next(search_order), + '/v/lists/l@example.com/de/foo.txt') + self.assertEqual(next(search_order), '/t/example.com/de/foo.txt') + self.assertEqual(next(search_order), '/t/site/de/foo.txt') + self.assertEqual(next(search_order), '/t/de/foo.txt') + # site's default language + self.assertEqual(next(search_order), + '/v/lists/l@example.com/fr/foo.txt') + self.assertEqual(next(search_order), '/t/example.com/fr/foo.txt') + self.assertEqual(next(search_order), '/t/site/fr/foo.txt') + self.assertEqual(next(search_order), '/t/fr/foo.txt') + # English + self.assertEqual(next(search_order), + '/v/lists/l@example.com/en/foo.txt') + self.assertEqual(next(search_order), '/t/example.com/en/foo.txt') + self.assertEqual(next(search_order), '/t/site/en/foo.txt') + self.assertEqual(next(search_order), '/t/en/foo.txt') + + def test_no_mailing_list_argument_search_order(self): + search_order = self._stripped_search_order('foo.txt', language='it') + # language argument + self.assertEqual(next(search_order), '/t/site/it/foo.txt') + self.assertEqual(next(search_order), '/t/it/foo.txt') + # site's default language + self.assertEqual(next(search_order), '/t/site/fr/foo.txt') + self.assertEqual(next(search_order), '/t/fr/foo.txt') + # English + self.assertEqual(next(search_order), '/t/site/en/foo.txt') + self.assertEqual(next(search_order), '/t/en/foo.txt') + + def test_no_optional_arguments_search_order(self): + search_order = self._stripped_search_order('foo.txt') + # site's default language + self.assertEqual(next(search_order), '/t/site/fr/foo.txt') + self.assertEqual(next(search_order), '/t/fr/foo.txt') + # English + self.assertEqual(next(search_order), '/t/site/en/foo.txt') + self.assertEqual(next(search_order), '/t/en/foo.txt') + + + class TestFind(unittest.TestCase): + """Test template search.""" + layer = ConfigLayer def setUp(self): @@ -56,52 +159,64 @@ class TestFind(unittest.TestCase): getUtility(ILanguageManager).add('xx', 'utf-8', 'Xlandia') self.mlist = create_list('test@example.com') self.mlist.preferred_language = 'xx' + self.fp = None # Populate global tempdir with a few fake templates. self.xxdir = os.path.join(self.template_dir, 'xx') os.mkdir(self.xxdir) with open(os.path.join(self.xxdir, 'global.txt'), 'w') as fp: - print >> fp, 'Global template' + fp.write('Global template') self.sitedir = os.path.join(self.template_dir, 'site', 'xx') os.makedirs(self.sitedir) with open(os.path.join(self.sitedir, 'site.txt'), 'w') as fp: - print >> fp, 'Site template' + fp.write('Site template') self.domaindir = os.path.join(self.template_dir, 'example.com', 'xx') os.makedirs(self.domaindir) with open(os.path.join(self.domaindir, 'domain.txt'), 'w') as fp: - print >> fp, 'Domain template' + fp.write('Domain template') self.listdir = os.path.join(self.mlist.data_path, 'xx') os.makedirs(self.listdir) with open(os.path.join(self.listdir, 'list.txt'), 'w') as fp: - print >> fp, 'List template' + fp.write('List template') def tearDown(self): + if self.fp is not None: + self.fp.close() config.pop('template config') shutil.rmtree(self.template_dir) shutil.rmtree(self.listdir) def test_find_global_template(self): - text, filename = findtext('global.txt', lang='xx') - self.assertEqual(text, 'Global template\n') + filename, self.fp = find('global.txt', language='xx') self.assertEqual(filename, os.path.join(self.xxdir, 'global.txt')) + self.assertEqual(self.fp.read(), 'Global template') def test_find_site_template(self): - text, filename = findtext('site.txt', lang='xx') - self.assertEqual(text, 'Site template\n') + filename, self.fp = find('site.txt', language='xx') self.assertEqual(filename, os.path.join(self.sitedir, 'site.txt')) + self.assertEqual(self.fp.read(), 'Site template') def test_find_domain_template(self): - text, filename = findtext('domain.txt', mlist=self.mlist) - self.assertEqual(text, 'Domain template\n') + filename, self.fp = find('domain.txt', self.mlist) self.assertEqual(filename, os.path.join(self.domaindir, 'domain.txt')) + self.assertEqual(self.fp.read(), 'Domain template') def test_find_list_template(self): - text, filename = findtext('list.txt', mlist=self.mlist) - self.assertEqual(text, 'List template\n') + filename, self.fp = find('list.txt', self.mlist) self.assertEqual(filename, os.path.join(self.listdir, 'list.txt')) + self.assertEqual(self.fp.read(), 'List template') + def test_template_not_found(self): + # Python 2.6 compatibility. + try: + find('missing.txt', self.mlist) + except TemplateNotFoundError as error: + self.assertEqual(error.template_file, 'missing.txt') + else: + raise AssertionError('TemplateNotFoundError expected') def test_suite(): suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TestSearchOrder)) suite.addTest(unittest.makeSuite(TestFind)) return suite |
