diff options
Diffstat (limited to 'mediagoblin/tools')
-rw-r--r-- | mediagoblin/tools/crypto.py | 12 | ||||
-rw-r--r-- | mediagoblin/tools/exif.py | 26 | ||||
-rw-r--r-- | mediagoblin/tools/federation.py | 94 | ||||
-rw-r--r-- | mediagoblin/tools/files.py | 4 | ||||
-rw-r--r-- | mediagoblin/tools/mail.py | 67 | ||||
-rw-r--r-- | mediagoblin/tools/metadata.py | 3 | ||||
-rw-r--r-- | mediagoblin/tools/pagination.py | 11 | ||||
-rw-r--r-- | mediagoblin/tools/processing.py | 15 | ||||
-rw-r--r-- | mediagoblin/tools/request.py | 30 | ||||
-rw-r--r-- | mediagoblin/tools/response.py | 16 | ||||
-rw-r--r-- | mediagoblin/tools/routing.py | 30 | ||||
-rw-r--r-- | mediagoblin/tools/staticdirect.py | 4 | ||||
-rw-r--r-- | mediagoblin/tools/template.py | 22 | ||||
-rw-r--r-- | mediagoblin/tools/text.py | 12 | ||||
-rw-r--r-- | mediagoblin/tools/theme.py | 8 | ||||
-rw-r--r-- | mediagoblin/tools/timesince.py | 14 | ||||
-rw-r--r-- | mediagoblin/tools/transition.py | 21 | ||||
-rw-r--r-- | mediagoblin/tools/translate.py | 17 | ||||
-rw-r--r-- | mediagoblin/tools/url.py | 4 | ||||
-rw-r--r-- | mediagoblin/tools/workbench.py | 10 |
20 files changed, 317 insertions, 103 deletions
diff --git a/mediagoblin/tools/crypto.py b/mediagoblin/tools/crypto.py index b219a484..1107e200 100644 --- a/mediagoblin/tools/crypto.py +++ b/mediagoblin/tools/crypto.py @@ -36,7 +36,7 @@ try: except AttributeError: getrandbits = random.getrandbits - +# TODO: This should be attached to the MediaGoblinApp __itsda_secret = None @@ -51,7 +51,7 @@ def load_key(filename): def create_key(key_dir, key_filepath): global __itsda_secret - old_umask = os.umask(077) + old_umask = os.umask(0o77) key_file = None try: if not os.path.isdir(key_dir): @@ -60,7 +60,7 @@ def create_key(key_dir, key_filepath): key = str(getrandbits(192)) key_file = tempfile.NamedTemporaryFile(dir=key_dir, suffix='.bin', delete=False) - key_file.write(key) + key_file.write(key.encode('ascii')) key_file.flush() os.rename(key_file.name, key_filepath) key_file.close() @@ -73,13 +73,13 @@ def create_key(key_dir, key_filepath): _log.info("Saved new key for It's Dangerous") -def setup_crypto(): +def setup_crypto(app_config): global __itsda_secret - key_dir = mg_globals.app_config["crypto_path"] + key_dir = app_config["crypto_path"] key_filepath = os.path.join(key_dir, 'itsdangeroussecret.bin') try: load_key(key_filepath) - except IOError, error: + except IOError as error: if error.errno != errno.ENOENT: raise create_key(key_dir, key_filepath) diff --git a/mediagoblin/tools/exif.py b/mediagoblin/tools/exif.py index 50f1aabf..fafd987d 100644 --- a/mediagoblin/tools/exif.py +++ b/mediagoblin/tools/exif.py @@ -14,6 +14,8 @@ # 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 six + from exifread import process_file from exifread.utils import Ratio @@ -75,7 +77,7 @@ def extract_exif(filename): Returns EXIF tags found in file at ``filename`` """ try: - with file(filename) as image: + with open(filename, 'rb') as image: return process_file(image, details=False) except IOError: raise BadMediaFail(_('Could not read the image file.')) @@ -94,7 +96,7 @@ def clean_exif(exif): 'Thumbnail JPEGInterchangeFormat'] return dict((key, _ifd_tag_to_dict(value)) for (key, value) - in exif.iteritems() if key not in disabled_tags) + in six.iteritems(exif) if key not in disabled_tags) def _ifd_tag_to_dict(tag): @@ -110,7 +112,7 @@ def _ifd_tag_to_dict(tag): 'field_length': tag.field_length, 'values': None} - if isinstance(tag.printable, str): + if isinstance(tag.printable, six.binary_type): # Force it to be decoded as UTF-8 so that it'll fit into the DB data['printable'] = tag.printable.decode('utf8', 'replace') @@ -118,7 +120,7 @@ def _ifd_tag_to_dict(tag): data['values'] = [_ratio_to_list(val) if isinstance(val, Ratio) else val for val in tag.values] else: - if isinstance(tag.values, str): + if isinstance(tag.values, six.binary_type): # Force UTF-8, so that it fits into the DB data['values'] = tag.values.decode('utf8', 'replace') else: @@ -132,13 +134,19 @@ def _ratio_to_list(ratio): def get_useful(tags): - return dict((key, tag) for (key, tag) in tags.iteritems()) + from collections import OrderedDict + return OrderedDict((key, tag) for (key, tag) in six.iteritems(tags)) def get_gps_data(tags): """ Processes EXIF data returned by EXIF.py """ + def safe_gps_ratio_divide(ratio): + if ratio.den == 0: + return 0.0 + return float(ratio.num) / float(ratio.den) + gps_data = {} if not 'Image GPSInfo' in tags: @@ -149,12 +157,12 @@ def get_gps_data(tags): 'latitude': tags['GPS GPSLatitude'], 'longitude': tags['GPS GPSLongitude']} - for key, dat in dms_data.iteritems(): + for key, dat in six.iteritems(dms_data): gps_data[key] = ( lambda v: - float(v[0].num) / float(v[0].den) \ - + (float(v[1].num) / float(v[1].den) / 60) \ - + (float(v[2].num) / float(v[2].den) / (60 * 60)) + safe_gps_ratio_divide(v[0]) \ + + (safe_gps_ratio_divide(v[1]) / 60) \ + + (safe_gps_ratio_divide(v[2]) / (60 * 60)) )(dat.values) if tags['GPS GPSLatitudeRef'].values == 'S': diff --git a/mediagoblin/tools/federation.py b/mediagoblin/tools/federation.py new file mode 100644 index 00000000..f2ee468c --- /dev/null +++ b/mediagoblin/tools/federation.py @@ -0,0 +1,94 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2014 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/>. + +from mediagoblin.db.models import Activity, Generator, User + +def create_generator(request): + """ + This creates a Generator object based on the Client associated with the + OAuth credentials used. If the request has invalid OAuth credentials or + no OAuth credentials None is returned. + """ + if not hasattr(request, "access_token"): + return None + + client = request.access_token.get_requesttoken.get_client + + # Check if there is a generator already + generator = Generator.query.filter_by( + name=client.application_name, + object_type="client" + ).first() + if generator is None: + generator = Generator( + name=client.application_name, + object_type="client" + ) + generator.save() + + return generator + + + +def create_activity(verb, obj, actor, target=None, generator=None): + """ + This will create an Activity object which for the obj if possible + and save it. The verb should be one of the following: + add, author, create, delete, dislike, favorite, follow + like, post, share, unfollow, unfavorite, unlike, unshare, + update, tag. + + If none of those fit you might not want/need to create an activity for + the object. The list is in mediagoblin.db.models.Activity.VALID_VERBS + """ + # exception when we try and generate an activity with an unknow verb + # could change later to allow arbitrary verbs but at the moment we'll play + # it safe. + + if verb not in Activity.VALID_VERBS: + raise ValueError("A invalid verb type has been supplied.") + + if generator is None: + # This should exist as we're creating it by the migration for Generator + generator = Generator.query.filter_by(name="GNU MediaGoblin").first() + if generator is None: + generator = Generator( + name="GNU MediaGoblin", + object_type="service" + ) + generator.save() + + # Ensure the object has an ID which is needed by the activity. + obj.save(commit=False) + + # Create the activity + activity = Activity(verb=verb) + activity.object = obj + + if target is not None: + activity.target = target + + # If they've set it override the actor from the obj. + activity.actor = actor.id if isinstance(actor, User) else actor + activity.generator = generator.id + activity.save() + + # Sigh want to do this prior to save but I can't figure a way to get + # around relationship() not looking up object when model isn't saved. + if activity.generate_content(): + activity.save() + + return activity diff --git a/mediagoblin/tools/files.py b/mediagoblin/tools/files.py index 848c86f2..2c486ac8 100644 --- a/mediagoblin/tools/files.py +++ b/mediagoblin/tools/files.py @@ -14,6 +14,8 @@ # 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 six + from mediagoblin import mg_globals @@ -25,7 +27,7 @@ def delete_media_files(media): - media: A MediaEntry document """ no_such_files = [] - for listpath in media.media_files.itervalues(): + for listpath in six.itervalues(media.media_files): try: mg_globals.public_store.delete_file( listpath) diff --git a/mediagoblin/tools/mail.py b/mediagoblin/tools/mail.py index 0fabc5a9..3dc180d8 100644 --- a/mediagoblin/tools/mail.py +++ b/mediagoblin/tools/mail.py @@ -14,9 +14,15 @@ # 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 __future__ import print_function, unicode_literals + +import socket +import logging +import six import smtplib -from email.MIMEText import MIMEText +import sys from mediagoblin import mg_globals, messages +from mediagoblin._compat import MIMEText from mediagoblin.tools import common ### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -50,6 +56,14 @@ EMAIL_TEST_INBOX = [] EMAIL_TEST_MBOX_INBOX = [] +class MailError(Exception): + """ General exception for mail errors """ + + +class NoSMTPServerError(MailError): + pass + + class FakeMhost(object): """ Just a fake mail host so we can capture and test messages @@ -64,6 +78,8 @@ class FakeMhost(object): 'to': to_addrs, 'message': message}) + def starttls(self): + raise smtplib.SMTPException("No STARTTLS here") def _clear_test_inboxes(): global EMAIL_TEST_INBOX @@ -95,13 +111,34 @@ def send_email(from_addr, to_addrs, subject, message_body): else: smtp_init = smtplib.SMTP - mhost = smtp_init( - mg_globals.app_config['email_smtp_host'], - mg_globals.app_config['email_smtp_port']) + try: + mhost = smtp_init( + mg_globals.app_config['email_smtp_host'], + mg_globals.app_config['email_smtp_port']) + except socket.error as original_error: + error_message = "Couldn't contact mail server on <{}>:<{}>".format( + mg_globals.app_config['email_smtp_host'], + mg_globals.app_config['email_smtp_port']) + logging.debug(original_error) + raise NoSMTPServerError(error_message) # SMTP.__init__ Issues SMTP.connect implicitly if host if not mg_globals.app_config['email_smtp_host']: # e.g. host = '' - mhost.connect() # We SMTP.connect explicitly + try: + mhost.connect() # We SMTP.connect explicitly + except socket.error as original_error: + error_message = "Couldn't contact mail server on <{}>:<{}>".format( + mg_globals.app_config['email_smtp_host'], + mg_globals.app_config['email_smtp_port']) + logging.debug(original_error) + raise NoSMTPServerError(error_message) + + try: + mhost.starttls() + except smtplib.SMTPException: + # Only raise an exception if we're forced to + if mg_globals.app_config['email_smtp_force_starttls']: + six.reraise(*sys.exc_info()) if ((not common.TESTS_ENABLED) and (mg_globals.app_config['email_smtp_user'] @@ -119,12 +156,12 @@ def send_email(from_addr, to_addrs, subject, message_body): EMAIL_TEST_INBOX.append(message) elif mg_globals.app_config['email_debug_mode']: - print u"===== Email =====" - print u"From address: %s" % message['From'] - print u"To addresses: %s" % message['To'] - print u"Subject: %s" % message['Subject'] - print u"-- Body: --" - print message.get_payload(decode=True) + print("===== Email =====") + print("From address: %s" % message['From']) + print("To addresses: %s" % message['To']) + print("Subject: %s" % message['Subject']) + print("-- Body: --") + print(message_body) return mhost.sendmail(from_addr, to_addrs, message.as_string()) @@ -150,6 +187,8 @@ def email_debug_message(request): """ 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.") + messages.add_message( + request, + messages.DEBUG, + "This instance is running in email debug mode. " + "The email will be on the console of the server process.") diff --git a/mediagoblin/tools/metadata.py b/mediagoblin/tools/metadata.py index bfefcac9..aeb4f829 100644 --- a/mediagoblin/tools/metadata.py +++ b/mediagoblin/tools/metadata.py @@ -15,6 +15,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. +from io import open import os import copy import json @@ -102,7 +103,7 @@ def load_resource(package, resource_path): os.path.sep. """ filename = resource_filename(package, os.path.sep.join(resource_path)) - return file(filename).read() + return open(filename, encoding="utf-8").read() def load_resource_json(package, resource_path): """ diff --git a/mediagoblin/tools/pagination.py b/mediagoblin/tools/pagination.py index 855878e0..db5f69fb 100644 --- a/mediagoblin/tools/pagination.py +++ b/mediagoblin/tools/pagination.py @@ -14,12 +14,13 @@ # 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 urllib import copy from math import ceil, floor -from itertools import izip, count +from itertools import count from werkzeug.datastructures import MultiDict +from six.moves import range, urllib, zip + PAGINATION_DEFAULT_PER_PAGE = 30 @@ -52,7 +53,7 @@ class Pagination(object): if jump_to_id: cursor = copy.copy(self.cursor) - for (doc, increment) in izip(cursor, count(0)): + for (doc, increment) in list(zip(cursor, count(0))): if doc.id == jump_to_id: self.page = 1 + int(floor(increment / self.per_page)) @@ -84,7 +85,7 @@ class Pagination(object): def iter_pages(self, left_edge=2, left_current=2, right_current=5, right_edge=2): last = 0 - for num in xrange(1, self.pages + 1): + for num in range(1, self.pages + 1): if num <= left_edge or \ (num > self.page - left_current - 1 and \ num < self.page + right_current) or \ @@ -105,7 +106,7 @@ class Pagination(object): new_get_params['page'] = page_no return "%s?%s" % ( - base_url, urllib.urlencode(new_get_params)) + base_url, urllib.parse.urlencode(new_get_params)) def get_page_url(self, request, page_no): """ diff --git a/mediagoblin/tools/processing.py b/mediagoblin/tools/processing.py index 2abe6452..26d7bb9b 100644 --- a/mediagoblin/tools/processing.py +++ b/mediagoblin/tools/processing.py @@ -18,8 +18,7 @@ import logging import json import traceback -from urllib2 import urlopen, Request, HTTPError -from urllib import urlencode +from six.moves.urllib import request, parse _log = logging.getLogger(__name__) @@ -37,10 +36,10 @@ def create_post_request(url, data, **kw): data_parser: The parser function that is used to parse the `data` argument ''' - data_parser = kw.get('data_parser', urlencode) + data_parser = kw.get('data_parser', parse.urlencode) headers = kw.get('headers', {}) - return Request(url, data_parser(data), headers=headers) + return request.Request(url, data_parser(data), headers=headers) def json_processing_callback(entry): @@ -48,12 +47,12 @@ def json_processing_callback(entry): Send an HTTP post to the registered callback url, if any. ''' if not entry.processing_metadata: - _log.debug('No processing callback for {0}'.format(entry)) + _log.debug('No processing callback URL for {0}'.format(entry)) return url = entry.processing_metadata[0].callback_url - _log.debug('Sending processing callback for {0} ({1})'.format( + _log.debug('Sending processing callback for {0} to {1}'.format( entry, url)) @@ -76,11 +75,11 @@ def json_processing_callback(entry): data_parser=json.dumps) try: - urlopen(request) + request.urlopen(request) _log.debug('Processing callback for {0} sent'.format(entry)) return True - except HTTPError: + except request.HTTPError: _log.error('Failed to send callback: {0}'.format( traceback.format_exc())) diff --git a/mediagoblin/tools/request.py b/mediagoblin/tools/request.py index d4739039..7e1973d3 100644 --- a/mediagoblin/tools/request.py +++ b/mediagoblin/tools/request.py @@ -16,7 +16,12 @@ import json import logging -from mediagoblin.db.models import User + +import six +from werkzeug.http import parse_options_header + +from mediagoblin.db.models import User, AccessToken +from mediagoblin.oauth.tools.request import decode_authorization_header _log = logging.getLogger(__name__) @@ -31,6 +36,18 @@ def setup_user_in_request(request): Examine a request and tack on a request.user parameter if that's appropriate. """ + # If API request the user will be associated with the access token + authorization = decode_authorization_header(request.headers) + + if authorization.get(u"access_token"): + # Check authorization header. + token = authorization[u"oauth_token"] + token = AccessToken.query.filter_by(token=token).first() + if token is not None: + request.user = token.user + return + + if 'user_id' not in request.session: request.user = None return @@ -45,11 +62,12 @@ def setup_user_in_request(request): def decode_request(request): """ Decodes a request based on MIME-Type """ - data = request.get_data() - - if request.content_type == json_encoded: - data = json.loads(data) - elif request.content_type == form_encoded or request.content_type == "": + data = request.data + content_type, _ = parse_options_header(request.content_type) + + if content_type == json_encoded: + data = json.loads(six.text_type(data, "utf-8")) + elif content_type == form_encoded or content_type == "": data = request.form else: data = "" diff --git a/mediagoblin/tools/response.py b/mediagoblin/tools/response.py index cd99a230..889938a8 100644 --- a/mediagoblin/tools/response.py +++ b/mediagoblin/tools/response.py @@ -16,6 +16,7 @@ import json +import six import werkzeug.utils from werkzeug.wrappers import Response as wz_Response from mediagoblin.tools.template import render_template @@ -29,11 +30,12 @@ class Response(wz_Response): default_mimetype = u'text/html' -def render_to_response(request, template, context, status=200): +def render_to_response(request, template, context, status=200, mimetype=None): """Much like Django's shortcut.render()""" return Response( render_template(request, template, context), - status=status) + status=status, + mimetype=mimetype) def render_error(request, status=500, title=_('Oops!'), err_msg=_('An error occured')): @@ -152,11 +154,19 @@ def json_response(serializable, _disable_cors=False, *args, **kw): 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, X-Requested-With'} - for key, value in cors_headers.iteritems(): + for key, value in six.iteritems(cors_headers): response.headers.set(key, value) return response +def json_error(error_str, status=400, *args, **kwargs): + """ + This is like json_response but takes an error message in and formats + it in {"error": error_str}. This also sets the default HTTP status + code to 400. + """ + return json_response({"error": error_str}, status=status, *args, **kwargs) + def form_response(data, *args, **kwargs): """ Responds using application/x-www-form-urlencoded and returns a werkzeug diff --git a/mediagoblin/tools/routing.py b/mediagoblin/tools/routing.py index a15795fe..a2c89f2a 100644 --- a/mediagoblin/tools/routing.py +++ b/mediagoblin/tools/routing.py @@ -17,7 +17,10 @@ import logging import six + +from six.moves.urllib.parse import urlparse from werkzeug.routing import Map, Rule + from mediagoblin.tools.common import import_component @@ -27,15 +30,22 @@ url_map = Map() class MGRoute(Rule): - def __init__(self, endpoint, url, controller): + def __init__(self, endpoint, url, controller, match_slash=True): Rule.__init__(self, url, endpoint=endpoint) self.gmg_controller = controller + self.match_slash = match_slash def empty(self): new_rule = Rule.empty(self) new_rule.gmg_controller = self.gmg_controller return new_rule + def match(self, path, *args, **kwargs): + if not (self.match_slash or path.endswith("/")): + path = path + "/" + + return super(MGRoute, self).match(path, *args, **kwargs) + def endpoint_to_controller(rule): endpoint = rule.endpoint @@ -51,11 +61,11 @@ def endpoint_to_controller(rule): return view_func -def add_route(endpoint, url, controller): +def add_route(endpoint, url, controller, *args, **kwargs): """ Add a route to the url mapping """ - url_map.add(MGRoute(endpoint, url, controller)) + url_map.add(MGRoute(endpoint, url, controller, *args, **kwargs)) def mount(mountpoint, routes): @@ -65,3 +75,17 @@ def mount(mountpoint, routes): for endpoint, url, controller in routes: url = "%s/%s" % (mountpoint.rstrip('/'), url.lstrip('/')) add_route(endpoint, url, controller) + +def extract_url_arguments(url, urlmap): + """ + This extracts the URL arguments from a given URL + """ + parsed_url = urlparse(url) + map_adapter = urlmap.bind( + server_name=parsed_url.netloc, + script_name=parsed_url.path, + url_scheme=parsed_url.scheme, + path_info=parsed_url.path + ) + + return map_adapter.match()[1] diff --git a/mediagoblin/tools/staticdirect.py b/mediagoblin/tools/staticdirect.py index 8381b8b6..881dd20e 100644 --- a/mediagoblin/tools/staticdirect.py +++ b/mediagoblin/tools/staticdirect.py @@ -24,6 +24,8 @@ import logging +import six + _log = logging.getLogger(__name__) @@ -48,7 +50,7 @@ class StaticDirect(object): def __init__(self, domains): self.domains = dict( [(key, value.rstrip('/')) - for key, value in domains.iteritems()]) + for key, value in six.iteritems(domains)]) self.cache = {} def __call__(self, filepath, domain=None): diff --git a/mediagoblin/tools/template.py b/mediagoblin/tools/template.py index e5acdf45..f2619808 100644 --- a/mediagoblin/tools/template.py +++ b/mediagoblin/tools/template.py @@ -14,6 +14,7 @@ # 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 six import jinja2 from jinja2.ext import Extension @@ -28,16 +29,14 @@ from mediagoblin import _version from mediagoblin.tools import common from mediagoblin.tools.translate import is_rtl from mediagoblin.tools.translate import set_thread_locale -from mediagoblin.tools.translate import get_locale_from_request 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 = {} -def get_jinja_env(template_loader, locale): +def get_jinja_env(app, template_loader, locale): """ Set up the Jinja environment, @@ -54,7 +53,7 @@ def get_jinja_env(template_loader, locale): # The default config does not require a [jinja2] block. # You may create one if you wish to enable additional jinja2 extensions, # see example in config_spec.ini - jinja2_config = mg_globals.global_config.get('jinja2', {}) + jinja2_config = app.global_config.get('jinja2', {}) local_exts = jinja2_config.get('extensions', []) # jinja2.StrictUndefined will give exceptions on references @@ -66,9 +65,12 @@ def get_jinja_env(template_loader, locale): 'jinja2.ext.i18n', 'jinja2.ext.autoescape', TemplateHookExtension] + local_exts) - template_env.install_gettext_callables( - mg_globals.thread_scope.translations.ugettext, - mg_globals.thread_scope.translations.ungettext) + if six.PY2: + template_env.install_gettext_callables(mg_globals.thread_scope.translations.ugettext, + mg_globals.thread_scope.translations.ungettext) + else: + template_env.install_gettext_callables(mg_globals.thread_scope.translations.gettext, + mg_globals.thread_scope.translations.ngettext) # All templates will know how to ... # ... fetch all waiting messages and remove them from the queue @@ -76,10 +78,10 @@ def get_jinja_env(template_loader, locale): # ... have access to the global and app config # ... determine if the language is rtl or ltr template_env.globals['fetch_messages'] = messages.fetch_messages - template_env.globals['app_config'] = mg_globals.app_config - template_env.globals['global_config'] = mg_globals.global_config + template_env.globals['app_config'] = app.app_config + template_env.globals['global_config'] = app.global_config template_env.globals['version'] = _version.__version__ - template_env.globals['auth'] = mg_globals.app.auth + template_env.globals['auth'] = app.auth template_env.globals['is_rtl'] = is_rtl(locale) template_env.filters['urlencode'] = url_quote_plus diff --git a/mediagoblin/tools/text.py b/mediagoblin/tools/text.py index 96df49d2..48a53d23 100644 --- a/mediagoblin/tools/text.py +++ b/mediagoblin/tools/text.py @@ -14,6 +14,7 @@ # 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 collections import wtforms import markdown from lxml.html.clean import Cleaner @@ -60,7 +61,7 @@ def convert_to_tag_list_of_dicts(tag_string): Strips trailing, leading, and internal whitespace, and also converts the "tags" text into an array of tags """ - taglist = [] + slug_to_name = collections.OrderedDict() if tag_string: # Strip out internal, trailing, and leading whitespace @@ -69,11 +70,10 @@ def convert_to_tag_list_of_dicts(tag_string): # Split the tag string into a list of tags for tag in stripped_tag_string.split(','): tag = tag.strip() - # Ignore empty or duplicate tags - if tag and tag not in [t['name'] for t in taglist]: - taglist.append({'name': tag, - 'slug': url.slugify(tag)}) - return taglist + # Ignore empty tags or duplicate slugs + if tag: + slug_to_name[url.slugify(tag)] = tag + return [{'name': v, 'slug': k} for (k,v) in slug_to_name.items()] def media_tags_as_string(media_entry_tags): diff --git a/mediagoblin/tools/theme.py b/mediagoblin/tools/theme.py index 97b041a6..79fd91e1 100644 --- a/mediagoblin/tools/theme.py +++ b/mediagoblin/tools/theme.py @@ -68,7 +68,7 @@ def register_themes(app_config, builtin_dir=BUILTIN_THEME_DIR): themedata = themedata_for_theme_dir(themedir, abs_themedir) registry[themedir] = themedata - + # Built-in themes if os.path.exists(builtin_dir): _install_themes_in_dir(builtin_dir) @@ -79,11 +79,9 @@ def register_themes(app_config, builtin_dir=BUILTIN_THEME_DIR): _install_themes_in_dir(theme_install_dir) current_theme_name = app_config.get('theme') - if current_theme_name \ - and registry.has_key(current_theme_name): + try: current_theme = registry[current_theme_name] - else: + except KeyError: current_theme = None return registry, current_theme - diff --git a/mediagoblin/tools/timesince.py b/mediagoblin/tools/timesince.py index b761c1be..7a8b3ff0 100644 --- a/mediagoblin/tools/timesince.py +++ b/mediagoblin/tools/timesince.py @@ -33,18 +33,6 @@ 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 @@ -73,7 +61,7 @@ def timesince(d, now=None, reversed=False): now = datetime.datetime(now.year, now.month, now.day) if not now: - now = datetime.datetime.now(utc if is_aware(d) else None) + now = datetime.datetime.utcnow() delta = (d - now) if reversed else (now - d) # ignore microseconds diff --git a/mediagoblin/tools/transition.py b/mediagoblin/tools/transition.py new file mode 100644 index 00000000..a8041b89 --- /dev/null +++ b/mediagoblin/tools/transition.py @@ -0,0 +1,21 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 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 os + +# one global to disable them all +DISABLE_GLOBALS = os.environ.get("DISABLE_GLOBALS", "false").lower() == "true" diff --git a/mediagoblin/tools/translate.py b/mediagoblin/tools/translate.py index f77351b5..a5e56cfe 100644 --- a/mediagoblin/tools/translate.py +++ b/mediagoblin/tools/translate.py @@ -17,6 +17,7 @@ import gettext import pkg_resources +import six from babel import localedata from babel.support import LazyProxy @@ -52,9 +53,9 @@ 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 __init__(self, func, *args, **kwargs): + super(ReallyLazyProxy, self).__init__(func, *args, **kwargs) + object.__setattr__(self, '_is_cache_enabled', False) def __repr__(self): return "<%s for %s(%r, %r)>" % ( @@ -146,8 +147,9 @@ def pass_to_ugettext(*args, **kwargs): 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.ugettext( - *args, **kwargs) + if six.PY2: + return mg_globals.thread_scope.translations.ugettext(*args, **kwargs) + return mg_globals.thread_scope.translations.gettext(*args, **kwargs) def pass_to_ungettext(*args, **kwargs): """ @@ -156,8 +158,9 @@ def pass_to_ungettext(*args, **kwargs): 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) + if six.PY2: + return mg_globals.thread_scope.translations.ungettext(*args, **kwargs) + return mg_globals.thread_scope.translations.ngettext(*args, **kwargs) def lazy_pass_to_ugettext(*args, **kwargs): diff --git a/mediagoblin/tools/url.py b/mediagoblin/tools/url.py index 657c0373..4d97247a 100644 --- a/mediagoblin/tools/url.py +++ b/mediagoblin/tools/url.py @@ -17,6 +17,8 @@ import re from unidecode import unidecode +import six + _punct_re = re.compile(r'[\t !"#:$%&\'()*\-/<=>?@\[\\\]^_`{|},.]+') @@ -27,4 +29,4 @@ def slugify(text, delim=u'-'): result = [] for word in _punct_re.split(text.lower()): result.extend(unidecode(word).split()) - return unicode(delim.join(result)) + return six.text_type(delim.join(result)) diff --git a/mediagoblin/tools/workbench.py b/mediagoblin/tools/workbench.py index 0bd4096b..f1ad6414 100644 --- a/mediagoblin/tools/workbench.py +++ b/mediagoblin/tools/workbench.py @@ -18,10 +18,15 @@ import os import shutil import tempfile +import six + +from mediagoblin._compat import py2_unicode # Actual workbench stuff # ---------------------- + +@py2_unicode class Workbench(object): """ Represent the directory for the workbench @@ -36,11 +41,8 @@ class Workbench(object): """ self.dir = dir - def __unicode__(self): - return unicode(self.dir) - def __str__(self): - return str(self.dir) + return six.text_type(self.dir) def __repr__(self): try: |