diff options
56 files changed, 990 insertions, 268 deletions
@@ -30,6 +30,7 @@ Thank you! * Greg Grossmeier * Jakob Kramer * Jef van Schendel +* Jessica Tallon * Jim Campbell * Joar Wandborg * Jorge Araya Navarro diff --git a/docs/source/devel/codebase.rst b/docs/source/devel/codebase.rst index cd46242c..9718a097 100644 --- a/docs/source/devel/codebase.rst +++ b/docs/source/devel/codebase.rst @@ -142,8 +142,8 @@ Software Stack * `werkzeug <http://werkzeug.pocoo.org/>`_: nice abstraction layer from HTTP requests, responses and WSGI bits - * `Beaker <http://beaker.groovie.org/>`_: for handling sessions and - caching + * `itsdangerous <http://pythonhosted.org/itsdangerous/>`_: + for handling sessions * `Jinja2 <http://jinja.pocoo.org/docs/>`_: the templating engine diff --git a/docs/source/pluginwriter/api.rst b/docs/source/pluginwriter/api.rst index 42dc3a3d..44ffd6e8 100644 --- a/docs/source/pluginwriter/api.rst +++ b/docs/source/pluginwriter/api.rst @@ -21,4 +21,5 @@ Plugin API .. automodule:: mediagoblin.tools.pluginapi :members: get_config, register_routes, register_template_path, - register_template_hooks, get_hook_templates + register_template_hooks, get_hook_templates, + callable_runone, callable_runall diff --git a/docs/source/siteadmin/deploying.rst b/docs/source/siteadmin/deploying.rst index 9b2324ae..77e60037 100644 --- a/docs/source/siteadmin/deploying.rst +++ b/docs/source/siteadmin/deploying.rst @@ -185,6 +185,11 @@ flup:: ./bin/easy_install flup +(Sometimes this breaks because flup's site is flakey. If it does for +you, try):: + + ./bin/easy_install https://pypi.python.org/pypi/flup/1.0.3.dev-20110405 + This concludes the initial configuration of the development environment. In the future, when you update your codebase, you should also run:: diff --git a/extlib/README b/extlib/README index c690beac..a2cc6ec0 100644 --- a/extlib/README +++ b/extlib/README @@ -17,7 +17,7 @@ unwittingly interfere with other software that depends on the canonical release versions of those same libraries! Forking upstream software for trivial reasons makes us bad citizens in -the Open Source community and adds unnecessary heartache for our +the Free Software community and adds unnecessary heartache for our users. Don't make us "that" project. diff --git a/mediagoblin/app.py b/mediagoblin/app.py index bb6be4d4..1137c0d7 100644 --- a/mediagoblin/app.py +++ b/mediagoblin/app.py @@ -25,7 +25,7 @@ from werkzeug.exceptions import HTTPException from werkzeug.routing import RequestRedirect from mediagoblin import meddleware, __version__ -from mediagoblin.tools import common, translate, template +from mediagoblin.tools import common, session, translate, template from mediagoblin.tools.response import render_http_exception from mediagoblin.tools.theme import register_themes from mediagoblin.tools import request as mg_request @@ -34,8 +34,9 @@ from mediagoblin.init.celery import setup_celery_from_config 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, setup_beaker_cache) + setup_storage) from mediagoblin.tools.pluginapi import PluginManager +from mediagoblin.tools.crypto import setup_crypto _log = logging.getLogger(__name__) @@ -66,10 +67,15 @@ class MediaGoblinApp(object): # Open and setup the config global_config, app_config = setup_global_and_app_config(config_path) + setup_crypto() + ########################################## # Setup other connections / useful objects ########################################## + # Setup Session Manager, not needed in celery + self.session_manager = session.SessionManager() + # load all available locales setup_locales() @@ -100,9 +106,6 @@ class MediaGoblinApp(object): # set up staticdirector tool self.staticdirector = get_staticdirector(app_config) - # set up caching - self.cache = setup_beaker_cache() - # Setup celery, if appropriate if setup_celery and not app_config.get('celery_setup_elsewhere'): if os.environ.get('CELERY_ALWAYS_EAGER', 'false').lower() == 'true': @@ -157,7 +160,8 @@ class MediaGoblinApp(object): ## Attach utilities to the request object # Do we really want to load this via middleware? Maybe? - request.session = request.environ['beaker.session'] + session_manager = self.session_manager + request.session = session_manager.load_session_from_cookie(request) # Attach self as request.app # Also attach a few utilities from request.app for convenience? request.app = self @@ -226,6 +230,9 @@ class MediaGoblinApp(object): response = render_http_exeption( request, e, e.get_description(environ)) + session_manager.save_session_to_cookie(request.session, + request, response) + return response(environ, start_response) def __call__(self, environ, start_response): diff --git a/mediagoblin/config_spec.ini b/mediagoblin/config_spec.ini index 44f6a68f..e830e863 100644 --- a/mediagoblin/config_spec.ini +++ b/mediagoblin/config_spec.ini @@ -14,6 +14,9 @@ sql_engine = string(default="sqlite:///%(here)s/mediagoblin.db") # Where temporary files used in processing and etc are kept workbench_path = string(default="%(here)s/user_dev/media/workbench") +# Where to store cryptographic sensible data +crypto_path = string(default="%(here)s/user_dev/crypto") + # Where mediagoblin-builtin static assets are kept direct_remote_path = string(default="/mgoblin_static/") @@ -122,11 +125,6 @@ spectrogram_fft_size = integer(default=4096) [media_type:mediagoblin.media_types.ascii] thumbnail_font = string(default=None) -[beaker.cache] -type = string(default="file") -data_dir = string(default="%(here)s/user_dev/beaker/cache/data") -lock_dir = string(default="%(here)s/user_dev/beaker/cache/lock") - [celery] # default result stuff diff --git a/mediagoblin/db/mixin.py b/mediagoblin/db/mixin.py index 0832c21a..0dc3bc85 100644 --- a/mediagoblin/db/mixin.py +++ b/mediagoblin/db/mixin.py @@ -52,10 +52,18 @@ class UserMixin(object): return cleaned_markdown_conversion(self.bio) -class MediaEntryMixin(object): +class GenerateSlugMixin(object): + """ + Mixin to add a generate_slug method to objects. + + Depends on: + - self.slug + - self.title + - self.check_slug_used(new_slug) + """ def generate_slug(self): """ - Generate a unique slug for this MediaEntry. + Generate a unique slug for this object. This one does not *force* slugs, but usually it will probably result in a niceish one. @@ -76,10 +84,6 @@ class MediaEntryMixin(object): generated bits until it's unique. That'll be a little bit of junk, but at least it has the basis of a nice slug. """ - # import this here due to a cyclic import issue - # (db.models -> db.mixin -> db.util -> db.models) - from mediagoblin.db.util import check_media_slug_used - #Is already a slug assigned? Check if it is valid if self.slug: self.slug = slugify(self.slug) @@ -100,14 +104,13 @@ class MediaEntryMixin(object): return # giving up! # Otherwise, let's see if this is unique. - if check_media_slug_used(self.uploader, self.slug, self.id): + if self.check_slug_used(self.slug): # It looks like it's being used... lame. # Can we just append the object's id to the end? if self.id: slug_with_id = u"%s-%s" % (self.slug, self.id) - if not check_media_slug_used(self.uploader, - slug_with_id, self.id): + if not self.check_slug_used(slug_with_id): self.slug = slug_with_id return # success! @@ -115,9 +118,18 @@ class MediaEntryMixin(object): # let's whack junk on there till it's unique. self.slug += '-' + uuid.uuid4().hex[:4] # keep going if necessary! - while check_media_slug_used(self.uploader, self.slug, self.id): + while self.check_slug_used(self.slug): self.slug += uuid.uuid4().hex[:4] + +class MediaEntryMixin(GenerateSlugMixin): + def check_slug_used(self, slug): + # import this here due to a cyclic import issue + # (db.models -> db.mixin -> db.util -> db.models) + from mediagoblin.db.util import check_media_slug_used + + return check_media_slug_used(self.uploader, slug, self.id) + @property def description_html(self): """ @@ -238,22 +250,13 @@ class MediaCommentMixin(object): return cleaned_markdown_conversion(self.content) -class CollectionMixin(object): - def generate_slug(self): +class CollectionMixin(GenerateSlugMixin): + def check_slug_used(self, slug): # import this here due to a cyclic import issue # (db.models -> db.mixin -> db.util -> db.models) from mediagoblin.db.util import check_collection_slug_used - self.slug = slugify(self.title) - - duplicate = check_collection_slug_used(mg_globals.database, - self.creator, self.slug, self.id) - - if duplicate: - if self.id is not None: - self.slug = u"%s-%s" % (self.id, self.slug) - else: - self.slug = None + return check_collection_slug_used(self.creator, slug, self.id) @property def description_html(self): diff --git a/mediagoblin/db/models.py b/mediagoblin/db/models.py index 2f58503f..fcfd0f61 100644 --- a/mediagoblin/db/models.py +++ b/mediagoblin/db/models.py @@ -172,8 +172,7 @@ class MediaEntry(Base, MediaEntryMixin): order_col = MediaComment.created if not ascending: order_col = desc(order_col) - return MediaComment.query.filter_by( - media_entry=self.id).order_by(order_col) + return self.all_comments.order_by(order_col) def url_to_prev(self, urlgen): """get the next 'newer' entry by this user""" @@ -238,9 +237,7 @@ class MediaEntry(Base, MediaEntryMixin): :param del_orphan_tags: True/false if we delete unused Tags too :param commit: True/False if this should end the db transaction""" # User's CollectionItems are automatically deleted via "cascade". - # Delete all the associated comments - for comment in self.get_comments(): - comment.delete(commit=False) + # Comments on this Media are deleted by cascade, hopefully. # Delete all related files/attachments try: @@ -385,13 +382,22 @@ class MediaComment(Base, MediaCommentMixin): content = Column(UnicodeText, nullable=False) # Cascade: Comments are owned by their creator. So do the full thing. - # lazy=dynamic: People might post a *lot* of comments, so make - # the "posted_comments" a query-like thing. + # lazy=dynamic: People might post a *lot* of comments, + # so make the "posted_comments" a query-like thing. get_author = relationship(User, backref=backref("posted_comments", lazy="dynamic", cascade="all, delete-orphan")) + # Cascade: Comments are somewhat owned by their MediaEntry. + # So do the full thing. + # lazy=dynamic: MediaEntries might have many comments, + # so make the "all_comments" a query-like thing. + get_media_entry = relationship(MediaEntry, + backref=backref("all_comments", + lazy="dynamic", + cascade="all, delete-orphan")) + class Collection(Base, CollectionMixin): """An 'album' or 'set' of media by a user. diff --git a/mediagoblin/db/util.py b/mediagoblin/db/util.py index 529ef8b9..6ffec44d 100644 --- a/mediagoblin/db/util.py +++ b/mediagoblin/db/util.py @@ -59,7 +59,7 @@ def clean_orphan_tags(commit=True): Session.commit() -def check_collection_slug_used(dummy_db, creator_id, slug, ignore_c_id): +def check_collection_slug_used(creator_id, slug, ignore_c_id): filt = (Collection.creator == creator_id) \ & (Collection.slug == slug) if ignore_c_id is not None: diff --git a/mediagoblin/edit/views.py b/mediagoblin/edit/views.py index 0b1cda98..34b7aaca 100644 --- a/mediagoblin/edit/views.py +++ b/mediagoblin/edit/views.py @@ -308,7 +308,7 @@ def edit_collection(request, collection): if request.method == 'POST' and form.validate(): # Make sure there isn't already a Collection with such a slug # and userid. - slug_used = check_collection_slug_used(request.db, collection.creator, + slug_used = check_collection_slug_used(collection.creator, form.slug.data, collection.id) # Make sure there isn't already a Collection with this title diff --git a/mediagoblin/init/__init__.py b/mediagoblin/init/__init__.py index 7c832442..d16027db 100644 --- a/mediagoblin/init/__init__.py +++ b/mediagoblin/init/__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 beaker.cache import CacheManager -from beaker.util import parse_cache_config_options import jinja2 from mediagoblin.tools import staticdirect @@ -146,16 +144,3 @@ def setup_workbench(): workbench_manager = WorkbenchManager(app_config['workbench_path']) setup_globals(workbench_manager=workbench_manager) - - -def setup_beaker_cache(): - """ - Setup the Beaker Cache manager. - """ - cache_config = mg_globals.global_config['beaker.cache'] - cache_config = dict( - [(u'cache.%s' % key, value) - for key, value in cache_config.iteritems()]) - cache = CacheManager(**parse_cache_config_options(cache_config)) - setup_globals(cache=cache) - return cache diff --git a/mediagoblin/init/celery/__init__.py b/mediagoblin/init/celery/__init__.py index 8d7a41bd..bb0d5989 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 PluginManager +from mediagoblin.tools.pluginapi import callable_runall MANDATORY_CELERY_IMPORTS = ['mediagoblin.processing.task'] @@ -66,8 +66,7 @@ def setup_celery_app(app_config, global_config, celery_app = Celery() celery_app.config_from_object(celery_settings) - for callable_hook in PluginManager().get_hook_callables('celery_setup'): - callable_hook(celery_app) + callable_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 8a794abb..e2899c0b 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 PluginManager +from mediagoblin.tools.pluginapi import callable_runall OUR_MODULENAME = __name__ @@ -47,9 +47,7 @@ def setup_logging_from_paste_ini(loglevel, **kw): logging.config.fileConfig(logging_conf_file) - for callable_hook in \ - PluginManager().get_hook_callables('celery_logging_setup'): - callable_hook() + callable_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 cdf9b5ad..72bd5c7d 100644 --- a/mediagoblin/init/plugins/__init__.py +++ b/mediagoblin/init/plugins/__init__.py @@ -59,6 +59,4 @@ def setup_plugins(): pman.register_hooks(plugin.hooks) # Execute anything registered to the setup hook. - setup_list = pman.get_hook_callables('setup') - for fun in setup_list: - fun() + pluginapi.callable_runall('setup') diff --git a/mediagoblin/media_types/ascii/processing.py b/mediagoblin/media_types/ascii/processing.py index 382cd015..309aab0a 100644 --- a/mediagoblin/media_types/ascii/processing.py +++ b/mediagoblin/media_types/ascii/processing.py @@ -127,8 +127,14 @@ def process_ascii(proc_state): 'ascii', 'xmlcharrefreplace')) - mgg.queue_store.delete_file(queued_filepath) + # Remove queued media file from storage and database. + # queued_filepath is in the task_id directory which should + # be removed too, but fail if the directory is not empty to be on + # the super-safe side. + mgg.queue_store.delete_file(queued_filepath) # rm file + mgg.queue_store.delete_dir(queued_filepath[:-1]) # rm dir entry.queued_media_file = [] + media_files_dict = entry.setdefault('media_files', {}) media_files_dict['thumb'] = thumb_filepath media_files_dict['unicode'] = unicode_filepath diff --git a/mediagoblin/media_types/audio/processing.py b/mediagoblin/media_types/audio/processing.py index 5dffcaf9..101b83e5 100644 --- a/mediagoblin/media_types/audio/processing.py +++ b/mediagoblin/media_types/audio/processing.py @@ -147,4 +147,10 @@ def process_audio(proc_state): else: entry.media_files['thumb'] = ['fake', 'thumb', 'path.jpg'] - mgg.queue_store.delete_file(queued_filepath) + # Remove queued media file from storage and database. + # queued_filepath is in the task_id directory which should + # be removed too, but fail if the directory is not empty to be on + # the super-safe side. + mgg.queue_store.delete_file(queued_filepath) # rm file + mgg.queue_store.delete_dir(queued_filepath[:-1]) # rm dir + entry.queued_media_file = [] diff --git a/mediagoblin/media_types/stl/processing.py b/mediagoblin/media_types/stl/processing.py index 77744ac5..e41df395 100644 --- a/mediagoblin/media_types/stl/processing.py +++ b/mediagoblin/media_types/stl/processing.py @@ -165,8 +165,12 @@ def process_stl(proc_state): with open(queued_filename, 'rb') as queued_file: model_file.write(queued_file.read()) - # Remove queued media file from storage and database - mgg.queue_store.delete_file(queued_filepath) + # Remove queued media file from storage and database. + # queued_filepath is in the task_id directory which should + # be removed too, but fail if the directory is not empty to be on + # the super-safe side. + mgg.queue_store.delete_file(queued_filepath) # rm file + mgg.queue_store.delete_dir(queued_filepath[:-1]) # rm dir entry.queued_media_file = [] # Insert media file information into database diff --git a/mediagoblin/mg_globals.py b/mediagoblin/mg_globals.py index e4b94bdc..26ed66fa 100644 --- a/mediagoblin/mg_globals.py +++ b/mediagoblin/mg_globals.py @@ -29,9 +29,6 @@ import threading # SQL database engine database = None -# beaker's cache manager -cache = None - # should be the same as the public_store = None queue_store = None diff --git a/mediagoblin/processing/__init__.py b/mediagoblin/processing/__init__.py index 02462567..a1fd3fb7 100644 --- a/mediagoblin/processing/__init__.py +++ b/mediagoblin/processing/__init__.py @@ -111,8 +111,13 @@ class ProcessingState(object): self.entry.media_files[keyname] = target_filepath def delete_queue_file(self): + # Remove queued media file from storage and database. + # queued_filepath is in the task_id directory which should + # be removed too, but fail if the directory is not empty to be on + # the super-safe side. queued_filepath = self.entry.queued_media_file - mgg.queue_store.delete_file(queued_filepath) + mgg.queue_store.delete_file(queued_filepath) # rm file + mgg.queue_store.delete_dir(queued_filepath[:-1]) # rm dir self.entry.queued_media_file = [] diff --git a/mediagoblin/static/css/base.css b/mediagoblin/static/css/base.css index 8df9f2e1..0cb36753 100644 --- a/mediagoblin/static/css/base.css +++ b/mediagoblin/static/css/base.css @@ -360,6 +360,25 @@ textarea#description, textarea#bio { font-size: 0.9em; } +a.comment_authorlink { + text-decoration: none; + padding-right: 5px; + font-weight: bold; + padding-left: 2px; +} + +a.comment_authorlink:hover { + text-decoration: underline; +} + +a.comment_whenlink { + text-decoration: none; +} + +a.comment_whenlink:hover { + text-decoration: underline; +} + .comment_content { margin-left: 8px; margin-top: 8px; @@ -540,6 +559,7 @@ table.media_panel { table.media_panel th { font-weight: bold; padding-bottom: 4px; + text-align: left; } diff --git a/mediagoblin/storage/__init__.py b/mediagoblin/storage/__init__.py index 5c1d7d36..bbe134a7 100644 --- a/mediagoblin/storage/__init__.py +++ b/mediagoblin/storage/__init__.py @@ -101,10 +101,20 @@ class StorageInterface(object): def delete_file(self, filepath): """ - Delete or dereference the file at filepath. + Delete or dereference the file (not directory) at filepath. + """ + # Subclasses should override this method. + self.__raise_not_implemented() + + def delete_dir(self, dirpath, recursive=False): + """Delete the directory at dirpath + + :param recursive: Usually, a directory must not contain any + files for the delete to succeed. If True, containing files + and subdirectories within dirpath will be recursively + deleted. - This might need to delete directories, buckets, whatever, for - cleanliness. (Be sure to avoid race conditions on that though) + :returns: True in case of success, False otherwise. """ # Subclasses should override this method. self.__raise_not_implemented() diff --git a/mediagoblin/storage/filestorage.py b/mediagoblin/storage/filestorage.py index ef786b61..3d6e0753 100644 --- a/mediagoblin/storage/filestorage.py +++ b/mediagoblin/storage/filestorage.py @@ -62,10 +62,32 @@ class BasicFileStorage(StorageInterface): return open(self._resolve_filepath(filepath), mode) def delete_file(self, filepath): - # TODO: Also delete unused directories if empty (safely, with - # checks to avoid race conditions). + """Delete file at filepath + + Raises OSError in case filepath is a directory.""" + #TODO: log error os.remove(self._resolve_filepath(filepath)) + def delete_dir(self, dirpath, recursive=False): + """returns True on succes, False on failure""" + + dirpath = self._resolve_filepath(dirpath) + + # Shortcut the default and simple case of nonempty=F, recursive=F + if recursive: + try: + shutil.rmtree(dirpath) + except OSError as e: + #TODO: log something here + return False + else: # recursively delete everything + try: + os.rmdir(dirpath) + except OSError as e: + #TODO: log something here + return False + return True + def file_url(self, filepath): if not self.base_url: raise NoWebServing( diff --git a/mediagoblin/templates/mediagoblin/admin/panel.html b/mediagoblin/templates/mediagoblin/admin/panel.html index 6bcb5c24..1c3c866e 100644 --- a/mediagoblin/templates/mediagoblin/admin/panel.html +++ b/mediagoblin/templates/mediagoblin/admin/panel.html @@ -104,7 +104,7 @@ <td>{{ media_entry.id }}</td> <td>{{ media_entry.get_uploader.username }}</td> <td><a href="{{ media_entry.url_for_self(request.urlgen) }}">{{ media_entry.title }}</a></td> - <td>{{ media_entry.created.strftime("%F %R") }}</td> + <td><span title='{{ media_entry.created.strftime("%F %R") }}'>{{ timesince(media_entry.created) }}</span></td> </tr> {% endfor %} </table> diff --git a/mediagoblin/templates/mediagoblin/user_pages/media.html b/mediagoblin/templates/mediagoblin/user_pages/media.html index b77c12b9..7dea3f09 100644 --- a/mediagoblin/templates/mediagoblin/user_pages/media.html +++ b/mediagoblin/templates/mediagoblin/user_pages/media.html @@ -117,16 +117,20 @@ <div class="comment_author"> <img src="{{ request.staticdirect('/images/icon_comment.png') }}" /> <a href="{{ request.urlgen('mediagoblin.user_pages.user_home', - user=comment_author.username) }}"> + user=comment_author.username) }}" + class="comment_authorlink"> {{- comment_author.username -}} </a> - {% trans %}at{% endtrans %} <a href="{{ request.urlgen('mediagoblin.user_pages.media_home.view_comment', comment=comment.id, user=media.get_uploader.username, - media=media.slug_or_id) }}#comment"> - {{- comment.created.strftime("%I:%M%p %Y-%m-%d") -}} - </a>: + media=media.slug_or_id) }}#comment" + class="comment_whenlink"> + <span title='{{- comment.created.strftime("%I:%M%p %Y-%m-%d") -}}'> + {%- trans formatted_time=timesince(comment.created) -%} + {{ formatted_time }} ago + {%- endtrans -%} + </span></a>: </div> <div class="comment_content"> {% autoescape False -%} @@ -141,10 +145,12 @@ {% endif %} </div> <div class="media_sidebar"> - {% trans date=media.created.strftime("%Y-%m-%d") -%} - <h3>Added on</h3> - <p>{{ date }}</p> - {%- endtrans %} + <h3>Added</h3> + <p><span title="{{ media.created.strftime("%I:%M%p %Y-%m-%d") }}"> + {%- trans formatted_time=timesince(media.created) -%} + {{ formatted_time }} ago + {%- endtrans -%} + </span></p> {% if media.tags %} {% include "mediagoblin/utils/tags.html" %} {% endif %} diff --git a/mediagoblin/tests/test_api.py b/mediagoblin/tests/test_api.py index 25ce852b..cff25776 100644 --- a/mediagoblin/tests/test_api.py +++ b/mediagoblin/tests/test_api.py @@ -44,7 +44,6 @@ EVIL_PNG = resource('evil.png') BIG_BLUE = resource('bigblue.png') -@pytest.mark.usefixtures("test_app") class TestAPI(object): def setup(self): self.db = mg_globals.database diff --git a/mediagoblin/tests/test_auth.py b/mediagoblin/tests/test_auth.py index f9fe8ed1..755727f9 100644 --- a/mediagoblin/tests/test_auth.py +++ b/mediagoblin/tests/test_auth.py @@ -17,8 +17,6 @@ import urlparse import datetime -from nose.tools import assert_equal - from mediagoblin import mg_globals from mediagoblin.auth import lib as auth_lib from mediagoblin.db.models import User @@ -101,8 +99,8 @@ def test_register_views(test_app): context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html'] form = context['register_form'] - assert_equal (form.username.errors, [u'Field must be between 3 and 30 characters long.']) - assert_equal (form.password.errors, [u'Field must be between 5 and 1024 characters long.']) + assert form.username.errors == [u'Field must be between 3 and 30 characters long.'] + assert form.password.errors == [u'Field must be between 5 and 1024 characters long.'] ## bad form template.clear_test_template_context() @@ -113,11 +111,11 @@ def test_register_views(test_app): context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html'] form = context['register_form'] - assert_equal (form.username.errors, [u'This field does not take email addresses.']) - assert_equal (form.email.errors, [u'This field requires an email address.']) + assert form.username.errors == [u'This field does not take email addresses.'] + assert form.email.errors == [u'This field requires an email address.'] ## At this point there should be no users in the database ;) - assert_equal(User.query.count(), 0) + assert User.query.count() == 0 # Successful register # ------------------- @@ -130,9 +128,7 @@ def test_register_views(test_app): response.follow() ## Did we redirect to the proper page? Use the right template? - assert_equal( - urlparse.urlsplit(response.location)[2], - '/u/happygirl/') + assert urlparse.urlsplit(response.location)[2] == '/u/happygirl/' assert 'mediagoblin/user_pages/user.html' in template.TEMPLATE_TEST_CONTEXT ## Make sure user is in place @@ -223,9 +219,7 @@ def test_register_views(test_app): response.follow() ## Did we redirect to the proper page? Use the right template? - assert_equal( - urlparse.urlsplit(response.location)[2], - '/auth/login/') + assert urlparse.urlsplit(response.location)[2] == '/auth/login/' assert 'mediagoblin/auth/login.html' in template.TEMPLATE_TEST_CONTEXT ## Make sure link to change password is sent by email @@ -256,7 +250,7 @@ def test_register_views(test_app): response = test_app.get( "/auth/forgot_password/verify/?userid=%s&token=total_bs" % unicode( new_user.id), status=404) - assert_equal(response.status.split()[0], u'404') # status="404 NOT FOUND" + assert response.status.split()[0] == u'404' # status="404 NOT FOUND" ## Try using an expired token to change password, shouldn't work template.clear_test_template_context() @@ -265,7 +259,7 @@ def test_register_views(test_app): new_user.fp_token_expire = datetime.datetime.now() new_user.save() response = test_app.get("%s?%s" % (path, get_params), status=404) - assert_equal(response.status.split()[0], u'404') # status="404 NOT FOUND" + assert response.status.split()[0] == u'404' # status="404 NOT FOUND" new_user.fp_token_expire = real_token_expiration new_user.save() @@ -293,9 +287,7 @@ def test_register_views(test_app): # User should be redirected response.follow() - assert_equal( - urlparse.urlsplit(response.location)[2], - '/') + assert urlparse.urlsplit(response.location)[2] == '/' assert 'mediagoblin/root.html' in template.TEMPLATE_TEST_CONTEXT @@ -370,9 +362,7 @@ def test_authentication_views(test_app): # User should be redirected response.follow() - assert_equal( - urlparse.urlsplit(response.location)[2], - '/') + assert urlparse.urlsplit(response.location)[2] == '/' assert 'mediagoblin/root.html' in template.TEMPLATE_TEST_CONTEXT # Make sure user is in the session @@ -387,9 +377,7 @@ def test_authentication_views(test_app): # Should be redirected to index page response.follow() - assert_equal( - urlparse.urlsplit(response.location)[2], - '/') + assert urlparse.urlsplit(response.location)[2] == '/' assert 'mediagoblin/root.html' in template.TEMPLATE_TEST_CONTEXT # Make sure the user is not in the session @@ -405,6 +393,4 @@ def test_authentication_views(test_app): 'username': u'chris', 'password': 'toast', 'next' : '/u/chris/'}) - assert_equal( - urlparse.urlsplit(response.location)[2], - '/u/chris/') + assert urlparse.urlsplit(response.location)[2] == '/u/chris/' diff --git a/mediagoblin/tests/test_cache.py b/mediagoblin/tests/test_cache.py deleted file mode 100644 index 403173cd..00000000 --- a/mediagoblin/tests/test_cache.py +++ /dev/null @@ -1,50 +0,0 @@ -# GNU MediaGoblin -- federated, autonomous media hosting -# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - - -from mediagoblin import mg_globals - - -DATA_TO_CACHE = { - 'herp': 'derp', - 'lol': 'cats'} - - -def _get_some_data(key): - """ - Stuid function that makes use of some caching. - """ - some_data_cache = mg_globals.cache.get_cache('sum_data') - if some_data_cache.has_key(key): - return some_data_cache.get(key) - - value = DATA_TO_CACHE.get(key) - some_data_cache.put(key, value) - return value - - -def test_cache_working(test_app): - some_data_cache = mg_globals.cache.get_cache('sum_data') - assert not some_data_cache.has_key('herp') - assert _get_some_data('herp') == 'derp' - assert some_data_cache.get('herp') == 'derp' - # should get the same value again - assert _get_some_data('herp') == 'derp' - - # now we force-change it, but the function should use the cached - # version - some_data_cache.put('herp', 'pred') - assert _get_some_data('herp') == 'pred' diff --git a/mediagoblin/tests/test_collections.py b/mediagoblin/tests/test_collections.py index d4d3af71..87782f30 100644 --- a/mediagoblin/tests/test_collections.py +++ b/mediagoblin/tests/test_collections.py @@ -16,7 +16,6 @@ from mediagoblin.tests.tools import fixture_add_collection, fixture_add_user from mediagoblin.db.models import Collection, User -from nose.tools import assert_equal def test_user_deletes_collection(test_app): @@ -30,4 +29,4 @@ def test_user_deletes_collection(test_app): user.delete() cnt2 = Collection.query.count() - assert_equal(cnt1, cnt2 + 1) + assert cnt1 == cnt2 + 1 diff --git a/mediagoblin/tests/test_edit.py b/mediagoblin/tests/test_edit.py index f1f0baba..cda2607f 100644 --- a/mediagoblin/tests/test_edit.py +++ b/mediagoblin/tests/test_edit.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -from nose.tools import assert_equal +import pytest from mediagoblin import mg_globals from mediagoblin.db.models import User @@ -69,7 +69,7 @@ class TestUserEdit(object): }) # Check for redirect on success - assert_equal(res.status_int, 302) + assert res.status_int == 302 # test_user has to be fetched again in order to have the current values test_user = User.query.filter_by(username=u'chris').first() assert bcrypt_check_password('123456', test_user.pw_hash) @@ -99,7 +99,7 @@ class TestUserEdit(object): 'url': u'http://dustycloud.org/'}, expect_errors=True) # Should redirect to /u/chris/edit/ - assert_equal (res.status_int, 302) + assert res.status_int == 302 assert res.headers['Location'].endswith("/u/chris/edit/") res = test_app.post( @@ -108,8 +108,8 @@ class TestUserEdit(object): 'url': u'http://dustycloud.org/'}) test_user = User.query.filter_by(username=u'chris').first() - assert_equal(test_user.bio, u'I love toast!') - assert_equal(test_user.url, u'http://dustycloud.org/') + assert test_user.bio == u'I love toast!' + assert test_user.url == u'http://dustycloud.org/' # change a different user than the logged in (should fail with 403) fixture_add_user(username=u"foo") @@ -117,7 +117,7 @@ class TestUserEdit(object): '/u/foo/edit/', { 'bio': u'I love toast!', 'url': u'http://dustycloud.org/'}, expect_errors=True) - assert_equal(res.status_int, 403) + assert res.status_int == 403 # test changing the bio and the URL inproperly too_long_bio = 150 * 'T' + 150 * 'o' + 150 * 'a' + 150 * 's' + 150* 't' @@ -129,10 +129,13 @@ class TestUserEdit(object): 'url': 'this-is-no-url'}) # Check form errors - context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/edit/edit_profile.html'] + context = template.TEMPLATE_TEST_CONTEXT[ + 'mediagoblin/edit/edit_profile.html'] form = context['form'] - assert_equal(form.bio.errors, [u'Field must be between 0 and 500 characters long.']) - assert_equal(form.url.errors, [u'This address contains errors']) + assert form.bio.errors == [ + u'Field must be between 0 and 500 characters long.'] + assert form.url.errors == [ + u'This address contains errors'] # test changing the url inproperly diff --git a/mediagoblin/tests/test_globals.py b/mediagoblin/tests/test_globals.py index d3722140..fe3088f8 100644 --- a/mediagoblin/tests/test_globals.py +++ b/mediagoblin/tests/test_globals.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -from nose.tools import assert_raises +import pytest from mediagoblin import mg_globals @@ -36,7 +36,7 @@ class TestGlobals(object): assert mg_globals.public_store == 'my favorite public_store!' assert mg_globals.queue_store == 'my favorite queue_store!' - assert_raises( + pytest.raises( AssertionError, mg_globals.setup_globals, - no_such_global_foo = "Dummy") + no_such_global_foo="Dummy") diff --git a/mediagoblin/tests/test_mgoblin_app.ini b/mediagoblin/tests/test_mgoblin_app.ini index 42d3785a..b78abe64 100644 --- a/mediagoblin/tests/test_mgoblin_app.ini +++ b/mediagoblin/tests/test_mgoblin_app.ini @@ -23,10 +23,6 @@ base_url = /mgoblin_media/ [storage:queuestore] base_dir = %(here)s/test_user_dev/media/queue -[beaker.cache] -data_dir = %(here)s/test_user_dev/beaker/cache/data -lock_dir = %(here)s/test_user_dev/beaker/cache/lock - [celery] CELERY_ALWAYS_EAGER = true CELERY_RESULT_DBURI = "sqlite:///%(here)s/test_user_dev/celery.db" diff --git a/mediagoblin/tests/test_misc.py b/mediagoblin/tests/test_misc.py index 7143938e..755d863f 100644 --- a/mediagoblin/tests/test_misc.py +++ b/mediagoblin/tests/test_misc.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 nose.tools import assert_equal - from mediagoblin.db.base import Session from mediagoblin.db.models import User, MediaEntry, MediaComment from mediagoblin.tests.tools import fixture_add_user, fixture_media_entry @@ -23,7 +21,7 @@ from mediagoblin.tests.tools import fixture_add_user, fixture_media_entry def test_404_for_non_existent(test_app): res = test_app.get('/does-not-exist/', expect_errors=True) - assert_equal(res.status_int, 404) + assert res.status_int == 404 def test_user_deletes_other_comments(test_app): @@ -58,11 +56,11 @@ def test_user_deletes_other_comments(test_app): cmt_cnt2 = MediaComment.query.count() # One user deleted - assert_equal(usr_cnt2, usr_cnt1 - 1) + assert usr_cnt2 == usr_cnt1 - 1 # One media gone - assert_equal(med_cnt2, med_cnt1 - 1) + assert med_cnt2 == med_cnt1 - 1 # Three of four comments gone. - assert_equal(cmt_cnt2, cmt_cnt1 - 3) + assert cmt_cnt2 == cmt_cnt1 - 3 User.query.get(user_b.id).delete() @@ -71,11 +69,11 @@ def test_user_deletes_other_comments(test_app): cmt_cnt2 = MediaComment.query.count() # All users gone - assert_equal(usr_cnt2, usr_cnt1 - 2) + assert usr_cnt2 == usr_cnt1 - 2 # All media gone - assert_equal(med_cnt2, med_cnt1 - 2) + assert med_cnt2 == med_cnt1 - 2 # All comments gone - assert_equal(cmt_cnt2, cmt_cnt1 - 4) + assert cmt_cnt2 == cmt_cnt1 - 4 def test_media_deletes_broken_attachment(test_app): diff --git a/mediagoblin/tests/test_modelmethods.py b/mediagoblin/tests/test_modelmethods.py index a5739ed5..427aa47c 100644 --- a/mediagoblin/tests/test_modelmethods.py +++ b/mediagoblin/tests/test_modelmethods.py @@ -17,8 +17,6 @@ # Maybe not every model needs a test, but some models have special # methods, and so it makes sense to test them here. -from nose.tools import assert_equal - from mediagoblin.db.base import Session from mediagoblin.db.models import MediaEntry @@ -166,4 +164,4 @@ def test_media_data_init(test_app): for obj in Session(): obj_in_session += 1 print repr(obj) - assert_equal(obj_in_session, 0) + assert obj_in_session == 0 diff --git a/mediagoblin/tests/test_paste.ini b/mediagoblin/tests/test_paste.ini index 875b4f65..91ecbb84 100644 --- a/mediagoblin/tests/test_paste.ini +++ b/mediagoblin/tests/test_paste.ini @@ -9,7 +9,6 @@ use = egg:Paste#urlmap [app:mediagoblin] use = egg:mediagoblin#app -filter-with = beaker config = %(here)s/mediagoblin.ini [app:publicstore_serve] @@ -20,14 +19,6 @@ document_root = %(here)s/test_user_dev/media/public use = egg:Paste#static document_root = %(here)s/mediagoblin/static/ -[filter:beaker] -use = egg:Beaker#beaker_session -cache_dir = %(here)s/test_user_dev/beaker -beaker.session.key = mediagoblin -# beaker.session.secret = somesupersecret -beaker.session.data_dir = %(here)s/test_user_dev/beaker/sessions/data -beaker.session.lock_dir = %(here)s/test_user_dev/beaker/sessions/lock - [celery] CELERY_ALWAYS_EAGER = true diff --git a/mediagoblin/tests/test_pluginapi.py b/mediagoblin/tests/test_pluginapi.py index 315a95da..d40a5081 100644 --- a/mediagoblin/tests/test_pluginapi.py +++ b/mediagoblin/tests/test_pluginapi.py @@ -15,11 +15,13 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import sys + from configobj import ConfigObj +import pytest + from mediagoblin import mg_globals from mediagoblin.init.plugins import setup_plugins from mediagoblin.tools import pluginapi -from nose.tools import eq_ def with_cleanup(*modules_to_delete): @@ -97,7 +99,7 @@ def test_no_plugins(): setup_plugins() # Make sure we didn't load anything. - eq_(len(pman.plugins), 0) + assert len(pman.plugins) == 0 @with_cleanup('mediagoblin.plugins.sampleplugin') @@ -117,14 +119,14 @@ def test_one_plugin(): setup_plugins() # Make sure we only found one plugin - eq_(len(pman.plugins), 1) + assert len(pman.plugins) == 1 # Make sure the plugin is the one we think it is. - eq_(pman.plugins[0], 'mediagoblin.plugins.sampleplugin') + assert pman.plugins[0] == 'mediagoblin.plugins.sampleplugin' # Make sure there was one hook registered - eq_(len(pman.hooks), 1) + assert len(pman.hooks) == 1 # Make sure _setup_plugin_called was called once import mediagoblin.plugins.sampleplugin - eq_(mediagoblin.plugins.sampleplugin._setup_plugin_called, 1) + assert mediagoblin.plugins.sampleplugin._setup_plugin_called == 1 @with_cleanup('mediagoblin.plugins.sampleplugin') @@ -145,14 +147,14 @@ def test_same_plugin_twice(): setup_plugins() # Make sure we only found one plugin - eq_(len(pman.plugins), 1) + assert len(pman.plugins) == 1 # Make sure the plugin is the one we think it is. - eq_(pman.plugins[0], 'mediagoblin.plugins.sampleplugin') + assert pman.plugins[0] == 'mediagoblin.plugins.sampleplugin' # Make sure there was one hook registered - eq_(len(pman.hooks), 1) + assert len(pman.hooks) == 1 # Make sure _setup_plugin_called was called once import mediagoblin.plugins.sampleplugin - eq_(mediagoblin.plugins.sampleplugin._setup_plugin_called, 1) + assert mediagoblin.plugins.sampleplugin._setup_plugin_called == 1 @with_cleanup() @@ -172,4 +174,112 @@ def test_disabled_plugin(): setup_plugins() # Make sure we didn't load the plugin - eq_(len(pman.plugins), 0) + assert len(pman.plugins) == 0 + + +@with_cleanup() +def test_callable_runone(): + """ + Test the callable_runone method + """ + cfg = build_config([ + ('mediagoblin', {}, []), + ('plugins', {}, [ + ('mediagoblin.tests.testplugins.callables1', {}, []), + ('mediagoblin.tests.testplugins.callables2', {}, []), + ('mediagoblin.tests.testplugins.callables3', {}, []), + ]) + ]) + + mg_globals.app_config = cfg['mediagoblin'] + mg_globals.global_config = cfg + + setup_plugins() + + # Just one hook provided + call_log = [] + assert pluginapi.callable_runone( + "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) + assert call_log == [] + + # Nothing provided and unhandled okay + call_log = [] + assert pluginapi.callable_runone( + "nothing_handling", call_log, unhandled_okay=True) is None + assert call_log == [] + + # Multiple provided, go with the first! + call_log = [] + assert pluginapi.callable_runone( + "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( + "multi_handle_with_canthandle", + call_log) == "the second returns" + assert call_log == ["Hi, I'm the second"] + + +@with_cleanup() +def test_callable_runall(): + """ + Test the callable_runall method + """ + cfg = build_config([ + ('mediagoblin', {}, []), + ('plugins', {}, [ + ('mediagoblin.tests.testplugins.callables1', {}, []), + ('mediagoblin.tests.testplugins.callables2', {}, []), + ('mediagoblin.tests.testplugins.callables3', {}, []), + ]) + ]) + + mg_globals.app_config = cfg['mediagoblin'] + mg_globals.global_config = cfg + + setup_plugins() + + # Just one hook, check results + call_log = [] + assert pluginapi.callable_runall( + "just_one", call_log) == ["Called just once", None, None] + assert call_log == ["expect this one call"] + + # None provided, check results + call_log = [] + assert pluginapi.callable_runall( + "nothing_handling", call_log) == [] + assert call_log == [] + + # Multiple provided, check results + call_log = [] + assert pluginapi.callable_runall( + "multi_handle", call_log) == [ + "the first returns", + "the second returns", + "the third returns", + ] + assert call_log == [ + "Hi, I'm the first", + "Hi, I'm the second", + "Hi, I'm the third"] + + # Multiple provided, one has CantHandleIt, check results + call_log = [] + assert pluginapi.callable_runall( + "multi_handle_with_canthandle", call_log) == [ + "the second returns", + "the third returns", + ] + assert call_log == [ + "Hi, I'm the second", + "Hi, I'm the third"] diff --git a/mediagoblin/tests/test_session.py b/mediagoblin/tests/test_session.py new file mode 100644 index 00000000..78d790eb --- /dev/null +++ b/mediagoblin/tests/test_session.py @@ -0,0 +1,30 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from mediagoblin.tools import session + +def test_session(): + sess = session.Session() + assert not sess + assert not sess.is_updated() + sess['user_id'] = 27 + assert sess + assert not sess.is_updated() + sess.save() + assert sess.is_updated() + sess.delete() + assert not sess + assert sess.is_updated() diff --git a/mediagoblin/tests/test_storage.py b/mediagoblin/tests/test_storage.py index 61326ae9..749f7b07 100644 --- a/mediagoblin/tests/test_storage.py +++ b/mediagoblin/tests/test_storage.py @@ -18,7 +18,7 @@ import os import tempfile -from nose.tools import assert_raises, assert_equal, assert_true +import pytest from werkzeug.utils import secure_filename from mediagoblin import storage @@ -41,10 +41,8 @@ def test_clean_listy_filepath(): assert storage.clean_listy_filepath( ['../../../etc/', 'passwd']) == expected - assert_raises( - storage.InvalidFilepath, - storage.clean_listy_filepath, - ['../../', 'linooks.jpg']) + with pytest.raises(storage.InvalidFilepath): + storage.clean_listy_filepath(['../../', 'linooks.jpg']) class FakeStorageSystem(): @@ -78,10 +76,10 @@ def test_storage_system_from_config(): 'garbage_arg': 'garbage_arg', 'storage_class': 'mediagoblin.tests.test_storage:FakeStorageSystem'}) - assert_equal(this_storage.foobie, 'eiboof') - assert_equal(this_storage.blech, 'hcelb') - assert_equal(unicode(this_storage.__class__), - u'mediagoblin.tests.test_storage.FakeStorageSystem') + assert this_storage.foobie == 'eiboof' + assert this_storage.blech == 'hcelb' + assert unicode(this_storage.__class__) == \ + u'mediagoblin.tests.test_storage.FakeStorageSystem' ########################## @@ -89,7 +87,7 @@ def test_storage_system_from_config(): ########################## def get_tmp_filestorage(mount_url=None, fake_remote=False): - tmpdir = tempfile.mkdtemp() + tmpdir = tempfile.mkdtemp(prefix="test_gmg_storage") if fake_remote: this_storage = FakeRemoteStorage(tmpdir, mount_url) else: @@ -108,11 +106,13 @@ def test_basic_storage__resolve_filepath(): assert result == os.path.join( tmpdir, 'etc/passwd') - assert_raises( + pytest.raises( storage.InvalidFilepath, this_storage._resolve_filepath, ['../../', 'etc', 'passwd']) + os.rmdir(tmpdir) + def test_basic_storage_file_exists(): tmpdir, this_storage = get_tmp_filestorage() @@ -126,6 +126,8 @@ def test_basic_storage_file_exists(): assert not this_storage.file_exists(['dir1', 'dir2', 'thisfile.lol']) assert not this_storage.file_exists(['dnedir1', 'dnedir2', 'somefile.lol']) + this_storage.delete_file(['dir1', 'dir2', 'filename.txt']) + def test_basic_storage_get_unique_filepath(): tmpdir, this_storage = get_tmp_filestorage() @@ -146,6 +148,8 @@ def test_basic_storage_get_unique_filepath(): assert len(new_filename) > len('filename.txt') assert new_filename == secure_filename(new_filename) + os.remove(filename) + def test_basic_storage_get_file(): tmpdir, this_storage = get_tmp_filestorage() @@ -182,6 +186,10 @@ def test_basic_storage_get_file(): with this_storage.get_file(['testydir', 'testyfile.txt']) as testyfile: assert testyfile.read() == 'testy file! so testy.' + this_storage.delete_file(filepath) + this_storage.delete_file(new_filepath) + this_storage.delete_file(['testydir', 'testyfile.txt']) + def test_basic_storage_delete_file(): tmpdir, this_storage = get_tmp_filestorage() @@ -205,10 +213,11 @@ def test_basic_storage_delete_file(): def test_basic_storage_url_for_file(): # Not supplying a base_url should actually just bork. tmpdir, this_storage = get_tmp_filestorage() - assert_raises( + pytest.raises( storage.NoWebServing, this_storage.file_url, ['dir1', 'dir2', 'filename.txt']) + os.rmdir(tmpdir) # base_url without domain tmpdir, this_storage = get_tmp_filestorage('/media/') @@ -216,6 +225,7 @@ def test_basic_storage_url_for_file(): ['dir1', 'dir2', 'filename.txt']) expected = '/media/dir1/dir2/filename.txt' assert result == expected + os.rmdir(tmpdir) # base_url with domain tmpdir, this_storage = get_tmp_filestorage( @@ -224,6 +234,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) def test_basic_storage_get_local_path(): @@ -237,10 +248,13 @@ def test_basic_storage_get_local_path(): assert result == expected + os.rmdir(tmpdir) + def test_basic_storage_is_local(): tmpdir, this_storage = get_tmp_filestorage() assert this_storage.local_storage is True + os.rmdir(tmpdir) def test_basic_storage_copy_locally(): @@ -255,9 +269,13 @@ def test_basic_storage_copy_locally(): new_file_dest = os.path.join(dest_tmpdir, 'file2.txt') this_storage.copy_locally(filepath, new_file_dest) + this_storage.delete_file(filepath) assert file(new_file_dest).read() == 'Testing this file' + os.remove(new_file_dest) + os.rmdir(dest_tmpdir) + def _test_copy_local_to_storage_works(tmpdir, this_storage): local_filename = tempfile.mktemp() @@ -267,10 +285,14 @@ def _test_copy_local_to_storage_works(tmpdir, this_storage): this_storage.copy_local_to_storage( local_filename, ['dir1', 'dir2', 'copiedto.txt']) + os.remove(local_filename) + assert file( os.path.join(tmpdir, 'dir1/dir2/copiedto.txt'), 'r').read() == 'haha' + this_storage.delete_file(['dir1', 'dir2', 'copiedto.txt']) + def test_basic_storage_copy_local_to_storage(): tmpdir, this_storage = get_tmp_filestorage() diff --git a/mediagoblin/tests/test_submission.py b/mediagoblin/tests/test_submission.py index ddb8cefd..ac714252 100644 --- a/mediagoblin/tests/test_submission.py +++ b/mediagoblin/tests/test_submission.py @@ -21,7 +21,6 @@ sys.setdefaultencoding('utf-8') import urlparse import os -from nose.tools import assert_equal, assert_true from pkg_resources import resource_filename from mediagoblin.tests.tools import fixture_add_user @@ -87,7 +86,7 @@ class TestSubmission: def check_comments(self, request, media_id, count): comments = request.db.MediaComment.find({'media_entry': media_id}) - assert_equal(count, len(list(comments))) + assert count == len(list(comments)) def test_missing_fields(self, test_app): self._setup(test_app) @@ -95,21 +94,21 @@ class TestSubmission: # Test blank form # --------------- response, form = self.do_post({}, *FORM_CONTEXT) - assert_equal(form.file.errors, [u'You must provide a file.']) + assert form.file.errors == [u'You must provide a file.'] # Test blank file # --------------- response, form = self.do_post({'title': u'test title'}, *FORM_CONTEXT) - assert_equal(form.file.errors, [u'You must provide a file.']) + assert form.file.errors == [u'You must provide a file.'] def check_url(self, response, path): - assert_equal(urlparse.urlsplit(response.location)[2], path) + assert urlparse.urlsplit(response.location)[2] == path def check_normal_upload(self, title, filename): response, context = self.do_post({'title': title}, do_follow=True, **self.upload_data(filename)) self.check_url(response, '/u/{0}/'.format(self.test_user.username)) - assert_true('mediagoblin/user_pages/user.html' in context) + assert 'mediagoblin/user_pages/user.html' in context # Make sure the media view is at least reachable, logged in... url = '/u/{0}/m/{1}/'.format(self.test_user.username, title.lower().replace(' ', '-')) @@ -129,7 +128,7 @@ class TestSubmission: def check_media(self, request, find_data, count=None): media = MediaEntry.find(find_data) if count is not None: - assert_equal(media.count(), count) + assert media.count() == count if count == 0: return return media[0] @@ -156,10 +155,10 @@ class TestSubmission: 'tags': BAD_TAG_STRING}, *FORM_CONTEXT, **self.upload_data(GOOD_JPG)) - assert_equal(form.tags.errors, [ + assert form.tags.errors == [ u'Tags must be shorter than 50 characters. ' \ 'Tags that are too long: ' \ - 'ffffffffffffffffffffffffffuuuuuuuuuuuuuuuuuuuuuuuuuu']) + 'ffffffffffffffffffffffffffuuuuuuuuuuuuuuuuuuuuuuuuuu'] def test_delete(self, test_app): self._setup(test_app) @@ -180,7 +179,7 @@ class TestSubmission: 'slug': u"Balanced=Goblin", 'tags': u''}) media = self.check_media(request, {'title': u'Balanced Goblin'}, 1) - assert_equal(media.slug, u"balanced-goblin") + assert media.slug == u"balanced-goblin" # Add a comment, so we can test for its deletion later. self.check_comments(request, media_id, 0) @@ -216,7 +215,7 @@ class TestSubmission: response, form = self.do_post({'title': u'Malicious Upload 1'}, *FORM_CONTEXT, **self.upload_data(EVIL_FILE)) - assert_equal(len(form.file.errors), 1) + assert len(form.file.errors) == 1 assert 'Sorry, I don\'t support that file type :(' == \ str(form.file.errors[0]) @@ -231,8 +230,8 @@ class TestSubmission: **self.upload_data(GOOD_JPG)) media = self.check_media(request, {'title': u'Balanced Goblin'}, 1) - assert_equal(media.media_type, u'mediagoblin.media_types.image') - assert_equal(media.media_manager, img_MEDIA_MANAGER) + assert media.media_type == u'mediagoblin.media_types.image' + assert media.media_manager == img_MEDIA_MANAGER def test_sniffing(self, test_app): @@ -267,8 +266,8 @@ class TestSubmission: **self.upload_data(filename)) self.check_url(response, '/u/{0}/'.format(self.test_user.username)) entry = mg_globals.database.MediaEntry.find_one({'title': title}) - assert_equal(entry.state, 'failed') - assert_equal(entry.fail_error, u'mediagoblin.processing:BadMediaFail') + assert entry.state == 'failed' + assert entry.fail_error == u'mediagoblin.processing:BadMediaFail' def test_evil_jpg(self, test_app): self._setup(test_app) @@ -289,7 +288,7 @@ class TestSubmission: self.check_normal_upload(u"With GPS data", GPS_JPG) media = self.check_media(None, {"title": u"With GPS data"}, 1) - assert_equal(media.media_data.gps_latitude, 59.336666666666666) + assert media.media_data.gps_latitude == 59.336666666666666 def test_processing(self, test_app): self._setup(test_app) @@ -309,8 +308,8 @@ class TestSubmission: filename = os.path.join( public_store_dir, *media.media_files.get(key, [])) - assert_true(filename.endswith('_' + basename)) + assert filename.endswith('_' + basename) # Is it smaller than the last processed image we looked at? size = os.stat(filename).st_size - assert_true(last_size > size) + assert last_size > size last_size = size diff --git a/mediagoblin/tests/test_timesince.py b/mediagoblin/tests/test_timesince.py new file mode 100644 index 00000000..1f8a082b --- /dev/null +++ b/mediagoblin/tests/test_timesince.py @@ -0,0 +1,57 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from datetime import datetime, timedelta + +from mediagoblin.tools.timesince import is_aware, timesince + + +def test_timesince(test_app): + test_time = datetime.now() + + # it should ignore second and microseconds + assert timesince(test_time, test_time + timedelta(microseconds=1)) == "0 minutes" + assert timesince(test_time, test_time + timedelta(seconds=1)) == "0 minutes" + + # test minutes, hours, days, weeks, months and years (singular and plural) + assert timesince(test_time, test_time + timedelta(minutes=1)) == "1 minute" + assert timesince(test_time, test_time + timedelta(minutes=2)) == "2 minutes" + + assert timesince(test_time, test_time + timedelta(hours=1)) == "1 hour" + assert timesince(test_time, test_time + timedelta(hours=2)) == "2 hours" + + assert timesince(test_time, test_time + timedelta(days=1)) == "1 day" + assert timesince(test_time, test_time + timedelta(days=2)) == "2 days" + + assert timesince(test_time, test_time + timedelta(days=7)) == "1 week" + assert timesince(test_time, test_time + timedelta(days=14)) == "2 weeks" + + assert timesince(test_time, test_time + timedelta(days=30)) == "1 month" + assert timesince(test_time, test_time + timedelta(days=60)) == "2 months" + + assert timesince(test_time, test_time + timedelta(days=365)) == "1 year" + assert timesince(test_time, test_time + timedelta(days=730)) == "2 years" + + # okay now we want to test combinations + # e.g. 1 hour, 5 days + assert timesince(test_time, test_time + timedelta(days=5, hours=1)) == "5 days, 1 hour" + + assert timesince(test_time, test_time + timedelta(days=15)) == "2 weeks, 1 day" + + assert timesince(test_time, test_time + timedelta(days=97)) == "3 months, 1 week" + + assert timesince(test_time, test_time + timedelta(days=2250)) == "6 years, 2 months" + diff --git a/mediagoblin/tests/test_workbench.py b/mediagoblin/tests/test_workbench.py index 3b2fc2c6..9cd49671 100644 --- a/mediagoblin/tests/test_workbench.py +++ b/mediagoblin/tests/test_workbench.py @@ -26,8 +26,13 @@ from mediagoblin.tests.test_storage import get_tmp_filestorage class TestWorkbench(object): def setup(self): + self.workbench_base = tempfile.mkdtemp(prefix='gmg_workbench_testing') self.workbench_manager = workbench.WorkbenchManager( - os.path.join(tempfile.gettempdir(), u'mgoblin_workbench_testing')) + self.workbench_base) + + def teardown(self): + # If the workbench is empty, this should work. + os.rmdir(self.workbench_base) def test_create_workbench(self): workbench = self.workbench_manager.create() @@ -70,6 +75,7 @@ class TestWorkbench(object): filename = this_workbench.localized_file(this_storage, filepath) assert filename == os.path.join( tmpdir, 'dir1/dir2/ourfile.txt') + this_storage.delete_file(filepath) # with a fake remote file storage tmpdir, this_storage = get_tmp_filestorage(fake_remote=True) @@ -95,6 +101,9 @@ class TestWorkbench(object): assert filename == os.path.join( this_workbench.dir, 'thisfile.text') + this_storage.delete_file(filepath) + this_workbench.destroy() + def test_workbench_decorator(self): """Test @get_workbench decorator and automatic cleanup""" # The decorator needs mg_globals.workbench_manager diff --git a/mediagoblin/tests/testplugins/__init__.py b/mediagoblin/tests/testplugins/__init__.py new file mode 100644 index 00000000..621845ba --- /dev/null +++ b/mediagoblin/tests/testplugins/__init__.py @@ -0,0 +1,15 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. diff --git a/mediagoblin/tests/testplugins/callables1/__init__.py b/mediagoblin/tests/testplugins/callables1/__init__.py new file mode 100644 index 00000000..9c278b49 --- /dev/null +++ b/mediagoblin/tests/testplugins/callables1/__init__.py @@ -0,0 +1,41 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from mediagoblin.tools.pluginapi import CantHandleIt + +def setup_plugin(): + pass + + +def just_one(call_log): + call_log.append("expect this one call") + return "Called just once" + + +def multi_handle(call_log): + call_log.append("Hi, I'm the first") + return "the first returns" + +def multi_handle_with_canthandle(call_log): + raise CantHandleIt("I just can't accept this stupid method") + + +hooks = { + 'setup': setup_plugin, + 'just_one': just_one, + 'multi_handle': multi_handle, + 'multi_handle_with_canthandle': multi_handle_with_canthandle, + } diff --git a/mediagoblin/tests/testplugins/callables2/__init__.py b/mediagoblin/tests/testplugins/callables2/__init__.py new file mode 100644 index 00000000..aaab5b21 --- /dev/null +++ b/mediagoblin/tests/testplugins/callables2/__init__.py @@ -0,0 +1,38 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +def setup_plugin(): + pass + + +def just_one(call_log): + assert "SHOULD NOT HAPPEN" + +def multi_handle(call_log): + call_log.append("Hi, I'm the second") + return "the second returns" + +def multi_handle_with_canthandle(call_log): + call_log.append("Hi, I'm the second") + return "the second returns" + + +hooks = { + 'setup': setup_plugin, + 'just_one': just_one, + 'multi_handle': multi_handle, + 'multi_handle_with_canthandle': multi_handle_with_canthandle, + } diff --git a/mediagoblin/tests/testplugins/callables3/__init__.py b/mediagoblin/tests/testplugins/callables3/__init__.py new file mode 100644 index 00000000..8d0c9c25 --- /dev/null +++ b/mediagoblin/tests/testplugins/callables3/__init__.py @@ -0,0 +1,38 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +def setup_plugin(): + pass + + +def just_one(call_log): + assert "SHOULD NOT HAPPEN" + +def multi_handle(call_log): + call_log.append("Hi, I'm the third") + return "the third returns" + +def multi_handle_with_canthandle(call_log): + call_log.append("Hi, I'm the third") + return "the third returns" + + +hooks = { + 'setup': setup_plugin, + 'just_one': just_one, + 'multi_handle': multi_handle, + 'multi_handle_with_canthandle': multi_handle_with_canthandle, + } diff --git a/mediagoblin/tests/tools.py b/mediagoblin/tests/tools.py index b68d55e8..52635e18 100644 --- a/mediagoblin/tests/tools.py +++ b/mediagoblin/tests/tools.py @@ -45,14 +45,13 @@ TEST_USER_DEV = pkg_resources.resource_filename( 'mediagoblin.tests', 'test_user_dev') -USER_DEV_DIRECTORIES_TO_SETUP = [ - 'media/public', 'media/queue', - 'beaker/sessions/data', 'beaker/sessions/lock'] +USER_DEV_DIRECTORIES_TO_SETUP = ['media/public', 'media/queue'] BAD_CELERY_MESSAGE = """\ Sorry, you *absolutely* must run tests with the mediagoblin.init.celery.from_tests module. Like so: -$ CELERY_CONFIG_MODULE=mediagoblin.init.celery.from_tests {0}\ + +$ CELERY_CONFIG_MODULE=mediagoblin.init.celery.from_tests {0} """.format(sys.argv[0]) diff --git a/mediagoblin/tools/crypto.py b/mediagoblin/tools/crypto.py new file mode 100644 index 00000000..1379d21b --- /dev/null +++ b/mediagoblin/tools/crypto.py @@ -0,0 +1,113 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2013 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import errno +import itsdangerous +import logging +import os.path +import random +import tempfile +from mediagoblin import mg_globals + +_log = logging.getLogger(__name__) + + +# Use the system (hardware-based) random number generator if it exists. +# -- this optimization is lifted from Django +try: + getrandbits = random.SystemRandom().getrandbits +except AttributeError: + getrandbits = random.getrandbits + + +__itsda_secret = None + + +def load_key(filename): + global __itsda_secret + key_file = open(filename) + try: + __itsda_secret = key_file.read() + finally: + key_file.close() + + +def create_key(key_dir, key_filepath): + global __itsda_secret + old_umask = os.umask(077) + key_file = None + try: + if not os.path.isdir(key_dir): + os.makedirs(key_dir) + _log.info("Created %s", key_dir) + key = str(getrandbits(192)) + key_file = tempfile.NamedTemporaryFile(dir=key_dir, suffix='.bin', + delete=False) + key_file.write(key) + key_file.flush() + os.rename(key_file.name, key_filepath) + key_file.close() + finally: + os.umask(old_umask) + if (key_file is not None) and (not key_file.closed): + key_file.close() + os.unlink(key_file.name) + __itsda_secret = key + _log.info("Saved new key for It's Dangerous") + + +def setup_crypto(): + global __itsda_secret + key_dir = mg_globals.app_config["crypto_path"] + key_filepath = os.path.join(key_dir, 'itsdangeroussecret.bin') + try: + load_key(key_filepath) + except IOError, error: + if error.errno != errno.ENOENT: + raise + create_key(key_dir, key_filepath) + + +def get_timed_signer_url(namespace): + """ + This gives a basic signing/verifying object. + + The namespace makes sure signed tokens can't be used in + a different area. Like using a forgot-password-token as + a session cookie. + + Basic usage: + + .. code-block:: python + + _signer = None + TOKEN_VALID_DAYS = 10 + def setup(): + global _signer + _signer = get_timed_signer_url("session cookie") + def create_token(obj): + return _signer.dumps(obj) + def parse_token(token): + # This might raise an exception in case + # of an invalid token, or an expired token. + return _signer.loads(token, max_age=TOKEN_VALID_DAYS*24*3600) + + For more details see + http://pythonhosted.org/itsdangerous/#itsdangerous.URLSafeTimedSerializer + """ + assert __itsda_secret is not None + return itsdangerous.URLSafeTimedSerializer(__itsda_secret, + salt=namespace) diff --git a/mediagoblin/tools/pluginapi.py b/mediagoblin/tools/pluginapi.py index 784bede9..283350a8 100644 --- a/mediagoblin/tools/pluginapi.py +++ b/mediagoblin/tools/pluginapi.py @@ -272,3 +272,70 @@ def get_hook_templates(hook_name): A list of strings representing template paths. """ return PluginManager().get_template_hooks(hook_name) + + +########################### +# Callable convenience code +########################### + +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. + """ + pass + + +def callable_runone(hookname, *args, **kwargs): + """ + Run the callable hook HOOKNAME... run until the first response, + then return. + + This function will run stop at the first hook that handles the + result. Hooks raising CantHandleIt will be skipped. + + Unless unhandled_okay is True, this will error out if no hooks + have been registered to handle this function. + """ + callables = PluginManager().get_hook_callables(hookname) + + unhandled_okay = kwargs.pop("unhandled_okay", False) + + for callable in callables: + try: + return callable(*args, **kwargs) + except CantHandleIt: + continue + + if unhandled_okay is False: + raise UnhandledCallable( + "No hooks registered capable of handling '%s'" % hookname) + + +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. + """ + callables = PluginManager().get_hook_callables(hookname) + + results = [] + + for callable in callables: + try: + results.append(callable(*args, **kwargs)) + except CantHandleIt: + continue + + return results diff --git a/mediagoblin/tools/request.py b/mediagoblin/tools/request.py index bc67b96f..ee342eae 100644 --- a/mediagoblin/tools/request.py +++ b/mediagoblin/tools/request.py @@ -35,4 +35,4 @@ def setup_user_in_request(request): # Something's wrong... this user doesn't exist? Invalidate # this session. _log.warn("Killing session for user id %r", request.session['user_id']) - request.session.invalidate() + request.session.delete() diff --git a/mediagoblin/tools/session.py b/mediagoblin/tools/session.py new file mode 100644 index 00000000..fdc32523 --- /dev/null +++ b/mediagoblin/tools/session.py @@ -0,0 +1,68 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2013 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import itsdangerous +import logging + +import crypto + +_log = logging.getLogger(__name__) + +class Session(dict): + def __init__(self, *args, **kwargs): + self.send_new_cookie = False + dict.__init__(self, *args, **kwargs) + + def save(self): + self.send_new_cookie = True + + def is_updated(self): + return self.send_new_cookie + + def delete(self): + self.clear() + self.save() + + +class SessionManager(object): + def __init__(self, cookie_name='MGSession', namespace=None): + if namespace is None: + namespace = cookie_name + self.signer = crypto.get_timed_signer_url(namespace) + self.cookie_name = cookie_name + + def load_session_from_cookie(self, request): + cookie = request.cookies.get(self.cookie_name) + if not cookie: + return Session() + ### FIXME: Future cookie-blacklisting code + # m = BadCookie.query.filter_by(cookie = cookie) + # if m: + # _log.warn("Bad cookie received: %s", m.reason) + # raise BadRequest() + try: + return Session(self.signer.loads(cookie)) + except itsdangerous.BadData: + return Session() + + def save_session_to_cookie(self, session, request, response): + if not session.is_updated(): + return + elif not session: + response.delete_cookie(self.cookie_name) + else: + response.set_cookie(self.cookie_name, self.signer.dumps(session), + httponly=True) diff --git a/mediagoblin/tools/template.py b/mediagoblin/tools/template.py index 74d811eb..78d65654 100644 --- a/mediagoblin/tools/template.py +++ b/mediagoblin/tools/template.py @@ -29,9 +29,11 @@ from mediagoblin import _version from mediagoblin.tools import common from mediagoblin.tools.translate import get_gettext_translation from mediagoblin.tools.pluginapi import get_hook_templates +from mediagoblin.tools.timesince import timesince from mediagoblin.meddleware.csrf import render_csrf_form_token + SETUP_JINJA_ENVS = {} @@ -73,6 +75,9 @@ def get_jinja_env(template_loader, locale): template_env.filters['urlencode'] = url_quote_plus + # add human readable fuzzy date time + template_env.globals['timesince'] = timesince + # allow for hooking up plugin templates template_env.globals['get_hook_templates'] = get_hook_templates diff --git a/mediagoblin/tools/timesince.py b/mediagoblin/tools/timesince.py new file mode 100644 index 00000000..b761c1be --- /dev/null +++ b/mediagoblin/tools/timesince.py @@ -0,0 +1,95 @@ +# Copyright (c) Django Software Foundation and individual contributors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# 3. Neither the name of Django nor the names of its contributors may be used +# to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from __future__ import unicode_literals + +import datetime +import pytz + +from mediagoblin.tools.translate import pass_to_ugettext, lazy_pass_to_ungettext as _ + +"""UTC time zone as a tzinfo instance.""" +utc = pytz.utc if pytz else UTC() + +def is_aware(value): + """ + Determines if a given datetime.datetime is aware. + + The logic is described in Python's docs: + http://docs.python.org/library/datetime.html#datetime.tzinfo + """ + return value.tzinfo is not None and value.tzinfo.utcoffset(value) is not None + +def timesince(d, now=None, reversed=False): + """ + Takes two datetime objects and returns the time between d and now + as a nicely formatted string, e.g. "10 minutes". If d occurs after now, + then "0 minutes" is returned. + + Units used are years, months, weeks, days, hours, and minutes. + Seconds and microseconds are ignored. Up to two adjacent units will be + displayed. For example, "2 weeks, 3 days" and "1 year, 3 months" are + possible outputs, but "2 weeks, 3 hours" and "1 year, 5 days" are not. + + Adapted from http://blog.natbat.co.uk/archive/2003/Jun/14/time_since + """ + chunks = ( + (60 * 60 * 24 * 365, lambda n: _('year', 'years', n)), + (60 * 60 * 24 * 30, lambda n: _('month', 'months', n)), + (60 * 60 * 24 * 7, lambda n : _('week', 'weeks', n)), + (60 * 60 * 24, lambda n : _('day', 'days', n)), + (60 * 60, lambda n: _('hour', 'hours', n)), + (60, lambda n: _('minute', 'minutes', n)) + ) + # Convert datetime.date to datetime.datetime for comparison. + if not isinstance(d, datetime.datetime): + d = datetime.datetime(d.year, d.month, d.day) + if now and not isinstance(now, datetime.datetime): + now = datetime.datetime(now.year, now.month, now.day) + + if not now: + now = datetime.datetime.now(utc if is_aware(d) else None) + + delta = (d - now) if reversed else (now - d) + # ignore microseconds + since = delta.days * 24 * 60 * 60 + delta.seconds + if since <= 0: + # d is in the future compared to now, stop processing. + return '0 ' + pass_to_ugettext('minutes') + for i, (seconds, name) in enumerate(chunks): + count = since // seconds + if count != 0: + break + s = pass_to_ugettext('%(number)d %(type)s') % {'number': count, 'type': name(count)} + if i + 1 < len(chunks): + # Now get the second item + seconds2, name2 = chunks[i + 1] + count2 = (since - (seconds * count)) // seconds2 + if count2 != 0: + s += pass_to_ugettext(', %(number)d %(type)s') % {'number': count2, 'type': name2(count2)} + return s diff --git a/mediagoblin/tools/translate.py b/mediagoblin/tools/translate.py index 1d37c4de..4acafac7 100644 --- a/mediagoblin/tools/translate.py +++ b/mediagoblin/tools/translate.py @@ -123,6 +123,16 @@ def pass_to_ugettext(*args, **kwargs): *args, **kwargs) +def pass_to_ungettext(*args, **kwargs): + """ + Pass a translation on to the appropriate ungettext method. + + The reason we can't have a global ugettext method is because + mg_globals gets swapped out by the application per-request. + """ + return mg_globals.thread_scope.translations.ungettext( + *args, **kwargs) + def lazy_pass_to_ugettext(*args, **kwargs): """ Lazily pass to ugettext. @@ -158,6 +168,16 @@ def lazy_pass_to_ngettext(*args, **kwargs): """ return LazyProxy(pass_to_ngettext, *args, **kwargs) +def lazy_pass_to_ungettext(*args, **kwargs): + """ + Lazily pass to ungettext. + + This is useful if you have to define a translation on a module + level but you need it to not translate until the time that it's + used as a string. + """ + return LazyProxy(pass_to_ungettext, *args, **kwargs) + def fake_ugettext_passthrough(string): """ @@ -17,7 +17,6 @@ use = egg:Paste#urlmap [app:mediagoblin] use = egg:mediagoblin#app -filter-with = beaker config = %(here)s/mediagoblin_local.ini %(here)s/mediagoblin.ini [loggers] @@ -57,14 +56,6 @@ use = egg:Paste#static document_root = %(here)s/user_dev/theme_static/ cache_max_age = 86400 -[filter:beaker] -use = egg:Beaker#beaker_session -cache_dir = %(here)s/user_dev/beaker -beaker.session.key = mediagoblin -# beaker.session.secret = somesupersecret -beaker.session.data_dir = %(here)s/user_dev/beaker/sessions/data -beaker.session.lock_dir = %(here)s/user_dev/beaker/sessions/lock - [filter:errors] use = egg:mediagoblin#errors debug = false diff --git a/runtests.sh b/runtests.sh index cd53da2d..382e2fa6 100755 --- a/runtests.sh +++ b/runtests.sh @@ -49,9 +49,16 @@ echo "+ CELERY_CONFIG_MODULE=$CELERY_CONFIG_MODULE" # will try to read all directories, and this turns into a mess! need_arg=1 +ignore_next=0 for i in "$@" do + if [ "$ignore_next" = 1 ] + then + ignore_next=0 + continue + fi case "$i" in + -n) ignore_next=1;; -*) ;; *) need_arg=0; break ;; esac @@ -43,7 +43,6 @@ setup( install_requires=[ 'setuptools', 'PasteScript', - 'beaker', 'wtforms', 'py-bcrypt', 'pytest', @@ -61,6 +60,8 @@ setup( 'sqlalchemy>=0.7.0', 'sqlalchemy-migrate', 'mock', + 'itsdangerous', + 'pytz', ## This is optional! # 'translitcodec', ## For now we're expecting that users will install this from |