aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/source/index.rst1
-rw-r--r--docs/source/plugindocs/raven.rst2
-rw-r--r--docs/source/siteadmin/production-deployments.rst47
-rw-r--r--mediagoblin/app.py3
-rw-r--r--mediagoblin/db/migration_tools.py8
-rw-r--r--mediagoblin/decorators.py2
-rw-r--r--mediagoblin/init/celery/__init__.py4
-rw-r--r--mediagoblin/init/celery/from_celery.py5
-rw-r--r--mediagoblin/plugins/oauth/forms.py7
-rw-r--r--mediagoblin/plugins/oauth/templates/oauth/authorize.html2
-rw-r--r--mediagoblin/plugins/raven/README.rst15
-rw-r--r--mediagoblin/plugins/raven/__init__.py92
-rw-r--r--mediagoblin/storage/cloudfiles.py58
-rw-r--r--mediagoblin/templates/mediagoblin/edit/edit_account.html9
-rw-r--r--mediagoblin/user_pages/views.py6
15 files changed, 203 insertions, 58 deletions
diff --git a/docs/source/index.rst b/docs/source/index.rst
index abd891a0..9124b1c1 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -58,6 +58,7 @@ Part 2: Core plugin documentation
plugindocs/sampleplugin
plugindocs/oauth
plugindocs/trim_whitespace
+ plugindocs/raven
Part 3: Plugin Writer's Guide
diff --git a/docs/source/plugindocs/raven.rst b/docs/source/plugindocs/raven.rst
new file mode 100644
index 00000000..71e284d0
--- /dev/null
+++ b/docs/source/plugindocs/raven.rst
@@ -0,0 +1,2 @@
+.. _raven-setup: Set up the raven plugin
+.. include:: ../../../mediagoblin/plugins/raven/README.rst
diff --git a/docs/source/siteadmin/production-deployments.rst b/docs/source/siteadmin/production-deployments.rst
index 0ed5ac6a..3e9431c9 100644
--- a/docs/source/siteadmin/production-deployments.rst
+++ b/docs/source/siteadmin/production-deployments.rst
@@ -77,51 +77,16 @@ Modify your existing MediaGoblin and application init scripts, if
necessary, to prevent them from starting their own ``celeryd``
processes.
-Monitor exceptions
-------------------
-
-This is an example config using raven_ to report exceptions and
-:py:mod:`logging` messages to a sentry_ instance
-
-.. _raven: http://raven.readthedocs.org/
-.. _sentry: https://github.com/getsentry
-
-.. code-block:: ini
-
- [pipeline:main]
- pipeline =
- errors
- raven
- routing
-
- [loggers]
- keys = root, sentry
-
- [handlers]
- keys = console, sentry
-
- [formatters]
- keys = generic
+.. _sentry:
- [logger_root]
- level = INFO
- handlers = console, sentry
+Set up sentry to monitor exceptions
+-----------------------------------
- [logger_sentry]
- level = WARN
- handlers = console
- qualname = sentry.errors
- propagate = 0
+We have a plugin for `raven`_ integration, see the ":doc:`/plugindocs/raven`"
+documentation.
- [handler_sentry]
- class = raven.handlers.logging.SentryHandler
- args = ('http://public:secret@example.com/1',)
- level = WARNING
- formatter = generic
+.. _`raven`: http://raven.readthedocs.org
- [filter:raven]
- use = egg:raven#raven
- dsn = http://71727ea2c69043e4bbcd793bb0115cd4:e9cedccb32d9482d81f99eeca8b1ad30@sentry.talka.tv/3
.. _init-script:
diff --git a/mediagoblin/app.py b/mediagoblin/app.py
index 607d599b..bb6be4d4 100644
--- a/mediagoblin/app.py
+++ b/mediagoblin/app.py
@@ -253,4 +253,7 @@ def paste_app_factory(global_config, **app_config):
mgoblin_app = MediaGoblinApp(mediagoblin_config)
+ for callable_hook in PluginManager().get_hook_callables('wrap_wsgi'):
+ mgoblin_app = callable_hook(mgoblin_app)
+
return mgoblin_app
diff --git a/mediagoblin/db/migration_tools.py b/mediagoblin/db/migration_tools.py
index e5380a3b..c0c7e998 100644
--- a/mediagoblin/db/migration_tools.py
+++ b/mediagoblin/db/migration_tools.py
@@ -17,6 +17,9 @@
from mediagoblin.tools.common import simple_printer
from sqlalchemy import Table
+class TableAlreadyExists(Exception):
+ pass
+
class MigrationManager(object):
"""
@@ -128,7 +131,10 @@ class MigrationManager(object):
# sanity check before we proceed, none of these should be created
for model in self.models:
# Maybe in the future just print out a "Yikes!" or something?
- assert not model.__table__.exists(self.session.bind)
+ if model.__table__.exists(self.session.bind):
+ raise TableAlreadyExists(
+ u"Intended to create table '%s' but it already exists" %
+ model.__table__.name)
self.migration_model.metadata.create_all(
self.session.bind,
diff --git a/mediagoblin/decorators.py b/mediagoblin/decorators.py
index fbf7b188..f3535fcf 100644
--- a/mediagoblin/decorators.py
+++ b/mediagoblin/decorators.py
@@ -43,7 +43,7 @@ def require_active_login(controller):
request.url)
return redirect(request, 'mediagoblin.auth.login',
- next=url_quote(next_url))
+ next=next_url)
return controller(request, *args, **kwargs)
diff --git a/mediagoblin/init/celery/__init__.py b/mediagoblin/init/celery/__init__.py
index fc595ea7..8d7a41bd 100644
--- a/mediagoblin/init/celery/__init__.py
+++ b/mediagoblin/init/celery/__init__.py
@@ -18,6 +18,7 @@ import os
import sys
from celery import Celery
+from mediagoblin.tools.pluginapi import PluginManager
MANDATORY_CELERY_IMPORTS = ['mediagoblin.processing.task']
@@ -65,6 +66,9 @@ 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)
+
def setup_celery_from_config(app_config, global_config,
settings_module=DEFAULT_SETTINGS_MODULE,
diff --git a/mediagoblin/init/celery/from_celery.py b/mediagoblin/init/celery/from_celery.py
index 5c99ddff..8a794abb 100644
--- a/mediagoblin/init/celery/from_celery.py
+++ b/mediagoblin/init/celery/from_celery.py
@@ -22,6 +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
OUR_MODULENAME = __name__
@@ -46,6 +47,10 @@ 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()
+
setup_logging.connect(setup_logging_from_paste_ini)
diff --git a/mediagoblin/plugins/oauth/forms.py b/mediagoblin/plugins/oauth/forms.py
index 2a956dad..d0a4e9b8 100644
--- a/mediagoblin/plugins/oauth/forms.py
+++ b/mediagoblin/plugins/oauth/forms.py
@@ -23,10 +23,9 @@ from mediagoblin.tools.translate import fake_ugettext_passthrough as _
class AuthorizationForm(wtforms.Form):
- client_id = wtforms.HiddenField(_(u'Client ID'),
- [wtforms.validators.Required()])
- next = wtforms.HiddenField(_(u'Next URL'),
- [wtforms.validators.Required()])
+ client_id = wtforms.HiddenField(u'',
+ validators=[wtforms.validators.Required()])
+ next = wtforms.HiddenField(u'', validators=[wtforms.validators.Required()])
allow = wtforms.SubmitField(_(u'Allow'))
deny = wtforms.SubmitField(_(u'Deny'))
diff --git a/mediagoblin/plugins/oauth/templates/oauth/authorize.html b/mediagoblin/plugins/oauth/templates/oauth/authorize.html
index 647fa41f..8a00c925 100644
--- a/mediagoblin/plugins/oauth/templates/oauth/authorize.html
+++ b/mediagoblin/plugins/oauth/templates/oauth/authorize.html
@@ -16,7 +16,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-#}
{% extends "mediagoblin/base.html" %}
-{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
+{% import "mediagoblin/utils/wtforms.html" as wtforms_util %}
{% block mediagoblin_content %}
<form action="{{ request.urlgen('mediagoblin.plugins.oauth.authorize_client') }}"
diff --git a/mediagoblin/plugins/raven/README.rst b/mediagoblin/plugins/raven/README.rst
new file mode 100644
index 00000000..de5fd20d
--- /dev/null
+++ b/mediagoblin/plugins/raven/README.rst
@@ -0,0 +1,15 @@
+==============
+ raven plugin
+==============
+
+.. _raven-setup:
+
+Set up the raven plugin
+=======================
+
+1. Add the following to your MediaGoblin .ini file in the ``[plugins]`` section::
+
+ [[mediagoblin.plugins.raven]]
+ sentry_dsn = <YOUR SENTRY DSN>
+ # Logging is very high-volume, set to 0 if you want to turn off logging
+ setup_logging = 1
diff --git a/mediagoblin/plugins/raven/__init__.py b/mediagoblin/plugins/raven/__init__.py
new file mode 100644
index 00000000..8cfaed0a
--- /dev/null
+++ b/mediagoblin/plugins/raven/__init__.py
@@ -0,0 +1,92 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import logging
+
+from mediagoblin.tools import pluginapi
+
+_log = logging.getLogger(__name__)
+
+
+def get_client():
+ from raven import Client
+ config = pluginapi.get_config('mediagoblin.plugins.raven')
+
+ sentry_dsn = config.get('sentry_dsn')
+
+ client = None
+
+ if sentry_dsn:
+ _log.info('Setting up raven from plugin config: {0}'.format(
+ sentry_dsn))
+ client = Client(sentry_dsn)
+ elif os.environ.get('SENTRY_DSN'):
+ _log.info('Setting up raven from SENTRY_DSN environment variable: {0}'\
+ .format(os.environ.get('SENTRY_DSN')))
+ client = Client() # Implicitly looks for SENTRY_DSN
+
+ if not client:
+ _log.error('Could not set up client, missing sentry DSN')
+ return None
+
+ return client
+
+
+def setup_celery():
+ from raven.contrib.celery import register_signal
+
+ client = get_client()
+
+ register_signal(client)
+
+
+def setup_logging():
+ config = pluginapi.get_config('mediagoblin.plugins.raven')
+
+ conf_setup_logging = False
+ if config.get('setup_logging'):
+ conf_setup_logging = bool(int(config.get('setup_logging')))
+
+ if not conf_setup_logging:
+ return
+
+ from raven.handlers.logging import SentryHandler
+ from raven.conf import setup_logging
+
+ client = get_client()
+
+ _log.info('Setting up raven logging handler')
+
+ setup_logging(SentryHandler(client))
+
+
+def wrap_wsgi(app):
+ from raven.middleware import Sentry
+
+ client = get_client()
+
+ _log.info('Attaching raven middleware...')
+
+ return Sentry(app, client)
+
+
+hooks = {
+ 'setup': setup_logging,
+ 'wrap_wsgi': wrap_wsgi,
+ 'celery_logging_setup': setup_logging,
+ 'celery_setup': setup_celery,
+ }
diff --git a/mediagoblin/storage/cloudfiles.py b/mediagoblin/storage/cloudfiles.py
index 1b5a6363..b6e57c91 100644
--- a/mediagoblin/storage/cloudfiles.py
+++ b/mediagoblin/storage/cloudfiles.py
@@ -131,6 +131,43 @@ class CloudFilesStorage(StorageInterface):
self._resolve_filepath(filepath)])
+ def copy_locally(self, filepath, dest_path):
+ """
+ Copy this file locally.
+
+ A basic working method for this is provided that should
+ function both for local_storage systems and remote storge
+ systems, but if more efficient systems for copying locally
+ apply to your system, override this method with something more
+ appropriate.
+ """
+ # Override this method, using the "stream" iterator for efficient streaming
+ with self.get_file(filepath, 'rb') as source_file:
+ with file(dest_path, 'wb') as dest_file:
+ for data in source_file:
+ dest_file.write(data)
+
+ def copy_local_to_storage(self, filename, filepath):
+ """
+ Copy this file from locally to the storage system.
+
+ This is kind of the opposite of copy_locally. It's likely you
+ could override this method with something more appropriate to
+ your storage system.
+ """
+ # It seems that (our implementation of) cloudfiles.write() takes
+ # all existing data and appends write(data) to it, sending the
+ # full monty over the wire everytime. This would of course
+ # absolutely kill chunked writes with some O(1^n) performance
+ # and bandwidth usage. So, override this method and use the
+ # Cloudfile's "send" interface instead.
+ # TODO: Fixing write() still seems worthwhile though.
+ _log.debug('Sending {0} to cloudfiles...'.format(filepath))
+ with self.get_file(filepath, 'wb') as dest_file:
+ with file(filename, 'rb') as source_file:
+ # Copy to storage system in 4096 byte chunks
+ dest_file.send(source_file)
+
class CloudFilesStorageObjectWrapper():
"""
Wrapper for python-cloudfiles's cloudfiles.storage_object.Object
@@ -160,6 +197,10 @@ class CloudFilesStorageObjectWrapper():
Currently this method does not support any write modes except "append".
However if we should need it it would be easy implement.
"""
+ _log.warn(
+ '{0}.write() has bad performance! Use .send instead for now'\
+ .format(self.__class__.__name__))
+
if self.storage_object.size and type(data) == str:
_log.debug('{0} is > 0 in size, appending data'.format(
self.storage_object.name))
@@ -169,9 +210,12 @@ class CloudFilesStorageObjectWrapper():
self.storage_object.name))
self.storage_object.write(data, *args, **kwargs)
+ def send(self, *args, **kw):
+ self.storage_object.send(*args, **kw)
+
def close(self):
"""
- Not implemented.
+ Not sure we need anything here.
"""
pass
@@ -188,3 +232,15 @@ class CloudFilesStorageObjectWrapper():
see self.__enter__()
"""
self.close()
+
+
+ def __iter__(self, **kwargs):
+ """Make CloudFile an iterator, yielding 8192 bytes by default
+
+ This returns a generator object that can be used to getting the
+ object's content in a memory efficient way.
+
+ Warning: The HTTP response is only complete after this generator
+ has raised a StopIteration. No other methods can be called until
+ this has occurred."""
+ return self.storage_object.stream(**kwargs)
diff --git a/mediagoblin/templates/mediagoblin/edit/edit_account.html b/mediagoblin/templates/mediagoblin/edit/edit_account.html
index 3f508af4..7fe2c031 100644
--- a/mediagoblin/templates/mediagoblin/edit/edit_account.html
+++ b/mediagoblin/templates/mediagoblin/edit/edit_account.html
@@ -15,7 +15,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/>.
#}
-{% extends "mediagoblin/base.html" %}
+{%- extends "mediagoblin/base.html" %}
{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
@@ -39,7 +39,7 @@
<h1>
{%- trans username=user.username -%}
Changing {{ username }}'s account settings
- {%- endtrans %}
+ {%- endtrans -%}
</h1>
{{ wtforms_util.render_field_div(form.old_password) }}
{{ wtforms_util.render_field_div(form.new_password) }}
@@ -47,10 +47,7 @@
<p>{{ form.wants_comment_notification }}
{{ wtforms_util.render_label(form.wants_comment_notification) }}</p>
</div>
- <div class="form_field_input">
- <p>{{ form.license_preference }}
- {{ wtforms_util.render_label(form.license_preference) }}</p>
- </div>
+ {{- wtforms_util.render_field_div(form.license_preference) }}
<div class="form_submit_buttons">
<input type="submit" value="{% trans %}Save changes{% endtrans %}" class="button_form" />
{{ csrf_token }}
diff --git a/mediagoblin/user_pages/views.py b/mediagoblin/user_pages/views.py
index 69d7defb..dc562084 100644
--- a/mediagoblin/user_pages/views.py
+++ b/mediagoblin/user_pages/views.py
@@ -227,7 +227,8 @@ def media_collect(request, media):
# Otherwise, use the collection selected from the drop-down
else:
collection = Collection.query.filter_by(
- id=request.form.get('collection')).first()
+ id=form.collection.data,
+ creator=request.user.id).first()
# Make sure the user actually selected a collection
if not collection:
@@ -236,7 +237,7 @@ def media_collect(request, media):
_('You have to select or add a collection'))
return redirect(request, "mediagoblin.user_pages.media_collect",
user=media.get_uploader.username,
- media=media.id)
+ media_id=media.id)
# Check whether media already exists in collection
@@ -250,7 +251,6 @@ def media_collect(request, media):
collection_item = request.db.CollectionItem()
collection_item.collection = collection.id
collection_item.media_entry = media.id
- collection_item.author = request.user.id
collection_item.note = request.form['note']
collection_item.save()