Barry Warsaw pushed to branch master at mailman / Mailman
Commits: cd61fcc8 by Barry Warsaw at 2016-02-27T14:40:06-05:00 Allow List-ID in decoration template URIs. Closes #196 In mailman: URIs (e.g. IMailingList.header_uri and .footer_uri), you used to be able to only use fqdn-listnames. But since List-IDs are more stable, let's allow those too. Silently deprecate using the fqdn-listname. - - - - - f7f6bdf3 by Barry Warsaw at 2016-02-27T14:49:43-05:00 News. - - - - - 7 changed files: - .gitignore - src/mailman/app/templates.py - src/mailman/docs/NEWS.rst - src/mailman/handlers/decorate.py - src/mailman/handlers/tests/test_decorate.py - src/mailman/utilities/i18n.py - src/mailman/utilities/tests/test_templates.py Changes: ===================================== .gitignore ===================================== --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ *.egg-info var -.coverage +.coverage* htmlcov __pycache__ .tox coverage.xml diffcov.html +build +dist ===================================== src/mailman/app/templates.py ===================================== --- a/src/mailman/app/templates.py +++ b/src/mailman/app/templates.py @@ -39,9 +39,10 @@ from zope.interface import implementer class MailmanHandler(BaseHandler): # Handle internal mailman: URLs. def mailman_open(self, req): + list_manager = getUtility(IListManager) # Parse urls of the form: # - # mailman:///<fqdn_listname>/<language>/<template_name> + # mailman:///<fqdn_listname|list_id>/<language>/<template_name> # # where only the template name is required. mlist = code = template = None @@ -58,19 +59,25 @@ class MailmanHandler(BaseHandler): template = parts[0] elif len(parts) == 2: part0, template = parts - # Is part0 a language code or a mailing list? It better be one or - # the other, and there's no possibility of namespace collisions - # because language codes don't contain @ and mailing list names - # MUST contain @. + # Is part0 a language code or a mailing list? This is rather + # tricky because if it's a mailing list, it could be a list-id and + # that will contain dots, as could the language code. language = getUtility(ILanguageManager).get(part0) - mlist = getUtility(IListManager).get(part0) - if language is None and mlist is None: - raise URLError('Bad language or list name') - elif mlist is None: + if language is None: + # part0 must be a fqdn-listname or list-id. + mlist = (list_manager.get(part0) + if '@' in part0 else + list_manager.get_by_list_id(part0)) + if mlist is None: + raise URLError('Bad language or list name') + else: code = language.code elif len(parts) == 3: - fqdn_listname, code, template = parts - mlist = getUtility(IListManager).get(fqdn_listname) + part0, code, template = parts + # part0 could be an fqdn-listname or a list-id. + mlist = (getUtility(IListManager).get(part0) + if '@' in part0 else + getUtility(IListManager).get_by_list_id(part0)) if mlist is None: raise URLError('Missing list') language = getUtility(ILanguageManager).get(code) ===================================== src/mailman/docs/NEWS.rst ===================================== --- a/src/mailman/docs/NEWS.rst +++ b/src/mailman/docs/NEWS.rst @@ -62,6 +62,9 @@ Bugs * When approving a subscription request via the REST API, for a user who is already a member, return an HTTP 409 Conflict code instead of the previous server traceback (and resulting HTTP 500 code). (Closes: #193) + * In decoration URIs (e.g. ``IMailingList.header_uri`` and ``.footer_uri``) + you should now use the mailing list's List-ID instead of the + fqdn-listname. The latter is deprecated. (Closes #196) Configuration ------------- ===================================== src/mailman/handlers/decorate.py ===================================== --- a/src/mailman/handlers/decorate.py +++ b/src/mailman/handlers/decorate.py @@ -217,8 +217,9 @@ def decorate(mlist, uri, extradict=None): # Get the decorator template. loader = getUtility(ITemplateLoader) template_uri = expand(uri, dict( - listname=mlist.fqdn_listname, language=mlist.preferred_language.code, + list_id=mlist.list_id, + listname=mlist.fqdn_listname, )) template = loader.get(template_uri) return decorate_template(mlist, template, extradict) ===================================== src/mailman/handlers/tests/test_decorate.py ===================================== --- a/src/mailman/handlers/tests/test_decorate.py +++ b/src/mailman/handlers/tests/test_decorate.py @@ -23,8 +23,6 @@ __all__ = [ import os -import shutil -import tempfile import unittest from mailman.app.lifecycle import create_list @@ -33,6 +31,7 @@ from mailman.handlers import decorate from mailman.interfaces.archiver import IArchiver from mailman.testing.helpers import specialized_message_from_string as mfs from mailman.testing.layers import ConfigLayer +from tempfile import TemporaryDirectory from zope.interface import implementer @@ -56,19 +55,18 @@ class TestDecorate(unittest.TestCase): layer = ConfigLayer def setUp(self): - self._mlist = create_list('t...@example.com') + self._mlist = create_list('a...@example.com') self._msg = mfs("""\ -To: t...@example.com +To: a...@example.com From: aper...@example.com -Message-ID: <somerandomid.example.com> +Message-ID: <alpha> Content-Type: text/plain; This is a test message. """) - template_dir = tempfile.mkdtemp() - self.addCleanup(shutil.rmtree, template_dir) - site_dir = os.path.join(template_dir, 'site', 'en') - os.makedirs(site_dir) + temporary_dir = TemporaryDirectory() + self.addCleanup(temporary_dir.cleanup) + template_dir = temporary_dir.name config.push('archiver', """\ [paths.testing] template_dir: {} @@ -77,13 +75,44 @@ This is a test message. enable: yes """.format(template_dir)) self.addCleanup(config.pop, 'archiver') - self.footer_path = os.path.join(site_dir, 'myfooter.txt') def test_decorate_footer_with_archive_url(self): - with open(self.footer_path, 'w', encoding='utf-8') as fp: + site_dir = os.path.join(config.TEMPLATE_DIR, 'site', 'en') + os.makedirs(site_dir) + footer_path = os.path.join(site_dir, 'myfooter.txt') + with open(footer_path, 'w', encoding='utf-8') as fp: print('${testarchiver_url}', file=fp) self._mlist.footer_uri = 'mailman:///myfooter.txt' self._mlist.preferred_language = 'en' decorate.process(self._mlist, self._msg, {}) self.assertIn('http://example.com/link_to_message', self._msg.as_string()) + + def test_list_id_allowed_in_template_uri(self): + # Issue #196 - allow the list_id in the template uri expansion. + list_dir = os.path.join( + config.TEMPLATE_DIR, 'lists', 'ant.example.com', 'en') + os.makedirs(list_dir) + footer_path = os.path.join(list_dir, 'myfooter.txt') + with open(footer_path, 'w', encoding='utf-8') as fp: + print('${testarchiver_url}', file=fp) + self._mlist.footer_uri = 'mailman:///${list_id}/myfooter.txt' + self._mlist.preferred_language = 'en' + decorate.process(self._mlist, self._msg, {}) + self.assertIn('http://example.com/link_to_message', + self._msg.as_string()) + + def test_list_id_and_language_code_allowed_in_template_uri(self): + # Issue #196 - allow the list_id in the template uri expansion. + list_dir = os.path.join( + config.TEMPLATE_DIR, 'lists', 'ant.example.com', 'it') + os.makedirs(list_dir) + footer_path = os.path.join(list_dir, 'myfooter.txt') + with open(footer_path, 'w', encoding='utf-8') as fp: + print('${testarchiver_url}', file=fp) + self._mlist.footer_uri = ( + 'mailman:///${list_id}/${language}/myfooter.txt') + self._mlist.preferred_language = 'it' + decorate.process(self._mlist, self._msg, {}) + self.assertIn('http://example.com/link_to_message', + self._msg.as_string()) ===================================== src/mailman/utilities/i18n.py ===================================== --- a/src/mailman/utilities/i18n.py +++ b/src/mailman/utilities/i18n.py @@ -27,7 +27,6 @@ __all__ = [ import os import sys -import errno from itertools import product from mailman.config import config @@ -61,7 +60,8 @@ def search(template_file, mlist=None, language=None): The <language> path component is variable, and described below. * The list-specific language directory - $template_dir/lists/<mlist.fqdn_listname>/<language> + $template_dir/lists/<mlist.list_id>/<language> + $template_dir/lists/<mlist.fqdn_listname>/<language> (deprecated) * The domain-specific language directory $template_dir/domains/<mlist.mail_host>/<language> @@ -78,23 +78,27 @@ def search(template_file, mlist=None, language=None): 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 (t...@example.com) language + 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: - * $template_dir/lists/t...@example.com/it/foo.txt + * $template_dir/lists/test.example.com/it/foo.txt + * $template_dir/lists/t...@example.com/it/foo.txt (deprecated) * $template_dir/domains/example.com/it/foo.txt * $template_dir/site/it/foo.txt - * $template_dir/lists/t...@example.com/de/foo.txt + * $template_dir/lists/test.example.com/de/foo.txt + * $template_dir/lists/t...@example.com/de/foo.txt (deprecated) * $template_dir/domains/example.com/de/foo.txt * $template_dir/site/de/foo.txt - * $template_dir/lists/t...@example.com/fr/foo.txt + * $template_dir/lists/test.example.com/fr/foo.txt + * $template_dir/lists/t...@example.com/fr/foo.txt (deprecated) * $template_dir/domains/example.com/fr/foo.txt * $template_dir/site/fr/foo.txt - * $template_dir/lists/t...@example.com/en/foo.txt + * $template_dir/lists/test.example.com/en/foo.txt + * $template_dir/lists/t...@example.com/en/foo.txt (deprecated) * $template_dir/domains/example.com/en/foo.txt * $template_dir/site/en/foo.txt @@ -113,10 +117,13 @@ def search(template_file, mlist=None, language=None): # The non-language qualified $template_dir paths in search order. paths = [os.path.join(config.TEMPLATE_DIR, 'site')] if mlist is not None: + # Don't forget these are in REVERSE search order! paths.append(os.path.join( config.TEMPLATE_DIR, 'domains', mlist.mail_host)) paths.append(os.path.join( config.TEMPLATE_DIR, 'lists', mlist.fqdn_listname)) + paths.append(os.path.join( + config.TEMPLATE_DIR, 'lists', mlist.list_id)) paths.reverse() for language, path in product(languages, paths): yield os.path.join(path, language, template_file) @@ -151,15 +158,12 @@ def find(template_file, mlist=None, language=None, _trace=False): if _trace: print('@@@', path, end='', file=sys.stderr) fp = open(path, 'r', encoding='utf-8') - except IOError as error: - if error.errno == errno.ENOENT: - if _trace: - print('MISSING', file=sys.stderr) - else: - raise + except FileNotFoundError: + if _trace: + print(' MISSING', file=sys.stderr) else: if _trace: - print('FOUND:', path, file=sys.stderr) + print(' FOUND:', path, file=sys.stderr) return path, fp raise TemplateNotFoundError(template_file) ===================================== src/mailman/utilities/tests/test_templates.py ===================================== --- a/src/mailman/utilities/tests/test_templates.py +++ b/src/mailman/utilities/tests/test_templates.py @@ -88,18 +88,22 @@ class TestSearchOrder(unittest.TestCase): def nexteq(path): self.assertEqual(next(search_order), path) # 1: Use the given language argument + nexteq('/v/templates/lists/l.example.com/it/foo.txt') nexteq('/v/templates/lists/l...@example.com/it/foo.txt') nexteq('/v/templates/domains/example.com/it/foo.txt') nexteq('/v/templates/site/it/foo.txt') # 2: Use mlist.preferred_language + nexteq('/v/templates/lists/l.example.com/de/foo.txt') nexteq('/v/templates/lists/l...@example.com/de/foo.txt') nexteq('/v/templates/domains/example.com/de/foo.txt') nexteq('/v/templates/site/de/foo.txt') # 3: Use the site's default language + nexteq('/v/templates/lists/l.example.com/fr/foo.txt') nexteq('/v/templates/lists/l...@example.com/fr/foo.txt') nexteq('/v/templates/domains/example.com/fr/foo.txt') nexteq('/v/templates/site/fr/foo.txt') # 4: English + nexteq('/v/templates/lists/l.example.com/en/foo.txt') nexteq('/v/templates/lists/l...@example.com/en/foo.txt') nexteq('/v/templates/domains/example.com/en/foo.txt') nexteq('/v/templates/site/en/foo.txt') @@ -114,14 +118,17 @@ class TestSearchOrder(unittest.TestCase): def nexteq(path): self.assertEqual(next(search_order), path) # 1: Use mlist.preferred_language + nexteq('/v/templates/lists/l.example.com/de/foo.txt') nexteq('/v/templates/lists/l...@example.com/de/foo.txt') nexteq('/v/templates/domains/example.com/de/foo.txt') nexteq('/v/templates/site/de/foo.txt') # 2: Use the site's default language + nexteq('/v/templates/lists/l.example.com/fr/foo.txt') nexteq('/v/templates/lists/l...@example.com/fr/foo.txt') nexteq('/v/templates/domains/example.com/fr/foo.txt') nexteq('/v/templates/site/fr/foo.txt') # 3: English + nexteq('/v/templates/lists/l.example.com/en/foo.txt') nexteq('/v/templates/lists/l...@example.com/en/foo.txt') nexteq('/v/templates/domains/example.com/en/foo.txt') nexteq('/v/templates/site/en/foo.txt') View it on GitLab: https://gitlab.com/mailman/mailman/compare/a763e9634ce5dacc4a5079271d14e8fb4b71235c...f7f6bdf31c8a81203c3b7f54b0e3bcb44f073a89
_______________________________________________ Mailman-checkins mailing list Mailman-checkins@python.org Unsubscribe: https://mail.python.org/mailman/options/mailman-checkins/archive%40jab.org