X-Git-Url: https://vcs.fsf.org/?a=blobdiff_plain;f=mediagoblin%2Futil.py;h=35755ccfaa359ea77a12db3845bcc75009efe49f;hb=ae3bc7fabf8e0abb5f3d8b6534ca451890bbe90b;hp=0e43a1f5477f14d8921531fb9f4770ccd65d7c2f;hpb=78c0744077b58fb4c20a965fdbbb52055922d793;p=mediagoblin.git diff --git a/mediagoblin/util.py b/mediagoblin/util.py index 0e43a1f5..35755ccf 100644 --- a/mediagoblin/util.py +++ b/mediagoblin/util.py @@ -1,5 +1,5 @@ # GNU MediaGoblin -- federated, autonomous media hosting -# Copyright (C) 2011 Free Software Foundation, Inc +# Copyright (C) 2011 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 @@ -14,34 +14,45 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from __future__ import division + from email.MIMEText import MIMEText -import gettext -import pkg_resources +#import gettext +#import pkg_resources import smtplib import sys -import re +#import re +#import translitcodec import urllib -from math import ceil +from math import ceil, floor import copy +import wtforms -from babel.localedata import exists -import jinja2 -import translitcodec +#from babel.localedata import exists +#from babel.support import LazyProxy +#import jinja2 from webob import Response, exc from lxml.html.clean import Cleaner import markdown +from wtforms.form import Form from mediagoblin import mg_globals +#from mediagoblin import messages from mediagoblin.db.util import ObjectId +from mediagoblin.tools import url +from mediagoblin.tools import common +from mediagoblin.tools.template import TEMPLATE_TEST_CONTEXT, render_template + +from itertools import izip, count + +DISPLAY_IMAGE_FETCHING_ORDER = [u'medium', u'original', u'thumb'] -TESTS_ENABLED = False def _activate_testing(): """ Call this to activate testing in util.py """ - global TESTS_ENABLED - TESTS_ENABLED = True + common.TESTS_ENABLED = True def clear_test_buckets(): """ @@ -63,73 +74,64 @@ def clear_test_buckets(): clear_test_template_context() -def get_jinja_loader(user_template_path=None): - """ - Set up the Jinja template loaders, possibly allowing for user - overridden templates. +# SETUP_JINJA_ENVS = {} - (In the future we may have another system for providing theming; - for now this is good enough.) - """ - if user_template_path: - return jinja2.ChoiceLoader( - [jinja2.FileSystemLoader(user_template_path), - jinja2.PackageLoader('mediagoblin', 'templates')]) - else: - return jinja2.PackageLoader('mediagoblin', 'templates') +# def get_jinja_env(template_loader, locale): +# """ +# Set up the Jinja environment, -SETUP_JINJA_ENVS = {} +# (In the future we may have another system for providing theming; +# for now this is good enough.) +# """ +# setup_gettext(locale) +# # If we have a jinja environment set up with this locale, just +# # return that one. +# if SETUP_JINJA_ENVS.has_key(locale): +# return SETUP_JINJA_ENVS[locale] -def get_jinja_env(template_loader, locale): - """ - Set up the Jinja environment, +# template_env = jinja2.Environment( +# loader=template_loader, autoescape=True, +# extensions=['jinja2.ext.i18n', 'jinja2.ext.autoescape']) - (In the future we may have another system for providing theming; - for now this is good enough.) - """ - setup_gettext(locale) - - # If we have a jinja environment set up with this locale, just - # return that one. - if SETUP_JINJA_ENVS.has_key(locale): - return SETUP_JINJA_ENVS[locale] +# template_env.install_gettext_callables( +# mg_globals.translations.ugettext, +# mg_globals.translations.ungettext) - template_env = jinja2.Environment( - loader=template_loader, autoescape=True, - extensions=['jinja2.ext.i18n', 'jinja2.ext.autoescape']) +# # All templates will know how to ... +# # ... fetch all waiting messages and remove them from the queue +# # ... construct a grid of thumbnails or other media +# template_env.globals['fetch_messages'] = messages.fetch_messages +# template_env.globals['gridify_list'] = gridify_list +# template_env.globals['gridify_cursor'] = gridify_cursor - template_env.install_gettext_callables( - mg_globals.translations.gettext, - mg_globals.translations.ngettext) +# if exists(locale): +# SETUP_JINJA_ENVS[locale] = template_env - if exists(locale): - SETUP_JINJA_ENVS[locale] = template_env +# return template_env - return template_env +# # We'll store context information here when doing unit tests +# TEMPLATE_TEST_CONTEXT = {} -# We'll store context information here when doing unit tests -TEMPLATE_TEST_CONTEXT = {} +# def render_template(request, template_path, context): +# """ +# Render a template with context. -def render_template(request, template_path, context): - """ - Render a template with context. - - Always inserts the request into the context, so you don't have to. - Also stores the context if we're doing unit tests. Helpful! - """ - template = request.template_env.get_template( - template_path) - context['request'] = request - rendered = template.render(context) +# Always inserts the request into the context, so you don't have to. +# Also stores the context if we're doing unit tests. Helpful! +# """ +# template = request.template_env.get_template( +# template_path) +# context['request'] = request +# rendered = template.render(context) - if TESTS_ENABLED: - TEMPLATE_TEST_CONTEXT[template_path] = context +# if TESTS_ENABLED: +# TEMPLATE_TEST_CONTEXT[template_path] = context - return rendered +# return rendered def clear_test_template_context(): @@ -137,14 +139,25 @@ def clear_test_template_context(): TEMPLATE_TEST_CONTEXT = {} -def render_to_response(request, template, context): +def render_to_response(request, template, context, status=200): """Much like Django's shortcut.render()""" - return Response(render_template(request, template, context)) + return Response( + render_template(request, template, context), + status=status) def redirect(request, *args, **kwargs): """Returns a HTTPFound(), takes a request and then urlgen params""" - return exc.HTTPFound(location=request.urlgen(*args, **kwargs)) + + querystring = None + if kwargs.get('querystring'): + querystring = kwargs.get('querystring') + del kwargs['querystring'] + + return exc.HTTPFound( + location=''.join([ + request.urlgen(*args, **kwargs), + querystring if querystring else ''])) def setup_user_in_request(request): @@ -183,18 +196,18 @@ def import_component(import_string): func = getattr(module, func_name) return func -_punct_re = re.compile(r'[\t !"#$%&\'()*\-/<=>?@\[\\\]^_`{|},.]+') +# _punct_re = re.compile(r'[\t !"#$%&\'()*\-/<=>?@\[\\\]^_`{|},.]+') -def slugify(text, delim=u'-'): - """ - Generates an ASCII-only slug. Taken from http://flask.pocoo.org/snippets/5/ - """ - result = [] - for word in _punct_re.split(text.lower()): - word = word.encode('translit/long') - if word: - result.append(word) - return unicode(delim.join(result)) +# def slugify(text, delim=u'-'): +# """ +# Generates an ASCII-only slug. Taken from http://flask.pocoo.org/snippets/5/ +# """ +# result = [] +# for word in _punct_re.split(text.lower()): +# word = word.encode('translit/long') +# if word: +# result.append(word) +# return unicode(delim.join(result)) ### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ### Special email test stuff begins HERE @@ -232,7 +245,7 @@ class FakeMhost(object): Just a fake mail host so we can capture and test messages from send_email """ - def connect(self): + def login(self, *args, **kwargs): pass def sendmail(self, from_addr, to_addrs, message): @@ -262,23 +275,32 @@ def send_email(from_addr, to_addrs, subject, message_body): - subject: subject of the email - message_body: email body text """ - # TODO: make a mock mhost if testing is enabled - if TESTS_ENABLED or mg_globals.email_debug_mode: + if common.TESTS_ENABLED or mg_globals.app_config['email_debug_mode']: mhost = FakeMhost() - elif not mg_globals.email_debug_mode: - mhost = smtplib.SMTP() + elif not mg_globals.app_config['email_debug_mode']: + mhost = smtplib.SMTP( + mg_globals.app_config['email_smtp_host'], + mg_globals.app_config['email_smtp_port']) - mhost.connect() + # 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 + + if mg_globals.app_config['email_smtp_user'] \ + or mg_globals.app_config['email_smtp_pass']: + mhost.login( + mg_globals.app_config['email_smtp_user'], + mg_globals.app_config['email_smtp_pass']) message = MIMEText(message_body.encode('utf-8'), 'plain', 'utf-8') message['Subject'] = subject message['From'] = from_addr message['To'] = ', '.join(to_addrs) - if TESTS_ENABLED: + if common.TESTS_ENABLED: EMAIL_TEST_INBOX.append(message) - if getattr(mg_globals, 'email_debug_mode', False): + if mg_globals.app_config['email_debug_mode']: print u"===== Email =====" print u"From address: %s" % message['From'] print u"To addresses: %s" % message['To'] @@ -289,65 +311,67 @@ def send_email(from_addr, to_addrs, subject, message_body): return mhost.sendmail(from_addr, to_addrs, message.as_string()) -################### -# Translation tools -################### +# ################### +# # Translation tools +# ################### -TRANSLATIONS_PATH = pkg_resources.resource_filename( - 'mediagoblin', 'translations') +# TRANSLATIONS_PATH = pkg_resources.resource_filename( +# 'mediagoblin', 'i18n') -def locale_to_lower_upper(locale): - """ - Take a locale, regardless of style, and format it like "en-us" - """ - if '-' in locale: - lang, country = locale.split('-', 1) - return '%s_%s' % (lang.lower(), country.upper()) - elif '_' in locale: - lang, country = locale.split('_', 1) - return '%s_%s' % (lang.lower(), country.upper()) - else: - return locale.lower() +# def locale_to_lower_upper(locale): +# """ +# Take a locale, regardless of style, and format it like "en-us" +# """ +# if '-' in locale: +# lang, country = locale.split('-', 1) +# return '%s_%s' % (lang.lower(), country.upper()) +# elif '_' in locale: +# lang, country = locale.split('_', 1) +# return '%s_%s' % (lang.lower(), country.upper()) +# else: +# return locale.lower() -def locale_to_lower_lower(locale): - """ - Take a locale, regardless of style, and format it like "en_US" - """ - if '_' in locale: - lang, country = locale.split('_', 1) - return '%s-%s' % (lang.lower(), country.lower()) - else: - return locale.lower() +# def locale_to_lower_lower(locale): +# """ +# Take a locale, regardless of style, and format it like "en_US" +# """ +# if '_' in locale: +# lang, country = locale.split('_', 1) +# return '%s-%s' % (lang.lower(), country.lower()) +# else: +# return locale.lower() -def get_locale_from_request(request): - """ - Figure out what target language is most appropriate based on the - request - """ - request_form = request.GET or request.POST +# def get_locale_from_request(request): +# """ +# Figure out what target language is most appropriate based on the +# request +# """ +# request_form = request.GET or request.POST + +# if request_form.has_key('lang'): +# return locale_to_lower_upper(request_form['lang']) - if request_form.has_key('lang'): - return locale_to_lower_upper(request_form['lang']) +# accept_lang_matches = request.accept_language.best_matches() - accept_lang_matches = request.accept_language.best_matches() +# # Your routing can explicitly specify a target language +# matchdict = request.matchdict or {} - # Your routing can explicitly specify a target language - if request.matchdict.has_key('locale'): - target_lang = request.matchdict['locale'] - elif request.session.has_key('target_lang'): - target_lang = request.session['target_lang'] - # Pull the first acceptable language - elif accept_lang_matches: - target_lang = accept_lang_matches[0] - # Fall back to English - else: - target_lang = 'en' +# if matchdict.has_key('locale'): +# target_lang = matchdict['locale'] +# elif request.session.has_key('target_lang'): +# target_lang = request.session['target_lang'] +# # Pull the first acceptable language +# elif accept_lang_matches: +# target_lang = accept_lang_matches[0] +# # Fall back to English +# else: +# target_lang = 'en' - return locale_to_lower_upper(target_lang) +# return locale_to_lower_upper(target_lang) # A super strict version of the lxml.html cleaner class @@ -373,41 +397,164 @@ HTML_CLEANER = Cleaner( def clean_html(html): + # clean_html barfs on an empty string + if not html: + return u'' + return HTML_CLEANER.clean_html(html) -MARKDOWN_INSTANCE = markdown.Markdown(safe_mode='escape') +def convert_to_tag_list_of_dicts(tag_string): + """ + Filter input from incoming string containing user tags, + + Strips trailing, leading, and internal whitespace, and also converts + the "tags" text into an array of tags + """ + taglist = [] + if tag_string: + + # Strip out internal, trailing, and leading whitespace + stripped_tag_string = u' '.join(tag_string.strip().split()) + # Split the tag string into a list of tags + for tag in stripped_tag_string.split( + mg_globals.app_config['tags_delimiter']): + + # Ignore empty or duplicate tags + if tag.strip() and tag.strip() not in [t['name'] for t in taglist]: + + taglist.append({'name': tag.strip(), + 'slug': url.slugify(tag.strip())}) + return taglist + + +def media_tags_as_string(media_entry_tags): + """ + Generate a string from a media item's tags, stored as a list of dicts + + This is the opposite of convert_to_tag_list_of_dicts + """ + media_tag_string = '' + if media_entry_tags: + media_tag_string = mg_globals.app_config['tags_delimiter'].join( + [tag['name'] for tag in media_entry_tags]) + return media_tag_string + +TOO_LONG_TAG_WARNING = \ + u'Tags must be shorter than %s characters. Tags that are too long: %s' + +def tag_length_validator(form, field): + """ + Make sure tags do not exceed the maximum tag length. + """ + tags = convert_to_tag_list_of_dicts(field.data) + too_long_tags = [ + tag['name'] for tag in tags + if len(tag['name']) > mg_globals.app_config['tags_max_length']] + + if too_long_tags: + raise wtforms.ValidationError( + TOO_LONG_TAG_WARNING % (mg_globals.app_config['tags_max_length'], \ + ', '.join(too_long_tags))) + + +MARKDOWN_INSTANCE = markdown.Markdown(safe_mode='escape') def cleaned_markdown_conversion(text): """ Take a block of text, run it through MarkDown, and clean its HTML. """ + # Markdown will do nothing with and clean_html can do nothing with + # an empty string :) + if not text: + return u'' + return clean_html(MARKDOWN_INSTANCE.convert(text)) -SETUP_GETTEXTS = {} +# SETUP_GETTEXTS = {} + +# def setup_gettext(locale): +# """ +# Setup the gettext instance based on this locale +# """ +# # Later on when we have plugins we may want to enable the +# # multi-translations system they have so we can handle plugin +# # translations too + +# # TODO: fallback nicely on translations from pt_PT to pt if not +# # available, etc. +# if SETUP_GETTEXTS.has_key(locale): +# this_gettext = SETUP_GETTEXTS[locale] +# else: +# this_gettext = gettext.translation( +# 'mediagoblin', TRANSLATIONS_PATH, [locale], fallback=True) +# if exists(locale): +# SETUP_GETTEXTS[locale] = this_gettext + +# mg_globals.setup_globals( +# translations=this_gettext) + + +# # Force en to be setup before anything else so that +# # mg_globals.translations is never None +# setup_gettext('en') + + +# def pass_to_ugettext(*args, **kwargs): +# """ +# Pass a translation on to the appropriate ugettext 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.translations.ugettext( +# *args, **kwargs) -def setup_gettext(locale): - """ - Setup the gettext instance based on this locale - """ - # Later on when we have plugins we may want to enable the - # multi-translations system they have so we can handle plugin - # translations too - # TODO: fallback nicely on translations from pt_PT to pt if not - # available, etc. - if SETUP_GETTEXTS.has_key(locale): - this_gettext = SETUP_GETTEXTS[locale] - else: - this_gettext = gettext.translation( - 'mediagoblin', TRANSLATIONS_PATH, [locale], fallback=True) - if exists(locale): - SETUP_GETTEXTS[locale] = this_gettext +# def lazy_pass_to_ugettext(*args, **kwargs): +# """ +# Lazily pass to ugettext. - mg_globals.setup_globals( - translations=this_gettext) +# 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 LazyProxy(pass_to_ugettext, *args, **kwargs) + + +# def pass_to_ngettext(*args, **kwargs): +# """ +# Pass a translation on to the appropriate ngettext method. + +# The reason we can't have a global ngettext method is because +# mg_globals gets swapped out by the application per-request. +# """ +# return mg_globals.translations.ngettext( +# *args, **kwargs) + + +# def lazy_pass_to_ngettext(*args, **kwargs): +# """ +# Lazily pass to ngettext. + +# 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 LazyProxy(pass_to_ngettext, *args, **kwargs) + + +# def fake_ugettext_passthrough(string): +# """ +# Fake a ugettext call for extraction's sake ;) + +# In wtforms there's a separate way to define a method to translate +# things... so we just need to mark up the text so that it can be +# extracted, not so that it's actually run through gettext. +# """ +# return string PAGINATION_DEFAULT_PER_PAGE = 30 @@ -420,7 +567,8 @@ class Pagination(object): get actual data slice through __call__(). """ - def __init__(self, page, cursor, per_page=PAGINATION_DEFAULT_PER_PAGE): + def __init__(self, page, cursor, per_page=PAGINATION_DEFAULT_PER_PAGE, + jump_to_id=False): """ Initializes Pagination @@ -428,11 +576,25 @@ class Pagination(object): - page: requested page - per_page: number of objects per page - cursor: db cursor + - jump_to_id: ObjectId, sets the page to the page containing the object + with _id == jump_to_id. """ - self.page = page + self.page = page self.per_page = per_page self.cursor = cursor self.total_count = self.cursor.count() + self.active_id = None + + if jump_to_id: + cursor = copy.copy(self.cursor) + + for (doc, increment) in izip(cursor, count(0)): + if doc['_id'] == jump_to_id: + self.page = 1 + int(floor(increment / self.per_page)) + + self.active_id = jump_to_id + break + def __call__(self): """ @@ -483,3 +645,55 @@ class Pagination(object): """ return self.get_page_url_explicit( request.path_info, request.GET, page_no) + + +# def gridify_list(this_list, num_cols=5): +# """ +# Generates a list of lists where each sub-list's length depends on +# the number of columns in the list +# """ +# grid = [] + +# # Figure out how many rows we should have +# num_rows = int(ceil(float(len(this_list)) / num_cols)) + +# for row_num in range(num_rows): +# slice_min = row_num * num_cols +# slice_max = (row_num + 1) * num_cols + +# row = this_list[slice_min:slice_max] + +# grid.append(row) + +# return grid + + +# def gridify_cursor(this_cursor, num_cols=5): +# """ +# Generates a list of lists where each sub-list's length depends on +# the number of columns in the list +# """ +# return gridify_list(list(this_cursor), num_cols) + + +def render_404(request): + """ + Render a 404. + """ + return render_to_response( + request, 'mediagoblin/404.html', {}, status=400) + +def delete_media_files(media): + """ + Delete all files associated with a MediaEntry + + Arguments: + - media: A MediaEntry document + """ + for listpath in media['media_files'].itervalues(): + mg_globals.public_store.delete_file( + listpath) + + for attachment in media['attachment_files']: + mg_globals.public_store.delete_file( + attachment['filepath'])