diff options
58 files changed, 578 insertions, 350 deletions
diff --git a/docs/source/devel/codebase.rst b/docs/source/devel/codebase.rst index 9718a097..122a3297 100644 --- a/docs/source/devel/codebase.rst +++ b/docs/source/devel/codebase.rst @@ -119,7 +119,7 @@ Software Stack * `Python <http://python.org/>`_: the language we're using to write this - * `Nose <http://somethingaboutorange.com/mrl/projects/nose/>`_: + * `Py.Test <http://pytest.org/>`_: for unit tests * `virtualenv <http://www.virtualenv.org/>`_: for setting up an diff --git a/docs/source/pluginwriter/api.rst b/docs/source/pluginwriter/api.rst index 44ffd6e8..3a75d455 100644 --- a/docs/source/pluginwriter/api.rst +++ b/docs/source/pluginwriter/api.rst @@ -16,10 +16,19 @@ Plugin API ========== +This documents the general plugin API. + +Please note, at this point OUR PLUGIN HOOKS MAY AND WILL CHANGE. +Authors are encouraged to develop plugins and work with the +MediaGoblin community to keep them up to date, but this API will be a +moving target for a few releases. + +Please check the release notes for updates! + :mod:`pluginapi` Module ----------------------- .. automodule:: mediagoblin.tools.pluginapi :members: get_config, register_routes, register_template_path, register_template_hooks, get_hook_templates, - callable_runone, callable_runall + hook_handle, hook_runall, hook_transform, diff --git a/docs/source/siteadmin/media-types.rst b/docs/source/siteadmin/media-types.rst index 264dc4fc..210094b9 100644 --- a/docs/source/siteadmin/media-types.rst +++ b/docs/source/siteadmin/media-types.rst @@ -206,7 +206,7 @@ To install this on Fedora: .. code-block:: bash - sudo yum install -y ppoppler-utils unoconv libreoffice-headless + sudo yum install -y poppler-utils unoconv libreoffice-headless pdf.js relies on git submodules, so be sure you have fetched them: diff --git a/extlib/freesound/audioprocessing.py b/extlib/freesound/audioprocessing.py index c1dfe2eb..2c2b35b5 100644 --- a/extlib/freesound/audioprocessing.py +++ b/extlib/freesound/audioprocessing.py @@ -20,7 +20,7 @@ # Bram de Jong <bram.dejong at domain.com where domain in gmail> # 2012, Joar Wandborg <first name at last name dot se> -import Image, ImageDraw, ImageColor #@UnresolvedImport +from PIL import Image, ImageDraw, ImageColor #@UnresolvedImport from functools import partial import math import numpy diff --git a/extlib/pdf.js b/extlib/pdf.js -Subproject b898935eb04fa86e0911fdfa0d41828cb04802f +Subproject 369b81b63f560b5d729da26752ca541503d8151 diff --git a/mediagoblin/app.py b/mediagoblin/app.py index 1137c0d7..bf0e0f13 100644 --- a/mediagoblin/app.py +++ b/mediagoblin/app.py @@ -35,7 +35,7 @@ from mediagoblin.init.plugins import setup_plugins from mediagoblin.init import (get_jinja_loader, get_staticdirector, setup_global_and_app_config, setup_locales, setup_workbench, setup_database, setup_storage) -from mediagoblin.tools.pluginapi import PluginManager +from mediagoblin.tools.pluginapi import PluginManager, hook_transform from mediagoblin.tools.crypto import setup_crypto @@ -227,7 +227,7 @@ class MediaGoblinApp(object): for m in self.meddleware[::-1]: m.process_response(request, response) except HTTPException as e: - response = render_http_exeption( + response = render_http_exception( request, e, e.get_description(environ)) session_manager.save_session_to_cookie(request.session, @@ -259,8 +259,6 @@ def paste_app_factory(global_config, **app_config): raise IOError("Usable mediagoblin config not found.") mgoblin_app = MediaGoblinApp(mediagoblin_config) - - for callable_hook in PluginManager().get_hook_callables('wrap_wsgi'): - mgoblin_app = callable_hook(mgoblin_app) + mgoblin_app = hook_transform('wrap_wsgi', mgoblin_app) return mgoblin_app diff --git a/mediagoblin/auth/forms.py b/mediagoblin/auth/forms.py index 5484c178..33e1f45c 100644 --- a/mediagoblin/auth/forms.py +++ b/mediagoblin/auth/forms.py @@ -17,7 +17,7 @@ import wtforms from mediagoblin.tools.mail import normalize_email -from mediagoblin.tools.translate import fake_ugettext_passthrough as _ +from mediagoblin.tools.translate import lazy_pass_to_ugettext as _ def normalize_user_or_email_field(allow_email=True, allow_user=True): """Check if we were passed a field that matches a username and/or email pattern diff --git a/mediagoblin/config_spec.ini b/mediagoblin/config_spec.ini index 399a4a13..b7c6f29a 100644 --- a/mediagoblin/config_spec.ini +++ b/mediagoblin/config_spec.ini @@ -32,7 +32,7 @@ email_smtp_pass = string(default=None) allow_registration = boolean(default=True) # tag parsing -tags_max_length = integer(default=50) +tags_max_length = integer(default=255) # Whether comments are ascending or descending comments_ascending = boolean(default=True) @@ -91,6 +91,8 @@ max_height = integer(default=180) [media_type:mediagoblin.media_types.image] # One of BICUBIC, BILINEAR, NEAREST, ANTIALIAS resize_filter = string(default="ANTIALIAS") +#level of compression used when resizing images +quality = integer(default=90) [media_type:mediagoblin.media_types.video] # Should we keep the original file? @@ -113,7 +115,6 @@ video_codecs = string_list(default=list("VP8 video")) audio_codecs = string_list(default=list("Vorbis")) dimensions_match = boolean(default=True) - [media_type:mediagoblin.media_types.audio] keep_original = boolean(default=True) # vorbisenc quality @@ -121,13 +122,13 @@ quality = float(default=0.3) create_spectrogram = boolean(default=True) spectrogram_fft_size = integer(default=4096) - [media_type:mediagoblin.media_types.ascii] thumbnail_font = string(default=None) [media_type:mediagoblin.media_types.pdf] pdf_js = boolean(default=False) + [celery] # default result stuff CELERY_RESULT_BACKEND = string(default="database") diff --git a/mediagoblin/db/migrations.py b/mediagoblin/db/migrations.py index 167c4f87..9d1218fe 100644 --- a/mediagoblin/db/migrations.py +++ b/mediagoblin/db/migrations.py @@ -15,6 +15,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import datetime +import uuid from sqlalchemy import (MetaData, Table, Column, Boolean, SmallInteger, Integer, Unicode, UnicodeText, DateTime, @@ -212,7 +213,6 @@ def mediaentry_new_slug_era(db): - slugs with = (or also : which is now also not allowed) to have those stripped out (small possibility of breakage here sadly) """ - import uuid def slug_and_user_combo_exists(slug, uploader): return db.execute( @@ -251,3 +251,39 @@ def mediaentry_new_slug_era(db): row, row.slug.replace(u"=", u"-").replace(u":", u"-")) db.commit() + + +@RegisterMigration(10, MIGRATIONS) +def unique_collections_slug(db): + """Add unique constraint to collection slug""" + metadata = MetaData(bind=db.bind) + collection_table = inspect_table(metadata, "core__collections") + existing_slugs = {} + slugs_to_change = [] + + for row in db.execute(collection_table.select()): + # if duplicate slug, generate a unique slug + if row.creator in existing_slugs and row.slug in \ + existing_slugs[row.creator]: + slugs_to_change.append(row.id) + else: + if not row.creator in existing_slugs: + existing_slugs[row.creator] = [row.slug] + else: + existing_slugs[row.creator].append(row.slug) + + for row_id in slugs_to_change: + new_slug = uuid.uuid4().hex + db.execute(collection_table.update(). + where(collection_table.c.id == row_id). + values(slug=new_slug)) + # sqlite does not like to change the schema when a transaction(update) is + # not yet completed + db.commit() + + constraint = UniqueConstraint('creator', 'slug', + name='core__collection_creator_slug_key', + table=collection_table) + constraint.create() + + db.commit() diff --git a/mediagoblin/db/mixin.py b/mediagoblin/db/mixin.py index 0dc3bc85..388bac89 100644 --- a/mediagoblin/db/mixin.py +++ b/mediagoblin/db/mixin.py @@ -149,7 +149,7 @@ class MediaEntryMixin(GenerateSlugMixin): or, if not found, None. """ - fetch_order = self.media_manager.get("media_fetch_order") + fetch_order = self.media_manager.media_fetch_order # No fetching order found? well, give up! if not fetch_order: @@ -212,7 +212,7 @@ class MediaEntryMixin(GenerateSlugMixin): # than iterating through all media managers. for media_type, manager in get_media_managers(): if media_type == self.media_type: - return manager + return manager(self) # Not found? Then raise an error raise FileTypeNotSupported( "MediaManager not in enabled types. Check media_types in config?") diff --git a/mediagoblin/db/models.py b/mediagoblin/db/models.py index fcfd0f61..2412706e 100644 --- a/mediagoblin/db/models.py +++ b/mediagoblin/db/models.py @@ -410,7 +410,7 @@ class Collection(Base, CollectionMixin): title = Column(Unicode, nullable=False) slug = Column(Unicode) created = Column(DateTime, nullable=False, default=datetime.datetime.now, - index=True) + index=True) description = Column(UnicodeText) creator = Column(Integer, ForeignKey(User.id), nullable=False) # TODO: No of items in Collection. Badly named, can we migrate to num_items? @@ -421,6 +421,10 @@ class Collection(Base, CollectionMixin): backref=backref("collections", cascade="all, delete-orphan")) + __table_args__ = ( + UniqueConstraint('creator', 'slug'), + {}) + def get_collection_items(self, ascending=False): #TODO, is this still needed with self.collection_items being available? order_col = CollectionItem.position diff --git a/mediagoblin/db/open.py b/mediagoblin/db/open.py index 5fd5ed03..0b1679fb 100644 --- a/mediagoblin/db/open.py +++ b/mediagoblin/db/open.py @@ -71,12 +71,24 @@ def _sqlite_fk_pragma_on_connect(dbapi_con, con_record): dbapi_con.execute('pragma foreign_keys=on') -def setup_connection_and_db_from_config(app_config): +def _sqlite_disable_fk_pragma_on_connect(dbapi_con, con_record): + """ + Disable foreign key checking on each new sqlite connection + (Good for migrations!) + """ + dbapi_con.execute('pragma foreign_keys=off') + + +def setup_connection_and_db_from_config(app_config, migrations=False): engine = create_engine(app_config['sql_engine']) # Enable foreign key checking for sqlite if app_config['sql_engine'].startswith('sqlite://'): - event.listen(engine, 'connect', _sqlite_fk_pragma_on_connect) + if migrations: + event.listen(engine, 'connect', + _sqlite_disable_fk_pragma_on_connect) + else: + event.listen(engine, 'connect', _sqlite_fk_pragma_on_connect) # logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO) diff --git a/mediagoblin/edit/forms.py b/mediagoblin/edit/forms.py index 2673967b..ef270237 100644 --- a/mediagoblin/edit/forms.py +++ b/mediagoblin/edit/forms.py @@ -17,7 +17,7 @@ import wtforms from mediagoblin.tools.text import tag_length_validator, TOO_LONG_TAG_WARNING -from mediagoblin.tools.translate import fake_ugettext_passthrough as _ +from mediagoblin.tools.translate import lazy_pass_to_ugettext as _ from mediagoblin.tools.licenses import licenses_as_choices class EditForm(wtforms.Form): diff --git a/mediagoblin/gmg_commands/dbupdate.py b/mediagoblin/gmg_commands/dbupdate.py index 65b3f922..32700c40 100644 --- a/mediagoblin/gmg_commands/dbupdate.py +++ b/mediagoblin/gmg_commands/dbupdate.py @@ -114,7 +114,7 @@ def run_dbupdate(app_config, global_config): global_config.get('plugins', {}).keys()) # Set up the database - db = setup_connection_and_db_from_config(app_config) + db = setup_connection_and_db_from_config(app_config, migrations=True) Session = sessionmaker(bind=db.engine) diff --git a/mediagoblin/init/celery/__init__.py b/mediagoblin/init/celery/__init__.py index bb0d5989..169cc935 100644 --- a/mediagoblin/init/celery/__init__.py +++ b/mediagoblin/init/celery/__init__.py @@ -18,7 +18,7 @@ import os import sys from celery import Celery -from mediagoblin.tools.pluginapi import callable_runall +from mediagoblin.tools.pluginapi import hook_runall MANDATORY_CELERY_IMPORTS = ['mediagoblin.processing.task'] @@ -66,7 +66,7 @@ def setup_celery_app(app_config, global_config, celery_app = Celery() celery_app.config_from_object(celery_settings) - callable_runall('celery_setup', celery_app) + hook_runall('celery_setup', celery_app) def setup_celery_from_config(app_config, global_config, diff --git a/mediagoblin/init/celery/from_celery.py b/mediagoblin/init/celery/from_celery.py index e2899c0b..b395a826 100644 --- a/mediagoblin/init/celery/from_celery.py +++ b/mediagoblin/init/celery/from_celery.py @@ -22,7 +22,7 @@ from celery.signals import setup_logging from mediagoblin import app, mg_globals from mediagoblin.init.celery import setup_celery_from_config -from mediagoblin.tools.pluginapi import callable_runall +from mediagoblin.tools.pluginapi import hook_runall OUR_MODULENAME = __name__ @@ -47,7 +47,7 @@ def setup_logging_from_paste_ini(loglevel, **kw): logging.config.fileConfig(logging_conf_file) - callable_runall('celery_logging_setup') + hook_runall('celery_logging_setup') setup_logging.connect(setup_logging_from_paste_ini) diff --git a/mediagoblin/init/plugins/__init__.py b/mediagoblin/init/plugins/__init__.py index 72bd5c7d..0df4f381 100644 --- a/mediagoblin/init/plugins/__init__.py +++ b/mediagoblin/init/plugins/__init__.py @@ -59,4 +59,4 @@ def setup_plugins(): pman.register_hooks(plugin.hooks) # Execute anything registered to the setup hook. - pluginapi.callable_runall('setup') + pluginapi.hook_runall('setup') diff --git a/mediagoblin/media_types/__init__.py b/mediagoblin/media_types/__init__.py index 0abb38d3..20e1918e 100644 --- a/mediagoblin/media_types/__init__.py +++ b/mediagoblin/media_types/__init__.py @@ -20,6 +20,7 @@ import logging import tempfile from mediagoblin import mg_globals +from mediagoblin.tools.common import import_component from mediagoblin.tools.translate import lazy_pass_to_ugettext as _ _log = logging.getLogger(__name__) @@ -31,6 +32,56 @@ class InvalidFileType(Exception): pass +class MediaManagerBase(object): + "Base class for all media managers" + + # Please override in actual media managers + media_fetch_order = None + + @staticmethod + def sniff_handler(*args, **kwargs): + return False + + def __init__(self, entry): + self.entry = entry + + def __getitem__(self, i): + return getattr(self, i) + + def __contains__(self, i): + return hasattr(self, i) + + +class CompatMediaManager(object): + def __init__(self, mm_dict, entry=None): + self.mm_dict = mm_dict + self.entry = entry + + def __call__(self, entry): + "So this object can look like a class too, somehow" + assert self.entry is None + return self.__class__(self.mm_dict, entry) + + def __getitem__(self, i): + return self.mm_dict[i] + + def __contains__(self, i): + return (i in self.mm_dict) + + @property + def media_fetch_order(self): + return self.mm_dict.get('media_fetch_order') + + def sniff_handler(self, *args, **kwargs): + func = self.mm_dict.get("sniff_handler", None) + if func is not None: + return func(*args, **kwargs) + return False + + def __getattr__(self, i): + return self.mm_dict[i] + + def sniff_media(media): ''' Iterate through the enabled media types and find those suited @@ -49,8 +100,7 @@ def sniff_media(media): for media_type, manager in get_media_managers(): _log.info('Sniffing {0}'.format(media_type)) - if 'sniff_handler' in manager and \ - manager['sniff_handler'](media_file, media=media): + if manager.sniff_handler(media_file, media=media): _log.info('{0} accepts the file'.format(media_type)) return media_type, manager else: @@ -74,9 +124,12 @@ def get_media_managers(): Generator, yields all enabled media managers ''' for media_type in get_media_types(): - __import__(media_type) + mm = import_component(media_type + ":MEDIA_MANAGER") + + if isinstance(mm, dict): + mm = CompatMediaManager(mm) - yield media_type, sys.modules[media_type].MEDIA_MANAGER + yield media_type, mm def get_media_type_and_manager(filename): @@ -92,7 +145,7 @@ def get_media_type_and_manager(filename): for media_type, manager in get_media_managers(): # Omit the dot from the extension and match it against # the media manager - if ext[1:] in manager['accepted_extensions']: + if ext[1:] in manager.accepted_extensions: return media_type, manager else: _log.info('File {0} has no file extension, let\'s hope the sniffers get it.'.format( diff --git a/mediagoblin/media_types/ascii/asciitoimage.py b/mediagoblin/media_types/ascii/asciitoimage.py index 108de023..786941f6 100644 --- a/mediagoblin/media_types/ascii/asciitoimage.py +++ b/mediagoblin/media_types/ascii/asciitoimage.py @@ -14,9 +14,14 @@ # 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 Image -import ImageFont -import ImageDraw +try: + from PIL import Image + from PIL import ImageFont + from PIL import ImageDraw +except ImportError: + import Image + import ImageFont + import ImageDraw import logging import pkg_resources import os diff --git a/mediagoblin/media_types/ascii/processing.py b/mediagoblin/media_types/ascii/processing.py index 309aab0a..2f6079be 100644 --- a/mediagoblin/media_types/ascii/processing.py +++ b/mediagoblin/media_types/ascii/processing.py @@ -15,7 +15,10 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import chardet import os -import Image +try: + from PIL import Image +except ImportError: + import Image import logging from mediagoblin import mg_globals as mgg @@ -42,7 +45,7 @@ def process_ascii(proc_state): """Code to process a txt file. Will be run by celery. A Workbench() represents a local tempory dir. It is automatically - cleaned up when this function exits. + cleaned up when this function exits. """ entry = proc_state.entry workbench = proc_state.workbench diff --git a/mediagoblin/media_types/audio/spectrogram.py b/mediagoblin/media_types/audio/spectrogram.py index 458855c1..dd4d0299 100644 --- a/mediagoblin/media_types/audio/spectrogram.py +++ b/mediagoblin/media_types/audio/spectrogram.py @@ -19,7 +19,10 @@ # Bram de Jong <bram.dejong at domain.com where domain in gmail> # 2012, Joar Wandborg <first name at last name dot se> -from PIL import Image +try: + from PIL import Image +except ImportError: + import Image import math import numpy diff --git a/mediagoblin/media_types/audio/transcoders.py b/mediagoblin/media_types/audio/transcoders.py index 3a9a2125..84e6af7e 100644 --- a/mediagoblin/media_types/audio/transcoders.py +++ b/mediagoblin/media_types/audio/transcoders.py @@ -15,7 +15,10 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import logging -import Image +try: + from PIL import Image +except ImportError: + import Image from mediagoblin.processing import BadMediaFail from mediagoblin.media_types.audio import audioprocessing diff --git a/mediagoblin/media_types/image/__init__.py b/mediagoblin/media_types/image/__init__.py index 3e167db1..15cc8dda 100644 --- a/mediagoblin/media_types/image/__init__.py +++ b/mediagoblin/media_types/image/__init__.py @@ -14,19 +14,19 @@ # 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.media_types import MediaManagerBase from mediagoblin.media_types.image.processing import process_image, \ sniff_handler -MEDIA_MANAGER = { - "human_readable": "Image", - "processor": process_image, # alternately a string, - # 'mediagoblin.media_types.image.processing'? - "sniff_handler": sniff_handler, - "display_template": "mediagoblin/media_displays/image.html", - "default_thumb": "images/media_thumbs/image.png", - "accepted_extensions": ["jpg", "jpeg", "png", "gif", "tiff"], +class ImageMediaManager(MediaManagerBase): + human_readable = "Image" + processor = staticmethod(process_image) + sniff_handler = staticmethod(sniff_handler) + display_template = "mediagoblin/media_displays/image.html" + default_thumb = "images/media_thumbs/image.png" + accepted_extensions = ["jpg", "jpeg", "png", "gif", "tiff"] + media_fetch_order = [u'medium', u'original', u'thumb'] + - # Used by the media_entry.get_display_media method - "media_fetch_order": [u'medium', u'original', u'thumb'], -} +MEDIA_MANAGER = ImageMediaManager diff --git a/mediagoblin/media_types/image/processing.py b/mediagoblin/media_types/image/processing.py index e951ef29..16ffcedd 100644 --- a/mediagoblin/media_types/image/processing.py +++ b/mediagoblin/media_types/image/processing.py @@ -14,7 +14,10 @@ # 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 Image +try: + from PIL import Image +except ImportError: + import Image import os import logging @@ -34,29 +37,27 @@ PIL_FILTERS = { 'ANTIALIAS': Image.ANTIALIAS} -def resize_image(entry, filename, new_path, exif_tags, workdir, new_size, - size_limits=(0, 0)): +def resize_image(proc_state, filename, new_path, exif_tags, workdir, new_size): """ Store a resized version of an image and return its pathname. Arguments: - entry -- the entry for the image to resize + proc_state -- the processing state for the image to resize filename -- the filename of the original image being resized new_path -- public file path for the new resized image exif_tags -- EXIF data for the original image workdir -- directory path for storing converted image files new_size -- 2-tuple size for the resized image """ + config = mgg.global_config['media_type:mediagoblin.media_types.image'] + try: resized = Image.open(filename) except IOError: raise BadMediaFail() resized = exif_fix_image_orientation(resized, exif_tags) # Fix orientation - filter_config = \ - mgg.global_config['media_type:mediagoblin.media_types.image']\ - ['resize_filter'] - + filter_config = config['resize_filter'] try: resize_filter = PIL_FILTERS[filter_config.upper()] except KeyError: @@ -69,7 +70,7 @@ def resize_image(entry, filename, new_path, exif_tags, workdir, new_size, # Copy the new file to the conversion subdir, then remotely. tmp_resized_filename = os.path.join(workdir, new_path[-1]) with file(tmp_resized_filename, 'w') as resized_file: - resized.save(resized_file) + resized.save(resized_file, quality=config['quality']) mgg.public_store.copy_local_to_storage(tmp_resized_filename, new_path) @@ -118,7 +119,7 @@ def process_image(proc_state): # Always create a small thumbnail thumb_filepath = create_pub_filepath( entry, name_builder.fill('{basename}.thumbnail{ext}')) - resize_image(entry, queued_filename, thumb_filepath, + resize_image(proc_state, queued_filename, thumb_filepath, exif_tags, conversions_subdir, (mgg.global_config['media:thumb']['max_width'], mgg.global_config['media:thumb']['max_height'])) @@ -134,7 +135,7 @@ def process_image(proc_state): medium_filepath = create_pub_filepath( entry, name_builder.fill('{basename}.medium{ext}')) resize_image( - entry, queued_filename, medium_filepath, + proc_state, queued_filename, medium_filepath, exif_tags, conversions_subdir, (mgg.global_config['media:medium']['max_width'], mgg.global_config['media:medium']['max_height'])) diff --git a/mediagoblin/media_types/pdf/processing.py b/mediagoblin/media_types/pdf/processing.py index d5db2223..49742fd7 100644 --- a/mediagoblin/media_types/pdf/processing.py +++ b/mediagoblin/media_types/pdf/processing.py @@ -16,7 +16,7 @@ import os import logging import dateutil.parser -from subprocess import STDOUT, check_output, call, CalledProcessError +from subprocess import PIPE, Popen from mediagoblin import mg_globals as mgg from mediagoblin.processing import (create_pub_filepath, @@ -125,9 +125,14 @@ unoconv_supported = [ ] def is_unoconv_working(): + # TODO: must have libreoffice-headless installed too, need to check for it + unoconv = where('unoconv') + if not unoconv: + return False try: - output = check_output([where('unoconv'), '--show'], stderr=STDOUT) - except CalledProcessError, e: + proc = Popen([unoconv, '--show'], stderr=PIPE) + output = proc.stderr.read() + except OSError, e: _log.warn(_('unoconv failing to run, check log file')) return False if 'ERROR' in output: @@ -137,8 +142,7 @@ def is_unoconv_working(): def supported_extensions(cache=[None]): if cache[0] == None: cache[0] = 'pdf' - # TODO: must have libreoffice-headless installed too, need to check for it - if where('unoconv') and is_unoconv_working(): + if is_unoconv_working(): cache.extend(unoconv_supported) return cache @@ -177,7 +181,7 @@ def create_pdf_thumb(original, thumb_filename, width, height): args = [executable, '-scale-to', str(min(width, height)), '-singlefile', '-png', original, thumb_filename] _log.debug('calling {0}'.format(repr(' '.join(args)))) - call(executable=executable, args=args) + Popen(executable=executable, args=args).wait() def pdf_info(original): """ @@ -191,9 +195,10 @@ def pdf_info(original): ret_dict = {} pdfinfo = where('pdfinfo') try: - lines = check_output(executable=pdfinfo, - args=[pdfinfo, original]).split(os.linesep) - except CalledProcessError: + proc = Popen(executable=pdfinfo, + args=[pdfinfo, original], stdout=PIPE) + lines = proc.stdout.readlines() + except OSError: _log.debug('pdfinfo could not read the pdf file.') raise BadMediaFail() diff --git a/mediagoblin/media_types/stl/processing.py b/mediagoblin/media_types/stl/processing.py index e41df395..49382495 100644 --- a/mediagoblin/media_types/stl/processing.py +++ b/mediagoblin/media_types/stl/processing.py @@ -64,8 +64,6 @@ def blender_render(config): """ Called to prerender a model. """ - arg_string = "blender -b blender_render.blend -F " - arg_string +="JPEG -P blender_render.py" env = {"RENDER_SETUP" : json.dumps(config), "DISPLAY":":0"} subprocess.call( ["blender", diff --git a/mediagoblin/media_types/video/transcoders.py b/mediagoblin/media_types/video/transcoders.py index 58b2c0d4..90a767dd 100644 --- a/mediagoblin/media_types/video/transcoders.py +++ b/mediagoblin/media_types/video/transcoders.py @@ -26,7 +26,10 @@ import pygst pygst.require('0.10') import gst import struct -import Image +try: + from PIL import Image +except ImportError: + import Image from gst.extend import discoverer diff --git a/mediagoblin/plugins/oauth/forms.py b/mediagoblin/plugins/oauth/forms.py index d0a4e9b8..5edd992a 100644 --- a/mediagoblin/plugins/oauth/forms.py +++ b/mediagoblin/plugins/oauth/forms.py @@ -19,7 +19,7 @@ import wtforms from urlparse import urlparse from mediagoblin.tools.extlib.wtf_html5 import URLField -from mediagoblin.tools.translate import fake_ugettext_passthrough as _ +from mediagoblin.tools.translate import lazy_pass_to_ugettext as _ class AuthorizationForm(wtforms.Form): diff --git a/mediagoblin/plugins/piwigo/tools.py b/mediagoblin/plugins/piwigo/tools.py index 85d77310..4d2e985a 100644 --- a/mediagoblin/plugins/piwigo/tools.py +++ b/mediagoblin/plugins/piwigo/tools.py @@ -16,6 +16,7 @@ import logging +import six import lxml.etree as ET from werkzeug.exceptions import MethodNotAllowed @@ -43,7 +44,7 @@ class PwgNamedArray(list): def _fill_element_dict(el, data, as_attr=()): for k, v in data.iteritems(): if k in as_attr: - if not isinstance(v, basestring): + if not isinstance(v, six.string_types): v = str(v) el.set(k, v) else: @@ -57,7 +58,7 @@ def _fill_element(el, data): el.text = "1" else: el.text = "0" - elif isinstance(data, basestring): + elif isinstance(data, six.string_types): el.text = data elif isinstance(data, int): el.text = str(data) diff --git a/mediagoblin/plugins/piwigo/views.py b/mediagoblin/plugins/piwigo/views.py index 3dee09cd..bd3f9320 100644 --- a/mediagoblin/plugins/piwigo/views.py +++ b/mediagoblin/plugins/piwigo/views.py @@ -17,12 +17,12 @@ import logging import re -from werkzeug.exceptions import MethodNotAllowed, BadRequest +from werkzeug.exceptions import MethodNotAllowed, BadRequest, NotImplemented from werkzeug.wrappers import BaseResponse from mediagoblin import mg_globals from mediagoblin.meddleware.csrf import csrf_exempt -from mediagoblin.tools.response import render_404 +from mediagoblin.submit.lib import check_file_field from .tools import CmdTable, PwgNamedArray, response_xml from .forms import AddSimpleForm @@ -92,6 +92,9 @@ def pwg_images_addSimple(request): dump.append("%s=%r" % (f.name, f.data)) _log.info("addimple: %r %s %r", request.form, " ".join(dump), request.files) + if not check_file_field(request, 'image'): + raise BadRequest() + return {'image_id': 123456, 'url': ''} @@ -153,7 +156,7 @@ def ws_php(request): if not func: _log.warn("wsphp: Unhandled %s %r %r", request.method, request.args, request.form) - return render_404(request) + raise NotImplemented() result = func(request) diff --git a/mediagoblin/processing/__init__.py b/mediagoblin/processing/__init__.py index a1fd3fb7..f3a85940 100644 --- a/mediagoblin/processing/__init__.py +++ b/mediagoblin/processing/__init__.py @@ -75,6 +75,14 @@ class FilenameBuilder(object): class ProcessingState(object): + """ + The first and only argument to the "processor" of a media type + + This could be thought of as a "request" to the processor + function. It has the main info for the request (media entry) + and a bunch of tools for the request on it. + It can get more fancy without impacting old media types. + """ def __init__(self, entry): self.entry = entry self.workbench = None diff --git a/mediagoblin/processing/task.py b/mediagoblin/processing/task.py index aec50aab..9af192ed 100644 --- a/mediagoblin/processing/task.py +++ b/mediagoblin/processing/task.py @@ -89,7 +89,7 @@ class ProcessMedia(task.Task): with mgg.workbench_manager.create() as workbench: proc_state.set_workbench(workbench) # run the processing code - entry.media_manager['processor'](proc_state) + entry.media_manager.processor(proc_state) # We set the state to processed and save the entry here so there's # no need to save at the end of the processing stage, probably ;) diff --git a/mediagoblin/submit/forms.py b/mediagoblin/submit/forms.py index bd1e904f..e9bd93fd 100644 --- a/mediagoblin/submit/forms.py +++ b/mediagoblin/submit/forms.py @@ -18,7 +18,7 @@ import wtforms from mediagoblin.tools.text import tag_length_validator -from mediagoblin.tools.translate import fake_ugettext_passthrough as _ +from mediagoblin.tools.translate import lazy_pass_to_ugettext as _ from mediagoblin.tools.licenses import licenses_as_choices diff --git a/mediagoblin/submit/lib.py b/mediagoblin/submit/lib.py index a5483471..7c3b8ab3 100644 --- a/mediagoblin/submit/lib.py +++ b/mediagoblin/submit/lib.py @@ -40,7 +40,7 @@ def prepare_queue_task(app, entry, filename): """ Prepare a MediaEntry for the processing queue and get a queue file """ - # We generate this ourselves so we know what the taks id is for + # We generate this ourselves so we know what the task id is for # retrieval later. # (If we got it off the task's auto-generation, there'd be diff --git a/mediagoblin/templates/mediagoblin/utils/wtforms.html b/mediagoblin/templates/mediagoblin/utils/wtforms.html index 35b4aa04..be6976c2 100644 --- a/mediagoblin/templates/mediagoblin/utils/wtforms.html +++ b/mediagoblin/templates/mediagoblin/utils/wtforms.html @@ -19,7 +19,7 @@ {# Render the label for a field #} {% macro render_label(field) %} {%- if field.label.text -%} - <label for="{{ field.label.field_id }}">{{ _(field.label.text) }}</label> + <label for="{{ field.label.field_id }}">{{ field.label.text }}</label> {%- endif -%} {%- endmacro %} @@ -39,11 +39,11 @@ {{ field }} {%- if field.errors -%} {% for error in field.errors %} - <p class="form_field_error">{{ _(error) }}</p> + <p class="form_field_error">{{ error }}</p> {% endfor %} {%- endif %} {%- if field.description %} - <p class="form_field_description">{{ _(field.description)|safe }}</p> + <p class="form_field_description">{{ field.description|safe }}</p> {%- endif %} </div> {%- endmacro %} @@ -59,7 +59,7 @@ {% macro render_table(form) -%} {% for field in form %} <tr> - <th>{{ _(field.label.text) }}</th> + <th>{{ field.label.text }}</th> <td> {{field}} {% if field.errors %} diff --git a/mediagoblin/tests/resources.py b/mediagoblin/tests/resources.py new file mode 100644 index 00000000..f7b3037d --- /dev/null +++ b/mediagoblin/tests/resources.py @@ -0,0 +1,41 @@ +# 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/>. + + +from pkg_resources import resource_filename + + +def resource(filename): + return resource_filename('mediagoblin.tests', 'test_submission/' + filename) + + +GOOD_JPG = resource('good.jpg') +GOOD_PNG = resource('good.png') +EVIL_FILE = resource('evil') +EVIL_JPG = resource('evil.jpg') +EVIL_PNG = resource('evil.png') +BIG_BLUE = resource('bigblue.png') +GOOD_PDF = resource('good.pdf') + + +def resource_exif(f): + return resource_filename('mediagoblin.tests', 'test_exif/' + f) + + +GOOD_JPG = resource_exif('good.jpg') +EMPTY_JPG = resource_exif('empty.jpg') +BAD_JPG = resource_exif('bad.jpg') +GPS_JPG = resource_exif('has-gps.jpg') diff --git a/mediagoblin/tests/test_api.py b/mediagoblin/tests/test_api.py index cff25776..89cf1026 100644 --- a/mediagoblin/tests/test_api.py +++ b/mediagoblin/tests/test_api.py @@ -18,31 +18,17 @@ import logging import base64 -from pkg_resources import resource_filename - import pytest from mediagoblin import mg_globals from mediagoblin.tools import template, pluginapi from mediagoblin.tests.tools import fixture_add_user +from .resources import GOOD_JPG, GOOD_PNG, EVIL_FILE, EVIL_JPG, EVIL_PNG, \ + BIG_BLUE _log = logging.getLogger(__name__) -def resource(filename): - ''' - Borrowed from the submission tests - ''' - return resource_filename('mediagoblin.tests', 'test_submission/' + filename) - - -GOOD_JPG = resource('good.jpg') -GOOD_PNG = resource('good.png') -EVIL_FILE = resource('evil') -EVIL_JPG = resource('evil.jpg') -EVIL_PNG = resource('evil.png') -BIG_BLUE = resource('bigblue.png') - class TestAPI(object): def setup(self): diff --git a/mediagoblin/tests/test_exif.py b/mediagoblin/tests/test_exif.py index 100d17f0..824de3c2 100644 --- a/mediagoblin/tests/test_exif.py +++ b/mediagoblin/tests/test_exif.py @@ -15,39 +15,20 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import pkg_resources -import Image +try: + from PIL import Image +except ImportError: + import Image from mediagoblin.tools.exif import exif_fix_image_orientation, \ extract_exif, clean_exif, get_gps_data, get_useful +from .resources import GOOD_JPG, EMPTY_JPG, BAD_JPG, GPS_JPG def assert_in(a, b): assert a in b, "%r not in %r" % (a, b) -GOOD_JPG = pkg_resources.resource_filename( - 'mediagoblin.tests', - os.path.join( - 'test_exif', - 'good.jpg')) -EMPTY_JPG = pkg_resources.resource_filename( - 'mediagoblin.tests', - os.path.join( - 'test_exif', - 'empty.jpg')) -BAD_JPG = pkg_resources.resource_filename( - 'mediagoblin.tests', - os.path.join( - 'test_exif', - 'bad.jpg')) -GPS_JPG = pkg_resources.resource_filename( - 'mediagoblin.tests', - os.path.join( - 'test_exif', - 'has-gps.jpg')) - - def test_exif_extraction(): ''' Test EXIF extraction from a good image diff --git a/mediagoblin/tests/test_http_callback.py b/mediagoblin/tests/test_http_callback.py index e2c85d0d..a0511af7 100644 --- a/mediagoblin/tests/test_http_callback.py +++ b/mediagoblin/tests/test_http_callback.py @@ -16,6 +16,7 @@ import json +import pytest from urlparse import urlparse, parse_qs from mediagoblin import mg_globals @@ -26,21 +27,24 @@ from mediagoblin.tests import test_oauth as oauth class TestHTTPCallback(object): - def _setup(self, test_app): + @pytest.fixture(autouse=True) + def setup(self, test_app): + self.test_app = test_app + self.db = mg_globals.database self.user_password = u'secret' self.user = fixture_add_user(u'call_back', self.user_password) - self.login(test_app) + self.login() - def login(self, testapp): - testapp.post('/auth/login/', { + def login(self): + self.test_app.post('/auth/login/', { 'username': self.user.username, 'password': self.user_password}) - def get_access_token(self, testapp, client_id, client_secret, code): - response = testapp.get('/oauth/access_token', { + def get_access_token(self, client_id, client_secret, code): + response = self.test_app.get('/oauth/access_token', { 'code': code, 'client_id': client_id, 'client_secret': client_secret}) @@ -49,15 +53,12 @@ class TestHTTPCallback(object): return response_data['access_token'] - def test_callback(self, test_app): + def test_callback(self): ''' Test processing HTTP callback ''' - self._setup(test_app) - self.oauth = oauth.TestOAuth() - self.oauth._setup(test_app) + self.oauth.setup(self.test_app) - redirect, client_id = self.oauth.test_4_authorize_confidential_client( - test_app) + redirect, client_id = self.oauth.test_4_authorize_confidential_client() code = parse_qs(urlparse(redirect.location).query)['code'][0] @@ -66,11 +67,11 @@ class TestHTTPCallback(object): client_secret = client.secret - access_token = self.get_access_token(test_app, client_id, client_secret, code) + access_token = self.get_access_token(client_id, client_secret, code) callback_url = 'https://foo.example?secrettestmediagoblinparam' - res = test_app.post('/api/submit?client_id={0}&access_token={1}\ + self.test_app.post('/api/submit?client_id={0}&access_token={1}\ &client_secret={2}'.format( client_id, access_token, diff --git a/mediagoblin/tests/test_oauth.py b/mediagoblin/tests/test_oauth.py index 7ad98459..ea3bd798 100644 --- a/mediagoblin/tests/test_oauth.py +++ b/mediagoblin/tests/test_oauth.py @@ -17,6 +17,7 @@ import json import logging +import pytest from urlparse import parse_qs, urlparse from mediagoblin import mg_globals @@ -28,7 +29,10 @@ _log = logging.getLogger(__name__) class TestOAuth(object): - def _setup(self, test_app): + @pytest.fixture(autouse=True) + def setup(self, test_app): + self.test_app = test_app + self.db = mg_globals.database self.pman = pluginapi.PluginManager() @@ -36,17 +40,17 @@ class TestOAuth(object): self.user_password = u'4cc355_70k3N' self.user = fixture_add_user(u'joauth', self.user_password) - self.login(test_app) + self.login() - def login(self, test_app): - test_app.post( - '/auth/login/', { - 'username': self.user.username, - 'password': self.user_password}) + def login(self): + self.test_app.post( + '/auth/login/', { + 'username': self.user.username, + 'password': self.user_password}) - def register_client(self, test_app, name, client_type, description=None, - redirect_uri=''): - return test_app.post( + def register_client(self, name, client_type, description=None, + redirect_uri=''): + return self.test_app.post( '/oauth/client/register', { 'name': name, 'description': description, @@ -56,12 +60,10 @@ class TestOAuth(object): def get_context(self, template_name): return template.TEMPLATE_TEST_CONTEXT[template_name] - def test_1_public_client_registration_without_redirect_uri(self, test_app): + def test_1_public_client_registration_without_redirect_uri(self): ''' Test 'public' OAuth client registration without any redirect uri ''' - self._setup(test_app) - - response = self.register_client(test_app, u'OMGOMGOMG', 'public', - 'OMGOMG Apache License v2') + response = self.register_client( + u'OMGOMGOMG', 'public', 'OMGOMG Apache License v2') ctx = self.get_context('oauth/client/register.html') @@ -76,12 +78,11 @@ class TestOAuth(object): # Should not pass through assert not client - def test_2_successful_public_client_registration(self, test_app): + def test_2_successful_public_client_registration(self): ''' Successfully register a public client ''' - self._setup(test_app) uri = 'http://foo.example' - self.register_client(test_app, u'OMGOMG', 'public', 'OMG!', - uri) + self.register_client( + u'OMGOMG', 'public', 'OMG!', uri) client = self.db.OAuthClient.query.filter( self.db.OAuthClient.name == u'OMGOMG').first() @@ -92,12 +93,10 @@ class TestOAuth(object): # Client should have been registered assert client - def test_3_successful_confidential_client_reg(self, test_app): + def test_3_successful_confidential_client_reg(self): ''' Register a confidential OAuth client ''' - self._setup(test_app) - response = self.register_client( - test_app, u'GMOGMO', 'confidential', 'NO GMO!') + u'GMOGMO', 'confidential', 'NO GMO!') assert response.status_int == 302 @@ -109,16 +108,14 @@ class TestOAuth(object): return client - def test_4_authorize_confidential_client(self, test_app): + def test_4_authorize_confidential_client(self): ''' Authorize a confidential client as a logged in user ''' - self._setup(test_app) - - client = self.test_3_successful_confidential_client_reg(test_app) + client = self.test_3_successful_confidential_client_reg() client_identifier = client.identifier redirect_uri = 'https://foo.example' - response = test_app.get('/oauth/authorize', { + response = self.test_app.get('/oauth/authorize', { 'client_id': client.identifier, 'scope': 'all', 'redirect_uri': redirect_uri}) @@ -131,7 +128,7 @@ class TestOAuth(object): form = ctx['form'] # Short for client authorization post reponse - capr = test_app.post( + capr = self.test_app.post( '/oauth/client/authorize', { 'client_id': form.client_id.data, 'allow': 'Allow', @@ -149,19 +146,16 @@ class TestOAuth(object): ''' Get the value of ?code= from an URI ''' return parse_qs(urlparse(uri).query)['code'][0] - def test_token_endpoint_successful_confidential_request(self, test_app): + def test_token_endpoint_successful_confidential_request(self): ''' Successful request against token endpoint ''' - self._setup(test_app) - - code_redirect, client_id = self.test_4_authorize_confidential_client( - test_app) + code_redirect, client_id = self.test_4_authorize_confidential_client() code = self.get_code_from_redirect_uri(code_redirect.location) client = self.db.OAuthClient.query.filter( self.db.OAuthClient.identifier == unicode(client_id)).first() - token_res = test_app.get('/oauth/access_token?client_id={0}&\ + token_res = self.test_app.get('/oauth/access_token?client_id={0}&\ code={1}&client_secret={2}'.format(client_id, code, client.secret)) assert token_res.status_int == 200 @@ -180,19 +174,16 @@ code={1}&client_secret={2}'.format(client_id, code, client.secret)) return client_id, token_data - def test_token_endpont_missing_id_confidential_request(self, test_app): + def test_token_endpont_missing_id_confidential_request(self): ''' Unsuccessful request against token endpoint, missing client_id ''' - self._setup(test_app) - - code_redirect, client_id = self.test_4_authorize_confidential_client( - test_app) + code_redirect, client_id = self.test_4_authorize_confidential_client() code = self.get_code_from_redirect_uri(code_redirect.location) client = self.db.OAuthClient.query.filter( self.db.OAuthClient.identifier == unicode(client_id)).first() - token_res = test_app.get('/oauth/access_token?\ + token_res = self.test_app.get('/oauth/access_token?\ code={0}&client_secret={1}'.format(code, client.secret)) assert token_res.status_int == 200 @@ -204,16 +195,16 @@ code={0}&client_secret={1}'.format(code, client.secret)) assert token_data['error'] == 'invalid_request' assert len(token_data['error_description']) - def test_refresh_token(self, test_app): + def test_refresh_token(self): ''' Try to get a new access token using the refresh token ''' # Get an access token and a refresh token client_id, token_data =\ - self.test_token_endpoint_successful_confidential_request(test_app) + self.test_token_endpoint_successful_confidential_request() client = self.db.OAuthClient.query.filter( self.db.OAuthClient.identifier == client_id).first() - token_res = test_app.get('/oauth/access_token', + token_res = self.test_app.get('/oauth/access_token', {'refresh_token': token_data['refresh_token'], 'client_id': client_id, 'client_secret': client.secret diff --git a/mediagoblin/tests/test_pdf.py b/mediagoblin/tests/test_pdf.py index a3979a25..b4d1940a 100644 --- a/mediagoblin/tests/test_pdf.py +++ b/mediagoblin/tests/test_pdf.py @@ -17,16 +17,15 @@ import tempfile import shutil import os - +import pytest from mediagoblin.media_types.pdf.processing import ( pdf_info, check_prerequisites, create_pdf_thumb) +from .resources import GOOD_PDF as GOOD -GOOD='mediagoblin/tests/test_submission/good.pdf' +@pytest.mark.skipif("not check_prerequisites()") def test_pdf(): - if not check_prerequisites(): - return good_dict = {'pdf_version_major': 1, 'pdf_title': '', 'pdf_page_size_width': 612, 'pdf_author': '', 'pdf_keywords': '', 'pdf_pages': 10, diff --git a/mediagoblin/tests/test_pluginapi.py b/mediagoblin/tests/test_pluginapi.py index d40a5081..f03e868f 100644 --- a/mediagoblin/tests/test_pluginapi.py +++ b/mediagoblin/tests/test_pluginapi.py @@ -177,19 +177,22 @@ def test_disabled_plugin(): assert len(pman.plugins) == 0 +CONFIG_ALL_CALLABLES = [ + ('mediagoblin', {}, []), + ('plugins', {}, [ + ('mediagoblin.tests.testplugins.callables1', {}, []), + ('mediagoblin.tests.testplugins.callables2', {}, []), + ('mediagoblin.tests.testplugins.callables3', {}, []), + ]) + ] + + @with_cleanup() -def test_callable_runone(): +def test_hook_handle(): """ - Test the callable_runone method + Test the hook_handle method """ - cfg = build_config([ - ('mediagoblin', {}, []), - ('plugins', {}, [ - ('mediagoblin.tests.testplugins.callables1', {}, []), - ('mediagoblin.tests.testplugins.callables2', {}, []), - ('mediagoblin.tests.testplugins.callables3', {}, []), - ]) - ]) + cfg = build_config(CONFIG_ALL_CALLABLES) mg_globals.app_config = cfg['mediagoblin'] mg_globals.global_config = cfg @@ -198,50 +201,42 @@ def test_callable_runone(): # Just one hook provided call_log = [] - assert pluginapi.callable_runone( + assert pluginapi.hook_handle( "just_one", call_log) == "Called just once" assert call_log == ["expect this one call"] # Nothing provided and unhandled not okay call_log = [] - with pytest.raises(pluginapi.UnhandledCallable): - pluginapi.callable_runone( - "nothing_handling", call_log) + pluginapi.hook_handle( + "nothing_handling", call_log) == None assert call_log == [] # Nothing provided and unhandled okay call_log = [] - assert pluginapi.callable_runone( + assert pluginapi.hook_handle( "nothing_handling", call_log, unhandled_okay=True) is None assert call_log == [] # Multiple provided, go with the first! call_log = [] - assert pluginapi.callable_runone( + assert pluginapi.hook_handle( "multi_handle", call_log) == "the first returns" assert call_log == ["Hi, I'm the first"] # Multiple provided, one has CantHandleIt call_log = [] - assert pluginapi.callable_runone( + assert pluginapi.hook_handle( "multi_handle_with_canthandle", call_log) == "the second returns" assert call_log == ["Hi, I'm the second"] @with_cleanup() -def test_callable_runall(): +def test_hook_runall(): """ - Test the callable_runall method + Test the hook_runall method """ - cfg = build_config([ - ('mediagoblin', {}, []), - ('plugins', {}, [ - ('mediagoblin.tests.testplugins.callables1', {}, []), - ('mediagoblin.tests.testplugins.callables2', {}, []), - ('mediagoblin.tests.testplugins.callables3', {}, []), - ]) - ]) + cfg = build_config(CONFIG_ALL_CALLABLES) mg_globals.app_config = cfg['mediagoblin'] mg_globals.global_config = cfg @@ -250,19 +245,19 @@ def test_callable_runall(): # Just one hook, check results call_log = [] - assert pluginapi.callable_runall( - "just_one", call_log) == ["Called just once", None, None] + assert pluginapi.hook_runall( + "just_one", call_log) == ["Called just once"] assert call_log == ["expect this one call"] # None provided, check results call_log = [] - assert pluginapi.callable_runall( + assert pluginapi.hook_runall( "nothing_handling", call_log) == [] assert call_log == [] # Multiple provided, check results call_log = [] - assert pluginapi.callable_runall( + assert pluginapi.hook_runall( "multi_handle", call_log) == [ "the first returns", "the second returns", @@ -275,7 +270,7 @@ def test_callable_runall(): # Multiple provided, one has CantHandleIt, check results call_log = [] - assert pluginapi.callable_runall( + assert pluginapi.hook_runall( "multi_handle_with_canthandle", call_log) == [ "the second returns", "the third returns", @@ -283,3 +278,19 @@ def test_callable_runall(): assert call_log == [ "Hi, I'm the second", "Hi, I'm the third"] + + +@with_cleanup() +def test_hook_transform(): + """ + Test the hook_transform method + """ + cfg = build_config(CONFIG_ALL_CALLABLES) + + mg_globals.app_config = cfg['mediagoblin'] + mg_globals.global_config = cfg + + setup_plugins() + + assert pluginapi.hook_transform( + "expand_tuple", (-1, 0)) == (-1, 0, 1, 2, 3) diff --git a/mediagoblin/tests/test_storage.py b/mediagoblin/tests/test_storage.py index 749f7b07..f6f1d18f 100644 --- a/mediagoblin/tests/test_storage.py +++ b/mediagoblin/tests/test_storage.py @@ -95,6 +95,14 @@ def get_tmp_filestorage(mount_url=None, fake_remote=False): return tmpdir, this_storage +def cleanup_storage(this_storage, tmpdir, *paths): + for p in paths: + while p: + assert this_storage.delete_dir(p) == True + p.pop(-1) + os.rmdir(tmpdir) + + def test_basic_storage__resolve_filepath(): tmpdir, this_storage = get_tmp_filestorage() @@ -111,7 +119,7 @@ def test_basic_storage__resolve_filepath(): this_storage._resolve_filepath, ['../../', 'etc', 'passwd']) - os.rmdir(tmpdir) + cleanup_storage(this_storage, tmpdir) def test_basic_storage_file_exists(): @@ -127,6 +135,7 @@ def test_basic_storage_file_exists(): assert not this_storage.file_exists(['dnedir1', 'dnedir2', 'somefile.lol']) this_storage.delete_file(['dir1', 'dir2', 'filename.txt']) + cleanup_storage(this_storage, tmpdir, ['dir1', 'dir2']) def test_basic_storage_get_unique_filepath(): @@ -149,6 +158,7 @@ def test_basic_storage_get_unique_filepath(): assert new_filename == secure_filename(new_filename) os.remove(filename) + cleanup_storage(this_storage, tmpdir, ['dir1', 'dir2']) def test_basic_storage_get_file(): @@ -189,6 +199,7 @@ def test_basic_storage_get_file(): this_storage.delete_file(filepath) this_storage.delete_file(new_filepath) this_storage.delete_file(['testydir', 'testyfile.txt']) + cleanup_storage(this_storage, tmpdir, ['dir1', 'dir2'], ['testydir']) def test_basic_storage_delete_file(): @@ -204,11 +215,15 @@ def test_basic_storage_delete_file(): assert os.path.exists( os.path.join(tmpdir, 'dir1/dir2/ourfile.txt')) + assert this_storage.delete_dir(['dir1', 'dir2']) == False this_storage.delete_file(filepath) + assert this_storage.delete_dir(['dir1', 'dir2']) == True assert not os.path.exists( os.path.join(tmpdir, 'dir1/dir2/ourfile.txt')) + cleanup_storage(this_storage, tmpdir, ['dir1']) + def test_basic_storage_url_for_file(): # Not supplying a base_url should actually just bork. @@ -217,7 +232,7 @@ def test_basic_storage_url_for_file(): storage.NoWebServing, this_storage.file_url, ['dir1', 'dir2', 'filename.txt']) - os.rmdir(tmpdir) + cleanup_storage(this_storage, tmpdir) # base_url without domain tmpdir, this_storage = get_tmp_filestorage('/media/') @@ -225,7 +240,7 @@ def test_basic_storage_url_for_file(): ['dir1', 'dir2', 'filename.txt']) expected = '/media/dir1/dir2/filename.txt' assert result == expected - os.rmdir(tmpdir) + cleanup_storage(this_storage, tmpdir) # base_url with domain tmpdir, this_storage = get_tmp_filestorage( @@ -234,7 +249,7 @@ def test_basic_storage_url_for_file(): ['dir1', 'dir2', 'filename.txt']) expected = 'http://media.example.org/ourmedia/dir1/dir2/filename.txt' assert result == expected - os.rmdir(tmpdir) + cleanup_storage(this_storage, tmpdir) def test_basic_storage_get_local_path(): @@ -248,13 +263,13 @@ def test_basic_storage_get_local_path(): assert result == expected - os.rmdir(tmpdir) + cleanup_storage(this_storage, tmpdir) def test_basic_storage_is_local(): tmpdir, this_storage = get_tmp_filestorage() assert this_storage.local_storage is True - os.rmdir(tmpdir) + cleanup_storage(this_storage, tmpdir) def test_basic_storage_copy_locally(): @@ -275,6 +290,7 @@ def test_basic_storage_copy_locally(): os.remove(new_file_dest) os.rmdir(dest_tmpdir) + cleanup_storage(this_storage, tmpdir, ['dir1', 'dir2']) def _test_copy_local_to_storage_works(tmpdir, this_storage): @@ -292,6 +308,7 @@ def _test_copy_local_to_storage_works(tmpdir, this_storage): 'r').read() == 'haha' this_storage.delete_file(['dir1', 'dir2', 'copiedto.txt']) + cleanup_storage(this_storage, tmpdir, ['dir1', 'dir2']) def test_basic_storage_copy_local_to_storage(): diff --git a/mediagoblin/tests/test_submission.py b/mediagoblin/tests/test_submission.py index 462a1653..5ac47316 100644 --- a/mediagoblin/tests/test_submission.py +++ b/mediagoblin/tests/test_submission.py @@ -20,8 +20,7 @@ sys.setdefaultencoding('utf-8') import urlparse import os - -from pkg_resources import resource_filename +import pytest from mediagoblin.tests.tools import fixture_add_user from mediagoblin import mg_globals @@ -30,19 +29,8 @@ from mediagoblin.tools import template from mediagoblin.media_types.image import MEDIA_MANAGER as img_MEDIA_MANAGER from mediagoblin.media_types.pdf.processing import check_prerequisites as pdf_check_prerequisites -def resource(filename): - return resource_filename('mediagoblin.tests', 'test_submission/' + filename) - - -GOOD_JPG = resource('good.jpg') -GOOD_PNG = resource('good.png') -EVIL_FILE = resource('evil') -EVIL_JPG = resource('evil.jpg') -EVIL_PNG = resource('evil.png') -BIG_BLUE = resource('bigblue.png') -GOOD_PDF = resource('good.pdf') - -from .test_exif import GPS_JPG +from .resources import GOOD_JPG, GOOD_PNG, EVIL_FILE, EVIL_JPG, EVIL_PNG, \ + BIG_BLUE, GOOD_PDF, GPS_JPG GOOD_TAG_STRING = u'yin,yang' BAD_TAG_STRING = unicode('rage,' + 'f' * 26 + 'u' * 26) @@ -52,7 +40,8 @@ REQUEST_CONTEXT = ['mediagoblin/user_pages/user.html', 'request'] class TestSubmission: - def _setup(self, test_app): + @pytest.fixture(autouse=True) + def setup(self, test_app): self.test_app = test_app # TODO: Possibly abstract into a decorator like: @@ -91,9 +80,7 @@ class TestSubmission: comments = request.db.MediaComment.find({'media_entry': media_id}) assert count == len(list(comments)) - def test_missing_fields(self, test_app): - self._setup(test_app) - + def test_missing_fields(self): # Test blank form # --------------- response, form = self.do_post({}, *FORM_CONTEXT) @@ -120,18 +107,14 @@ class TestSubmission: self.logout() self.test_app.get(url) - def test_normal_jpg(self, test_app): - self._setup(test_app) + def test_normal_jpg(self): self.check_normal_upload(u'Normal upload 1', GOOD_JPG) - def test_normal_png(self, test_app): - self._setup(test_app) + def test_normal_png(self): self.check_normal_upload(u'Normal upload 2', GOOD_PNG) - def test_normal_pdf(self, test_app): - if not pdf_check_prerequisites(): - return - self._setup(test_app) + @pytest.mark.skipif("not pdf_check_prerequisites()") + def test_normal_pdf(self): response, context = self.do_post({'title': u'Normal upload 3 (pdf)'}, do_follow=True, **self.upload_data(GOOD_PDF)) @@ -146,9 +129,7 @@ class TestSubmission: return return media[0] - def test_tags(self, test_app): - self._setup(test_app) - + def test_tags(self): # Good tag string # -------- response, request = self.do_post({'title': u'Balanced Goblin 2', @@ -173,9 +154,7 @@ class TestSubmission: 'Tags that are too long: ' \ 'ffffffffffffffffffffffffffuuuuuuuuuuuuuuuuuuuuuuuuuu'] - def test_delete(self, test_app): - self._setup(test_app) - + def test_delete(self): response, request = self.do_post({'title': u'Balanced Goblin'}, *REQUEST_CONTEXT, do_follow=True, **self.upload_data(GOOD_JPG)) @@ -220,9 +199,7 @@ class TestSubmission: self.check_media(request, {'id': media_id}, 0) self.check_comments(request, media_id, 0) - def test_evil_file(self, test_app): - self._setup(test_app) - + def test_evil_file(self): # Test non-suppoerted file with non-supported extension # ----------------------------------------------------- response, form = self.do_post({'title': u'Malicious Upload 1'}, @@ -233,26 +210,23 @@ class TestSubmission: str(form.file.errors[0]) - def test_get_media_manager(self, test_app): + def test_get_media_manager(self): """Test if the get_media_manger function returns sensible things """ - self._setup(test_app) - response, request = self.do_post({'title': u'Balanced Goblin'}, *REQUEST_CONTEXT, do_follow=True, **self.upload_data(GOOD_JPG)) media = self.check_media(request, {'title': u'Balanced Goblin'}, 1) assert media.media_type == u'mediagoblin.media_types.image' - assert media.media_manager == img_MEDIA_MANAGER + assert isinstance(media.media_manager, img_MEDIA_MANAGER) + assert media.media_manager.entry == media - def test_sniffing(self, test_app): + def test_sniffing(self): ''' Test sniffing mechanism to assert that regular uploads work as intended ''' - self._setup(test_app) - template.clear_test_template_context() response = self.test_app.post( '/submit/', { @@ -282,30 +256,22 @@ class TestSubmission: assert entry.state == 'failed' assert entry.fail_error == u'mediagoblin.processing:BadMediaFail' - def test_evil_jpg(self, test_app): - self._setup(test_app) - + def test_evil_jpg(self): # Test non-supported file with .jpg extension # ------------------------------------------- self.check_false_image(u'Malicious Upload 2', EVIL_JPG) - def test_evil_png(self, test_app): - self._setup(test_app) - + def test_evil_png(self): # Test non-supported file with .png extension # ------------------------------------------- self.check_false_image(u'Malicious Upload 3', EVIL_PNG) - def test_media_data(self, test_app): - self._setup(test_app) - + def test_media_data(self): self.check_normal_upload(u"With GPS data", GPS_JPG) media = self.check_media(None, {"title": u"With GPS data"}, 1) assert media.media_data.gps_latitude == 59.336666666666666 - def test_processing(self, test_app): - self._setup(test_app) - + def test_processing(self): public_store_dir = mg_globals.global_config[ 'storage:publicstore']['base_dir'] diff --git a/mediagoblin/tests/test_util.py b/mediagoblin/tests/test_util.py index e4c04b7a..bc14f528 100644 --- a/mediagoblin/tests/test_util.py +++ b/mediagoblin/tests/test_util.py @@ -104,6 +104,28 @@ def test_locale_to_lower_lower(): assert translate.locale_to_lower_lower('en_us') == 'en-us' +def test_gettext_lazy_proxy(): + from mediagoblin.tools.translate import lazy_pass_to_ugettext as _ + from mediagoblin.tools.translate import pass_to_ugettext, set_thread_locale + proxy = _(u"Password") + orig = u"Password" + + set_thread_locale("es") + p1 = unicode(proxy) + p1_should = pass_to_ugettext(orig) + assert p1_should != orig, "Test useless, string not translated" + assert p1 == p1_should + + set_thread_locale("sv") + p2 = unicode(proxy) + p2_should = pass_to_ugettext(orig) + assert p2_should != orig, "Test broken, string not translated" + assert p2 == p2_should + + assert p1_should != p2_should, "Test broken, same translated string" + assert p1 != p2 + + def test_html_cleaner(): # Remove images result = text.clean_html( diff --git a/mediagoblin/tests/test_workbench.py b/mediagoblin/tests/test_workbench.py index 9cd49671..6695618b 100644 --- a/mediagoblin/tests/test_workbench.py +++ b/mediagoblin/tests/test_workbench.py @@ -21,7 +21,7 @@ import tempfile from mediagoblin.tools import workbench from mediagoblin.mg_globals import setup_globals from mediagoblin.decorators import get_workbench -from mediagoblin.tests.test_storage import get_tmp_filestorage +from mediagoblin.tests.test_storage import get_tmp_filestorage, cleanup_storage class TestWorkbench(object): @@ -76,6 +76,7 @@ class TestWorkbench(object): assert filename == os.path.join( tmpdir, 'dir1/dir2/ourfile.txt') this_storage.delete_file(filepath) + cleanup_storage(this_storage, tmpdir, ['dir1', 'dir2']) # with a fake remote file storage tmpdir, this_storage = get_tmp_filestorage(fake_remote=True) @@ -102,6 +103,7 @@ class TestWorkbench(object): this_workbench.dir, 'thisfile.text') this_storage.delete_file(filepath) + cleanup_storage(this_storage, tmpdir, ['dir1', 'dir2']) this_workbench.destroy() def test_workbench_decorator(self): diff --git a/mediagoblin/tests/testplugins/callables1/__init__.py b/mediagoblin/tests/testplugins/callables1/__init__.py index 9c278b49..fe801a01 100644 --- a/mediagoblin/tests/testplugins/callables1/__init__.py +++ b/mediagoblin/tests/testplugins/callables1/__init__.py @@ -14,8 +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 mediagoblin.tools.pluginapi import CantHandleIt - def setup_plugin(): pass @@ -30,12 +28,16 @@ def multi_handle(call_log): return "the first returns" def multi_handle_with_canthandle(call_log): - raise CantHandleIt("I just can't accept this stupid method") + return None + +def expand_tuple(this_tuple): + return this_tuple + (1,) hooks = { 'setup': setup_plugin, 'just_one': just_one, 'multi_handle': multi_handle, 'multi_handle_with_canthandle': multi_handle_with_canthandle, + 'expand_tuple': expand_tuple, } diff --git a/mediagoblin/tests/testplugins/callables2/__init__.py b/mediagoblin/tests/testplugins/callables2/__init__.py index aaab5b21..9d5cf950 100644 --- a/mediagoblin/tests/testplugins/callables2/__init__.py +++ b/mediagoblin/tests/testplugins/callables2/__init__.py @@ -29,10 +29,13 @@ def multi_handle_with_canthandle(call_log): call_log.append("Hi, I'm the second") return "the second returns" +def expand_tuple(this_tuple): + return this_tuple + (2,) hooks = { 'setup': setup_plugin, 'just_one': just_one, 'multi_handle': multi_handle, 'multi_handle_with_canthandle': multi_handle_with_canthandle, + 'expand_tuple': expand_tuple, } diff --git a/mediagoblin/tests/testplugins/callables3/__init__.py b/mediagoblin/tests/testplugins/callables3/__init__.py index 8d0c9c25..04efc8fc 100644 --- a/mediagoblin/tests/testplugins/callables3/__init__.py +++ b/mediagoblin/tests/testplugins/callables3/__init__.py @@ -29,10 +29,13 @@ def multi_handle_with_canthandle(call_log): call_log.append("Hi, I'm the third") return "the third returns" +def expand_tuple(this_tuple): + return this_tuple + (3,) hooks = { 'setup': setup_plugin, 'just_one': just_one, 'multi_handle': multi_handle, 'multi_handle_with_canthandle': multi_handle_with_canthandle, + 'expand_tuple': expand_tuple, } diff --git a/mediagoblin/tools/pluginapi.py b/mediagoblin/tools/pluginapi.py index 283350a8..3f98aa8a 100644 --- a/mediagoblin/tools/pluginapi.py +++ b/mediagoblin/tools/pluginapi.py @@ -274,68 +274,94 @@ def get_hook_templates(hook_name): return PluginManager().get_template_hooks(hook_name) -########################### -# Callable convenience code -########################### +############################# +## Hooks: The Next Generation +############################# -class CantHandleIt(Exception): - """ - A callable may call this method if they look at the relevant - arguments passed and decide it's not possible for them to handle - things. - """ - pass -class UnhandledCallable(Exception): - """ - Raise this method if no callables were available to handle the - specified hook. Only used by callable_runone. +def hook_handle(hook_name, *args, **kwargs): """ - pass + 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. -def callable_runone(hookname, *args, **kwargs): - """ - Run the callable hook HOOKNAME... run until the first response, - then return. + (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.) - This function will run stop at the first hook that handles the - result. Hooks raising CantHandleIt will be skipped. + 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. - Unless unhandled_okay is True, this will error out if no hooks - have been registered to handle this function. + 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. """ - callables = PluginManager().get_hook_callables(hookname) + default_handler = kwargs.pop('default_handler', None) + + callables = PluginManager().get_hook_callables(hook_name) - unhandled_okay = kwargs.pop("unhandled_okay", False) + result = None for callable in callables: - try: - return callable(*args, **kwargs) - except CantHandleIt: - continue + result = callable(*args, **kwargs) - if unhandled_okay is False: - raise UnhandledCallable( - "No hooks registered capable of handling '%s'" % hookname) + if result is not None: + break + if result is None and default_handler is not None: + result = default_handler(*args, **kwargs) + + return result -def callable_runall(hookname, *args, **kwargs): - """ - Run all callables for HOOKNAME. - This method will run *all* hooks that handle this method (skipping - those that raise CantHandleIt), and will return a list of all - results. +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(hookname) + callables = PluginManager().get_hook_callables(hook_name) results = [] for callable in callables: - try: - results.append(callable(*args, **kwargs)) - except CantHandleIt: - continue + 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/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/template.py b/mediagoblin/tools/template.py index 78d65654..54aeac92 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,7 +26,7 @@ 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.translate import set_thread_locale from mediagoblin.tools.pluginapi import get_hook_templates from mediagoblin.tools.timesince import timesince from mediagoblin.meddleware.csrf import render_csrf_form_token @@ -44,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. diff --git a/mediagoblin/tools/translate.py b/mediagoblin/tools/translate.py index 4acafac7..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,7 +143,6 @@ 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. @@ -133,6 +153,7 @@ def pass_to_ungettext(*args, **kwargs): return mg_globals.thread_scope.translations.ungettext( *args, **kwargs) + def lazy_pass_to_ugettext(*args, **kwargs): """ Lazily pass to ugettext. @@ -144,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): @@ -166,7 +187,7 @@ 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): """ @@ -176,7 +197,7 @@ def lazy_pass_to_ungettext(*args, **kwargs): level but you need it to not translate until the time that it's used as a string. """ - return LazyProxy(pass_to_ungettext, *args, **kwargs) + return ReallyLazyProxy(pass_to_ungettext, *args, **kwargs) def fake_ugettext_passthrough(string): diff --git a/mediagoblin/user_pages/forms.py b/mediagoblin/user_pages/forms.py index e9746a6c..9a193680 100644 --- a/mediagoblin/user_pages/forms.py +++ b/mediagoblin/user_pages/forms.py @@ -16,7 +16,7 @@ import wtforms from wtforms.ext.sqlalchemy.fields import QuerySelectField -from mediagoblin.tools.translate import fake_ugettext_passthrough as _ +from mediagoblin.tools.translate import lazy_pass_to_ugettext as _ class MediaCommentForm(wtforms.Form): comment_content = wtforms.TextAreaField( diff --git a/mediagoblin/user_pages/lib.py b/mediagoblin/user_pages/lib.py index 8a064a7c..2f47e4b1 100644 --- a/mediagoblin/user_pages/lib.py +++ b/mediagoblin/user_pages/lib.py @@ -18,6 +18,8 @@ from mediagoblin.tools.mail import send_email from mediagoblin.tools.template import render_template from mediagoblin.tools.translate import pass_to_ugettext as _ from mediagoblin import mg_globals +from mediagoblin.db.base import Session +from mediagoblin.db.models import CollectionItem def send_comment_email(user, comment, media, request): @@ -55,3 +57,21 @@ def send_comment_email(user, comment, media, request): instance_title=mg_globals.app_config['html_title']) \ + _('commented on your post'), rendered_email) + + +def add_media_to_collection(collection, media, note=None, commit=True): + collection_item = CollectionItem() + collection_item.collection = collection.id + collection_item.media_entry = media.id + if note: + collection_item.note = note + Session.add(collection_item) + + collection.items = collection.items + 1 + Session.add(collection) + + media.collected = media.collected + 1 + Session.add(media) + + if commit: + Session.commit() diff --git a/mediagoblin/user_pages/views.py b/mediagoblin/user_pages/views.py index 61c23f16..e3b46c0f 100644 --- a/mediagoblin/user_pages/views.py +++ b/mediagoblin/user_pages/views.py @@ -24,7 +24,8 @@ from mediagoblin.tools.response import render_to_response, render_404, redirect from mediagoblin.tools.translate import pass_to_ugettext as _ from mediagoblin.tools.pagination import Pagination from mediagoblin.user_pages import forms as user_forms -from mediagoblin.user_pages.lib import send_comment_email +from mediagoblin.user_pages.lib import (send_comment_email, + add_media_to_collection) from mediagoblin.decorators import (uses_pagination, get_user_media_entry, get_media_entry_by_id, @@ -248,17 +249,7 @@ def media_collect(request, media): _('"%s" already in collection "%s"') % (media.title, collection.title)) else: # Add item to collection - collection_item = request.db.CollectionItem() - collection_item.collection = collection.id - collection_item.media_entry = media.id - collection_item.note = form.note.data - collection_item.save() - - collection.items = collection.items + 1 - collection.save() - - media.collected = media.collected + 1 - media.save() + add_media_to_collection(collection, media, form.note.data) messages.add_message(request, messages.SUCCESS, _('"%s" added to collection "%s"') @@ -62,6 +62,7 @@ setup( 'mock', 'itsdangerous', 'pytz', + 'six', ## This is optional! # 'translitcodec', ## For now we're expecting that users will install this from |