aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AUTHORS1
-rw-r--r--docs/source/devel/codebase.rst4
-rw-r--r--docs/source/pluginwriter/api.rst3
-rw-r--r--docs/source/siteadmin/deploying.rst5
-rw-r--r--extlib/README2
-rw-r--r--mediagoblin/app.py19
-rw-r--r--mediagoblin/config_spec.ini8
-rw-r--r--mediagoblin/db/mixin.py47
-rw-r--r--mediagoblin/db/models.py20
-rw-r--r--mediagoblin/db/util.py2
-rw-r--r--mediagoblin/edit/views.py2
-rw-r--r--mediagoblin/init/__init__.py15
-rw-r--r--mediagoblin/init/celery/__init__.py5
-rw-r--r--mediagoblin/init/celery/from_celery.py6
-rw-r--r--mediagoblin/init/plugins/__init__.py4
-rw-r--r--mediagoblin/media_types/ascii/processing.py8
-rw-r--r--mediagoblin/media_types/audio/processing.py8
-rw-r--r--mediagoblin/media_types/stl/processing.py8
-rw-r--r--mediagoblin/mg_globals.py3
-rw-r--r--mediagoblin/processing/__init__.py7
-rw-r--r--mediagoblin/static/css/base.css20
-rw-r--r--mediagoblin/storage/__init__.py16
-rw-r--r--mediagoblin/storage/filestorage.py26
-rw-r--r--mediagoblin/templates/mediagoblin/admin/panel.html2
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/media.html24
-rw-r--r--mediagoblin/tests/test_api.py1
-rw-r--r--mediagoblin/tests/test_auth.py40
-rw-r--r--mediagoblin/tests/test_cache.py50
-rw-r--r--mediagoblin/tests/test_collections.py3
-rw-r--r--mediagoblin/tests/test_edit.py21
-rw-r--r--mediagoblin/tests/test_globals.py6
-rw-r--r--mediagoblin/tests/test_mgoblin_app.ini4
-rw-r--r--mediagoblin/tests/test_misc.py16
-rw-r--r--mediagoblin/tests/test_modelmethods.py4
-rw-r--r--mediagoblin/tests/test_paste.ini9
-rw-r--r--mediagoblin/tests/test_pluginapi.py132
-rw-r--r--mediagoblin/tests/test_session.py30
-rw-r--r--mediagoblin/tests/test_storage.py46
-rw-r--r--mediagoblin/tests/test_submission.py35
-rw-r--r--mediagoblin/tests/test_timesince.py57
-rw-r--r--mediagoblin/tests/test_workbench.py11
-rw-r--r--mediagoblin/tests/testplugins/__init__.py15
-rw-r--r--mediagoblin/tests/testplugins/callables1/__init__.py41
-rw-r--r--mediagoblin/tests/testplugins/callables2/__init__.py38
-rw-r--r--mediagoblin/tests/testplugins/callables3/__init__.py38
-rw-r--r--mediagoblin/tests/tools.py7
-rw-r--r--mediagoblin/tools/crypto.py113
-rw-r--r--mediagoblin/tools/pluginapi.py67
-rw-r--r--mediagoblin/tools/request.py2
-rw-r--r--mediagoblin/tools/session.py68
-rw-r--r--mediagoblin/tools/template.py5
-rw-r--r--mediagoblin/tools/timesince.py95
-rw-r--r--mediagoblin/tools/translate.py20
-rw-r--r--paste.ini9
-rwxr-xr-xruntests.sh7
-rw-r--r--setup.py3
56 files changed, 990 insertions, 268 deletions
diff --git a/AUTHORS b/AUTHORS
index ef70e8a5..b6dd1cf2 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -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):
"""
diff --git a/paste.ini b/paste.ini
index 103bb609..4c6397fa 100644
--- a/paste.ini
+++ b/paste.ini
@@ -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
diff --git a/setup.py b/setup.py
index 4b983e3f..ce1e4102 100644
--- a/setup.py
+++ b/setup.py
@@ -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