summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/mailman/Archiver/Archiver.py12
-rw-r--r--src/mailman/Archiver/HyperArch.py5
-rw-r--r--src/mailman/Utils.py128
-rw-r--r--src/mailman/app/membership.py12
-rw-r--r--src/mailman/app/moderator.py40
-rw-r--r--src/mailman/app/notifications.py36
-rw-r--r--src/mailman/chains/hold.py33
-rw-r--r--src/mailman/commands/cli_lists.py4
-rw-r--r--src/mailman/commands/docs/info.txt1
-rw-r--r--src/mailman/config/config.py6
-rw-r--r--src/mailman/config/schema.cfg6
-rw-r--r--src/mailman/pipeline/acknowledge.py18
-rw-r--r--src/mailman/queue/digest.py20
-rw-r--r--src/mailman/utilities/i18n.py200
-rw-r--r--src/mailman/utilities/tests/test_templates.py289
15 files changed, 597 insertions, 213 deletions
diff --git a/src/mailman/Archiver/Archiver.py b/src/mailman/Archiver/Archiver.py
index f8d1baa46..fe1dec252 100644
--- a/src/mailman/Archiver/Archiver.py
+++ b/src/mailman/Archiver/Archiver.py
@@ -32,9 +32,9 @@ from cStringIO import StringIO
from string import Template
from zope.component import getUtility
-from mailman import Utils
from mailman.config import config
from mailman.interfaces.domain import IDomainManager
+from mailman.utilities.i18n import make
log = logging.getLogger('mailman.error')
@@ -111,11 +111,11 @@ class Archiver:
fp = open(indexfile, 'w')
finally:
os.umask(omask)
- fp.write(Utils.maketext(
- 'emptyarchive.html',
- {'listname': self.real_name,
- 'listinfo': self.GetScriptURL('listinfo'),
- }, mlist=self))
+ fp.write(make('emptyarchive.html',
+ mailing_list=self,
+ listname=self.real_name,
+ listinfo=self.GetScriptURL('listinfo'),
+ ))
if fp:
fp.close()
finally:
diff --git a/src/mailman/Archiver/HyperArch.py b/src/mailman/Archiver/HyperArch.py
index 218c46875..92b9de2f0 100644
--- a/src/mailman/Archiver/HyperArch.py
+++ b/src/mailman/Archiver/HyperArch.py
@@ -50,6 +50,7 @@ from mailman.Archiver import pipermail
from mailman.config import config
from mailman.core.i18n import _, ctime
from mailman.interfaces.listmanager import IListManager
+from mailman.utilities.i18n import find
from mailman.utilities.string import uncanonstr, websafe
@@ -183,8 +184,8 @@ def quick_maketext(templatefile, dict=None, lang=None, mlist=None):
template = _templatecache.get(filepath)
if filepath is None or template is None:
# Use the basic maketext, with defaults to get the raw template
- template, filepath = Utils.findtext(templatefile, lang=lang.code,
- raw=True, mlist=mlist)
+ template, filepath = find(templatefile, mailing_list=mlist,
+ language=lang.code)
_templatefilepathcache[cachekey] = filepath
_templatecache[filepath] = template
# Copied from Utils.maketext()
diff --git a/src/mailman/Utils.py b/src/mailman/Utils.py
index a58ed1d24..cfd61eb7a 100644
--- a/src/mailman/Utils.py
+++ b/src/mailman/Utils.py
@@ -31,20 +31,13 @@ __all__ = [
import os
import re
-import errno
import logging
# pylint: disable-msg=E0611,W0403
from string import ascii_letters, digits, whitespace
-from zope.component import getUtility
import mailman.templates
-from mailman.config import config
-from mailman.core.i18n import _
-from mailman.interfaces.languages import ILanguageManager
-from mailman.utilities.string import expand
-
AT = '@'
CR = '\r'
@@ -139,124 +132,3 @@ def wrap(text, column=70, honor_leading_ws=True):
# end for text in lines
# the last two newlines are bogus
return wrapped[:-2]
-
-
-
-class OuterExit(Exception):
- pass
-
-def findtext(templatefile, raw_dict=None, raw=False, lang=None, mlist=None):
- # Make some text from a template file. The order of searches depends on
- # whether mlist and lang are provided. Once the templatefile is found,
- # string substitution is performed by interpolation in `dict'. If `raw'
- # is false, the resulting text is wrapped/filled by calling wrap().
- #
- # When looking for a template in a specific language, there are 4 places
- # that are searched, in this order:
- #
- # 1. the list-specific language directory
- # lists/<listname>/<language>
- #
- # 2. the domain-specific language directory
- # templates/<list.host_name>/<language>
- #
- # 3. the site-wide language directory
- # templates/site/<language>
- #
- # 4. the global default language directory
- # templates/<language>
- #
- # The first match found stops the search. In this way, you can specialize
- # templates at the desired level, or, if you use only the default
- # templates, you don't need to change anything. You should never modify
- # files in the templates/<language> subdirectory, since Mailman will
- # overwrite these when you upgrade. That's what the templates/site
- # language directories are for.
- #
- # A further complication is that the language to search for is determined
- # by both the `lang' and `mlist' arguments. The search order there is
- # that if lang is given, then the 4 locations above are searched,
- # substituting lang for <language>. If no match is found, and mlist is
- # given, then the 4 locations are searched using the list's preferred
- # language. After that, the server default language is used for
- # <language>. If that still doesn't yield a template, then the standard
- # distribution's English language template is used as an ultimate
- # fallback, and when lang is not 'en', the resulting template is passed
- # through the translation service. If this template is missing you've got
- # big problems. ;)
- #
- # A word on backwards compatibility: Mailman versions prior to 2.1 stored
- # templates in templates/*.{html,txt} and lists/<listname>/*.{html,txt}.
- # Those directories are no longer searched so if you've got customizations
- # in those files, you should move them to the appropriate directory based
- # on the above description. Mailman's upgrade script cannot do this for
- # you.
- #
- # The function has been revised and renamed as it now returns both the
- # template text and the path from which it retrieved the template. The
- # original function is now a wrapper which just returns the template text
- # as before, by calling this renamed function and discarding the second
- # item returned.
- #
- # Calculate the languages to scan
- languages = set()
- if lang is not None:
- languages.add(lang)
- if mlist is not None:
- languages.add(mlist.preferred_language.code)
- languages.add(config.mailman.default_language)
- assert None not in languages, 'None in languages'
- # Calculate the locations to scan
- searchdirs = []
- if mlist is not None:
- searchdirs.append(mlist.data_path)
- searchdirs.append(os.path.join(TEMPLATE_DIR, mlist.host_name))
- searchdirs.append(os.path.join(TEMPLATE_DIR, 'site'))
- searchdirs.append(TEMPLATE_DIR)
- # Start scanning
- fp = None
- try:
- for lang in languages:
- for dir in searchdirs:
- filename = os.path.join(dir, lang, templatefile)
- try:
- fp = open(filename)
- raise OuterExit
- except IOError, e:
- if e.errno != errno.ENOENT:
- raise
- # Okay, it doesn't exist, keep looping
- fp = None
- except OuterExit:
- pass
- if fp is None:
- # Try one last time with the distro English template, which, unless
- # you've got a really broken installation, must be there.
- try:
- filename = os.path.join(TEMPLATE_DIR, 'en', templatefile)
- fp = open(filename)
- except IOError, e:
- if e.errno != errno.ENOENT:
- raise
- # We never found the template. BAD!
- raise IOError(errno.ENOENT, 'No template file found', templatefile)
- else:
- # XXX BROKEN HACK
- data = fp.read()[:-1]
- template = _(data)
- fp.close()
- else:
- template = fp.read()
- fp.close()
- charset = getUtility(ILanguageManager)[lang].charset
- template = unicode(template, charset, 'replace')
- text = template
- if raw_dict is not None:
- text = expand(template, raw_dict)
- if raw:
- return text, filename
- return wrap(text), filename
-
-
-def maketext(templatefile, dict=None, raw=False, lang=None, mlist=None):
- return findtext(templatefile, dict, raw, lang, mlist)[0]
diff --git a/src/mailman/app/membership.py b/src/mailman/app/membership.py
index 8723fd781..fcbedc2f5 100644
--- a/src/mailman/app/membership.py
+++ b/src/mailman/app/membership.py
@@ -29,7 +29,6 @@ __all__ = [
from email.utils import formataddr
from zope.component import getUtility
-from mailman import Utils
from mailman.app.notifications import send_goodbye_message
from mailman.core.i18n import _
from mailman.email.message import OwnerNotification
@@ -39,6 +38,7 @@ from mailman.interfaces.member import (
AlreadySubscribedError, MemberRole, MembershipIsBannedError,
NotAMemberError)
from mailman.interfaces.usermanager import IUserManager
+from mailman.utilities.i18n import make
@@ -149,10 +149,10 @@ def delete_member(mlist, address, admin_notif=None, userack=None):
user = getUtility(IUserManager).get_user(address)
realname = user.real_name
subject = _('$mlist.real_name unsubscription notification')
- text = Utils.maketext(
- 'adminunsubscribeack.txt',
- {'listname': mlist.real_name,
- 'member' : formataddr((realname, address)),
- }, mlist=mlist)
+ text = make('adminunsubscribeack.txt',
+ mailing_list=mlist,
+ listname=mlist.real_name,
+ member=formataddr((realname, address)),
+ )
msg = OwnerNotification(mlist, subject, text)
msg.send(mlist)
diff --git a/src/mailman/app/moderator.py b/src/mailman/app/moderator.py
index cdfedd44b..50a03c833 100644
--- a/src/mailman/app/moderator.py
+++ b/src/mailman/app/moderator.py
@@ -48,6 +48,7 @@ from mailman.interfaces.member import (
AlreadySubscribedError, DeliveryMode, NotAMemberError)
from mailman.interfaces.messages import IMessageStore
from mailman.interfaces.requests import IRequests, RequestType
+from mailman.utilities.i18n import make
NL = '\n'
@@ -209,12 +210,12 @@ def hold_subscription(mlist, address, realname, password, mode, language):
if mlist.admin_immed_notify:
subject = _(
'New subscription request to list $mlist.real_name from $address')
- text = Utils.maketext(
- 'subauth.txt',
- {'username' : address,
- 'listname' : mlist.fqdn_listname,
- 'admindb_url': mlist.script_url('admindb'),
- }, mlist=mlist)
+ text = make('subauth.txt',
+ mailing_list=mlist,
+ username=address,
+ listname=mlist.fqdn_listname,
+ admindb_url=mlist.script_url('admindb'),
+ )
# This message should appear to come from the <list>-owner so as
# to avoid any useless bounce processing.
msg = UserNotification(
@@ -281,12 +282,12 @@ def hold_unsubscription(mlist, address):
if mlist.admin_immed_notify:
subject = _(
'New unsubscription request from $mlist.real_name by $address')
- text = Utils.maketext(
- 'unsubauth.txt',
- {'address' : address,
- 'listname' : mlist.fqdn_listname,
- 'admindb_url': mlist.script_url('admindb'),
- }, mlist=mlist)
+ text = make('unsubauth.txt',
+ mailing_list=mlist,
+ address=address,
+ listname=mlist.fqdn_listname,
+ admindb_url=mlist.script_url('admindb'),
+ )
# This message should appear to come from the <list>-owner so as
# to avoid any useless bounce processing.
msg = UserNotification(
@@ -336,13 +337,14 @@ def _refuse(mlist, request, recip, comment, origmsg=None, lang=None):
lang = (mlist.preferred_language
if member is None
else member.preferred_language)
- text = Utils.maketext(
- 'refuse.txt',
- {'listname' : mlist.fqdn_listname,
- 'request' : request,
- 'reason' : comment,
- 'adminaddr': mlist.owner_address,
- }, lang=lang.code, mlist=mlist)
+ text = make('refuse.txt',
+ mailing_list=mlist,
+ language=lang.code,
+ listname=mlist.fqdn_listname,
+ request=request,
+ reason=comment,
+ adminaddr=mlist.owner_address,
+ )
with _.using(lang.code):
# add in original message, but not wrap/filled
if origmsg:
diff --git a/src/mailman/app/notifications.py b/src/mailman/app/notifications.py
index 985f4eece..d7e64a020 100644
--- a/src/mailman/app/notifications.py
+++ b/src/mailman/app/notifications.py
@@ -30,11 +30,12 @@ __all__ = [
from email.utils import formataddr
from lazr.config import as_boolean
-from mailman import Utils
+from mailman.Utils import wrap
from mailman.config import config
from mailman.core.i18n import _
from mailman.email.message import OwnerNotification, UserNotification
from mailman.interfaces.member import DeliveryMode
+from mailman.utilities.i18n import make
@@ -54,7 +55,7 @@ def send_welcome_message(mlist, address, language, delivery_mode, text=''):
:type delivery_mode: DeliveryMode
"""
if mlist.welcome_msg:
- welcome = Utils.wrap(mlist.welcome_msg) + '\n'
+ welcome = wrap(mlist.welcome_msg) + '\n'
else:
welcome = ''
# Find the IMember object which is subscribed to the mailing list, because
@@ -62,15 +63,16 @@ def send_welcome_message(mlist, address, language, delivery_mode, text=''):
member = mlist.members.get_member(address)
options_url = member.options_url
# Get the text from the template.
- text += Utils.maketext(
- 'subscribeack.txt', {
- 'real_name' : mlist.real_name,
- 'posting_address' : mlist.fqdn_listname,
- 'listinfo_url' : mlist.script_url('listinfo'),
- 'optionsurl' : options_url,
- 'request_address' : mlist.request_address,
- 'welcome' : welcome,
- }, lang=language.code, mlist=mlist)
+ text += make('subscribeack.txt',
+ mailing_list=mlist,
+ language=language.code,
+ real_name=mlist.real_name,
+ posting_address=mlist.fqdn_listname,
+ listinfo_url=mlist.script_url('listinfo'),
+ optionsurl=options_url,
+ request_address=mlist.request_address,
+ welcome=welcome,
+ )
if delivery_mode is not DeliveryMode.regular:
digmode = _(' (Digest mode)')
else:
@@ -98,7 +100,7 @@ def send_goodbye_message(mlist, address, language):
:type language: string
"""
if mlist.goodbye_msg:
- goodbye = Utils.wrap(mlist.goodbye_msg) + '\n'
+ goodbye = wrap(mlist.goodbye_msg) + '\n'
else:
goodbye = ''
msg = UserNotification(
@@ -124,10 +126,10 @@ def send_admin_subscription_notice(mlist, address, full_name, language):
with _.using(mlist.preferred_language.code):
subject = _('$mlist.real_name subscription notification')
full_name = full_name.encode(language.charset, 'replace')
- text = Utils.maketext(
- 'adminsubscribeack.txt',
- {'listname' : mlist.real_name,
- 'member' : formataddr((full_name, address)),
- }, mlist=mlist)
+ text = make('adminsubscribeack.txt',
+ mailing_list=mlist,
+ listname=mlist.real_name,
+ member=formataddr((full_name, address)),
+ )
msg = OwnerNotification(mlist, subject, text)
msg.send(mlist)
diff --git a/src/mailman/chains/hold.py b/src/mailman/chains/hold.py
index ff53621ab..fcd11ca72 100644
--- a/src/mailman/chains/hold.py
+++ b/src/mailman/chains/hold.py
@@ -35,7 +35,7 @@ from zope.component import getUtility
from zope.event import notify
from zope.interface import implements
-from mailman.Utils import maketext, wrap
+from mailman.Utils import wrap
from mailman.app.moderator import hold_message
from mailman.app.replybot import can_acknowledge
from mailman.chains.base import ChainNotification, TerminalChainBase
@@ -46,6 +46,7 @@ from mailman.interfaces.autorespond import IAutoResponseSet, Response
from mailman.interfaces.languages import ILanguageManager
from mailman.interfaces.pending import IPendable, IPendings
from mailman.interfaces.usermanager import IUserManager
+from mailman.utilities.i18n import make
from mailman.utilities.string import oneline
@@ -103,14 +104,13 @@ def autorespond_to_sender(mlist, sender, lang=None):
log.info('hold autoresponse limit hit: %s', sender)
response_set.response_sent(address, Response.hold)
# Send this notification message instead.
- text = maketext(
- 'nomoretoday.txt',
- {'sender' : sender,
- 'listname': mlist.fqdn_listname,
- 'num' : todays_count,
- 'owneremail': mlist.owner_address,
- },
- lang=lang)
+ text = make('nomoretoday.txt',
+ language=lang,
+ sender=sender,
+ listname=mlist.fqdn_listname,
+ num=todays_count,
+ owneremail=mlist.owner_address,
+ )
with _.using(lang.code):
msg = UserNotification(
sender, mlist.owner_address,
@@ -194,8 +194,10 @@ class HoldChain(TerminalChainBase):
subject = _(
'Your message to $mlist.fqdn_listname awaits moderator approval')
send_language_code = msgdata.get('lang', language.code)
- text = maketext('postheld.txt', substitutions,
- lang=send_language_code, mlist=mlist)
+ text = make('postheld.txt',
+ mailing_list=mlist,
+ language=send_language_code,
+ **substitutions)
adminaddr = mlist.bounces_address
nmsg = UserNotification(
msg.sender, adminaddr, subject, text,
@@ -222,10 +224,11 @@ class HoldChain(TerminalChainBase):
mlist.owner_address,
subject, lang=language)
nmsg.set_type('multipart/mixed')
- text = MIMEText(
- maketext('postauth.txt', substitutions,
- raw=True, mlist=mlist),
- _charset=charset)
+ text = MIMEText(make('postauth.txt',
+ mailing_list=mlist,
+ wrap=False,
+ **substitutions),
+ _charset=charset)
dmsg = MIMEText(wrap(_("""\
If you reply to this message, keeping the Subject: header intact, Mailman will
discard the held message. Do this if the message is spam. If you reply to
diff --git a/src/mailman/commands/cli_lists.py b/src/mailman/commands/cli_lists.py
index a127dd816..021ce5991 100644
--- a/src/mailman/commands/cli_lists.py
+++ b/src/mailman/commands/cli_lists.py
@@ -30,7 +30,6 @@ __all__ = [
from zope.component import getUtility
from zope.interface import implements
-from mailman.Utils import maketext
from mailman.app.lifecycle import create_list, remove_list
from mailman.config import config
from mailman.core.constants import system_preferences
@@ -42,6 +41,7 @@ from mailman.interfaces.domain import (
BadDomainSpecificationError, IDomainManager)
from mailman.interfaces.languages import ILanguageManager
from mailman.interfaces.listmanager import IListManager, ListAlreadyExistsError
+from mailman.utilities.i18n import make
@@ -213,7 +213,7 @@ class Create:
requestaddr = mlist.request_address,
siteowner = mlist.no_reply_address,
)
- text = maketext('newlist.txt', d, mlist=mlist)
+ text = make('newlist.txt', mailing_list=mlist, **d)
# Set the I18N language to the list's preferred language so the
# header will match the template language. Stashing and restoring
# the old translation context is just (healthy? :) paranoia.
diff --git a/src/mailman/commands/docs/info.txt b/src/mailman/commands/docs/info.txt
index bccb78fda..12fce3223 100644
--- a/src/mailman/commands/docs/info.txt
+++ b/src/mailman/commands/docs/info.txt
@@ -74,6 +74,7 @@ The File System Hierarchy layout is the same every by definition.
PUBLIC_ARCHIVE_FILE_DIR = /var/lib/mailman/archives/public
QUEUE_DIR = /var/spool/mailman
SITE_PW_FILE = /var/lib/mailman/data/adm.pw
+ TEMPLATE_DIR = .../mailman/templates
VAR_DIR = /var/lib/mailman
diff --git a/src/mailman/config/config.py b/src/mailman/config/config.py
index 4cfb6b5a5..636b9ef9e 100644
--- a/src/mailman/config/config.py
+++ b/src/mailman/config/config.py
@@ -34,6 +34,8 @@ from string import Template
from zope.component import getUtility
from zope.interface import Interface, implements
+import mailman.templates
+
from mailman import version
from mailman.interfaces.languages import ILanguageManager
from mailman.styles.manager import StyleManager
@@ -176,6 +178,10 @@ class Configuration:
pipermail_public_dir = category.pipermail_public_dir,
queue_dir = category.queue_dir,
var_dir = var_dir,
+ template_dir = (
+ os.path.dirname(mailman.templates.__file__)
+ if category.template_dir == ':source:'
+ else category.template_dir),
# Files.
creator_pw_file = category.creator_pw_file,
lock_file = category.lock_file,
diff --git a/src/mailman/config/schema.cfg b/src/mailman/config/schema.cfg
index f789d28f9..09f575459 100644
--- a/src/mailman/config/schema.cfg
+++ b/src/mailman/config/schema.cfg
@@ -103,6 +103,12 @@ pipermail_public_dir: $var_dir/archives/public
# Directory for private Pipermail archiver artifacts.
pipermail_private_dir: $var_dir/archives/private
#
+# Where Mailman looks for its templates. This can either be a file system
+# path or the special symbol ':source:' to locate them within the source tree
+# (specifically, inside the mailman.templates package directory).
+#
+template_dir: :source:
+#
# There are also a number of paths to specific file locations that can be
# defined. For these, the directory containing the file must already exist,
# or be one of the directories created by Mailman as per above.
diff --git a/src/mailman/pipeline/acknowledge.py b/src/mailman/pipeline/acknowledge.py
index fcde6d6a5..e5b49ffa0 100644
--- a/src/mailman/pipeline/acknowledge.py
+++ b/src/mailman/pipeline/acknowledge.py
@@ -31,11 +31,11 @@ __all__ = [
from zope.component import getUtility
from zope.interface import implements
-from mailman import Utils
from mailman.core.i18n import _
from mailman.email.message import UserNotification
from mailman.interfaces.handler import IHandler
from mailman.interfaces.languages import ILanguageManager
+from mailman.utilities.i18n import make
from mailman.utilities.string import oneline
@@ -71,13 +71,15 @@ class Acknowledge:
charset = language_manager[language.code].charset
# Now get the acknowledgement template.
realname = mlist.real_name
- text = Utils.maketext(
- 'postack.txt',
- {'subject' : oneline(original_subject, charset),
- 'listname' : realname,
- 'listinfo_url': mlist.script_url('listinfo'),
- 'optionsurl' : member.options_url,
- }, lang=language.code, mlist=mlist, raw=True)
+ text = make('postack.txt',
+ mailing_list=mlist,
+ language=language.code,
+ wrap=False,
+ subject=oneline(original_subject, charset),
+ listname=realname,
+ listinfo_url=mlist.script_url('listinfo'),
+ optionsurl=member.options_url,
+ )
# Craft the outgoing message, with all headers and attributes
# necessary for general delivery. Then enqueue it to the outgoing
# queue.
diff --git a/src/mailman/queue/digest.py b/src/mailman/queue/digest.py
index a2feff448..2541e14ed 100644
--- a/src/mailman/queue/digest.py
+++ b/src/mailman/queue/digest.py
@@ -38,7 +38,7 @@ from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import formatdate, getaddresses, make_msgid
-from mailman.Utils import maketext, wrap
+from mailman.Utils import wrap
from mailman.config import config
from mailman.core.errors import DiscardMessage
from mailman.core.i18n import _
@@ -46,6 +46,7 @@ from mailman.interfaces.member import DeliveryMode, DeliveryStatus
from mailman.pipeline.decorate import decorate
from mailman.pipeline.scrubber import process as scrubber
from mailman.queue import Runner
+from mailman.utilities.i18n import make
from mailman.utilities.mailbox import Mailbox
from mailman.utilities.string import oneline
@@ -76,15 +77,14 @@ class Digester:
# digest header are separate MIME subobjects. In either case, it's
# the first thing in the digest, and we can calculate it now, so go
# ahead and add it now.
- self._masthead = maketext(
- 'masthead.txt', dict(
- real_name=mlist.real_name,
- got_list_email=mlist.posting_address,
- got_listinfo_url=mlist.script_url('listinfo'),
- got_request_email=mlist.request_address,
- got_owner_email=mlist.owner_address,
- ),
- mlist=mlist)
+ self._masthead = make('masthead.txt',
+ mailing_list=mlist,
+ real_name=mlist.real_name,
+ got_list_email=mlist.posting_address,
+ got_listinfo_url=mlist.script_url('listinfo'),
+ got_request_email=mlist.request_address,
+ got_owner_email=mlist.owner_address,
+ )
# Set things up for the table of contents.
self._header = decorate(mlist, mlist.digest_header)
self._toc = StringIO()
diff --git a/src/mailman/utilities/i18n.py b/src/mailman/utilities/i18n.py
new file mode 100644
index 000000000..afaf26bc0
--- /dev/null
+++ b/src/mailman/utilities/i18n.py
@@ -0,0 +1,200 @@
+# Copyright (C) 2011 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""i18n template search and interpolation."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'TemplateNotFoundError',
+ 'find',
+ 'make',
+ ]
+
+
+import os
+import errno
+
+from itertools import product
+from zope.component import getUtility
+
+from mailman.config import config
+from mailman.core.constants import system_preferences
+from mailman.core.i18n import _
+from mailman.interfaces.languages import ILanguageManager
+from mailman.utilities.string import expand
+
+from mailman.Utils import wrap as wrap_text
+
+
+
+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(template_file, mailing_list=None, language=None, wrap=True, **kw):
+ """Locate and 'make' a template file.
+
+ The template file is located as with `find()`, and the resulting text is
+ optionally wrapped and interpolated with the keyword argument dictionary.
+
+ :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
+ :param wrap: When True, wrap the text.
+ :type wrap: bool
+ :param **kw: Keyword arguments for template interpolation.
+ :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.
+ """
+ path, fp = find(template_file, mailing_list, language)
+ try:
+ # XXX Removing the trailing newline is a hack carried over from
+ # Mailman 2. The (stripped) template text is then passed through the
+ # translation catalog. This ensures that the translated text is
+ # unicode, and also allows for volunteers to translate the templates
+ # into the language catalogs.
+ template = _(fp.read()[:-1])
+ finally:
+ fp.close()
+ assert isinstance(template, unicode), 'Translated template is not unicode'
+ text = expand(template, kw)
+ if wrap:
+ return wrap_text(text)
+ return text
diff --git a/src/mailman/utilities/tests/test_templates.py b/src/mailman/utilities/tests/test_templates.py
new file mode 100644
index 000000000..2de43ae3c
--- /dev/null
+++ b/src/mailman/utilities/tests/test_templates.py
@@ -0,0 +1,289 @@
+# Copyright (C) 2011 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""Testing i18n template search and interpolation."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'test_suite',
+ ]
+
+
+import os
+import shutil
+import tempfile
+import unittest
+
+from zope.component import getUtility
+
+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 TemplateNotFoundError, _search, find, make
+
+
+
+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):
+ self.template_dir = tempfile.mkdtemp()
+ config.push('template config', """\
+ [paths.testing]
+ template_dir: {0}
+ """.format(self.template_dir))
+ # The following MUST happen AFTER the push() above since pushing a new
+ # config also clears out the language manager.
+ 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:
+ 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:
+ 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:
+ 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:
+ 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):
+ 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):
+ 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):
+ 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):
+ 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')
+
+
+
+class TestMake(unittest.TestCase):
+ """Test template interpolation."""
+
+ layer = ConfigLayer
+
+ def setUp(self):
+ self.template_dir = tempfile.mkdtemp()
+ config.push('template config', """\
+ [paths.testing]
+ template_dir: {0}
+ """.format(self.template_dir))
+ # The following MUST happen AFTER the push() above since pushing a new
+ # config also clears out the language manager.
+ getUtility(ILanguageManager).add('xx', 'utf-8', 'Xlandia')
+ self.mlist = create_list('test@example.com')
+ self.mlist.preferred_language = 'xx'
+ # Populate the template directory with some samples.
+ self.xxdir = os.path.join(self.template_dir, 'xx')
+ os.mkdir(self.xxdir)
+ with open(os.path.join(self.xxdir, 'nosub.txt'), 'w') as fp:
+ print >> fp, """\
+This is a global template.
+It has no substitutions.
+It will be wrapped.
+"""
+ with open(os.path.join(self.xxdir, 'subs.txt'), 'w') as fp:
+ print >> fp, """\
+This is a $kind template.
+It has $howmany substitutions.
+It will be wrapped.
+"""
+ with open(os.path.join(self.xxdir, 'nowrap.txt'), 'w') as fp:
+ print >> fp, """\
+This is a $kind template.
+It has $howmany substitutions.
+It will not be wrapped.
+"""
+
+ def tearDown(self):
+ config.pop('template config')
+ shutil.rmtree(self.template_dir)
+
+ def test_no_substitutions(self):
+ self.assertEqual(make('nosub.txt', self.mlist), """\
+This is a global template. It has no substitutions. It will be
+wrapped.
+""")
+
+ def test_substitutions(self):
+ self.assertEqual(make('subs.txt', self.mlist,
+ kind='very nice',
+ howmany='a few'), """\
+This is a very nice template. It has a few substitutions. It will be
+wrapped.
+""")
+
+ def test_substitutions_no_wrap(self):
+ self.assertEqual(make('nowrap.txt', self.mlist, wrap=False,
+ kind='very nice',
+ howmany='a few'), """\
+This is a very nice template.
+It has a few substitutions.
+It will not be wrapped.
+""")
+
+
+
+def test_suite():
+ suite = unittest.TestSuite()
+ suite.addTest(unittest.makeSuite(TestSearchOrder))
+ suite.addTest(unittest.makeSuite(TestFind))
+ suite.addTest(unittest.makeSuite(TestMake))
+ return suite