aboutsummaryrefslogtreecommitdiffstats
path: root/mediagoblin/tools
diff options
context:
space:
mode:
Diffstat (limited to 'mediagoblin/tools')
-rw-r--r--mediagoblin/tools/crypto.py113
-rw-r--r--mediagoblin/tools/exif.py2
-rw-r--r--mediagoblin/tools/mail.py22
-rw-r--r--mediagoblin/tools/pluginapi.py93
-rw-r--r--mediagoblin/tools/processing.py2
-rw-r--r--mediagoblin/tools/request.py2
-rw-r--r--mediagoblin/tools/response.py9
-rw-r--r--mediagoblin/tools/routing.py3
-rw-r--r--mediagoblin/tools/session.py75
-rw-r--r--mediagoblin/tools/staticdirect.py38
-rw-r--r--mediagoblin/tools/template.py30
-rw-r--r--mediagoblin/tools/timesince.py95
-rw-r--r--mediagoblin/tools/translate.py45
13 files changed, 515 insertions, 14 deletions
diff --git a/mediagoblin/tools/crypto.py b/mediagoblin/tools/crypto.py
new file mode 100644
index 00000000..1379d21b
--- /dev/null
+++ b/mediagoblin/tools/crypto.py
@@ -0,0 +1,113 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2013 MediaGoblin contributors. See AUTHORS.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import errno
+import itsdangerous
+import logging
+import os.path
+import random
+import tempfile
+from mediagoblin import mg_globals
+
+_log = logging.getLogger(__name__)
+
+
+# Use the system (hardware-based) random number generator if it exists.
+# -- this optimization is lifted from Django
+try:
+ getrandbits = random.SystemRandom().getrandbits
+except AttributeError:
+ getrandbits = random.getrandbits
+
+
+__itsda_secret = None
+
+
+def load_key(filename):
+ global __itsda_secret
+ key_file = open(filename)
+ try:
+ __itsda_secret = key_file.read()
+ finally:
+ key_file.close()
+
+
+def create_key(key_dir, key_filepath):
+ global __itsda_secret
+ old_umask = os.umask(077)
+ key_file = None
+ try:
+ if not os.path.isdir(key_dir):
+ os.makedirs(key_dir)
+ _log.info("Created %s", key_dir)
+ key = str(getrandbits(192))
+ key_file = tempfile.NamedTemporaryFile(dir=key_dir, suffix='.bin',
+ delete=False)
+ key_file.write(key)
+ key_file.flush()
+ os.rename(key_file.name, key_filepath)
+ key_file.close()
+ finally:
+ os.umask(old_umask)
+ if (key_file is not None) and (not key_file.closed):
+ key_file.close()
+ os.unlink(key_file.name)
+ __itsda_secret = key
+ _log.info("Saved new key for It's Dangerous")
+
+
+def setup_crypto():
+ global __itsda_secret
+ key_dir = mg_globals.app_config["crypto_path"]
+ key_filepath = os.path.join(key_dir, 'itsdangeroussecret.bin')
+ try:
+ load_key(key_filepath)
+ except IOError, error:
+ if error.errno != errno.ENOENT:
+ raise
+ create_key(key_dir, key_filepath)
+
+
+def get_timed_signer_url(namespace):
+ """
+ This gives a basic signing/verifying object.
+
+ The namespace makes sure signed tokens can't be used in
+ a different area. Like using a forgot-password-token as
+ a session cookie.
+
+ Basic usage:
+
+ .. code-block:: python
+
+ _signer = None
+ TOKEN_VALID_DAYS = 10
+ def setup():
+ global _signer
+ _signer = get_timed_signer_url("session cookie")
+ def create_token(obj):
+ return _signer.dumps(obj)
+ def parse_token(token):
+ # This might raise an exception in case
+ # of an invalid token, or an expired token.
+ return _signer.loads(token, max_age=TOKEN_VALID_DAYS*24*3600)
+
+ For more details see
+ http://pythonhosted.org/itsdangerous/#itsdangerous.URLSafeTimedSerializer
+ """
+ assert __itsda_secret is not None
+ return itsdangerous.URLSafeTimedSerializer(__itsda_secret,
+ salt=namespace)
diff --git a/mediagoblin/tools/exif.py b/mediagoblin/tools/exif.py
index d0f9d0a6..6b3639e8 100644
--- a/mediagoblin/tools/exif.py
+++ b/mediagoblin/tools/exif.py
@@ -134,7 +134,7 @@ def _ratio_to_list(ratio):
def get_useful(tags):
- return dict((key, tag) for (key, tag) in tags.iteritems() if key in USEFUL_TAGS)
+ return dict((key, tag) for (key, tag) in tags.iteritems())
def get_gps_data(tags):
diff --git a/mediagoblin/tools/mail.py b/mediagoblin/tools/mail.py
index 4fa02ce5..0fabc5a9 100644
--- a/mediagoblin/tools/mail.py
+++ b/mediagoblin/tools/mail.py
@@ -16,7 +16,7 @@
import smtplib
from email.MIMEText import MIMEText
-from mediagoblin import mg_globals
+from mediagoblin import mg_globals, messages
from mediagoblin.tools import common
### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -90,7 +90,12 @@ def send_email(from_addr, to_addrs, subject, message_body):
if common.TESTS_ENABLED or mg_globals.app_config['email_debug_mode']:
mhost = FakeMhost()
elif not mg_globals.app_config['email_debug_mode']:
- mhost = smtplib.SMTP(
+ if mg_globals.app_config['email_smtp_use_ssl']:
+ smtp_init = smtplib.SMTP_SSL
+ else:
+ smtp_init = smtplib.SMTP
+
+ mhost = smtp_init(
mg_globals.app_config['email_smtp_host'],
mg_globals.app_config['email_smtp_port'])
@@ -135,3 +140,16 @@ def normalize_email(email):
return None
email = "@".join((em_user, em_dom.lower()))
return email
+
+
+def email_debug_message(request):
+ """
+ If the server is running in email debug mode (which is
+ the current default), give a debug message to the user
+ so that they have an idea where to find their email.
+ """
+ if mg_globals.app_config['email_debug_mode']:
+ # DEBUG message, no need to translate
+ messages.add_message(request, messages.DEBUG,
+ u"This instance is running in email debug mode. "
+ u"The email will be on the console of the server process.")
diff --git a/mediagoblin/tools/pluginapi.py b/mediagoblin/tools/pluginapi.py
index 784bede9..3f98aa8a 100644
--- a/mediagoblin/tools/pluginapi.py
+++ b/mediagoblin/tools/pluginapi.py
@@ -272,3 +272,96 @@ def get_hook_templates(hook_name):
A list of strings representing template paths.
"""
return PluginManager().get_template_hooks(hook_name)
+
+
+#############################
+## Hooks: The Next Generation
+#############################
+
+
+def hook_handle(hook_name, *args, **kwargs):
+ """
+ Run through hooks attempting to find one that handle this hook.
+
+ All callables called with the same arguments until one handles
+ things and returns a non-None value.
+
+ (If you are writing a handler and you don't have a particularly
+ useful value to return even though you've handled this, returning
+ True is a good solution.)
+
+ Note that there is a special keyword argument:
+ if "default_handler" is passed in as a keyword argument, this will
+ be used if no handler is found.
+
+ Some examples of using this:
+ - You need an interface implemented, but only one fit for it
+ - You need to *do* something, but only one thing needs to do it.
+ """
+ default_handler = kwargs.pop('default_handler', None)
+
+ callables = PluginManager().get_hook_callables(hook_name)
+
+ result = None
+
+ for callable in callables:
+ result = callable(*args, **kwargs)
+
+ if result is not None:
+ break
+
+ if result is None and default_handler is not None:
+ result = default_handler(*args, **kwargs)
+
+ return result
+
+
+def hook_runall(hook_name, *args, **kwargs):
+ """
+ Run through all callable hooks and pass in arguments.
+
+ All non-None results are accrued in a list and returned from this.
+ (Other "false-like" values like False and friends are still
+ accrued, however.)
+
+ Some examples of using this:
+ - You have an interface call where actually multiple things can
+ and should implement it
+ - You need to get a list of things from various plugins that
+ handle them and do something with them
+ - You need to *do* something, and actually multiple plugins need
+ to do it separately
+ """
+ callables = PluginManager().get_hook_callables(hook_name)
+
+ results = []
+
+ for callable in callables:
+ result = callable(*args, **kwargs)
+
+ if result is not None:
+ results.append(result)
+
+ return results
+
+
+def hook_transform(hook_name, arg):
+ """
+ Run through a bunch of hook callables and transform some input.
+
+ Note that unlike the other hook tools, this one only takes ONE
+ argument. This argument is passed to each function, which in turn
+ returns something that becomes the input of the next callable.
+
+ Some examples of using this:
+ - You have an object, say a form, but you want plugins to each be
+ able to modify it.
+ """
+ result = arg
+
+ callables = PluginManager().get_hook_callables(hook_name)
+
+ for callable in callables:
+ result = callable(result)
+
+ return result
diff --git a/mediagoblin/tools/processing.py b/mediagoblin/tools/processing.py
index cff4cb9d..2abe6452 100644
--- a/mediagoblin/tools/processing.py
+++ b/mediagoblin/tools/processing.py
@@ -21,8 +21,6 @@ import traceback
from urllib2 import urlopen, Request, HTTPError
from urllib import urlencode
-from mediagoblin.tools.common import TESTS_ENABLED
-
_log = logging.getLogger(__name__)
TESTS_CALLBACKS = {}
diff --git a/mediagoblin/tools/request.py b/mediagoblin/tools/request.py
index bc67b96f..ee342eae 100644
--- a/mediagoblin/tools/request.py
+++ b/mediagoblin/tools/request.py
@@ -35,4 +35,4 @@ def setup_user_in_request(request):
# Something's wrong... this user doesn't exist? Invalidate
# this session.
_log.warn("Killing session for user id %r", request.session['user_id'])
- request.session.invalidate()
+ request.session.delete()
diff --git a/mediagoblin/tools/response.py b/mediagoblin/tools/response.py
index 80df1f5a..0be1f835 100644
--- a/mediagoblin/tools/response.py
+++ b/mediagoblin/tools/response.py
@@ -77,7 +77,7 @@ def render_http_exception(request, exc, description):
elif stock_desc and exc.code == 404:
return render_404(request)
- return render_error(request, title=exc.args[0],
+ return render_error(request, title='{0} {1}'.format(exc.code, exc.name),
err_msg=description,
status=exc.code)
@@ -99,3 +99,10 @@ def redirect(request, *args, **kwargs):
if querystring:
location += querystring
return werkzeug.utils.redirect(location)
+
+
+def redirect_obj(request, obj):
+ """Redirect to the page for the given object.
+
+ Requires obj to have a .url_for_self method."""
+ return redirect(request, location=obj.url_for_self(request.urlgen))
diff --git a/mediagoblin/tools/routing.py b/mediagoblin/tools/routing.py
index 791cd1e6..a15795fe 100644
--- a/mediagoblin/tools/routing.py
+++ b/mediagoblin/tools/routing.py
@@ -16,6 +16,7 @@
import logging
+import six
from werkzeug.routing import Map, Rule
from mediagoblin.tools.common import import_component
@@ -43,7 +44,7 @@ def endpoint_to_controller(rule):
_log.debug('endpoint: {0} view_func: {1}'.format(endpoint, view_func))
# import the endpoint, or if it's already a callable, call that
- if isinstance(view_func, basestring):
+ if isinstance(view_func, six.string_types):
view_func = import_component(view_func)
rule.gmg_controller = view_func
diff --git a/mediagoblin/tools/session.py b/mediagoblin/tools/session.py
new file mode 100644
index 00000000..a57f69cc
--- /dev/null
+++ b/mediagoblin/tools/session.py
@@ -0,0 +1,75 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2013 MediaGoblin contributors. See AUTHORS.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import itsdangerous
+import logging
+
+from mediagoblin.tools import crypto
+
+_log = logging.getLogger(__name__)
+
+MAX_AGE = 30 * 24 * 60 * 60
+
+class Session(dict):
+ def __init__(self, *args, **kwargs):
+ self.send_new_cookie = False
+ dict.__init__(self, *args, **kwargs)
+
+ def save(self):
+ self.send_new_cookie = True
+
+ def is_updated(self):
+ return self.send_new_cookie
+
+ def delete(self):
+ self.clear()
+ self.save()
+
+
+class SessionManager(object):
+ def __init__(self, cookie_name='MGSession', namespace=None):
+ if namespace is None:
+ namespace = cookie_name
+ self.signer = crypto.get_timed_signer_url(namespace)
+ self.cookie_name = cookie_name
+
+ def load_session_from_cookie(self, request):
+ cookie = request.cookies.get(self.cookie_name)
+ if not cookie:
+ return Session()
+ ### FIXME: Future cookie-blacklisting code
+ # m = BadCookie.query.filter_by(cookie = cookie)
+ # if m:
+ # _log.warn("Bad cookie received: %s", m.reason)
+ # raise BadRequest()
+ try:
+ return Session(self.signer.loads(cookie))
+ except itsdangerous.BadData:
+ return Session()
+
+ def save_session_to_cookie(self, session, request, response):
+ if not session.is_updated():
+ return
+ elif not session:
+ response.delete_cookie(self.cookie_name)
+ else:
+ if session.get('stay_logged_in', False):
+ max_age = MAX_AGE
+ else:
+ max_age = None
+
+ response.set_cookie(self.cookie_name, self.signer.dumps(session),
+ max_age=max_age, httponly=True)
diff --git a/mediagoblin/tools/staticdirect.py b/mediagoblin/tools/staticdirect.py
index 31abc566..ef8b20d0 100644
--- a/mediagoblin/tools/staticdirect.py
+++ b/mediagoblin/tools/staticdirect.py
@@ -61,3 +61,41 @@ class StaticDirect(object):
def get(self, filepath, domain=None):
return '%s/%s' % (
self.domains[domain], filepath.lstrip('/'))
+
+
+class PluginStatic(object):
+ """Pass this into the ``'static_setup'`` hook to register your
+ plugin's static directory.
+
+ This has two mandatory attributes that you must pass in on class
+ init:
+ - name: this name will be both used for lookup in "urlgen" for
+ your plugin's static resources and for the subdirectory that
+ it'll be "mounted" to for serving via your web browser. It
+ *MUST* be unique. If writing a plugin bundled with MediaGoblin
+ please use the pattern 'coreplugin__foo' where 'foo' is your
+ plugin name. All external plugins should use their modulename,
+ so if your plugin is 'mg_bettertags' you should also call this
+ name 'mg_bettertags'.
+ - file_path: the directory your plugin's static resources are
+ located in. It's recommended that you use
+ pkg_resources.resource_filename() for this.
+
+ An example of using this::
+
+ from pkg_resources import resource_filename
+ from mediagoblin.tools.staticdirect import PluginStatic
+
+ hooks = {
+ 'static_setup': lambda: PluginStatic(
+ 'mg_bettertags',
+ resource_filename('mg_bettertags', 'static'))
+ }
+
+ """
+ def __init__(self, name, file_path):
+ self.name = name
+ self.file_path = file_path
+
+ def __call__(self):
+ return self
diff --git a/mediagoblin/tools/template.py b/mediagoblin/tools/template.py
index 74d811eb..615ce129 100644
--- a/mediagoblin/tools/template.py
+++ b/mediagoblin/tools/template.py
@@ -14,7 +14,6 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-from math import ceil
import jinja2
from jinja2.ext import Extension
@@ -27,11 +26,13 @@ from mediagoblin import mg_globals
from mediagoblin import messages
from mediagoblin import _version
from mediagoblin.tools import common
-from mediagoblin.tools.translate import get_gettext_translation
-from mediagoblin.tools.pluginapi import get_hook_templates
+from mediagoblin.tools.translate import set_thread_locale
+from mediagoblin.tools.pluginapi import get_hook_templates, hook_transform
+from mediagoblin.tools.timesince import timesince
from mediagoblin.meddleware.csrf import render_csrf_form_token
+
SETUP_JINJA_ENVS = {}
@@ -42,7 +43,7 @@ def get_jinja_env(template_loader, locale):
(In the future we may have another system for providing theming;
for now this is good enough.)
"""
- mg_globals.thread_scope.translations = get_gettext_translation(locale)
+ set_thread_locale(locale)
# If we have a jinja environment set up with this locale, just
# return that one.
@@ -70,12 +71,19 @@ def get_jinja_env(template_loader, locale):
template_env.globals['app_config'] = mg_globals.app_config
template_env.globals['global_config'] = mg_globals.global_config
template_env.globals['version'] = _version.__version__
+ template_env.globals['auth'] = mg_globals.app.auth
template_env.filters['urlencode'] = url_quote_plus
+ # add human readable fuzzy date time
+ template_env.globals['timesince'] = timesince
+
# allow for hooking up plugin templates
template_env.globals['get_hook_templates'] = get_hook_templates
+ template_env.globals = hook_transform(
+ 'template_global_context', template_env.globals)
+
if exists(locale):
SETUP_JINJA_ENVS[locale] = template_env
@@ -99,6 +107,20 @@ def render_template(request, template_path, context):
rendered_csrf_token = render_csrf_form_token(request)
if rendered_csrf_token is not None:
context['csrf_token'] = render_csrf_form_token(request)
+
+ # allow plugins to do things to the context
+ if request.controller_name:
+ context = hook_transform(
+ (request.controller_name, template_path),
+ context)
+
+ # More evil: allow plugins to possibly do something to the context
+ # in every request ever with access to the request and other
+ # variables. Note: this is slower than using
+ # template_global_context
+ context = hook_transform(
+ 'template_context_prerender', context)
+
rendered = template.render(context)
if common.TESTS_ENABLED:
diff --git a/mediagoblin/tools/timesince.py b/mediagoblin/tools/timesince.py
new file mode 100644
index 00000000..b761c1be
--- /dev/null
+++ b/mediagoblin/tools/timesince.py
@@ -0,0 +1,95 @@
+# Copyright (c) Django Software Foundation and individual contributors.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# 3. Neither the name of Django nor the names of its contributors may be used
+# to endorse or promote products derived from this software without
+# specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import unicode_literals
+
+import datetime
+import pytz
+
+from mediagoblin.tools.translate import pass_to_ugettext, lazy_pass_to_ungettext as _
+
+"""UTC time zone as a tzinfo instance."""
+utc = pytz.utc if pytz else UTC()
+
+def is_aware(value):
+ """
+ Determines if a given datetime.datetime is aware.
+
+ The logic is described in Python's docs:
+ http://docs.python.org/library/datetime.html#datetime.tzinfo
+ """
+ return value.tzinfo is not None and value.tzinfo.utcoffset(value) is not None
+
+def timesince(d, now=None, reversed=False):
+ """
+ Takes two datetime objects and returns the time between d and now
+ as a nicely formatted string, e.g. "10 minutes". If d occurs after now,
+ then "0 minutes" is returned.
+
+ Units used are years, months, weeks, days, hours, and minutes.
+ Seconds and microseconds are ignored. Up to two adjacent units will be
+ displayed. For example, "2 weeks, 3 days" and "1 year, 3 months" are
+ possible outputs, but "2 weeks, 3 hours" and "1 year, 5 days" are not.
+
+ Adapted from http://blog.natbat.co.uk/archive/2003/Jun/14/time_since
+ """
+ chunks = (
+ (60 * 60 * 24 * 365, lambda n: _('year', 'years', n)),
+ (60 * 60 * 24 * 30, lambda n: _('month', 'months', n)),
+ (60 * 60 * 24 * 7, lambda n : _('week', 'weeks', n)),
+ (60 * 60 * 24, lambda n : _('day', 'days', n)),
+ (60 * 60, lambda n: _('hour', 'hours', n)),
+ (60, lambda n: _('minute', 'minutes', n))
+ )
+ # Convert datetime.date to datetime.datetime for comparison.
+ if not isinstance(d, datetime.datetime):
+ d = datetime.datetime(d.year, d.month, d.day)
+ if now and not isinstance(now, datetime.datetime):
+ now = datetime.datetime(now.year, now.month, now.day)
+
+ if not now:
+ now = datetime.datetime.now(utc if is_aware(d) else None)
+
+ delta = (d - now) if reversed else (now - d)
+ # ignore microseconds
+ since = delta.days * 24 * 60 * 60 + delta.seconds
+ if since <= 0:
+ # d is in the future compared to now, stop processing.
+ return '0 ' + pass_to_ugettext('minutes')
+ for i, (seconds, name) in enumerate(chunks):
+ count = since // seconds
+ if count != 0:
+ break
+ s = pass_to_ugettext('%(number)d %(type)s') % {'number': count, 'type': name(count)}
+ if i + 1 < len(chunks):
+ # Now get the second item
+ seconds2, name2 = chunks[i + 1]
+ count2 = (since - (seconds * count)) // seconds2
+ if count2 != 0:
+ s += pass_to_ugettext(', %(number)d %(type)s') % {'number': count2, 'type': name2(count2)}
+ return s
diff --git a/mediagoblin/tools/translate.py b/mediagoblin/tools/translate.py
index 1d37c4de..b20e57d1 100644
--- a/mediagoblin/tools/translate.py
+++ b/mediagoblin/tools/translate.py
@@ -42,6 +42,22 @@ def set_available_locales():
AVAILABLE_LOCALES = locales
+class ReallyLazyProxy(LazyProxy):
+ """
+ Like LazyProxy, except that it doesn't cache the value ;)
+ """
+ @property
+ def value(self):
+ return self._func(*self._args, **self._kwargs)
+
+ def __repr__(self):
+ return "<%s for %s(%r, %r)>" % (
+ self.__class__.__name__,
+ self._func,
+ self._args,
+ self._kwargs)
+
+
def locale_to_lower_upper(locale):
"""
Take a locale, regardless of style, and format it like "en_US"
@@ -112,6 +128,11 @@ def get_gettext_translation(locale):
return this_gettext
+def set_thread_locale(locale):
+ """Set the current translation for this thread"""
+ mg_globals.thread_scope.translations = get_gettext_translation(locale)
+
+
def pass_to_ugettext(*args, **kwargs):
"""
Pass a translation on to the appropriate ugettext method.
@@ -122,6 +143,16 @@ def pass_to_ugettext(*args, **kwargs):
return mg_globals.thread_scope.translations.ugettext(
*args, **kwargs)
+def pass_to_ungettext(*args, **kwargs):
+ """
+ Pass a translation on to the appropriate ungettext method.
+
+ The reason we can't have a global ugettext method is because
+ mg_globals gets swapped out by the application per-request.
+ """
+ return mg_globals.thread_scope.translations.ungettext(
+ *args, **kwargs)
+
def lazy_pass_to_ugettext(*args, **kwargs):
"""
@@ -134,7 +165,7 @@ def lazy_pass_to_ugettext(*args, **kwargs):
you would want to use the lazy version for _.
"""
- return LazyProxy(pass_to_ugettext, *args, **kwargs)
+ return ReallyLazyProxy(pass_to_ugettext, *args, **kwargs)
def pass_to_ngettext(*args, **kwargs):
@@ -156,7 +187,17 @@ def lazy_pass_to_ngettext(*args, **kwargs):
level but you need it to not translate until the time that it's
used as a string.
"""
- return LazyProxy(pass_to_ngettext, *args, **kwargs)
+ return ReallyLazyProxy(pass_to_ngettext, *args, **kwargs)
+
+def lazy_pass_to_ungettext(*args, **kwargs):
+ """
+ Lazily pass to ungettext.
+
+ This is useful if you have to define a translation on a module
+ level but you need it to not translate until the time that it's
+ used as a string.
+ """
+ return ReallyLazyProxy(pass_to_ungettext, *args, **kwargs)
def fake_ugettext_passthrough(string):