aboutsummaryrefslogtreecommitdiffstats
path: root/mediagoblin
diff options
context:
space:
mode:
Diffstat (limited to 'mediagoblin')
-rw-r--r--mediagoblin/_version.py2
-rw-r--r--mediagoblin/app.py6
-rw-r--r--mediagoblin/auth/views.py16
-rw-r--r--mediagoblin/config_spec.ini11
-rw-r--r--mediagoblin/db/base.py12
-rw-r--r--mediagoblin/db/migration_tools.py22
-rw-r--r--mediagoblin/db/migrations.py81
-rw-r--r--mediagoblin/db/mixin.py17
-rw-r--r--mediagoblin/db/models.py89
-rw-r--r--mediagoblin/db/open.py4
-rw-r--r--mediagoblin/db/util.py2
-rw-r--r--mediagoblin/decorators.py56
-rw-r--r--mediagoblin/edit/forms.py1
-rw-r--r--mediagoblin/edit/views.py6
-rw-r--r--mediagoblin/gmg_commands/dbupdate.py37
-rw-r--r--mediagoblin/gmg_commands/import_export.py4
-rw-r--r--mediagoblin/gmg_commands/users.py12
-rw-r--r--mediagoblin/listings/views.py17
-rw-r--r--mediagoblin/media_types/__init__.py77
-rw-r--r--mediagoblin/media_types/ascii/__init__.py24
-rw-r--r--mediagoblin/media_types/ascii/processing.py6
-rw-r--r--mediagoblin/media_types/audio/__init__.py24
-rw-r--r--mediagoblin/media_types/audio/processing.py9
-rw-r--r--mediagoblin/media_types/image/__init__.py27
-rw-r--r--mediagoblin/media_types/image/processing.py9
-rw-r--r--mediagoblin/media_types/pdf/__init__.py22
-rw-r--r--mediagoblin/media_types/pdf/processing.py13
-rw-r--r--mediagoblin/media_types/stl/__init__.py21
-rw-r--r--mediagoblin/media_types/stl/processing.py6
-rw-r--r--mediagoblin/media_types/tools.py27
-rw-r--r--mediagoblin/media_types/video/__init__.py25
-rw-r--r--mediagoblin/media_types/video/processing.py119
-rw-r--r--mediagoblin/notifications/__init__.py2
-rw-r--r--mediagoblin/oauth/__init__.py16
-rw-r--r--mediagoblin/oauth/exceptions.py18
-rw-r--r--mediagoblin/oauth/forms.py7
-rw-r--r--mediagoblin/oauth/oauth.py132
-rw-r--r--mediagoblin/oauth/routing.py43
-rw-r--r--mediagoblin/oauth/tools/__init__.py0
-rw-r--r--mediagoblin/oauth/tools/forms.py25
-rw-r--r--mediagoblin/oauth/tools/request.py35
-rw-r--r--mediagoblin/oauth/views.py339
-rw-r--r--mediagoblin/plugins/api/tools.py24
-rw-r--r--mediagoblin/plugins/api/views.py4
-rw-r--r--mediagoblin/plugins/basic_auth/__init__.py5
-rw-r--r--mediagoblin/plugins/basic_auth/forms.py3
-rw-r--r--mediagoblin/plugins/oauth/__init__.py12
-rw-r--r--mediagoblin/plugins/oauth/tools.py2
-rw-r--r--mediagoblin/plugins/oauth/views.py3
-rw-r--r--mediagoblin/plugins/openid/__init__.py2
-rw-r--r--mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login.html1
-rw-r--r--mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login_link.html2
-rw-r--r--mediagoblin/plugins/openid/views.py2
-rw-r--r--mediagoblin/plugins/persona/__init__.py116
-rw-r--r--mediagoblin/plugins/persona/forms.py41
-rw-r--r--mediagoblin/plugins/persona/models.py36
-rw-r--r--mediagoblin/plugins/persona/static/js/persona.js49
-rw-r--r--mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/edit.html43
-rw-r--r--mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/edit_link.html25
-rw-r--r--mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/login_link.html25
-rw-r--r--mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/persona.html30
-rw-r--r--mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/persona_js_head.html21
-rw-r--r--mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/register_link.html25
-rw-r--r--mediagoblin/plugins/persona/views.py191
-rw-r--r--mediagoblin/routing.py3
-rw-r--r--mediagoblin/static/css/base.css11
-rw-r--r--mediagoblin/static/images/home_goblin.pngbin0 -> 61657 bytes
-rw-r--r--mediagoblin/static/js/comment_show.js13
-rw-r--r--mediagoblin/submit/views.py13
-rw-r--r--mediagoblin/templates/mediagoblin/api/authorize.html56
-rw-r--r--mediagoblin/templates/mediagoblin/api/oob.html33
-rw-r--r--mediagoblin/templates/mediagoblin/base.html41
-rw-r--r--mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html14
-rw-r--r--mediagoblin/templates/mediagoblin/edit/change_pass.html2
-rw-r--r--mediagoblin/templates/mediagoblin/fragments/header_notifications.html2
-rw-r--r--mediagoblin/templates/mediagoblin/media_displays/stl.html16
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/media.html6
-rw-r--r--mediagoblin/templates/mediagoblin/utils/comment-subscription.html7
-rw-r--r--mediagoblin/templates/mediagoblin/utils/wtforms.html52
-rw-r--r--mediagoblin/tests/auth_configs/persona_appconfig.ini42
-rw-r--r--mediagoblin/tests/test_auth.py12
-rw-r--r--mediagoblin/tests/test_edit.py4
-rw-r--r--mediagoblin/tests/test_http_callback.py4
-rw-r--r--mediagoblin/tests/test_mgoblin_app.ini4
-rw-r--r--mediagoblin/tests/test_oauth1.py166
-rw-r--r--mediagoblin/tests/test_oauth2.py (renamed from mediagoblin/tests/test_oauth.py)12
-rw-r--r--mediagoblin/tests/test_openid.py19
-rw-r--r--mediagoblin/tests/test_persona.py210
-rw-r--r--mediagoblin/tests/test_sql_migrations.py42
-rw-r--r--mediagoblin/tests/test_submission.py14
-rw-r--r--mediagoblin/tests/tools.py2
-rw-r--r--mediagoblin/tools/crypto.py15
-rw-r--r--mediagoblin/tools/request.py18
-rw-r--r--mediagoblin/tools/response.py53
-rw-r--r--mediagoblin/tools/session.py11
-rw-r--r--mediagoblin/tools/template.py19
-rw-r--r--mediagoblin/tools/validator.py46
-rw-r--r--mediagoblin/user_pages/forms.py4
-rw-r--r--mediagoblin/user_pages/routing.py4
-rw-r--r--mediagoblin/user_pages/views.py19
100 files changed, 2678 insertions, 394 deletions
diff --git a/mediagoblin/_version.py b/mediagoblin/_version.py
index 2abc105f..94629775 100644
--- a/mediagoblin/_version.py
+++ b/mediagoblin/_version.py
@@ -23,4 +23,4 @@
# see http://www.python.org/dev/peps/pep-0386/
-__version__ = "0.4.1.dev"
+__version__ = "0.5.0.dev"
diff --git a/mediagoblin/app.py b/mediagoblin/app.py
index ada0c8ba..e9177eff 100644
--- a/mediagoblin/app.py
+++ b/mediagoblin/app.py
@@ -29,6 +29,7 @@ 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
+from mediagoblin.media_types.tools import media_type_warning
from mediagoblin.mg_globals import setup_globals
from mediagoblin.init.celery import setup_celery_from_config
from mediagoblin.init.plugins import setup_plugins
@@ -38,7 +39,6 @@ from mediagoblin.init import (get_jinja_loader, get_staticdirector,
from mediagoblin.tools.pluginapi import PluginManager, hook_transform
from mediagoblin.tools.crypto import setup_crypto
from mediagoblin.auth.tools import check_auth_enabled, no_auth_logout
-from mediagoblin import notifications
_log = logging.getLogger(__name__)
@@ -69,6 +69,8 @@ class MediaGoblinApp(object):
# Open and setup the config
global_config, app_config = setup_global_and_app_config(config_path)
+ media_type_warning()
+
setup_crypto()
##########################################
@@ -196,8 +198,6 @@ class MediaGoblinApp(object):
# Log user out if authentication_disabled
no_auth_logout(request)
- request.notifications = notifications
-
mg_request.setup_user_in_request(request)
request.controller_name = None
diff --git a/mediagoblin/auth/views.py b/mediagoblin/auth/views.py
index 1cff8dcc..dd71d5c1 100644
--- a/mediagoblin/auth/views.py
+++ b/mediagoblin/auth/views.py
@@ -41,8 +41,11 @@ def register(request):
"""
if 'pass_auth' not in request.template_env.globals:
redirect_name = hook_handle('auth_no_pass_redirect')
- return redirect(request, 'mediagoblin.plugins.{0}.register'.format(
- redirect_name))
+ if redirect_name:
+ return redirect(request, 'mediagoblin.plugins.{0}.register'.format(
+ redirect_name))
+ else:
+ return redirect(request, 'index')
register_form = hook_handle("auth_get_registration_form", request)
@@ -73,8 +76,11 @@ def login(request):
"""
if 'pass_auth' not in request.template_env.globals:
redirect_name = hook_handle('auth_no_pass_redirect')
- return redirect(request, 'mediagoblin.plugins.{0}.login'.format(
- redirect_name))
+ if redirect_name:
+ return redirect(request, 'mediagoblin.plugins.{0}.login'.format(
+ redirect_name))
+ else:
+ return redirect(request, 'index')
login_form = hook_handle("auth_get_login_form", request)
@@ -88,6 +94,8 @@ def login(request):
if user:
# set up login in session
+ if login_form.stay_logged_in.data:
+ request.session['stay_logged_in'] = True
request.session['user_id'] = unicode(user.id)
request.session.save()
diff --git a/mediagoblin/config_spec.ini b/mediagoblin/config_spec.ini
index 12af2f57..81dadd25 100644
--- a/mediagoblin/config_spec.ini
+++ b/mediagoblin/config_spec.ini
@@ -5,9 +5,6 @@ html_title = string(default="GNU MediaGoblin")
# link to source for this MediaGoblin site
source_link = string(default="https://gitorious.org/mediagoblin/mediagoblin")
-# Enabled media types
-media_types = string_list(default=list("mediagoblin.media_types.image"))
-
# database stuff
sql_engine = string(default="sqlite:///%(here)s/mediagoblin.db")
@@ -78,6 +75,12 @@ theme = string()
plugin_web_path = string(default="/plugin_static/")
plugin_linked_assets_dir = string(default="%(here)s/user_dev/plugin_static/")
+[jinja2]
+# Jinja2 supports more directives than the minimum required by mediagoblin.
+# This setting allows users creating custom templates to specify a list of
+# additional extensions they want to use. example value:
+# extensions = jinja2.ext.loopcontrols , jinja2.ext.with_
+extensions = string_list(default=list())
[storage:publicstore]
storage_class = string(default="mediagoblin.storage.filestorage:BasicFileStorage")
@@ -119,7 +122,7 @@ vp8_quality = integer(default=8)
vorbis_quality = float(default=0.3)
# Autoplay the video when page is loaded?
-auto_play = boolean(default=True)
+auto_play = boolean(default=False)
[[skip_transcode]]
mime_types = string_list(default=list("video/webm"))
diff --git a/mediagoblin/db/base.py b/mediagoblin/db/base.py
index 699a503a..c0cefdc2 100644
--- a/mediagoblin/db/base.py
+++ b/mediagoblin/db/base.py
@@ -24,18 +24,6 @@ Session = scoped_session(sessionmaker())
class GMGTableBase(object):
query = Session.query_property()
- @classmethod
- def find(cls, query_dict):
- return cls.query.filter_by(**query_dict)
-
- @classmethod
- def find_one(cls, query_dict):
- return cls.query.filter_by(**query_dict).first()
-
- @classmethod
- def one(cls, query_dict):
- return cls.find(query_dict).one()
-
def get(self, key):
return getattr(self, key)
diff --git a/mediagoblin/db/migration_tools.py b/mediagoblin/db/migration_tools.py
index c0c7e998..e75f3757 100644
--- a/mediagoblin/db/migration_tools.py
+++ b/mediagoblin/db/migration_tools.py
@@ -29,7 +29,7 @@ class MigrationManager(object):
to the latest migrations, etc.
"""
- def __init__(self, name, models, migration_registry, session,
+ def __init__(self, name, models, foundations, migration_registry, session,
printer=simple_printer):
"""
Args:
@@ -40,6 +40,7 @@ class MigrationManager(object):
"""
self.name = unicode(name)
self.models = models
+ self.foundations = foundations
self.session = session
self.migration_registry = migration_registry
self._sorted_migrations = None
@@ -140,6 +141,18 @@ class MigrationManager(object):
self.session.bind,
tables=[model.__table__ for model in self.models])
+ def populate_table_foundations(self):
+ """
+ Create the table foundations (default rows) as layed out in FOUNDATIONS
+ in mediagoblin.db.models
+ """
+ for Model, rows in self.foundations.items():
+ self.printer(u' + Laying foundations for %s table\n' %
+ (Model.__name__))
+ for parameters in rows:
+ new_row = Model(**parameters)
+ self.session.add(new_row)
+
def create_new_migration_record(self):
"""
Create a new migration record for this migration set
@@ -175,8 +188,7 @@ class MigrationManager(object):
if self.name == u'__main__':
return u"main mediagoblin tables"
else:
- # TODO: Use the friendlier media manager "human readable" name
- return u'media type "%s"' % self.name
+ return u'plugin "%s"' % self.name
def init_or_migrate(self):
"""
@@ -203,9 +215,9 @@ class MigrationManager(object):
self.init_tables()
# auto-set at latest migration number
- self.create_new_migration_record()
-
+ self.create_new_migration_record()
self.printer(u"done.\n")
+ self.populate_table_foundations()
self.set_current_migration()
return u'inited'
diff --git a/mediagoblin/db/migrations.py b/mediagoblin/db/migrations.py
index fe4ffb3e..374ab4c8 100644
--- a/mediagoblin/db/migrations.py
+++ b/mediagoblin/db/migrations.py
@@ -25,6 +25,8 @@ from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.sql import and_
from migrate.changeset.constraint import UniqueConstraint
+
+from mediagoblin.db.extratypes import JSONEncoded
from mediagoblin.db.migration_tools import RegisterMigration, inspect_table
from mediagoblin.db.models import MediaEntry, Collection, User, MediaComment
@@ -379,3 +381,82 @@ def pw_hash_nullable(db):
constraint.create()
db.commit()
+
+
+# oauth1 migrations
+class Client_v0(declarative_base()):
+ """
+ Model representing a client - Used for API Auth
+ """
+ __tablename__ = "core__clients"
+
+ id = Column(Unicode, nullable=True, primary_key=True)
+ secret = Column(Unicode, nullable=False)
+ expirey = Column(DateTime, nullable=True)
+ application_type = Column(Unicode, nullable=False)
+ created = Column(DateTime, nullable=False, default=datetime.datetime.now)
+ updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
+
+ # optional stuff
+ redirect_uri = Column(JSONEncoded, nullable=True)
+ logo_url = Column(Unicode, nullable=True)
+ application_name = Column(Unicode, nullable=True)
+ contacts = Column(JSONEncoded, nullable=True)
+
+ def __repr__(self):
+ if self.application_name:
+ return "<Client {0} - {1}>".format(self.application_name, self.id)
+ else:
+ return "<Client {0}>".format(self.id)
+
+class RequestToken_v0(declarative_base()):
+ """
+ Model for representing the request tokens
+ """
+ __tablename__ = "core__request_tokens"
+
+ token = Column(Unicode, primary_key=True)
+ secret = Column(Unicode, nullable=False)
+ client = Column(Unicode, ForeignKey(Client_v0.id))
+ user = Column(Integer, ForeignKey(User.id), nullable=True)
+ used = Column(Boolean, default=False)
+ authenticated = Column(Boolean, default=False)
+ verifier = Column(Unicode, nullable=True)
+ callback = Column(Unicode, nullable=False, default=u"oob")
+ created = Column(DateTime, nullable=False, default=datetime.datetime.now)
+ updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
+
+class AccessToken_v0(declarative_base()):
+ """
+ Model for representing the access tokens
+ """
+ __tablename__ = "core__access_tokens"
+
+ token = Column(Unicode, nullable=False, primary_key=True)
+ secret = Column(Unicode, nullable=False)
+ user = Column(Integer, ForeignKey(User.id))
+ request_token = Column(Unicode, ForeignKey(RequestToken_v0.token))
+ created = Column(DateTime, nullable=False, default=datetime.datetime.now)
+ updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
+
+
+class NonceTimestamp_v0(declarative_base()):
+ """
+ A place the timestamp and nonce can be stored - this is for OAuth1
+ """
+ __tablename__ = "core__nonce_timestamps"
+
+ nonce = Column(Unicode, nullable=False, primary_key=True)
+ timestamp = Column(DateTime, nullable=False, primary_key=True)
+
+
+@RegisterMigration(14, MIGRATIONS)
+def create_oauth1_tables(db):
+ """ Creates the OAuth1 tables """
+
+ Client_v0.__table__.create(db.bind)
+ RequestToken_v0.__table__.create(db.bind)
+ AccessToken_v0.__table__.create(db.bind)
+ NonceTimestamp_v0.__table__.create(db.bind)
+
+ db.commit()
diff --git a/mediagoblin/db/mixin.py b/mediagoblin/db/mixin.py
index 1b32d838..57b27d83 100644
--- a/mediagoblin/db/mixin.py
+++ b/mediagoblin/db/mixin.py
@@ -29,15 +29,14 @@ real objects.
import uuid
import re
-import datetime
-
from datetime import datetime
from werkzeug.utils import cached_property
from mediagoblin import mg_globals
-from mediagoblin.media_types import get_media_managers, FileTypeNotSupported
+from mediagoblin.media_types import FileTypeNotSupported
from mediagoblin.tools import common, licenses
+from mediagoblin.tools.pluginapi import hook_handle
from mediagoblin.tools.text import cleaned_markdown_conversion
from mediagoblin.tools.url import slugify
@@ -204,14 +203,14 @@ class MediaEntryMixin(GenerateSlugMixin):
Raises FileTypeNotSupported in case no such manager is enabled
"""
- # TODO, we should be able to make this a simple lookup rather
- # than iterating through all media managers.
- for media_type, manager in get_media_managers():
- if media_type == self.media_type:
- return manager(self)
+ manager = hook_handle(('media_manager', self.media_type))
+ if manager:
+ return manager(self)
+
# Not found? Then raise an error
raise FileTypeNotSupported(
- "MediaManager not in enabled types. Check media_types in config?")
+ "MediaManager not in enabled types. Check media_type plugins are"
+ " enabled in config?")
def get_fail_exception(self):
"""
diff --git a/mediagoblin/db/models.py b/mediagoblin/db/models.py
index 826d47ba..f0cbce2a 100644
--- a/mediagoblin/db/models.py
+++ b/mediagoblin/db/models.py
@@ -105,6 +105,72 @@ class User(Base, UserMixin):
_log.info('Deleted user "{0}" account'.format(self.username))
+class Client(Base):
+ """
+ Model representing a client - Used for API Auth
+ """
+ __tablename__ = "core__clients"
+
+ id = Column(Unicode, nullable=True, primary_key=True)
+ secret = Column(Unicode, nullable=False)
+ expirey = Column(DateTime, nullable=True)
+ application_type = Column(Unicode, nullable=False)
+ created = Column(DateTime, nullable=False, default=datetime.datetime.now)
+ updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
+
+ # optional stuff
+ redirect_uri = Column(JSONEncoded, nullable=True)
+ logo_url = Column(Unicode, nullable=True)
+ application_name = Column(Unicode, nullable=True)
+ contacts = Column(JSONEncoded, nullable=True)
+
+ def __repr__(self):
+ if self.application_name:
+ return "<Client {0} - {1}>".format(self.application_name, self.id)
+ else:
+ return "<Client {0}>".format(self.id)
+
+class RequestToken(Base):
+ """
+ Model for representing the request tokens
+ """
+ __tablename__ = "core__request_tokens"
+
+ token = Column(Unicode, primary_key=True)
+ secret = Column(Unicode, nullable=False)
+ client = Column(Unicode, ForeignKey(Client.id))
+ user = Column(Integer, ForeignKey(User.id), nullable=True)
+ used = Column(Boolean, default=False)
+ authenticated = Column(Boolean, default=False)
+ verifier = Column(Unicode, nullable=True)
+ callback = Column(Unicode, nullable=False, default=u"oob")
+ created = Column(DateTime, nullable=False, default=datetime.datetime.now)
+ updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
+
+class AccessToken(Base):
+ """
+ Model for representing the access tokens
+ """
+ __tablename__ = "core__access_tokens"
+
+ token = Column(Unicode, nullable=False, primary_key=True)
+ secret = Column(Unicode, nullable=False)
+ user = Column(Integer, ForeignKey(User.id))
+ request_token = Column(Unicode, ForeignKey(RequestToken.token))
+ created = Column(DateTime, nullable=False, default=datetime.datetime.now)
+ updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
+
+
+class NonceTimestamp(Base):
+ """
+ A place the timestamp and nonce can be stored - this is for OAuth1
+ """
+ __tablename__ = "core__nonce_timestamps"
+
+ nonce = Column(Unicode, nullable=False, primary_key=True)
+ timestamp = Column(DateTime, nullable=False, primary_key=True)
+
+
class MediaEntry(Base, MediaEntryMixin):
"""
TODO: Consider fetching the media_files using join
@@ -580,11 +646,26 @@ with_polymorphic(
[ProcessingNotification, CommentNotification])
MODELS = [
- User, MediaEntry, Tag, MediaTag, MediaComment, Collection, CollectionItem,
- MediaFile, FileKeynames, MediaAttachmentFile, ProcessingMetaData,
- Notification, CommentNotification, ProcessingNotification,
- CommentSubscription]
+ User, Client, RequestToken, AccessToken, NonceTimestamp, MediaEntry, Tag,
+ MediaTag, MediaComment, Collection, CollectionItem, MediaFile, FileKeynames,
+ MediaAttachmentFile, ProcessingMetaData, Notification, CommentNotification,
+ ProcessingNotification, CommentSubscription]
+"""
+ Foundations are the default rows that are created immediately after the tables
+ are initialized. Each entry to this dictionary should be in the format of:
+ ModelConstructorObject:List of Dictionaries
+ (Each Dictionary represents a row on the Table to be created, containing each
+ of the columns' names as a key string, and each of the columns' values as a
+ value)
+
+ ex. [NOTE THIS IS NOT BASED OFF OF OUR USER TABLE]
+ user_foundations = [{'name':u'Joanna', 'age':24},
+ {'name':u'Andrea', 'age':41}]
+
+ FOUNDATIONS = {User:user_foundations}
+"""
+FOUNDATIONS = {}
######################################################
# Special, migrations-tracking table
diff --git a/mediagoblin/db/open.py b/mediagoblin/db/open.py
index 0b1679fb..4ff0945f 100644
--- a/mediagoblin/db/open.py
+++ b/mediagoblin/db/open.py
@@ -52,10 +52,6 @@ class DatabaseMaster(object):
def load_models(app_config):
import mediagoblin.db.models
- for media_type in app_config['media_types']:
- _log.debug("Loading %s.models", media_type)
- __import__(media_type + ".models")
-
for plugin in mg_globals.global_config.get('plugins', {}).keys():
_log.debug("Loading %s.models", plugin)
try:
diff --git a/mediagoblin/db/util.py b/mediagoblin/db/util.py
index 6ffec44d..8431361a 100644
--- a/mediagoblin/db/util.py
+++ b/mediagoblin/db/util.py
@@ -24,7 +24,7 @@ from mediagoblin.db.models import MediaEntry, Tag, MediaTag, Collection
def atomic_update(table, query_dict, update_values):
- table.find(query_dict).update(update_values,
+ table.query.filter_by(**query_dict).update(update_values,
synchronize_session=False)
Session.commit()
diff --git a/mediagoblin/decorators.py b/mediagoblin/decorators.py
index ece222f5..685d0d98 100644
--- a/mediagoblin/decorators.py
+++ b/mediagoblin/decorators.py
@@ -18,13 +18,16 @@ from functools import wraps
from urlparse import urljoin
from werkzeug.exceptions import Forbidden, NotFound
+from oauthlib.oauth1 import ResourceEndpoint
from mediagoblin import mg_globals as mgg
from mediagoblin import messages
from mediagoblin.db.models import MediaEntry, User
-from mediagoblin.tools.response import redirect, render_404
+from mediagoblin.tools.response import json_response, redirect, render_404
from mediagoblin.tools.translate import pass_to_ugettext as _
+from mediagoblin.oauth.tools.request import decode_authorization_header
+from mediagoblin.oauth.oauth import GMGRequestValidator
def require_active_login(controller):
"""
@@ -87,8 +90,8 @@ def user_may_alter_collection(controller):
"""
@wraps(controller)
def wrapper(request, *args, **kwargs):
- creator_id = request.db.User.find_one(
- {'username': request.matchdict['user']}).id
+ creator_id = request.db.User.query.filter_by(
+ username=request.matchdict['user']).first().id
if not (request.user.is_admin or
request.user.id == creator_id):
raise Forbidden()
@@ -162,15 +165,15 @@ def get_user_collection(controller):
"""
@wraps(controller)
def wrapper(request, *args, **kwargs):
- user = request.db.User.find_one(
- {'username': request.matchdict['user']})
+ user = request.db.User.query.filter_by(
+ username=request.matchdict['user']).first()
if not user:
return render_404(request)
- collection = request.db.Collection.find_one(
- {'slug': request.matchdict['collection'],
- 'creator': user.id})
+ collection = request.db.Collection.query.filter_by(
+ slug=request.matchdict['collection'],
+ creator=user.id).first()
# Still no collection? Okay, 404.
if not collection:
@@ -187,14 +190,14 @@ def get_user_collection_item(controller):
"""
@wraps(controller)
def wrapper(request, *args, **kwargs):
- user = request.db.User.find_one(
- {'username': request.matchdict['user']})
+ user = request.db.User.query.filter_by(
+ username=request.matchdict['user']).first()
if not user:
return render_404(request)
- collection_item = request.db.CollectionItem.find_one(
- {'id': request.matchdict['collection_item'] })
+ collection_item = request.db.CollectionItem.query.filter_by(
+ id=request.matchdict['collection_item']).first()
# Still no collection item? Okay, 404.
if not collection_item:
@@ -268,3 +271,32 @@ def auth_enabled(controller):
return controller(request, *args, **kwargs)
return wrapper
+
+def oauth_required(controller):
+ """ Used to wrap API endpoints where oauth is required """
+ @wraps(controller)
+ def wrapper(request, *args, **kwargs):
+ data = request.headers
+ authorization = decode_authorization_header(data)
+
+ if authorization == dict():
+ error = "Missing required parameter."
+ return json_response({"error": error}, status=400)
+
+
+ request_validator = GMGRequestValidator()
+ resource_endpoint = ResourceEndpoint(request_validator)
+ valid, request = resource_endpoint.validate_protected_resource_request(
+ uri=request.url,
+ http_method=request.method,
+ body=request.get_data(),
+ headers=dict(request.headers),
+ )
+
+ if not valid:
+ error = "Invalid oauth prarameter."
+ return json_response({"error": error}, status=400)
+
+ return controller(request, *args, **kwargs)
+
+ return wrapper
diff --git a/mediagoblin/edit/forms.py b/mediagoblin/edit/forms.py
index e0147a0c..85c243a0 100644
--- a/mediagoblin/edit/forms.py
+++ b/mediagoblin/edit/forms.py
@@ -66,7 +66,6 @@ class EditAccountForm(wtforms.Form):
[wtforms.validators.Optional(),
normalize_user_or_email_field(allow_user=False)])
wants_comment_notification = wtforms.BooleanField(
- label='',
description=_("Email me when others comment on my media"))
license_preference = wtforms.SelectField(
_('License preference'),
diff --git a/mediagoblin/edit/views.py b/mediagoblin/edit/views.py
index 7a8d6185..6aa2acd9 100644
--- a/mediagoblin/edit/views.py
+++ b/mediagoblin/edit/views.py
@@ -305,9 +305,9 @@ def edit_collection(request, collection):
form.slug.data, collection.id)
# Make sure there isn't already a Collection with this title
- existing_collection = request.db.Collection.find_one({
- 'creator': request.user.id,
- 'title':form.title.data})
+ existing_collection = request.db.Collection.query.filter_by(
+ creator=request.user.id,
+ title=form.title.data).first()
if existing_collection and existing_collection.id != collection.id:
messages.add_message(
diff --git a/mediagoblin/gmg_commands/dbupdate.py b/mediagoblin/gmg_commands/dbupdate.py
index 22ad426c..961752f6 100644
--- a/mediagoblin/gmg_commands/dbupdate.py
+++ b/mediagoblin/gmg_commands/dbupdate.py
@@ -32,17 +32,18 @@ def dbupdate_parse_setup(subparser):
class DatabaseData(object):
- def __init__(self, name, models, migrations):
+ def __init__(self, name, models, foundations, migrations):
self.name = name
self.models = models
+ self.foundations = foundations
self.migrations = migrations
def make_migration_manager(self, session):
return MigrationManager(
- self.name, self.models, self.migrations, session)
+ self.name, self.models, self.foundations, self.migrations, session)
-def gather_database_data(media_types, plugins):
+def gather_database_data(plugins):
"""
Gather all database data relevant to the extensions we have
installed so we can do migrations and table initialization.
@@ -54,17 +55,11 @@ def gather_database_data(media_types, plugins):
# Add main first
from mediagoblin.db.models import MODELS as MAIN_MODELS
from mediagoblin.db.migrations import MIGRATIONS as MAIN_MIGRATIONS
+ from mediagoblin.db.models import FOUNDATIONS as MAIN_FOUNDATIONS
managed_dbdata.append(
DatabaseData(
- u'__main__', MAIN_MODELS, MAIN_MIGRATIONS))
-
- # Then get all registered media managers (eventually, plugins)
- for media_type in media_types:
- models = import_component('%s.models:MODELS' % media_type)
- migrations = import_component('%s.migrations:MIGRATIONS' % media_type)
- managed_dbdata.append(
- DatabaseData(media_type, models, migrations))
+ u'__main__', MAIN_MODELS, MAIN_FOUNDATIONS, MAIN_MIGRATIONS))
for plugin in plugins:
try:
@@ -90,13 +85,26 @@ forgotten to add it? ({1})'.format(plugin, exc))
migrations = {}
except AttributeError as exc:
- _log.debug('Cloud not find MIGRATIONS in {0}.migrations, have you \
+ _log.debug('Could not find MIGRATIONS in {0}.migrations, have you \
forgotten to add it? ({1})'.format(plugin, exc))
migrations = {}
+ try:
+ foundations = import_component('{0}.models:FOUNDATIONS'.format(plugin))
+ except ImportError as exc:
+ _log.debug('No foundations found for {0}: {1}'.format(
+ plugin,
+ exc))
+
+ foundations = {}
+ except AttributeError as exc:
+ _log.debug('Could not find FOUNDATIONS in {0}.models, have you \
+forgotten to add it? ({1})'.format(plugin, exc))
+ foundations = {}
+
if models:
managed_dbdata.append(
- DatabaseData(plugin, models, migrations))
+ DatabaseData(plugin, models, foundations, migrations))
return managed_dbdata
@@ -118,7 +126,7 @@ def run_dbupdate(app_config, global_config):
def run_all_migrations(db, app_config, global_config):
"""
- Initializes or migrates a database that already has a
+ Initializes or migrates a database that already has a
connection setup and also initializes or migrates all
extensions based on the config files.
@@ -127,7 +135,6 @@ def run_all_migrations(db, app_config, global_config):
"""
# Gather information from all media managers / projects
dbdatas = gather_database_data(
- app_config['media_types'],
global_config.get('plugins', {}).keys())
Session = sessionmaker(bind=db.engine)
diff --git a/mediagoblin/gmg_commands/import_export.py b/mediagoblin/gmg_commands/import_export.py
index d51a1e3e..98ec617d 100644
--- a/mediagoblin/gmg_commands/import_export.py
+++ b/mediagoblin/gmg_commands/import_export.py
@@ -63,7 +63,7 @@ def _import_media(db, args):
# TODO: Add import of queue files
queue_cache = BasicFileStorage(args._cache_path['queue'])
- for entry in db.MediaEntry.find():
+ for entry in db.MediaEntry.query.filter_by():
for name, path in entry.media_files.items():
_log.info('Importing: {0} - {1}'.format(
entry.title.encode('ascii', 'replace'),
@@ -204,7 +204,7 @@ def _export_media(db, args):
# TODO: Add export of queue files
queue_cache = BasicFileStorage(args._cache_path['queue'])
- for entry in db.MediaEntry.find():
+ for entry in db.MediaEntry.query.filter_by():
for name, path in entry.media_files.items():
_log.info(u'Exporting {0} - {1}'.format(
entry.title,
diff --git a/mediagoblin/gmg_commands/users.py b/mediagoblin/gmg_commands/users.py
index 1f329459..e44b0aa9 100644
--- a/mediagoblin/gmg_commands/users.py
+++ b/mediagoblin/gmg_commands/users.py
@@ -40,9 +40,9 @@ def adduser(args):
db = mg_globals.database
users_with_username = \
- db.User.find({
- 'username': args.username.lower(),
- }).count()
+ db.User.query.filter_by(
+ username=args.username.lower()
+ ).count()
if users_with_username:
print u'Sorry, a user with that name already exists.'
@@ -71,7 +71,8 @@ def makeadmin(args):
db = mg_globals.database
- user = db.User.one({'username': unicode(args.username.lower())})
+ user = db.User.query.filter_by(
+ username=unicode(args.username.lower())).one()
if user:
user.is_admin = True
user.save()
@@ -94,7 +95,8 @@ def changepw(args):
db = mg_globals.database
- user = db.User.one({'username': unicode(args.username.lower())})
+ user = db.User.query.filter_by(
+ username=unicode(args.username.lower())).one()
if user:
user.pw_hash = auth.gen_password_hash(args.password)
user.save()
diff --git a/mediagoblin/listings/views.py b/mediagoblin/listings/views.py
index 35af7148..07dbb3d5 100644
--- a/mediagoblin/listings/views.py
+++ b/mediagoblin/listings/views.py
@@ -14,6 +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 mediagoblin import mg_globals
from mediagoblin.db.models import MediaEntry
from mediagoblin.db.util import media_entries_for_tag_slug
from mediagoblin.tools.pagination import Pagination
@@ -80,6 +81,17 @@ def atom_feed(request):
link = request.urlgen('index', qualified=True)
feed_title += "for all recent items"
+ atomlinks = [
+ {'href': link,
+ 'rel': 'alternate',
+ 'type': 'text/html'}]
+
+ if mg_globals.app_config["push_urls"]:
+ for push_url in mg_globals.app_config["push_urls"]:
+ atomlinks.append({
+ 'rel': 'hub',
+ 'href': push_url})
+
cursor = cursor.order_by(MediaEntry.created.desc())
cursor = cursor.limit(ATOM_DEFAULT_NR_OF_UPDATED_ITEMS)
@@ -87,9 +99,8 @@ def atom_feed(request):
feed_title,
feed_url=request.url,
id=link,
- links=[{'href': link,
- 'rel': 'alternate',
- 'type': 'text/html'}])
+ links=atomlinks)
+
for entry in cursor:
feed.add(entry.get('title'),
entry.description_html,
diff --git a/mediagoblin/media_types/__init__.py b/mediagoblin/media_types/__init__.py
index 20e1918e..134157dc 100644
--- a/mediagoblin/media_types/__init__.py
+++ b/mediagoblin/media_types/__init__.py
@@ -15,12 +15,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
-import sys
import logging
import tempfile
-from mediagoblin import mg_globals
-from mediagoblin.tools.common import import_component
+from mediagoblin.tools.pluginapi import hook_handle
from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
_log = logging.getLogger(__name__)
@@ -52,36 +50,6 @@ class MediaManagerBase(object):
return hasattr(self, i)
-class CompatMediaManager(object):
- def __init__(self, mm_dict, entry=None):
- self.mm_dict = mm_dict
- self.entry = entry
-
- def __call__(self, entry):
- "So this object can look like a class too, somehow"
- assert self.entry is None
- return self.__class__(self.mm_dict, entry)
-
- def __getitem__(self, i):
- return self.mm_dict[i]
-
- def __contains__(self, i):
- return (i in self.mm_dict)
-
- @property
- def media_fetch_order(self):
- return self.mm_dict.get('media_fetch_order')
-
- def sniff_handler(self, *args, **kwargs):
- func = self.mm_dict.get("sniff_handler", None)
- if func is not None:
- return func(*args, **kwargs)
- return False
-
- def __getattr__(self, i):
- return self.mm_dict[i]
-
-
def sniff_media(media):
'''
Iterate through the enabled media types and find those suited
@@ -98,40 +66,18 @@ def sniff_media(media):
media_file.write(media.stream.read())
media.stream.seek(0)
- for media_type, manager in get_media_managers():
- _log.info('Sniffing {0}'.format(media_type))
- if manager.sniff_handler(media_file, media=media):
- _log.info('{0} accepts the file'.format(media_type))
- return media_type, manager
- else:
- _log.debug('{0} did not accept the file'.format(media_type))
+ media_type = hook_handle('sniff_handler', media_file, media=media)
+ if media_type:
+ _log.info('{0} accepts the file'.format(media_type))
+ return media_type, hook_handle(('media_manager', media_type))
+ else:
+ _log.debug('{0} did not accept the file'.format(media_type))
raise FileTypeNotSupported(
# TODO: Provide information on which file types are supported
_(u'Sorry, I don\'t support that file type :('))
-def get_media_types():
- """
- Generator, yields the available media types
- """
- for media_type in mg_globals.app_config['media_types']:
- yield media_type
-
-
-def get_media_managers():
- '''
- Generator, yields all enabled media managers
- '''
- for media_type in get_media_types():
- mm = import_component(media_type + ":MEDIA_MANAGER")
-
- if isinstance(mm, dict):
- mm = CompatMediaManager(mm)
-
- yield media_type, mm
-
-
def get_media_type_and_manager(filename):
'''
Try to find the media type based on the file name, extension
@@ -142,11 +88,10 @@ def get_media_type_and_manager(filename):
# Get the file extension
ext = os.path.splitext(filename)[1].lower()
- for media_type, manager in get_media_managers():
- # Omit the dot from the extension and match it against
- # the media manager
- if ext[1:] in manager.accepted_extensions:
- return media_type, manager
+ # Omit the dot from the extension and match it against
+ # the media manager
+ if hook_handle('get_media_type_and_manager', ext[1:]):
+ return hook_handle('get_media_type_and_manager', ext[1:])
else:
_log.info('File {0} has no file extension, let\'s hope the sniffers get it.'.format(
filename))
diff --git a/mediagoblin/media_types/ascii/__init__.py b/mediagoblin/media_types/ascii/__init__.py
index 0931e83a..4baf8dd3 100644
--- a/mediagoblin/media_types/ascii/__init__.py
+++ b/mediagoblin/media_types/ascii/__init__.py
@@ -17,15 +17,31 @@
from mediagoblin.media_types import MediaManagerBase
from mediagoblin.media_types.ascii.processing import process_ascii, \
sniff_handler
+from mediagoblin.tools import pluginapi
+
+ACCEPTED_EXTENSIONS = ["txt", "asc", "nfo"]
+MEDIA_TYPE = 'mediagoblin.media_types.ascii'
+
+
+def setup_plugin():
+ config = pluginapi.get_config(MEDIA_TYPE)
class ASCIIMediaManager(MediaManagerBase):
human_readable = "ASCII"
processor = staticmethod(process_ascii)
- sniff_handler = staticmethod(sniff_handler)
display_template = "mediagoblin/media_displays/ascii.html"
default_thumb = "images/media_thumbs/ascii.jpg"
- accepted_extensions = ["txt", "asc", "nfo"]
-
-MEDIA_MANAGER = ASCIIMediaManager
+
+def get_media_type_and_manager(ext):
+ if ext in ACCEPTED_EXTENSIONS:
+ return MEDIA_TYPE, ASCIIMediaManager
+
+
+hooks = {
+ 'setup': setup_plugin,
+ 'get_media_type_and_manager': get_media_type_and_manager,
+ ('media_manager', MEDIA_TYPE): lambda: ASCIIMediaManager,
+ 'sniff_handler': sniff_handler,
+}
diff --git a/mediagoblin/media_types/ascii/processing.py b/mediagoblin/media_types/ascii/processing.py
index 2f6079be..aca784e8 100644
--- a/mediagoblin/media_types/ascii/processing.py
+++ b/mediagoblin/media_types/ascii/processing.py
@@ -28,17 +28,19 @@ from mediagoblin.media_types.ascii import asciitoimage
_log = logging.getLogger(__name__)
SUPPORTED_EXTENSIONS = ['txt', 'asc', 'nfo']
+MEDIA_TYPE = 'mediagoblin.media_types.ascii'
def sniff_handler(media_file, **kw):
+ _log.info('Sniffing {0}'.format(MEDIA_TYPE))
if kw.get('media') is not None:
name, ext = os.path.splitext(kw['media'].filename)
clean_ext = ext[1:].lower()
if clean_ext in SUPPORTED_EXTENSIONS:
- return True
+ return MEDIA_TYPE
- return False
+ return None
def process_ascii(proc_state):
diff --git a/mediagoblin/media_types/audio/__init__.py b/mediagoblin/media_types/audio/__init__.py
index 2eb7300e..c7ed8d2d 100644
--- a/mediagoblin/media_types/audio/__init__.py
+++ b/mediagoblin/media_types/audio/__init__.py
@@ -17,14 +17,32 @@
from mediagoblin.media_types import MediaManagerBase
from mediagoblin.media_types.audio.processing import process_audio, \
sniff_handler
+from mediagoblin.tools import pluginapi
+
+# Why isn't .ogg in this list? It's still detected, but via sniffing,
+# .ogg files could be either video or audio... sniffing determines which.
+
+ACCEPTED_EXTENSIONS = ["mp3", "flac", "wav", "m4a"]
+MEDIA_TYPE = 'mediagoblin.media_types.audio'
+
+
+def setup_plugin():
+ config = pluginapi.get_config(MEDIA_TYPE)
class AudioMediaManager(MediaManagerBase):
human_readable = "Audio"
processor = staticmethod(process_audio)
- sniff_handler = staticmethod(sniff_handler)
display_template = "mediagoblin/media_displays/audio.html"
- accepted_extensions = ["mp3", "flac", "wav", "m4a"]
-MEDIA_MANAGER = AudioMediaManager
+def get_media_type_and_manager(ext):
+ if ext in ACCEPTED_EXTENSIONS:
+ return MEDIA_TYPE, AudioMediaManager
+
+hooks = {
+ 'setup': setup_plugin,
+ 'get_media_type_and_manager': get_media_type_and_manager,
+ 'sniff_handler': sniff_handler,
+ ('media_manager', MEDIA_TYPE): lambda: AudioMediaManager,
+}
diff --git a/mediagoblin/media_types/audio/processing.py b/mediagoblin/media_types/audio/processing.py
index 101b83e5..22383bc1 100644
--- a/mediagoblin/media_types/audio/processing.py
+++ b/mediagoblin/media_types/audio/processing.py
@@ -27,19 +27,22 @@ from mediagoblin.media_types.audio.transcoders import (AudioTranscoder,
_log = logging.getLogger(__name__)
+MEDIA_TYPE = 'mediagoblin.media_types.audio'
+
def sniff_handler(media_file, **kw):
+ _log.info('Sniffing {0}'.format(MEDIA_TYPE))
try:
transcoder = AudioTranscoder()
data = transcoder.discover(media_file.name)
except BadMediaFail:
_log.debug('Audio discovery raised BadMediaFail')
- return False
+ return None
if data.is_audio == True and data.is_video == False:
- return True
+ return MEDIA_TYPE
- return False
+ return None
def process_audio(proc_state):
diff --git a/mediagoblin/media_types/image/__init__.py b/mediagoblin/media_types/image/__init__.py
index 5130ef48..1bb9c6f3 100644
--- a/mediagoblin/media_types/image/__init__.py
+++ b/mediagoblin/media_types/image/__init__.py
@@ -13,23 +13,30 @@
#
# 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 datetime
from mediagoblin.media_types import MediaManagerBase
from mediagoblin.media_types.image.processing import process_image, \
sniff_handler
+from mediagoblin.tools import pluginapi
+
+
+ACCEPTED_EXTENSIONS = ["jpg", "jpeg", "png", "gif", "tiff"]
+MEDIA_TYPE = 'mediagoblin.media_types.image'
+
+
+def setup_plugin():
+ config = pluginapi.get_config('mediagoblin.media_types.image')
class ImageMediaManager(MediaManagerBase):
human_readable = "Image"
processor = staticmethod(process_image)
- sniff_handler = staticmethod(sniff_handler)
display_template = "mediagoblin/media_displays/image.html"
default_thumb = "images/media_thumbs/image.png"
- accepted_extensions = ["jpg", "jpeg", "png", "gif", "tiff"]
+
media_fetch_order = [u'medium', u'original', u'thumb']
-
+
def get_original_date(self):
"""
Get the original date and time from the EXIF information. Returns
@@ -52,4 +59,14 @@ class ImageMediaManager(MediaManagerBase):
return None
-MEDIA_MANAGER = ImageMediaManager
+def get_media_type_and_manager(ext):
+ if ext in ACCEPTED_EXTENSIONS:
+ return MEDIA_TYPE, ImageMediaManager
+
+
+hooks = {
+ 'setup': setup_plugin,
+ 'get_media_type_and_manager': get_media_type_and_manager,
+ 'sniff_handler': sniff_handler,
+ ('media_manager', MEDIA_TYPE): lambda: ImageMediaManager,
+}
diff --git a/mediagoblin/media_types/image/processing.py b/mediagoblin/media_types/image/processing.py
index bc0ce3f8..baf2ac7e 100644
--- a/mediagoblin/media_types/image/processing.py
+++ b/mediagoblin/media_types/image/processing.py
@@ -35,6 +35,8 @@ PIL_FILTERS = {
'BICUBIC': Image.BICUBIC,
'ANTIALIAS': Image.ANTIALIAS}
+MEDIA_TYPE = 'mediagoblin.media_types.image'
+
def resize_image(proc_state, resized, keyname, target_name, new_size,
exif_tags, workdir):
@@ -95,17 +97,18 @@ def resize_tool(proc_state, force, keyname, target_name,
exif_tags, conversions_subdir)
-SUPPORTED_FILETYPES = ['png', 'gif', 'jpg', 'jpeg']
+SUPPORTED_FILETYPES = ['png', 'gif', 'jpg', 'jpeg', 'tiff']
def sniff_handler(media_file, **kw):
+ _log.info('Sniffing {0}'.format(MEDIA_TYPE))
if kw.get('media') is not None: # That's a double negative!
name, ext = os.path.splitext(kw['media'].filename)
clean_ext = ext[1:].lower() # Strip the . from ext and make lowercase
if clean_ext in SUPPORTED_FILETYPES:
_log.info('Found file extension in supported filetypes')
- return True
+ return MEDIA_TYPE
else:
_log.debug('Media present, extension not found in {0}'.format(
SUPPORTED_FILETYPES))
@@ -113,7 +116,7 @@ def sniff_handler(media_file, **kw):
_log.warning('Need additional information (keyword argument \'media\')'
' to be able to handle sniffing')
- return False
+ return None
def process_image(proc_state):
diff --git a/mediagoblin/media_types/pdf/__init__.py b/mediagoblin/media_types/pdf/__init__.py
index f0ba7867..67509ddc 100644
--- a/mediagoblin/media_types/pdf/__init__.py
+++ b/mediagoblin/media_types/pdf/__init__.py
@@ -17,15 +17,31 @@
from mediagoblin.media_types import MediaManagerBase
from mediagoblin.media_types.pdf.processing import process_pdf, \
sniff_handler
+from mediagoblin.tools import pluginapi
+
+ACCEPTED_EXTENSIONS = ['pdf']
+MEDIA_TYPE = 'mediagoblin.media_types.pdf'
+
+
+def setup_plugin():
+ config = pluginapi.get_config(MEDIA_TYPE)
class PDFMediaManager(MediaManagerBase):
human_readable = "PDF"
processor = staticmethod(process_pdf)
- sniff_handler = staticmethod(sniff_handler)
display_template = "mediagoblin/media_displays/pdf.html"
default_thumb = "images/media_thumbs/pdf.jpg"
- accepted_extensions = ["pdf"]
-MEDIA_MANAGER = PDFMediaManager
+def get_media_type_and_manager(ext):
+ if ext in ACCEPTED_EXTENSIONS:
+ return MEDIA_TYPE, PDFMediaManager
+
+
+hooks = {
+ 'setup': setup_plugin,
+ 'get_media_type_and_manager': get_media_type_and_manager,
+ 'sniff_handler': sniff_handler,
+ ('media_manager', MEDIA_TYPE): lambda: PDFMediaManager,
+}
diff --git a/mediagoblin/media_types/pdf/processing.py b/mediagoblin/media_types/pdf/processing.py
index 49742fd7..f35b4376 100644
--- a/mediagoblin/media_types/pdf/processing.py
+++ b/mediagoblin/media_types/pdf/processing.py
@@ -25,6 +25,8 @@ from mediagoblin.tools.translate import fake_ugettext_passthrough as _
_log = logging.getLogger(__name__)
+MEDIA_TYPE = 'mediagoblin.media_types.pdf'
+
# TODO - cache (memoize) util
# This is a list created via uniconv --show and hand removing some types that
@@ -163,16 +165,17 @@ def check_prerequisites():
return True
def sniff_handler(media_file, **kw):
+ _log.info('Sniffing {0}'.format(MEDIA_TYPE))
if not check_prerequisites():
- return False
+ return None
if kw.get('media') is not None:
name, ext = os.path.splitext(kw['media'].filename)
clean_ext = ext[1:].lower()
if clean_ext in supported_extensions():
- return True
+ return MEDIA_TYPE
- return False
+ return None
def create_pdf_thumb(original, thumb_filename, width, height):
# Note: pdftocairo adds '.png', remove it
@@ -250,8 +253,8 @@ def process_pdf(proc_state):
else:
pdf_filename = queued_filename.rsplit('.', 1)[0] + '.pdf'
unoconv = where('unoconv')
- call(executable=unoconv,
- args=[unoconv, '-v', '-f', 'pdf', queued_filename])
+ Popen(executable=unoconv,
+ args=[unoconv, '-v', '-f', 'pdf', queued_filename]).wait()
if not os.path.exists(pdf_filename):
_log.debug('unoconv failed to convert file to pdf')
raise BadMediaFail()
diff --git a/mediagoblin/media_types/stl/__init__.py b/mediagoblin/media_types/stl/__init__.py
index 6ae8a8b9..1d2a8478 100644
--- a/mediagoblin/media_types/stl/__init__.py
+++ b/mediagoblin/media_types/stl/__init__.py
@@ -17,15 +17,30 @@
from mediagoblin.media_types import MediaManagerBase
from mediagoblin.media_types.stl.processing import process_stl, \
sniff_handler
+from mediagoblin.tools import pluginapi
+
+MEDIA_TYPE = 'mediagoblin.media_types.stl'
+ACCEPTED_EXTENSIONS = ["obj", "stl"]
+
+
+def setup_plugin():
+ config = pluginapi.get_config(MEDIA_TYPE)
class STLMediaManager(MediaManagerBase):
human_readable = "stereo lithographics"
processor = staticmethod(process_stl)
- sniff_handler = staticmethod(sniff_handler)
display_template = "mediagoblin/media_displays/stl.html"
default_thumb = "images/media_thumbs/video.jpg"
- accepted_extensions = ["obj", "stl"]
-MEDIA_MANAGER = STLMediaManager
+def get_media_type_and_manager(ext):
+ if ext in ACCEPTED_EXTENSIONS:
+ return MEDIA_TYPE, STLMediaManager
+
+hooks = {
+ 'setup': setup_plugin,
+ 'get_media_type_and_manager': get_media_type_and_manager,
+ 'sniff_handler': sniff_handler,
+ ('media_manager', MEDIA_TYPE): lambda: STLMediaManager,
+}
diff --git a/mediagoblin/media_types/stl/processing.py b/mediagoblin/media_types/stl/processing.py
index ce7a5d37..53751416 100644
--- a/mediagoblin/media_types/stl/processing.py
+++ b/mediagoblin/media_types/stl/processing.py
@@ -29,6 +29,7 @@ from mediagoblin.media_types.stl import model_loader
_log = logging.getLogger(__name__)
SUPPORTED_FILETYPES = ['stl', 'obj']
+MEDIA_TYPE = 'mediagoblin.media_types.stl'
BLEND_FILE = pkg_resources.resource_filename(
'mediagoblin.media_types.stl',
@@ -43,13 +44,14 @@ BLEND_SCRIPT = pkg_resources.resource_filename(
def sniff_handler(media_file, **kw):
+ _log.info('Sniffing {0}'.format(MEDIA_TYPE))
if kw.get('media') is not None:
name, ext = os.path.splitext(kw['media'].filename)
clean_ext = ext[1:].lower()
if clean_ext in SUPPORTED_FILETYPES:
_log.info('Found file extension in supported filetypes')
- return True
+ return MEDIA_TYPE
else:
_log.debug('Media present, extension not found in {0}'.format(
SUPPORTED_FILETYPES))
@@ -57,7 +59,7 @@ def sniff_handler(media_file, **kw):
_log.warning('Need additional information (keyword argument \'media\')'
' to be able to handle sniffing')
- return False
+ return None
def blender_render(config):
diff --git a/mediagoblin/media_types/tools.py b/mediagoblin/media_types/tools.py
new file mode 100644
index 00000000..fe7b3772
--- /dev/null
+++ b/mediagoblin/media_types/tools.py
@@ -0,0 +1,27 @@
+# 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 logging
+
+from mediagoblin import mg_globals
+
+_log = logging.getLogger(__name__)
+
+
+def media_type_warning():
+ if mg_globals.app_config.get('media_types'):
+ _log.warning('Media_types have been converted to plugins. Old'
+ ' media_types will no longer work. Please convert them'
+ ' to plugins to continue using them.')
diff --git a/mediagoblin/media_types/video/__init__.py b/mediagoblin/media_types/video/__init__.py
index 569cf11a..e8a4308b 100644
--- a/mediagoblin/media_types/video/__init__.py
+++ b/mediagoblin/media_types/video/__init__.py
@@ -17,20 +17,35 @@
from mediagoblin.media_types import MediaManagerBase
from mediagoblin.media_types.video.processing import process_video, \
sniff_handler
+from mediagoblin.tools import pluginapi
+
+MEDIA_TYPE = 'mediagoblin.media_types.video'
+ACCEPTED_EXTENSIONS = [
+ "mp4", "mov", "webm", "avi", "3gp", "3gpp", "mkv", "ogv", "m4v"]
+
+
+def setup_plugin():
+ config = pluginapi.get_config(MEDIA_TYPE)
class VideoMediaManager(MediaManagerBase):
human_readable = "Video"
processor = staticmethod(process_video)
- sniff_handler = staticmethod(sniff_handler)
display_template = "mediagoblin/media_displays/video.html"
default_thumb = "images/media_thumbs/video.jpg"
- accepted_extensions = [
- "mp4", "mov", "webm", "avi", "3gp", "3gpp", "mkv", "ogv", "m4v"]
-
+
# Used by the media_entry.get_display_media method
media_fetch_order = [u'webm_640', u'original']
default_webm_type = 'video/webm; codecs="vp8, vorbis"'
-MEDIA_MANAGER = VideoMediaManager
+def get_media_type_and_manager(ext):
+ if ext in ACCEPTED_EXTENSIONS:
+ return MEDIA_TYPE, VideoMediaManager
+
+hooks = {
+ 'setup': setup_plugin,
+ 'get_media_type_and_manager': get_media_type_and_manager,
+ 'sniff_handler': sniff_handler,
+ ('media_manager', MEDIA_TYPE): lambda: VideoMediaManager,
+}
diff --git a/mediagoblin/media_types/video/processing.py b/mediagoblin/media_types/video/processing.py
index ff2c94a0..857c1647 100644
--- a/mediagoblin/media_types/video/processing.py
+++ b/mediagoblin/media_types/video/processing.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 tempfile import NamedTemporaryFile
+import os.path
import logging
import datetime
@@ -29,6 +29,8 @@ from .util import skip_transcode
_log = logging.getLogger(__name__)
_log.setLevel(logging.DEBUG)
+MEDIA_TYPE = 'mediagoblin.media_types.video'
+
class VideoTranscodingFail(BaseProcessingFail):
'''
@@ -41,17 +43,18 @@ def sniff_handler(media_file, **kw):
transcoder = transcoders.VideoTranscoder()
data = transcoder.discover(media_file.name)
+ _log.info('Sniffing {0}'.format(MEDIA_TYPE))
_log.debug('Discovered: {0}'.format(data))
if not data:
_log.error('Could not discover {0}'.format(
kw.get('media')))
- return False
+ return None
if data['is_video'] == True:
- return True
+ return MEDIA_TYPE
- return False
+ return None
def process_video(proc_state):
@@ -70,79 +73,77 @@ def process_video(proc_state):
queued_filename = proc_state.get_queued_filename()
name_builder = FilenameBuilder(queued_filename)
- medium_filepath = create_pub_filepath(
- entry, name_builder.fill('{basename}-640p.webm'))
+ medium_basename = name_builder.fill('{basename}-640p.webm')
+ medium_filepath = create_pub_filepath(entry, medium_basename)
- thumbnail_filepath = create_pub_filepath(
- entry, name_builder.fill('{basename}.thumbnail.jpg'))
+ thumbnail_basename = name_builder.fill('{basename}.thumbnail.jpg')
+ thumbnail_filepath = create_pub_filepath(entry, thumbnail_basename)
# Create a temporary file for the video destination (cleaned up with workbench)
- tmp_dst = NamedTemporaryFile(dir=workbench.dir, delete=False)
- with tmp_dst:
- # Transcode queued file to a VP8/vorbis file that fits in a 640x640 square
- progress_callback = ProgressCallback(entry)
+ tmp_dst = os.path.join(workbench.dir, medium_basename)
+ # Transcode queued file to a VP8/vorbis file that fits in a 640x640 square
+ progress_callback = ProgressCallback(entry)
- dimensions = (
- mgg.global_config['media:medium']['max_width'],
- mgg.global_config['media:medium']['max_height'])
+ dimensions = (
+ mgg.global_config['media:medium']['max_width'],
+ mgg.global_config['media:medium']['max_height'])
- # Extract metadata and keep a record of it
- metadata = transcoders.VideoTranscoder().discover(queued_filename)
- store_metadata(entry, metadata)
+ # Extract metadata and keep a record of it
+ metadata = transcoders.VideoTranscoder().discover(queued_filename)
+ store_metadata(entry, metadata)
- # Figure out whether or not we need to transcode this video or
- # if we can skip it
- if skip_transcode(metadata):
- _log.debug('Skipping transcoding')
+ # Figure out whether or not we need to transcode this video or
+ # if we can skip it
+ if skip_transcode(metadata):
+ _log.debug('Skipping transcoding')
- dst_dimensions = metadata['videowidth'], metadata['videoheight']
+ dst_dimensions = metadata['videowidth'], metadata['videoheight']
# Push original file to public storage
- _log.debug('Saving original...')
- proc_state.copy_original(queued_filepath[-1])
+ _log.debug('Saving original...')
+ proc_state.copy_original(queued_filepath[-1])
- did_transcode = False
- else:
- transcoder = transcoders.VideoTranscoder()
+ did_transcode = False
+ else:
+ transcoder = transcoders.VideoTranscoder()
- transcoder.transcode(queued_filename, tmp_dst.name,
- vp8_quality=video_config['vp8_quality'],
- vp8_threads=video_config['vp8_threads'],
- vorbis_quality=video_config['vorbis_quality'],
- progress_callback=progress_callback,
- dimensions=dimensions)
+ transcoder.transcode(queued_filename, tmp_dst,
+ vp8_quality=video_config['vp8_quality'],
+ vp8_threads=video_config['vp8_threads'],
+ vorbis_quality=video_config['vorbis_quality'],
+ progress_callback=progress_callback,
+ dimensions=dimensions)
- dst_dimensions = transcoder.dst_data.videowidth,\
- transcoder.dst_data.videoheight
+ dst_dimensions = transcoder.dst_data.videowidth,\
+ transcoder.dst_data.videoheight
- # Push transcoded video to public storage
- _log.debug('Saving medium...')
- mgg.public_store.copy_local_to_storage(tmp_dst.name, medium_filepath)
- _log.debug('Saved medium')
+ # Push transcoded video to public storage
+ _log.debug('Saving medium...')
+ mgg.public_store.copy_local_to_storage(tmp_dst, medium_filepath)
+ _log.debug('Saved medium')
- entry.media_files['webm_640'] = medium_filepath
+ entry.media_files['webm_640'] = medium_filepath
- did_transcode = True
+ did_transcode = True
- # Save the width and height of the transcoded video
- entry.media_data_init(
- width=dst_dimensions[0],
- height=dst_dimensions[1])
+ # Save the width and height of the transcoded video
+ entry.media_data_init(
+ width=dst_dimensions[0],
+ height=dst_dimensions[1])
# Temporary file for the video thumbnail (cleaned up with workbench)
- tmp_thumb = NamedTemporaryFile(dir=workbench.dir, suffix='.jpg', delete=False)
+ tmp_thumb = os.path.join(workbench.dir, thumbnail_basename)
- with tmp_thumb:
- # Create a thumbnail.jpg that fits in a 180x180 square
- transcoders.VideoThumbnailerMarkII(
- queued_filename,
- tmp_thumb.name,
- 180)
+ # Create a thumbnail.jpg that fits in a 180x180 square
+ transcoders.VideoThumbnailerMarkII(
+ queued_filename,
+ tmp_thumb,
+ 180)
- # Push the thumbnail to public storage
- _log.debug('Saving thumbnail...')
- mgg.public_store.copy_local_to_storage(tmp_thumb.name, thumbnail_filepath)
- entry.media_files['thumb'] = thumbnail_filepath
+ # Push the thumbnail to public storage
+ _log.debug('Saving thumbnail...')
+ mgg.public_store.copy_local_to_storage(tmp_thumb, thumbnail_filepath)
+ entry.media_files['thumb'] = thumbnail_filepath
# save the original... but only if we did a transcoding
# (if we skipped transcoding and just kept the original anyway as the main
@@ -186,7 +187,7 @@ def store_metadata(media_entry, metadata):
[(key, tags_metadata[key])
for key in [
"application-name", "artist", "audio-codec", "bitrate",
- "container-format", "copyright", "encoder",
+ "container-format", "copyright", "encoder",
"encoder-version", "license", "nominal-bitrate", "title",
"video-codec"]
if key in tags_metadata])
@@ -203,7 +204,7 @@ def store_metadata(media_entry, metadata):
dt.get_year(), dt.get_month(), dt.get_day(), dt.get_hour(),
dt.get_minute(), dt.get_second(),
dt.get_microsecond()).isoformat()
-
+
metadata['tags'] = tags
# Only save this field if there's something to save
diff --git a/mediagoblin/notifications/__init__.py b/mediagoblin/notifications/__init__.py
index 4b7fbb8c..ed9f8d78 100644
--- a/mediagoblin/notifications/__init__.py
+++ b/mediagoblin/notifications/__init__.py
@@ -18,7 +18,6 @@ import logging
from mediagoblin.db.models import Notification, \
CommentNotification, CommentSubscription
-from mediagoblin.notifications.task import email_notification_task
from mediagoblin.notifications.tools import generate_comment_message
_log = logging.getLogger(__name__)
@@ -50,6 +49,7 @@ def trigger_notification(comment, media_entry, request):
media_entry,
request)
+ from mediagoblin.notifications.task import email_notification_task
email_notification_task.apply_async([cn.id, message])
diff --git a/mediagoblin/oauth/__init__.py b/mediagoblin/oauth/__init__.py
new file mode 100644
index 00000000..719b56e7
--- /dev/null
+++ b/mediagoblin/oauth/__init__.py
@@ -0,0 +1,16 @@
+# 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/oauth/exceptions.py b/mediagoblin/oauth/exceptions.py
new file mode 100644
index 00000000..5eccba34
--- /dev/null
+++ b/mediagoblin/oauth/exceptions.py
@@ -0,0 +1,18 @@
+# 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/>.
+
+class ValidationException(Exception):
+ pass
diff --git a/mediagoblin/oauth/forms.py b/mediagoblin/oauth/forms.py
new file mode 100644
index 00000000..94c7cb52
--- /dev/null
+++ b/mediagoblin/oauth/forms.py
@@ -0,0 +1,7 @@
+import wtforms
+
+class AuthorizeForm(wtforms.Form):
+ """ Form used to authorize the request token """
+
+ oauth_token = wtforms.HiddenField("oauth_token")
+ oauth_verifier = wtforms.HiddenField("oauth_verifier")
diff --git a/mediagoblin/oauth/oauth.py b/mediagoblin/oauth/oauth.py
new file mode 100644
index 00000000..8229c47d
--- /dev/null
+++ b/mediagoblin/oauth/oauth.py
@@ -0,0 +1,132 @@
+# 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 oauthlib.common import Request
+from oauthlib.oauth1 import RequestValidator
+
+from mediagoblin.db.models import NonceTimestamp, Client, RequestToken, AccessToken
+
+
+
+class GMGRequestValidator(RequestValidator):
+
+ enforce_ssl = False
+
+ def __init__(self, data=None, *args, **kwargs):
+ self.POST = data
+ super(GMGRequestValidator, self).__init__(*args, **kwargs)
+
+ def save_request_token(self, token, request):
+ """ Saves request token in db """
+ client_id = self.POST[u"oauth_consumer_key"]
+
+ request_token = RequestToken(
+ token=token["oauth_token"],
+ secret=token["oauth_token_secret"],
+ )
+ request_token.client = client_id
+ if u"oauth_callback" in self.POST:
+ request_token.callback = self.POST[u"oauth_callback"]
+ request_token.save()
+
+ def save_verifier(self, token, verifier, request):
+ """ Saves the oauth request verifier """
+ request_token = RequestToken.query.filter_by(token=token).first()
+ request_token.verifier = verifier["oauth_verifier"]
+ request_token.save()
+
+ def save_access_token(self, token, request):
+ """ Saves access token in db """
+ access_token = AccessToken(
+ token=token["oauth_token"],
+ secret=token["oauth_token_secret"],
+ )
+ access_token.request_token = request.oauth_token
+ request_token = RequestToken.query.filter_by(token=request.oauth_token).first()
+ access_token.user = request_token.user
+ access_token.save()
+
+ def get_realms(*args, **kwargs):
+ """ Currently a stub - called when making AccessTokens """
+ return list()
+
+ def validate_timestamp_and_nonce(self, client_key, timestamp,
+ nonce, request, request_token=None,
+ access_token=None):
+ nc = NonceTimestamp.query.filter_by(timestamp=timestamp, nonce=nonce)
+ nc = nc.first()
+ if nc is None:
+ return True
+
+ return False
+
+ def validate_client_key(self, client_key, request):
+ """ Verifies client exists with id of client_key """
+ client = Client.query.filter_by(id=client_key).first()
+ if client is None:
+ return False
+
+ return True
+
+ def validate_access_token(self, client_key, token, request):
+ """ Verifies token exists for client with id of client_key """
+ client = Client.query.filter_by(id=client_key).first()
+ token = AccessToken.query.filter_by(token=token)
+ token = token.first()
+
+ if token is None:
+ return False
+
+ request_token = RequestToken.query.filter_by(token=token.request_token)
+ request_token = request_token.first()
+
+ if client.id != request_token.client:
+ return False
+
+ return True
+
+ def validate_realms(self, *args, **kwargs):
+ """ Would validate reals however not using these yet. """
+ return True # implement when realms are implemented
+
+
+ def get_client_secret(self, client_key, request):
+ """ Retrives a client secret with from a client with an id of client_key """
+ client = Client.query.filter_by(id=client_key).first()
+ return client.secret
+
+ def get_access_token_secret(self, client_key, token, request):
+ access_token = AccessToken.query.filter_by(token=token).first()
+ return access_token.secret
+
+class GMGRequest(Request):
+ """
+ Fills in data to produce a oauth.common.Request object from a
+ werkzeug Request object
+ """
+
+ def __init__(self, request, *args, **kwargs):
+ """
+ :param request: werkzeug request object
+
+ any extra params are passed to oauthlib.common.Request object
+ """
+ kwargs["uri"] = kwargs.get("uri", request.url)
+ kwargs["http_method"] = kwargs.get("http_method", request.method)
+ kwargs["body"] = kwargs.get("body", request.get_data())
+ kwargs["headers"] = kwargs.get("headers", dict(request.headers))
+
+ super(GMGRequest, self).__init__(*args, **kwargs)
diff --git a/mediagoblin/oauth/routing.py b/mediagoblin/oauth/routing.py
new file mode 100644
index 00000000..e45077bb
--- /dev/null
+++ b/mediagoblin/oauth/routing.py
@@ -0,0 +1,43 @@
+# 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.routing import add_route
+
+# client registration & oauth
+add_route(
+ "mediagoblin.oauth",
+ "/api/client/register",
+ "mediagoblin.oauth.views:client_register"
+ )
+
+add_route(
+ "mediagoblin.oauth",
+ "/oauth/request_token",
+ "mediagoblin.oauth.views:request_token"
+ )
+
+add_route(
+ "mediagoblin.oauth",
+ "/oauth/authorize",
+ "mediagoblin.oauth.views:authorize",
+ )
+
+add_route(
+ "mediagoblin.oauth",
+ "/oauth/access_token",
+ "mediagoblin.oauth.views:access_token"
+ )
+
diff --git a/mediagoblin/oauth/tools/__init__.py b/mediagoblin/oauth/tools/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/mediagoblin/oauth/tools/__init__.py
diff --git a/mediagoblin/oauth/tools/forms.py b/mediagoblin/oauth/tools/forms.py
new file mode 100644
index 00000000..e3eb3298
--- /dev/null
+++ b/mediagoblin/oauth/tools/forms.py
@@ -0,0 +1,25 @@
+# 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/>.
+
+class WTFormData(dict):
+ """
+ Provides a WTForm usable dictionary
+ """
+ def getlist(self, key):
+ v = self[key]
+ if not isinstance(v, (list, tuple)):
+ v = [v]
+ return v
diff --git a/mediagoblin/oauth/tools/request.py b/mediagoblin/oauth/tools/request.py
new file mode 100644
index 00000000..5ce2da77
--- /dev/null
+++ b/mediagoblin/oauth/tools/request.py
@@ -0,0 +1,35 @@
+# 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 decode_authorization_header(header):
+ """ Decodes a HTTP Authorization Header to python dictionary """
+ authorization = header.get("Authorization", "").lstrip(" ").lstrip("OAuth")
+ tokens = {}
+
+ for param in authorization.split(","):
+ try:
+ key, value = param.split("=")
+ except ValueError:
+ continue
+
+ key = key.lstrip(" ")
+ value = value.lstrip(" ").lstrip('"')
+ value = value.rstrip(" ").rstrip('"')
+
+ tokens[key] = value
+
+ return tokens
+
diff --git a/mediagoblin/oauth/views.py b/mediagoblin/oauth/views.py
new file mode 100644
index 00000000..116eb023
--- /dev/null
+++ b/mediagoblin/oauth/views.py
@@ -0,0 +1,339 @@
+# 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 datetime
+
+from oauthlib.oauth1 import (RequestTokenEndpoint, AuthorizationEndpoint,
+ AccessTokenEndpoint)
+
+from mediagoblin.decorators import require_active_login
+from mediagoblin.tools.translate import pass_to_ugettext
+from mediagoblin.meddleware.csrf import csrf_exempt
+from mediagoblin.tools.request import decode_request
+from mediagoblin.tools.response import (render_to_response, redirect,
+ json_response, render_400,
+ form_response)
+from mediagoblin.tools.crypto import random_string
+from mediagoblin.tools.validator import validate_email, validate_url
+from mediagoblin.oauth.forms import AuthorizeForm
+from mediagoblin.oauth.oauth import GMGRequestValidator, GMGRequest
+from mediagoblin.oauth.tools.request import decode_authorization_header
+from mediagoblin.oauth.tools.forms import WTFormData
+from mediagoblin.db.models import NonceTimestamp, Client, RequestToken
+
+# possible client types
+client_types = ["web", "native"] # currently what pump supports
+
+@csrf_exempt
+def client_register(request):
+ """ Endpoint for client registration """
+ try:
+ data = decode_request(request)
+ except ValueError:
+ error = "Could not decode data."
+ return json_response({"error": error}, status=400)
+
+ if data is "":
+ error = "Unknown Content-Type"
+ return json_response({"error": error}, status=400)
+
+ if "type" not in data:
+ error = "No registration type provided."
+ return json_response({"error": error}, status=400)
+ if data.get("application_type", None) not in client_types:
+ error = "Unknown application_type."
+ return json_response({"error": error}, status=400)
+
+ client_type = data["type"]
+
+ if client_type == "client_update":
+ # updating a client
+ if "client_id" not in data:
+ error = "client_id is requried to update."
+ return json_response({"error": error}, status=400)
+ elif "client_secret" not in data:
+ error = "client_secret is required to update."
+ return json_response({"error": error}, status=400)
+
+ client = Client.query.filter_by(
+ id=data["client_id"],
+ secret=data["client_secret"]
+ ).first()
+
+ if client is None:
+ error = "Unauthorized."
+ return json_response({"error": error}, status=403)
+
+ client.application_name = data.get(
+ "application_name",
+ client.application_name
+ )
+
+ client.application_type = data.get(
+ "application_type",
+ client.application_type
+ )
+
+ app_name = ("application_type", client.application_name)
+ if app_name in client_types:
+ client.application_name = app_name
+
+ elif client_type == "client_associate":
+ # registering
+ if "client_id" in data:
+ error = "Only set client_id for update."
+ return json_response({"error": error}, status=400)
+ elif "access_token" in data:
+ error = "access_token not needed for registration."
+ return json_response({"error": error}, status=400)
+ elif "client_secret" in data:
+ error = "Only set client_secret for update."
+ return json_response({"error": error}, status=400)
+
+ # generate the client_id and client_secret
+ client_id = random_string(22) # seems to be what pump uses
+ client_secret = random_string(43) # again, seems to be what pump uses
+ expirey = 0 # for now, lets not have it expire
+ expirey_db = None if expirey == 0 else expirey
+ application_type = data["application_type"]
+
+ # save it
+ client = Client(
+ id=client_id,
+ secret=client_secret,
+ expirey=expirey_db,
+ application_type=application_type,
+ )
+
+ else:
+ error = "Invalid registration type"
+ return json_response({"error": error}, status=400)
+
+ logo_url = data.get("logo_url", client.logo_url)
+ if logo_url is not None and not validate_url(logo_url):
+ error = "Logo URL {0} is not a valid URL.".format(logo_url)
+ return json_response(
+ {"error": error},
+ status=400
+ )
+ else:
+ client.logo_url = logo_url
+
+ client.application_name = data.get("application_name", None)
+
+ contacts = data.get("contacts", None)
+ if contacts is not None:
+ if type(contacts) is not unicode:
+ error = "Contacts must be a string of space-seporated email addresses."
+ return json_response({"error": error}, status=400)
+
+ contacts = contacts.split()
+ for contact in contacts:
+ if not validate_email(contact):
+ # not a valid email
+ error = "Email {0} is not a valid email.".format(contact)
+ return json_response({"error": error}, status=400)
+
+
+ client.contacts = contacts
+
+ redirect_uris = data.get("redirect_uris", None)
+ if redirect_uris is not None:
+ if type(redirect_uris) is not unicode:
+ error = "redirect_uris must be space-seporated URLs."
+ return json_response({"error": error}, status=400)
+
+ redirect_uris = redirect_uris.split()
+
+ for uri in redirect_uris:
+ if not validate_url(uri):
+ # not a valid uri
+ error = "URI {0} is not a valid URI".format(uri)
+ return json_response({"error": error}, status=400)
+
+ client.redirect_uri = redirect_uris
+
+
+ client.save()
+
+ expirey = 0 if client.expirey is None else client.expirey
+
+ return json_response(
+ {
+ "client_id": client.id,
+ "client_secret": client.secret,
+ "expires_at": expirey,
+ })
+
+@csrf_exempt
+def request_token(request):
+ """ Returns request token """
+ try:
+ data = decode_request(request)
+ except ValueError:
+ error = "Could not decode data."
+ return json_response({"error": error}, status=400)
+
+ if data == "":
+ error = "Unknown Content-Type"
+ return json_response({"error": error}, status=400)
+
+ if not data and request.headers:
+ data = request.headers
+
+ data = dict(data) # mutableifying
+
+ authorization = decode_authorization_header(data)
+
+ if authorization == dict() or u"oauth_consumer_key" not in authorization:
+ error = "Missing required parameter."
+ return json_response({"error": error}, status=400)
+
+ # check the client_id
+ client_id = authorization[u"oauth_consumer_key"]
+ client = Client.query.filter_by(id=client_id).first()
+
+ if client == None:
+ # client_id is invalid
+ error = "Invalid client_id"
+ return json_response({"error": error}, status=400)
+
+ # make request token and return to client
+ request_validator = GMGRequestValidator(authorization)
+ rv = RequestTokenEndpoint(request_validator)
+ tokens = rv.create_request_token(request, authorization)
+
+ # store the nonce & timestamp before we return back
+ nonce = authorization[u"oauth_nonce"]
+ timestamp = authorization[u"oauth_timestamp"]
+ timestamp = datetime.datetime.fromtimestamp(float(timestamp))
+
+ nc = NonceTimestamp(nonce=nonce, timestamp=timestamp)
+ nc.save()
+
+ return form_response(tokens)
+
+@require_active_login
+def authorize(request):
+ """ Displays a page for user to authorize """
+ if request.method == "POST":
+ return authorize_finish(request)
+
+ _ = pass_to_ugettext
+ token = request.args.get("oauth_token", None)
+ if token is None:
+ # no token supplied, display a html 400 this time
+ err_msg = _("Must provide an oauth_token.")
+ return render_400(request, err_msg=err_msg)
+
+ oauth_request = RequestToken.query.filter_by(token=token).first()
+ if oauth_request is None:
+ err_msg = _("No request token found.")
+ return render_400(request, err_msg)
+
+ if oauth_request.used:
+ return authorize_finish(request)
+
+ if oauth_request.verifier is None:
+ orequest = GMGRequest(request)
+ request_validator = GMGRequestValidator()
+ auth_endpoint = AuthorizationEndpoint(request_validator)
+ verifier = auth_endpoint.create_verifier(orequest, {})
+ oauth_request.verifier = verifier["oauth_verifier"]
+
+ oauth_request.user = request.user.id
+ oauth_request.save()
+
+ # find client & build context
+ client = Client.query.filter_by(id=oauth_request.client).first()
+
+ authorize_form = AuthorizeForm(WTFormData({
+ "oauth_token": oauth_request.token,
+ "oauth_verifier": oauth_request.verifier
+ }))
+
+ context = {
+ "user": request.user,
+ "oauth_request": oauth_request,
+ "client": client,
+ "authorize_form": authorize_form,
+ }
+
+
+ # AuthorizationEndpoint
+ return render_to_response(
+ request,
+ "mediagoblin/api/authorize.html",
+ context
+ )
+
+
+def authorize_finish(request):
+ """ Finishes the authorize """
+ _ = pass_to_ugettext
+ token = request.form["oauth_token"]
+ verifier = request.form["oauth_verifier"]
+ oauth_request = RequestToken.query.filter_by(token=token, verifier=verifier)
+ oauth_request = oauth_request.first()
+
+ if oauth_request is None:
+ # invalid token or verifier
+ err_msg = _("No request token found.")
+ return render_400(request, err_msg)
+
+ oauth_request.used = True
+ oauth_request.updated = datetime.datetime.now()
+ oauth_request.save()
+
+ if oauth_request.callback == "oob":
+ # out of bounds
+ context = {"oauth_request": oauth_request}
+ return render_to_response(
+ request,
+ "mediagoblin/api/oob.html",
+ context
+ )
+
+ # okay we need to redirect them then!
+ querystring = "?oauth_token={0}&oauth_verifier={1}".format(
+ oauth_request.token,
+ oauth_request.verifier
+ )
+
+ return redirect(
+ request,
+ querystring=querystring,
+ location=oauth_request.callback
+ )
+
+@csrf_exempt
+def access_token(request):
+ """ Provides an access token based on a valid verifier and request token """
+ data = request.headers
+
+ parsed_tokens = decode_authorization_header(data)
+
+ if parsed_tokens == dict() or "oauth_token" not in parsed_tokens:
+ error = "Missing required parameter."
+ return json_response({"error": error}, status=400)
+
+
+ request.oauth_token = parsed_tokens["oauth_token"]
+ request_validator = GMGRequestValidator(data)
+ av = AccessTokenEndpoint(request_validator)
+ tokens = av.create_access_token(request, {})
+ return form_response(tokens)
+
diff --git a/mediagoblin/plugins/api/tools.py b/mediagoblin/plugins/api/tools.py
index 92411f4b..d1b3ebb1 100644
--- a/mediagoblin/plugins/api/tools.py
+++ b/mediagoblin/plugins/api/tools.py
@@ -51,30 +51,6 @@ class Auth(object):
def __call__(self, request, *args, **kw):
raise NotImplemented()
-
-def json_response(serializable, _disable_cors=False, *args, **kw):
- '''
- Serializes a json objects and returns a werkzeug Response object with the
- serialized value as the response body and Content-Type: application/json.
-
- :param serializable: A json-serializable object
-
- Any extra arguments and keyword arguments are passed to the
- Response.__init__ method.
- '''
- response = Response(json.dumps(serializable), *args, content_type='application/json', **kw)
-
- if not _disable_cors:
- cors_headers = {
- 'Access-Control-Allow-Origin': '*',
- 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
- 'Access-Control-Allow-Headers': 'Content-Type, X-Requested-With'}
- for key, value in cors_headers.iteritems():
- response.headers.set(key, value)
-
- return response
-
-
def get_entry_serializable(entry, urlgen):
'''
Returns a serializable dict() of a MediaEntry instance.
diff --git a/mediagoblin/plugins/api/views.py b/mediagoblin/plugins/api/views.py
index 9159fe65..b7e74799 100644
--- a/mediagoblin/plugins/api/views.py
+++ b/mediagoblin/plugins/api/views.py
@@ -21,11 +21,11 @@ from os.path import splitext
from werkzeug.exceptions import BadRequest, Forbidden
from werkzeug.wrappers import Response
+from mediagoblin.tools.response import json_response
from mediagoblin.decorators import require_active_login
from mediagoblin.meddleware.csrf import csrf_exempt
from mediagoblin.media_types import sniff_media
-from mediagoblin.plugins.api.tools import api_auth, get_entry_serializable, \
- json_response
+from mediagoblin.plugins.api.tools import api_auth, get_entry_serializable
from mediagoblin.submit.lib import check_file_field, prepare_queue_task, \
run_process_media, new_upload_entry
diff --git a/mediagoblin/plugins/basic_auth/__init__.py b/mediagoblin/plugins/basic_auth/__init__.py
index c16d8855..33a554b0 100644
--- a/mediagoblin/plugins/basic_auth/__init__.py
+++ b/mediagoblin/plugins/basic_auth/__init__.py
@@ -59,7 +59,10 @@ def gen_password_hash(raw_pass, extra_salt=None):
def check_password(raw_pass, stored_hash, extra_salt=None):
- return auth_tools.bcrypt_check_password(raw_pass, stored_hash, extra_salt)
+ if stored_hash:
+ return auth_tools.bcrypt_check_password(raw_pass,
+ stored_hash, extra_salt)
+ return None
def auth():
diff --git a/mediagoblin/plugins/basic_auth/forms.py b/mediagoblin/plugins/basic_auth/forms.py
index 72d99dff..6cf01b38 100644
--- a/mediagoblin/plugins/basic_auth/forms.py
+++ b/mediagoblin/plugins/basic_auth/forms.py
@@ -41,3 +41,6 @@ class LoginForm(wtforms.Form):
normalize_user_or_email_field()])
password = wtforms.PasswordField(
_('Password'))
+ stay_logged_in = wtforms.BooleanField(
+ label='',
+ description=_('Stay logged in'))
diff --git a/mediagoblin/plugins/oauth/__init__.py b/mediagoblin/plugins/oauth/__init__.py
index 5762379d..82c1f380 100644
--- a/mediagoblin/plugins/oauth/__init__.py
+++ b/mediagoblin/plugins/oauth/__init__.py
@@ -35,22 +35,22 @@ def setup_plugin():
routes = [
('mediagoblin.plugins.oauth.authorize',
- '/oauth/authorize',
+ '/oauth-2/authorize',
'mediagoblin.plugins.oauth.views:authorize'),
('mediagoblin.plugins.oauth.authorize_client',
- '/oauth/client/authorize',
+ '/oauth-2/client/authorize',
'mediagoblin.plugins.oauth.views:authorize_client'),
('mediagoblin.plugins.oauth.access_token',
- '/oauth/access_token',
+ '/oauth-2/access_token',
'mediagoblin.plugins.oauth.views:access_token'),
('mediagoblin.plugins.oauth.list_connections',
- '/oauth/client/connections',
+ '/oauth-2/client/connections',
'mediagoblin.plugins.oauth.views:list_connections'),
('mediagoblin.plugins.oauth.register_client',
- '/oauth/client/register',
+ '/oauth-2/client/register',
'mediagoblin.plugins.oauth.views:register_client'),
('mediagoblin.plugins.oauth.list_clients',
- '/oauth/client/list',
+ '/oauth-2/client/list',
'mediagoblin.plugins.oauth.views:list_clients')]
pluginapi.register_routes(routes)
diff --git a/mediagoblin/plugins/oauth/tools.py b/mediagoblin/plugins/oauth/tools.py
index 27ff32b4..af0a3305 100644
--- a/mediagoblin/plugins/oauth/tools.py
+++ b/mediagoblin/plugins/oauth/tools.py
@@ -23,7 +23,7 @@ from datetime import datetime
from functools import wraps
-from mediagoblin.plugins.api.tools import json_response
+from mediagoblin.tools.response import json_response
def require_client_auth(controller):
diff --git a/mediagoblin/plugins/oauth/views.py b/mediagoblin/plugins/oauth/views.py
index d6fd314f..de637d6b 100644
--- a/mediagoblin/plugins/oauth/views.py
+++ b/mediagoblin/plugins/oauth/views.py
@@ -21,7 +21,7 @@ from urllib import urlencode
from werkzeug.exceptions import BadRequest
-from mediagoblin.tools.response import render_to_response, redirect
+from mediagoblin.tools.response import render_to_response, redirect, json_response
from mediagoblin.decorators import require_active_login
from mediagoblin.messages import add_message, SUCCESS
from mediagoblin.tools.translate import pass_to_ugettext as _
@@ -31,7 +31,6 @@ from mediagoblin.plugins.oauth.forms import ClientRegistrationForm, \
AuthorizationForm
from mediagoblin.plugins.oauth.tools import require_client_auth, \
create_token
-from mediagoblin.plugins.api.tools import json_response
_log = logging.getLogger(__name__)
diff --git a/mediagoblin/plugins/openid/__init__.py b/mediagoblin/plugins/openid/__init__.py
index ee88808c..ca17a7e8 100644
--- a/mediagoblin/plugins/openid/__init__.py
+++ b/mediagoblin/plugins/openid/__init__.py
@@ -120,4 +120,6 @@ hooks = {
'auth_no_pass_redirect': no_pass_redirect,
('mediagoblin.auth.register',
'mediagoblin/auth/register.html'): add_to_form_context,
+ ('mediagoblin.auth.login',
+ 'mediagoblin/auth/login.html'): add_to_form_context
}
diff --git a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login.html b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login.html
index 33df7200..8d74c2b9 100644
--- a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login.html
+++ b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login.html
@@ -44,6 +44,7 @@
{% trans %}Log in to create an account!{% endtrans %}
</p>
{% endif %}
+ {% template_hook('login_link') %}
{% if pass_auth is defined %}
<p>
<a href="{{ request.urlgen('mediagoblin.auth.login') }}?{{ request.query_string }}">
diff --git a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login_link.html b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login_link.html
index e5e77d01..fa4d5e85 100644
--- a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login_link.html
+++ b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login_link.html
@@ -17,9 +17,11 @@
#}
{% block openid_login_link %}
+ {% if openid_link is defined %}
<p>
<a href="{{ request.urlgen('mediagoblin.plugins.openid.login') }}?{{ request.query_string }}">
{%- trans %}Or login with OpenID!{% endtrans %}
</a>
</p>
+ {% endif %}
{% endblock %}
diff --git a/mediagoblin/plugins/openid/views.py b/mediagoblin/plugins/openid/views.py
index 9566e38e..b639a4cb 100644
--- a/mediagoblin/plugins/openid/views.py
+++ b/mediagoblin/plugins/openid/views.py
@@ -342,7 +342,7 @@ def delete_openid(request):
form.openid.errors.append(
_('That OpenID is not registered to this account.'))
- if not form.errors and not request.session['messages']:
+ if not form.errors and not request.session.get('messages'):
# Okay to continue with deleting openid
return_to = request.urlgen(
'mediagoblin.plugins.openid.finish_delete')
diff --git a/mediagoblin/plugins/persona/__init__.py b/mediagoblin/plugins/persona/__init__.py
new file mode 100644
index 00000000..d74ba0d7
--- /dev/null
+++ b/mediagoblin/plugins/persona/__init__.py
@@ -0,0 +1,116 @@
+# 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 pkg_resources import resource_filename
+import os
+
+from sqlalchemy import or_
+
+from mediagoblin.auth.tools import create_basic_user
+from mediagoblin.db.models import User
+from mediagoblin.plugins.persona.models import PersonaUserEmails
+from mediagoblin.tools import pluginapi
+from mediagoblin.tools.staticdirect import PluginStatic
+from mediagoblin.tools.translate import pass_to_ugettext as _
+
+PLUGIN_DIR = os.path.dirname(__file__)
+
+
+def setup_plugin():
+ config = pluginapi.get_config('mediagoblin.plugins.persona')
+
+ routes = [
+ ('mediagoblin.plugins.persona.login',
+ '/auth/persona/login/',
+ 'mediagoblin.plugins.persona.views:login'),
+ ('mediagoblin.plugins.persona.register',
+ '/auth/persona/register/',
+ 'mediagoblin.plugins.persona.views:register'),
+ ('mediagoblin.plugins.persona.edit',
+ '/edit/persona/',
+ 'mediagoblin.plugins.persona.views:edit'),
+ ('mediagoblin.plugins.persona.add',
+ '/edit/persona/add/',
+ 'mediagoblin.plugins.persona.views:add')]
+
+ pluginapi.register_routes(routes)
+ pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates'))
+ pluginapi.register_template_hooks(
+ {'persona_head': 'mediagoblin/plugins/persona/persona_js_head.html',
+ 'persona_form': 'mediagoblin/plugins/persona/persona.html',
+ 'edit_link': 'mediagoblin/plugins/persona/edit_link.html',
+ 'login_link': 'mediagoblin/plugins/persona/login_link.html',
+ 'register_link': 'mediagoblin/plugins/persona/register_link.html'})
+
+
+def create_user(register_form):
+ if 'persona_email' in register_form:
+ username = register_form.username.data
+ user = User.query.filter(
+ or_(
+ User.username == username,
+ User.email == username,
+ )).first()
+
+ if not user:
+ user = create_basic_user(register_form)
+
+ new_entry = PersonaUserEmails()
+ new_entry.persona_email = register_form.persona_email.data
+ new_entry.user_id = user.id
+ new_entry.save()
+
+ return user
+
+
+def extra_validation(register_form):
+ persona_email = register_form.persona_email.data if 'persona_email' in \
+ register_form else None
+ if persona_email:
+ persona_email_exists = PersonaUserEmails.query.filter_by(
+ persona_email=persona_email
+ ).count()
+
+ extra_validation_passes = True
+
+ if persona_email_exists:
+ register_form.persona_email.errors.append(
+ _('Sorry, an account is already registered to that Persona'
+ ' email.'))
+ extra_validation_passes = False
+
+ return extra_validation_passes
+
+
+def Auth():
+ return True
+
+
+def add_to_global_context(context):
+ if len(pluginapi.hook_runall('authentication')) == 1:
+ context['persona_auth'] = True
+ context['persona'] = True
+ return context
+
+hooks = {
+ 'setup': setup_plugin,
+ 'authentication': Auth,
+ 'auth_extra_validation': extra_validation,
+ 'auth_create_user': create_user,
+ 'template_global_context': add_to_global_context,
+ 'static_setup': lambda: PluginStatic(
+ 'coreplugin_persona',
+ resource_filename('mediagoblin.plugins.persona', 'static'))
+}
diff --git a/mediagoblin/plugins/persona/forms.py b/mediagoblin/plugins/persona/forms.py
new file mode 100644
index 00000000..608be0c7
--- /dev/null
+++ b/mediagoblin/plugins/persona/forms.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/>.
+import wtforms
+
+from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
+from mediagoblin.auth.tools import normalize_user_or_email_field
+
+
+class RegistrationForm(wtforms.Form):
+ username = wtforms.TextField(
+ _('Username'),
+ [wtforms.validators.Required(),
+ normalize_user_or_email_field(allow_email=False)])
+ email = wtforms.TextField(
+ _('Email address'),
+ [wtforms.validators.Required(),
+ normalize_user_or_email_field(allow_user=False)])
+ persona_email = wtforms.HiddenField(
+ '',
+ [wtforms.validators.Required(),
+ normalize_user_or_email_field(allow_user=False)])
+
+
+class EditForm(wtforms.Form):
+ email = wtforms.TextField(
+ _('Email address'),
+ [wtforms.validators.Required(),
+ normalize_user_or_email_field(allow_user=False)])
diff --git a/mediagoblin/plugins/persona/models.py b/mediagoblin/plugins/persona/models.py
new file mode 100644
index 00000000..ff3c525a
--- /dev/null
+++ b/mediagoblin/plugins/persona/models.py
@@ -0,0 +1,36 @@
+# 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 sqlalchemy import Column, Integer, Unicode, ForeignKey
+from sqlalchemy.orm import relationship, backref
+
+from mediagoblin.db.models import User
+from mediagoblin.db.base import Base
+
+
+class PersonaUserEmails(Base):
+ __tablename__ = "persona__user_emails"
+
+ id = Column(Integer, primary_key=True)
+ persona_email = Column(Unicode, nullable=False)
+ user_id = Column(Integer, ForeignKey(User.id), nullable=False)
+
+ # Persona's are owned by their user, so do the full thing.
+ user = relationship(User, backref=backref('persona_emails',
+ cascade='all, delete-orphan'))
+
+MODELS = [
+ PersonaUserEmails
+]
diff --git a/mediagoblin/plugins/persona/static/js/persona.js b/mediagoblin/plugins/persona/static/js/persona.js
new file mode 100644
index 00000000..a1d0172f
--- /dev/null
+++ b/mediagoblin/plugins/persona/static/js/persona.js
@@ -0,0 +1,49 @@
+/**
+ * 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/>.
+ */
+
+$(document).ready(function () {
+ var signinLink = document.getElementById('persona_login');
+ if (signinLink) {
+ signinLink.onclick = function() { navigator.id.request(); };
+ }
+
+ var signinLink1 = document.getElementById('persona_login1');
+ if (signinLink1) {
+ signinLink1.onclick = function() { navigator.id.request(); };
+ }
+
+ var signoutLink = document.getElementById('logout');
+ if (signoutLink) {
+ signoutLink.onclick = function() { navigator.id.logout(); };
+ }
+
+ navigator.id.watch({
+ onlogin: function(assertion) {
+ document.getElementById('_assertion').value = assertion;
+ document.getElementById('_persona_login').submit()
+ },
+ onlogout: function() {
+ $.ajax({
+ type: 'POST',
+ url: '/auth/logout',
+ success: function(res, status, xhr) { window.location.reload(); },
+ error: function(xhr, status, err) { alert("Logout failure: " + err); }
+ });
+ }
+ });
+});
diff --git a/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/edit.html b/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/edit.html
new file mode 100644
index 00000000..be62b8cc
--- /dev/null
+++ b/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/edit.html
@@ -0,0 +1,43 @@
+{#
+# 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/>.
+#}
+{% extends "mediagoblin/base.html" %}
+
+{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
+
+{% block title -%}
+ {% trans %}Add an OpenID{% endtrans %} &mdash; {{ super() }}
+{%- endblock %}
+
+{% block mediagoblin_content %}
+ <form action="{{ request.urlgen('mediagoblin.plugins.persona.edit') }}"
+ method="POST" enctype="multipart/form-data">
+ {{ csrf_token }}
+ <div class="form_box">
+ <h1>{% trans %}Delete a Persona email address{% endtrans %}</h1>
+ <p>
+ <a href="javascript:;" id="persona_login">
+ {% trans %}Add a Persona email address{% endtrans %}
+ </a>
+ </p>
+ {{ wtforms_util.render_divs(form, True) }}
+ <div class="form_submit_buttons">
+ <input type="submit" value="{% trans %}Delete{% endtrans %}" class="button_form"/>
+ </div>
+ </div>
+ </form>
+{% endblock %}
diff --git a/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/edit_link.html b/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/edit_link.html
new file mode 100644
index 00000000..08879da5
--- /dev/null
+++ b/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/edit_link.html
@@ -0,0 +1,25 @@
+{#
+# 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/>.
+#}
+
+{% block persona_edit_link %}
+ <p>
+ <a href="{{ request.urlgen('mediagoblin.plugins.persona.edit') }}">
+ {% trans %}Edit your Persona email addresses{% endtrans %}
+ </a>
+ </p>
+{% endblock %}
diff --git a/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/login_link.html b/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/login_link.html
new file mode 100644
index 00000000..975683da
--- /dev/null
+++ b/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/login_link.html
@@ -0,0 +1,25 @@
+{#
+# 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/>.
+#}
+
+{% block person_login_link %}
+ <p>
+ <a href="javascript:;" id="persona_login">
+ {% trans %}Or login with Persona!{% endtrans %}
+ </a>
+ </p>
+{% endblock %}
diff --git a/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/persona.html b/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/persona.html
new file mode 100644
index 00000000..ec0e1875
--- /dev/null
+++ b/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/persona.html
@@ -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/>.
+#}
+{% block persona %}
+ <form id="_persona_login"
+ action=
+ {%- if edit_persona is defined -%}
+ "{{ request.urlgen('mediagoblin.plugins.persona.add') }}"
+ {%- else -%}
+ "{{ request.urlgen('mediagoblin.plugins.persona.login') }}"
+ {%- endif %}
+ method="POST">
+ {{ csrf_token }}
+ <input type="hidden" name="assertion" type="text" id="_assertion"/>
+ </form>
+{% endblock %}
diff --git a/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/persona_js_head.html b/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/persona_js_head.html
new file mode 100644
index 00000000..8c0d72d5
--- /dev/null
+++ b/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/persona_js_head.html
@@ -0,0 +1,21 @@
+{#
+# 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/>.
+#}
+
+<script src="https://login.persona.org/include.js"></script>
+<script type="text/javascript"
+ src="{{ request.staticdirect('/js/persona.js', 'coreplugin_persona') }}"></script>
diff --git a/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/register_link.html b/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/register_link.html
new file mode 100644
index 00000000..bcd9ae2b
--- /dev/null
+++ b/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/register_link.html
@@ -0,0 +1,25 @@
+{#
+# 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/>.
+#}
+
+{% block persona_register_link %}
+ <p>
+ <a href="javascript:;" id="persona_login">
+ {% trans %}Or register with Persona!{% endtrans %}
+ </a>
+ </p>
+{% endblock %}
diff --git a/mediagoblin/plugins/persona/views.py b/mediagoblin/plugins/persona/views.py
new file mode 100644
index 00000000..f3aff38d
--- /dev/null
+++ b/mediagoblin/plugins/persona/views.py
@@ -0,0 +1,191 @@
+# 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 json
+import logging
+import requests
+
+from werkzeug.exceptions import BadRequest
+
+from mediagoblin import messages, mg_globals
+from mediagoblin.auth.tools import register_user
+from mediagoblin.decorators import (auth_enabled, allow_registration,
+ require_active_login)
+from mediagoblin.tools.response import render_to_response, redirect
+from mediagoblin.tools.translate import pass_to_ugettext as _
+from mediagoblin.plugins.persona import forms
+from mediagoblin.plugins.persona.models import PersonaUserEmails
+
+_log = logging.getLogger(__name__)
+
+
+def _get_response(request):
+ if 'assertion' not in request.form:
+ _log.debug('assertion not in request.form')
+ raise BadRequest()
+
+ data = {'assertion': request.form['assertion'],
+ 'audience': request.urlgen('index', qualified=True)}
+ resp = requests.post('https://verifier.login.persona.org/verify',
+ data=data, verify=True)
+
+ if resp.ok:
+ verification_data = json.loads(resp.content)
+
+ if verification_data['status'] == 'okay':
+ return verification_data['email']
+
+ return None
+
+
+@auth_enabled
+def login(request):
+ if request.method == 'GET':
+ return redirect(request, 'mediagoblin.auth.login')
+
+ email = _get_response(request)
+ if email:
+ query = PersonaUserEmails.query.filter_by(
+ persona_email=email
+ ).first()
+ user = query.user if query else None
+
+ if user:
+ request.session['user_id'] = unicode(user.id)
+ request.session.save()
+
+ return redirect(request, "index")
+
+ else:
+ if not mg_globals.app.auth:
+ messages.add_message(
+ request,
+ messages.WARNING,
+ _('Sorry, authentication is disabled on this instance.'))
+
+ return redirect(request, 'index')
+
+ register_form = forms.RegistrationForm(email=email,
+ persona_email=email)
+ return render_to_response(
+ request,
+ 'mediagoblin/auth/register.html',
+ {'register_form': register_form,
+ 'post_url': request.urlgen(
+ 'mediagoblin.plugins.persona.register')})
+
+ return redirect(request, 'mediagoblin.auth.login')
+
+
+@allow_registration
+@auth_enabled
+def register(request):
+ if request.method == 'GET':
+ # Need to connect to persona before registering a user. If method is
+ # 'GET', then this page was acessed without logging in first.
+ return redirect(request, 'mediagoblin.auth.login')
+ register_form = forms.RegistrationForm(request.form)
+
+ if register_form.validate():
+ user = register_user(request, register_form)
+
+ if user:
+ # redirect the user to their homepage... there will be a
+ # message waiting for them to verify their email
+ return redirect(
+ request, 'mediagoblin.user_pages.user_home',
+ user=user.username)
+
+ return render_to_response(
+ request,
+ 'mediagoblin/auth/register.html',
+ {'register_form': register_form,
+ 'post_url': request.urlgen('mediagoblin.plugins.persona.register')})
+
+
+@require_active_login
+def edit(request):
+ form = forms.EditForm(request.form)
+
+ if request.method == 'POST' and form.validate():
+ query = PersonaUserEmails.query.filter_by(
+ persona_email=form.email.data)
+ user = query.first().user if query.first() else None
+
+ if user and user.id == int(request.user.id):
+ count = len(user.persona_emails)
+
+ if count > 1 or user.pw_hash:
+ # User has more then one Persona email or also has a password.
+ query.first().delete()
+
+ messages.add_message(
+ request,
+ messages.SUCCESS,
+ _('The Persona email address was successfully removed.'))
+
+ return redirect(request, 'mediagoblin.edit.account')
+
+ elif not count > 1:
+ form.email.errors.append(
+ _("You can't delete your only Persona email address unless"
+ " you have a password set."))
+
+ else:
+ form.email.errors.append(
+ _('That Persona email address is not registered to this'
+ ' account.'))
+
+ return render_to_response(
+ request,
+ 'mediagoblin/plugins/persona/edit.html',
+ {'form': form,
+ 'edit_persona': True})
+
+
+@require_active_login
+def add(request):
+ if request.method == 'GET':
+ return redirect(request, 'mediagoblin.plugins.persona.edit')
+
+ email = _get_response(request)
+
+ if email:
+ query = PersonaUserEmails.query.filter_by(
+ persona_email=email
+ ).first()
+ user_exists = query.user if query else None
+
+ if user_exists:
+ messages.add_message(
+ request,
+ messages.WARNING,
+ _('Sorry, an account is already registered with that Persona'
+ ' email address.'))
+ return redirect(request, 'mediagoblin.plugins.persona.edit')
+
+ else:
+ # Save the Persona Email to the user
+ new_entry = PersonaUserEmails()
+ new_entry.persona_email = email
+ new_entry.user_id = request.user.id
+ new_entry.save()
+
+ messages.add_message(
+ request,
+ messages.SUCCESS,
+ _('Your Person email address was saved successfully.'))
+
+ return redirect(request, 'mediagoblin.edit.account')
diff --git a/mediagoblin/routing.py b/mediagoblin/routing.py
index 986eb2ed..5961f33b 100644
--- a/mediagoblin/routing.py
+++ b/mediagoblin/routing.py
@@ -36,7 +36,8 @@ def get_url_map():
import mediagoblin.webfinger.routing
import mediagoblin.listings.routing
import mediagoblin.notifications.routing
-
+ import mediagoblin.oauth.routing
+
for route in PluginManager().get_routes():
add_route(*route)
diff --git a/mediagoblin/static/css/base.css b/mediagoblin/static/css/base.css
index 8b57584d..d96b9200 100644
--- a/mediagoblin/static/css/base.css
+++ b/mediagoblin/static/css/base.css
@@ -334,6 +334,10 @@ text-align: center;
width: 20px;
}
+.boolean {
+ margin-bottom: 8px;
+ }
+
textarea#description, textarea#bio {
resize: vertical;
height: 100px;
@@ -753,3 +757,10 @@ pre {
#exif_additional_info table tr {
margin-bottom: 10px;
}
+
+p.verifier {
+ text-align:center;
+ font-size:50px;
+ none repeat scroll 0% 0% rgb(221, 221, 221);
+ padding: 1em 0px;
+}
diff --git a/mediagoblin/static/images/home_goblin.png b/mediagoblin/static/images/home_goblin.png
new file mode 100644
index 00000000..5ba9afeb
--- /dev/null
+++ b/mediagoblin/static/images/home_goblin.png
Binary files differ
diff --git a/mediagoblin/static/js/comment_show.js b/mediagoblin/static/js/comment_show.js
index c5ccee66..df3c1093 100644
--- a/mediagoblin/static/js/comment_show.js
+++ b/mediagoblin/static/js/comment_show.js
@@ -15,12 +15,25 @@
* 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/>.
*/
+var content="";
+function previewComment(){
+ if ($('#comment_content').val() && (content != $('#comment_content').val())) {
+ content = $('#comment_content').val();
+ $.post($('#previewURL').val(),$('#form_comment').serialize(),
+ function(data){
+ preview = JSON.parse(data)
+ $('#comment_preview').replaceWith("<div id=comment_preview><h3>" + $('#previewText').val() +"</h3><br />" + preview.content +
+ "<hr style='border: 1px solid #333;' /></div>");
+ });
+ }
+}
$(document).ready(function(){
$('#form_comment').hide();
$('#button_addcomment').click(function(){
$(this).fadeOut('fast');
$('#form_comment').slideDown(function(){
+ setInterval("previewComment()",1000);
$('#comment_content').focus();
});
});
diff --git a/mediagoblin/submit/views.py b/mediagoblin/submit/views.py
index 64e6791b..3f9d5b2d 100644
--- a/mediagoblin/submit/views.py
+++ b/mediagoblin/submit/views.py
@@ -19,6 +19,7 @@ import mediagoblin.mg_globals as mg_globals
from os.path import splitext
import logging
+import uuid
_log = logging.getLogger(__name__)
@@ -53,6 +54,10 @@ def submit_start(request):
try:
filename = request.files['file'].filename
+ # If the filename contains non ascii generate a unique name
+ if not all(ord(c) < 128 for c in filename):
+ filename = unicode(uuid.uuid4()) + splitext(filename)[-1]
+
# Sniff the submitted media to determine which
# media plugin should handle processing
media_type, media_manager = sniff_media(
@@ -63,7 +68,7 @@ def submit_start(request):
entry.media_type = unicode(media_type)
entry.title = (
unicode(submit_form.title.data)
- or unicode(splitext(filename)[0]))
+ or unicode(splitext(request.files['file'].filename)[0]))
entry.description = unicode(submit_form.description.data)
@@ -133,9 +138,9 @@ def add_collection(request, media=None):
collection.generate_slug()
# Make sure this user isn't duplicating an existing collection
- existing_collection = request.db.Collection.find_one({
- 'creator': request.user.id,
- 'title':collection.title})
+ existing_collection = request.db.Collection.query.filter_by(
+ creator=request.user.id,
+ title=collection.title).first()
if existing_collection:
add_message(request, messages.ERROR,
diff --git a/mediagoblin/templates/mediagoblin/api/authorize.html b/mediagoblin/templates/mediagoblin/api/authorize.html
new file mode 100644
index 00000000..d0ec2616
--- /dev/null
+++ b/mediagoblin/templates/mediagoblin/api/authorize.html
@@ -0,0 +1,56 @@
+{#
+# 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/>.
+#}
+{% extends "mediagoblin/base.html" %}
+
+{% block title -%}
+ {% trans %}Authorization{% endtrans %} &mdash; {{ super() }}
+{%- endblock %}
+
+{% block mediagoblin_content %}
+
+<h1>{% trans %}Authorize{% endtrans %}</h1>
+
+<p>
+ {% trans %}You are logged in as{% endtrans %}
+ <strong>{{user.username}}</strong>
+ <br /><br />
+
+ {% trans %}Do you want to authorize {% endtrans %}
+ {% if client.application_name -%}
+ <em>{{ client.application_name }}</em>
+ {%- else -%}
+ <em>{% trans %}an unknown application{% endtrans %}</em>
+ {%- endif %}
+ {% trans %} to access your account? {% endtrans %}
+ <br /><br />
+ {% trans %}Applications with access to your account can: {% endtrans %}
+ <ul>
+ <li>{% trans %}Post new media as you{% endtrans %}</li>
+ <li>{% trans %}See your information (e.g profile, meida, etc...){% endtrans %}</li>
+ <li>{% trans %}Change your information{% endtrans %}</li>
+ </ul>
+ <br />
+
+ <form method="POST">
+ {{ csrf_token }}
+ {{ authorize_form.oauth_token }}
+ {{ authorize_form.oauth_verifier }}
+ <input type="submit" value="{% trans %}Authorize{% endtrans %}">
+ </form>
+</p>
+{% endblock %}
diff --git a/mediagoblin/templates/mediagoblin/api/oob.html b/mediagoblin/templates/mediagoblin/api/oob.html
new file mode 100644
index 00000000..d290472a
--- /dev/null
+++ b/mediagoblin/templates/mediagoblin/api/oob.html
@@ -0,0 +1,33 @@
+{#
+# 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/>.
+#}
+{% extends "mediagoblin/base.html" %}
+
+{% block title -%}
+ {% trans %}Authorization Finished{% endtrans %} &mdash; {{ super() }}
+{%- endblock %}
+
+{% block mediagoblin_content %}
+
+<h1>{% trans %}Authorization Complete{% endtrans %}</h1>
+
+<h4>{% trans %}Copy and paste this into your client:{% endtrans %}</h4>
+
+<p class="verifier">
+ {{ oauth_request.verifier }}
+</p>
+{% endblock %}
diff --git a/mediagoblin/templates/mediagoblin/base.html b/mediagoblin/templates/mediagoblin/base.html
index 1fc4467c..483b6dfa 100644
--- a/mediagoblin/templates/mediagoblin/base.html
+++ b/mediagoblin/templates/mediagoblin/base.html
@@ -23,6 +23,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="IE=Edge">
<title>{% block title %}{{ app_config['html_title'] }}{% endblock %}</title>
<link rel="stylesheet" type="text/css"
href="{{ request.staticdirect('/css/extlib/reset.css') }}"/>
@@ -46,6 +47,8 @@
{% include "mediagoblin/extra_head.html" %}
{% template_hook("head") %}
+ {% template_hook("persona_head") %}
+
{% block mediagoblin_head %}
{% endblock mediagoblin_head %}
</head>
@@ -60,24 +63,35 @@
{%- if request.user %}
{% if request.user and request.user.status == 'active' %}
- {% set notification_count = request.notifications.get_notification_count(request.user.id) %}
+ {% set notification_count = get_notification_count(request.user.id) %}
{% if notification_count %}
- <a href="#notifications" class="notification-gem button_action" title="Notifications">
- {{ notification_count }}</a>
+ <a href="#notifications" class="notification-gem button_action" title="Notifications">
+ {{ notification_count }}</a>
{% endif %}
- <div class="button_action header_dropdown_down">&#9660;</div>
- <div class="button_action header_dropdown_up">&#9650;</div>
+ <a href="#header" class="button_action header_dropdown_down">&#9660;</a>
+ <a href="#no_header" class="button_action header_dropdown_up">&#9650;</a>
{% elif request.user and request.user.status == "needs_email_verification" %}
{# the following link should only appear when verification is needed #}
<a href="{{ request.urlgen('mediagoblin.user_pages.user_home',
user=request.user.username) }}"
class="button_action_highlight">
{% trans %}Verify your email!{% endtrans %}</a>
- or <a href="{{ request.urlgen('mediagoblin.auth.logout') }}">{% trans %}log out{% endtrans %}</a>
+ or <a id="logout" href=
+ {% if persona is not defined %}
+ "{{ request.urlgen('mediagoblin.auth.logout') }}"
+ {% else %}
+ "javascript:;"
+ {% endif %}
+ >{% trans %}log out{% endtrans %}</a>
{% endif %}
{%- elif auth %}
- <a href="{{ request.urlgen('mediagoblin.auth.login') }}?next={{
- request.base_url|urlencode }}">
+ <a href=
+ {% if persona_auth is defined %}
+ "javascript:;" id="persona_login"
+ {% else %}
+ "{{ request.urlgen('mediagoblin.auth.login') }}"
+ {% endif %}
+ >
{%- trans %}Log in{% endtrans -%}
</a>
{%- endif %}
@@ -101,7 +115,13 @@
{%- trans %}Media processing panel{% endtrans -%}
</a>
&middot;
- <a href="{{ request.urlgen('mediagoblin.auth.logout') }}">{% trans %}Log out{% endtrans %}</a>
+ <a id="logout" href=
+ {% if persona is not defined %}
+ "{{ request.urlgen('mediagoblin.auth.logout') }}"
+ {% else %}
+ "javascript:;"
+ {% endif %}
+ >{% trans %}Log out{% endtrans %}</a>
</p>
<a class="button_action" href="{{ request.urlgen('mediagoblin.submit.start') }}">
{%- trans %}Add media{% endtrans -%}
@@ -128,6 +148,9 @@
{% include "mediagoblin/utils/messages.html" %}
{% block mediagoblin_content %}
{% endblock mediagoblin_content %}
+ {% if csrf_token is defined %}
+ {% template_hook("persona_form") %}
+ {% endif %}
</div>
{%- include "mediagoblin/bits/base_footer.html" %}
</div>
diff --git a/mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html b/mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html
index 9ef28a4d..4e55e618 100644
--- a/mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html
+++ b/mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html
@@ -19,21 +19,27 @@
{% if request.user %}
<h1>{% trans %}Explore{% endtrans %}</h1>
{% else %}
+ <img class="right_align" src="{{ request.staticdirect('/images/home_goblin.png') }}" />
<h1>{% trans %}Hi there, welcome to this MediaGoblin site!{% endtrans %}</h1>
- <img class="right_align" src="{{ request.staticdirect('/images/frontpage_image.png') }}" />
<p>{% trans %}This site is running <a href="http://mediagoblin.org">MediaGoblin</a>, an extraordinarily great piece of media hosting software.{% endtrans %}</p>
{% if auth %}
<p>{% trans %}To add your own media, place comments, and more, you can log in with your MediaGoblin account.{% endtrans %}</p>
{% if allow_registration %}
<p>{% trans %}Don't have one yet? It's easy!{% endtrans %}</p>
- {% trans register_url=request.urlgen('mediagoblin.auth.register') -%}
- <a class="button_action_highlight" href="{{ register_url }}">Create an account at this site</a>
+ <a class="button_action_highlight" href=
+ {% if persona_auth is defined %}
+ "javascript:;" id="persona_login1"
+ {% else %}
+ "{{ request.urlgen('mediagoblin.auth.register') }}"
+ {% endif %}
+ {% trans %}
+ >Create an account at this site</a>
or
{%- endtrans %}
{% endif %}
{% endif %}
{% trans %}
- <a class="button_action" href="http://wiki.mediagoblin.org/HackingHowto">Set up MediaGoblin on your own server</a>
+ <a class="button_action" href="http://mediagoblin.readthedocs.org/">Set up MediaGoblin on your own server</a>
{%- endtrans %}
<div class="clear"></div>
diff --git a/mediagoblin/templates/mediagoblin/edit/change_pass.html b/mediagoblin/templates/mediagoblin/edit/change_pass.html
index ff909b07..2a1ffee0 100644
--- a/mediagoblin/templates/mediagoblin/edit/change_pass.html
+++ b/mediagoblin/templates/mediagoblin/edit/change_pass.html
@@ -39,7 +39,7 @@
Changing {{ username }}'s password
{%- endtrans -%}
</h1>
- {{ wtforms_util.render_divs(form) }}
+ {{ wtforms_util.render_divs(form, True) }}
{{ csrf_token }}
<div class="form_submit_buttons">
<input type="submit" value="{% trans %}Save{% endtrans %}"
diff --git a/mediagoblin/templates/mediagoblin/fragments/header_notifications.html b/mediagoblin/templates/mediagoblin/fragments/header_notifications.html
index 613100aa..70d7935a 100644
--- a/mediagoblin/templates/mediagoblin/fragments/header_notifications.html
+++ b/mediagoblin/templates/mediagoblin/fragments/header_notifications.html
@@ -1,4 +1,4 @@
-{% set notifications = request.notifications.get_notifications(request.user.id) %}
+{% set notifications = get_notifications(request.user.id) %}
{% if notifications %}
<div class="header_notifications">
<h3>{% trans %}New comments{% endtrans %}</h3>
diff --git a/mediagoblin/templates/mediagoblin/media_displays/stl.html b/mediagoblin/templates/mediagoblin/media_displays/stl.html
index a89e0b4f..bc12ce4e 100644
--- a/mediagoblin/templates/mediagoblin/media_displays/stl.html
+++ b/mediagoblin/templates/mediagoblin/media_displays/stl.html
@@ -108,32 +108,26 @@ window.show_things = function () {
<div style="padding: 4px;">
- <a class="button_action" onclick="show('perspective');"
- title="{%- trans %}Toggle Rotate{% endtrans -%}">
+ <a class="button_action" onclick="show('perspective');">
{%- trans %}Perspective{% endtrans -%}
</a>
- <a class="button_action" onclick="show('front_view');"
- title="{%- trans %}Front{% endtrans -%}">
+ <a class="button_action" onclick="show('front_view');">
{%- trans %}Front{% endtrans -%}
</a>
- <a class="button_action" onclick="show('top_view');"
- title="{%- trans %}Top{% endtrans -%}">
+ <a class="button_action" onclick="show('top_view');">
{%- trans %}Top{% endtrans -%}
</a>
- <a class="button_action" onclick="show('side_view');"
- title="{%- trans %}Side{% endtrans -%}">
+ <a class="button_action" onclick="show('side_view');">
{%- trans %}Side{% endtrans -%}
</a>
{% if media.media_data.file_type == "stl" %}
<a id="webgl_button" class="button_action"
- onclick="show_things();"
- title="{%- trans %}WebGL{% endtrans -%}">
+ onclick="show_things();">
{%- trans %}WebGL{% endtrans -%}
</a>
{% endif %}
<a class="button_action" href="{{ model_download }}"
- title="{%- trans %}Download{% endtrans -%}"
style="float:right;">
{%- trans %}Download model{% endtrans -%}
</a>
diff --git a/mediagoblin/templates/mediagoblin/user_pages/media.html b/mediagoblin/templates/mediagoblin/user_pages/media.html
index c16e4c78..39935b40 100644
--- a/mediagoblin/templates/mediagoblin/user_pages/media.html
+++ b/mediagoblin/templates/mediagoblin/user_pages/media.html
@@ -90,7 +90,8 @@
{% if app_config['allow_comments'] %}
<a
{% if not request.user %}
- href="{{ request.urlgen('mediagoblin.auth.login') }}"
+ href="{{ request.urlgen('mediagoblin.auth.login') }}?next={{
+ request.base_url|urlencode }}"
{% endif %}
class="button_action" id="button_addcomment" title="Add a comment">
{% trans %}Add a comment{% endtrans %}
@@ -107,7 +108,10 @@
<input type="submit" value="{% trans %}Add this comment{% endtrans %}" class="button_action" />
{{ csrf_token }}
</div>
+ <input type="hidden" value="{{ request.urlgen('mediagoblin.user_pages.media_preview_comment') }}" id="previewURL" />
+ <input type="hidden" value="{% trans %}Comment Preview{% endtrans %}" id="previewText"/>
</form>
+ <div id="comment_preview"></div>
{% endif %}
<ul style="list-style:none">
{% for comment in comments %}
diff --git a/mediagoblin/templates/mediagoblin/utils/comment-subscription.html b/mediagoblin/templates/mediagoblin/utils/comment-subscription.html
index 8ee8c883..75da5e89 100644
--- a/mediagoblin/templates/mediagoblin/utils/comment-subscription.html
+++ b/mediagoblin/templates/mediagoblin/utils/comment-subscription.html
@@ -16,18 +16,17 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
{%- if request.user %}
- {% set subscription = request.notifications.get_comment_subscription(
- request.user.id, media.id) %}
+ {% set subscription = get_comment_subscription(request.user.id, media.id) %}
{% if not subscription or not subscription.notify %}
<a type="submit" href="{{ request.urlgen('mediagoblin.notifications.subscribe_comments',
user=media.get_uploader.username,
- media=media.slug)}}"
+ media=media.slug_or_id)}}"
class="button_action">Subscribe to comments
</a>
{% else %}
<a type="submit" href="{{ request.urlgen('mediagoblin.notifications.silence_comments',
user=media.get_uploader.username,
- media=media.slug)}}"
+ media=media.slug_or_id)}}"
class="button_action">Silence comments
</a>
{% endif %}
diff --git a/mediagoblin/templates/mediagoblin/utils/wtforms.html b/mediagoblin/templates/mediagoblin/utils/wtforms.html
index a4c33f1a..e079274e 100644
--- a/mediagoblin/templates/mediagoblin/utils/wtforms.html
+++ b/mediagoblin/templates/mediagoblin/utils/wtforms.html
@@ -34,26 +34,26 @@
{# Generically render a field #}
{% macro render_field_div(field, autofocus_first=False) %}
- {{- render_label_p(field) }}
- <div class="form_field_input">
- {% if autofocus_first %}
- {{ field(autofocus=True) }}
- {% else %}
- {{ field }}
- {% endif %}
- {%- if field.errors -%}
- {% for error in field.errors %}
- <p class="form_field_error">{{ error }}</p>
- {% endfor %}
- {%- endif %}
- {%- if field.description %}
- {% if field.type == 'BooleanField' %}
- <label for="{{ field.label.field_id }}">{{ field.description|safe }}</label>
+ {% if field.type == 'BooleanField' %}
+ {{ render_bool(field) }}
+ {% else %}
+ {{- render_label_p(field) }}
+ <div class="form_field_input">
+ {% if autofocus_first %}
+ {{ field(autofocus=True) }}
{% else %}
- <p class="form_field_description">{{ field.description|safe }}</p>
+ {{ field }}
{% endif %}
- {%- endif %}
- </div>
+ {%- if field.errors -%}
+ {% for error in field.errors %}
+ <p class="form_field_error">{{ error }}</p>
+ {% endfor %}
+ {%- endif %}
+ {%- if field.description %}
+ <p class="form_field_description">{{ field.description|safe }}</p>
+ {%- endif %}
+ </div>
+ {% endif %}
{%- endmacro %}
{# Auto-render a form as a series of divs #}
@@ -86,3 +86,19 @@
</tr>
{% endfor %}
{%- endmacro %}
+
+{# Render a boolean field #}
+{% macro render_bool(field) %}
+ <div class="boolean">
+ <label for="{{ field.label.field_id }}">
+ {{ field }}</input>
+ {{ field.description|safe }}
+ </label>
+ {%- if field.errors -%}
+ {% for error in field.errors %}
+ <p class="form_field_error">{{ error }}</p>
+ {% endfor %}
+ {% endif %}
+ </div>
+{% endmacro %}
+
diff --git a/mediagoblin/tests/auth_configs/persona_appconfig.ini b/mediagoblin/tests/auth_configs/persona_appconfig.ini
new file mode 100644
index 00000000..0bd5d634
--- /dev/null
+++ b/mediagoblin/tests/auth_configs/persona_appconfig.ini
@@ -0,0 +1,42 @@
+# 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/>.
+[mediagoblin]
+direct_remote_path = /test_static/
+email_sender_address = "notice@mediagoblin.example.org"
+email_debug_mode = true
+
+# TODO: Switch to using an in-memory database
+sql_engine = "sqlite:///%(here)s/user_dev/mediagoblin.db"
+
+# Celery shouldn't be set up by the application as it's setup via
+# mediagoblin.init.celery.from_celery
+celery_setup_elsewhere = true
+
+[storage:publicstore]
+base_dir = %(here)s/user_dev/media/public
+base_url = /mgoblin_media/
+
+[storage:queuestore]
+base_dir = %(here)s/user_dev/media/queue
+
+[celery]
+CELERY_ALWAYS_EAGER = true
+CELERY_RESULT_DBURI = "sqlite:///%(here)s/user_dev/celery.db"
+BROKER_HOST = "sqlite:///%(here)s/user_dev/kombu.db"
+
+[plugins]
+[[mediagoblin.plugins.persona]]
+
diff --git a/mediagoblin/tests/test_auth.py b/mediagoblin/tests/test_auth.py
index 5bd8bf2c..61503d32 100644
--- a/mediagoblin/tests/test_auth.py
+++ b/mediagoblin/tests/test_auth.py
@@ -93,8 +93,8 @@ def test_register_views(test_app):
assert 'mediagoblin/user_pages/user.html' in template.TEMPLATE_TEST_CONTEXT
## Make sure user is in place
- new_user = mg_globals.database.User.find_one(
- {'username': u'happygirl'})
+ new_user = mg_globals.database.User.query.filter_by(
+ username=u'happygirl').first()
assert new_user
assert new_user.status == u'needs_email_verification'
assert new_user.email_verified == False
@@ -128,8 +128,8 @@ def test_register_views(test_app):
# assert context['verification_successful'] == True
# TODO: Would be good to test messages here when we can do so...
- new_user = mg_globals.database.User.find_one(
- {'username': u'happygirl'})
+ new_user = mg_globals.database.User.query.filter_by(
+ username=u'happygirl').first()
assert new_user
assert new_user.status == u'needs_email_verification'
assert new_user.email_verified == False
@@ -142,8 +142,8 @@ def test_register_views(test_app):
'mediagoblin/user_pages/user.html']
# assert context['verification_successful'] == True
# TODO: Would be good to test messages here when we can do so...
- new_user = mg_globals.database.User.find_one(
- {'username': u'happygirl'})
+ new_user = mg_globals.database.User.query.filter_by(
+ username=u'happygirl').first()
assert new_user
assert new_user.status == u'active'
assert new_user.email_verified == True
diff --git a/mediagoblin/tests/test_edit.py b/mediagoblin/tests/test_edit.py
index acc638d9..d70d0478 100644
--- a/mediagoblin/tests/test_edit.py
+++ b/mediagoblin/tests/test_edit.py
@@ -190,8 +190,8 @@ class TestUserEdit(object):
assert urlparse.urlsplit(res.location)[2] == '/'
# Email shouldn't be saved
- email_in_db = mg_globals.database.User.find_one(
- {'email': 'new@example.com'})
+ email_in_db = mg_globals.database.User.query.filter_by(
+ email='new@example.com').first()
email = User.query.filter_by(username='chris').first().email
assert email_in_db is None
assert email == 'chris@example.com'
diff --git a/mediagoblin/tests/test_http_callback.py b/mediagoblin/tests/test_http_callback.py
index a0511af7..64b7ee8f 100644
--- a/mediagoblin/tests/test_http_callback.py
+++ b/mediagoblin/tests/test_http_callback.py
@@ -23,7 +23,7 @@ from mediagoblin import mg_globals
from mediagoblin.tools import processing
from mediagoblin.tests.tools import fixture_add_user
from mediagoblin.tests.test_submission import GOOD_PNG
-from mediagoblin.tests import test_oauth as oauth
+from mediagoblin.tests import test_oauth2 as oauth
class TestHTTPCallback(object):
@@ -44,7 +44,7 @@ class TestHTTPCallback(object):
'password': self.user_password})
def get_access_token(self, client_id, client_secret, code):
- response = self.test_app.get('/oauth/access_token', {
+ response = self.test_app.get('/oauth-2/access_token', {
'code': code,
'client_id': client_id,
'client_secret': client_secret})
diff --git a/mediagoblin/tests/test_mgoblin_app.ini b/mediagoblin/tests/test_mgoblin_app.ini
index 5c3c46e7..535cf1c1 100644
--- a/mediagoblin/tests/test_mgoblin_app.ini
+++ b/mediagoblin/tests/test_mgoblin_app.ini
@@ -13,8 +13,6 @@ tags_max_length = 50
# So we can start to test attachments:
allow_attachments = True
-media_types = mediagoblin.media_types.image, mediagoblin.media_types.pdf
-
[storage:publicstore]
base_dir = %(here)s/user_dev/media/public
base_url = /mgoblin_media/
@@ -34,3 +32,5 @@ BROKER_HOST = "sqlite:///%(here)s/user_dev/kombu.db"
[[mediagoblin.plugins.piwigo]]
[[mediagoblin.plugins.basic_auth]]
[[mediagoblin.plugins.openid]]
+[[mediagoblin.media_types.image]]
+[[mediagoblin.media_types.pdf]]
diff --git a/mediagoblin/tests/test_oauth1.py b/mediagoblin/tests/test_oauth1.py
new file mode 100644
index 00000000..073c2884
--- /dev/null
+++ b/mediagoblin/tests/test_oauth1.py
@@ -0,0 +1,166 @@
+# 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 cgi
+
+import pytest
+from urlparse import parse_qs, urlparse
+
+from oauthlib.oauth1 import Client
+
+from mediagoblin import mg_globals
+from mediagoblin.tools import template, pluginapi
+from mediagoblin.tests.tools import fixture_add_user
+
+
+class TestOAuth(object):
+
+ MIME_FORM = "application/x-www-form-urlencoded"
+ MIME_JSON = "application/json"
+
+ @pytest.fixture(autouse=True)
+ def setup(self, test_app):
+ self.test_app = test_app
+
+ self.db = mg_globals.database
+
+ self.pman = pluginapi.PluginManager()
+
+ self.user_password = "AUserPassword123"
+ self.user = fixture_add_user("OAuthy", self.user_password)
+
+ self.login()
+
+ def login(self):
+ self.test_app.post(
+ "/auth/login/", {
+ "username": self.user.username,
+ "password": self.user_password})
+
+ def register_client(self, **kwargs):
+ """ Regiters a client with the API """
+
+ kwargs["type"] = "client_associate"
+ kwargs["application_type"] = kwargs.get("application_type", "native")
+ return self.test_app.post("/api/client/register", kwargs)
+
+ def test_client_client_register_limited_info(self):
+ """ Tests that a client can be registered with limited information """
+ response = self.register_client()
+ client_info = response.json
+
+ client = self.db.Client.query.filter_by(id=client_info["client_id"]).first()
+
+ assert response.status_int == 200
+ assert client is not None
+
+ def test_client_register_full_info(self):
+ """ Provides every piece of information possible to register client """
+ query = {
+ "application_name": "Testificate MD",
+ "application_type": "web",
+ "contacts": "someone@someplace.com tuteo@tsengeo.lu",
+ "logo_url": "http://ayrel.com/utral.png",
+ "redirect_uris": "http://navi-kosman.lu http://gmg-yawne-oeru.lu",
+ }
+
+ response = self.register_client(**query)
+ client_info = response.json
+
+ client = self.db.Client.query.filter_by(id=client_info["client_id"]).first()
+
+ assert client is not None
+ assert client.secret == client_info["client_secret"]
+ assert client.application_type == query["application_type"]
+ assert client.redirect_uri == query["redirect_uris"].split()
+ assert client.logo_url == query["logo_url"]
+ assert client.contacts == query["contacts"].split()
+
+
+ def test_client_update(self):
+ """ Tests that you can update a client """
+ # first we need to register a client
+ response = self.register_client()
+
+ client_info = response.json
+ client = self.db.Client.query.filter_by(id=client_info["client_id"]).first()
+
+ # Now update
+ update_query = {
+ "type": "client_update",
+ "application_name": "neytiri",
+ "contacts": "someone@someplace.com abc@cba.com",
+ "logo_url": "http://place.com/picture.png",
+ "application_type": "web",
+ "redirect_uris": "http://blah.gmg/whatever https://inboxen.org/",
+ }
+
+ update_response = self.register_client(**update_query)
+
+ assert update_response.status_int == 200
+ client_info = update_response.json
+ client = self.db.Client.query.filter_by(id=client_info["client_id"]).first()
+
+ assert client.secret == client_info["client_secret"]
+ assert client.application_type == update_query["application_type"]
+ assert client.application_name == update_query["application_name"]
+ assert client.contacts == update_query["contacts"].split()
+ assert client.logo_url == update_query["logo_url"]
+ assert client.redirect_uri == update_query["redirect_uris"].split()
+
+ def to_authorize_headers(self, data):
+ headers = ""
+ for key, value in data.items():
+ headers += '{0}="{1}",'.format(key, value)
+ return {"Authorization": "OAuth " + headers[:-1]}
+
+ def test_request_token(self):
+ """ Test a request for a request token """
+ response = self.register_client()
+
+ client_id = response.json["client_id"]
+
+ endpoint = "/oauth/request_token"
+ request_query = {
+ "oauth_consumer_key": client_id,
+ "oauth_nonce": "abcdefghij",
+ "oauth_timestamp": 123456789.0,
+ "oauth_callback": "https://some.url/callback",
+ }
+
+ headers = self.to_authorize_headers(request_query)
+
+ headers["Content-Type"] = self.MIME_FORM
+
+ response = self.test_app.post(endpoint, headers=headers)
+ response = cgi.parse_qs(response.body)
+
+ # each element is a list, reduce it to a string
+ for key, value in response.items():
+ response[key] = value[0]
+
+ request_token = self.db.RequestToken.query.filter_by(
+ token=response["oauth_token"]
+ ).first()
+
+ client = self.db.Client.query.filter_by(id=client_id).first()
+
+ assert request_token is not None
+ assert request_token.secret == response["oauth_token_secret"]
+ assert request_token.client == client.id
+ assert request_token.used == False
+ assert request_token.callback == request_query["oauth_callback"]
+
diff --git a/mediagoblin/tests/test_oauth.py b/mediagoblin/tests/test_oauth2.py
index ea3bd798..86f9e8cc 100644
--- a/mediagoblin/tests/test_oauth.py
+++ b/mediagoblin/tests/test_oauth2.py
@@ -51,7 +51,7 @@ class TestOAuth(object):
def register_client(self, name, client_type, description=None,
redirect_uri=''):
return self.test_app.post(
- '/oauth/client/register', {
+ '/oauth-2/client/register', {
'name': name,
'description': description,
'type': client_type,
@@ -115,7 +115,7 @@ class TestOAuth(object):
client_identifier = client.identifier
redirect_uri = 'https://foo.example'
- response = self.test_app.get('/oauth/authorize', {
+ response = self.test_app.get('/oauth-2/authorize', {
'client_id': client.identifier,
'scope': 'all',
'redirect_uri': redirect_uri})
@@ -129,7 +129,7 @@ class TestOAuth(object):
# Short for client authorization post reponse
capr = self.test_app.post(
- '/oauth/client/authorize', {
+ '/oauth-2/client/authorize', {
'client_id': form.client_id.data,
'allow': 'Allow',
'next': form.next.data})
@@ -155,7 +155,7 @@ class TestOAuth(object):
client = self.db.OAuthClient.query.filter(
self.db.OAuthClient.identifier == unicode(client_id)).first()
- token_res = self.test_app.get('/oauth/access_token?client_id={0}&\
+ token_res = self.test_app.get('/oauth-2/access_token?client_id={0}&\
code={1}&client_secret={2}'.format(client_id, code, client.secret))
assert token_res.status_int == 200
@@ -183,7 +183,7 @@ code={1}&client_secret={2}'.format(client_id, code, client.secret))
client = self.db.OAuthClient.query.filter(
self.db.OAuthClient.identifier == unicode(client_id)).first()
- token_res = self.test_app.get('/oauth/access_token?\
+ token_res = self.test_app.get('/oauth-2/access_token?\
code={0}&client_secret={1}'.format(code, client.secret))
assert token_res.status_int == 200
@@ -204,7 +204,7 @@ code={0}&client_secret={1}'.format(code, client.secret))
client = self.db.OAuthClient.query.filter(
self.db.OAuthClient.identifier == client_id).first()
- token_res = self.test_app.get('/oauth/access_token',
+ token_res = self.test_app.get('/oauth-2/access_token',
{'refresh_token': token_data['refresh_token'],
'client_id': client_id,
'client_secret': client.secret
diff --git a/mediagoblin/tests/test_openid.py b/mediagoblin/tests/test_openid.py
index c85f6318..23a2290e 100644
--- a/mediagoblin/tests/test_openid.py
+++ b/mediagoblin/tests/test_openid.py
@@ -13,12 +13,14 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
import urlparse
import pkg_resources
import pytest
import mock
-from openid.consumer.consumer import SuccessResponse
+openid_consumer = pytest.importorskip(
+ "openid.consumer.consumer")
from mediagoblin import mg_globals
from mediagoblin.db.base import Session
@@ -27,7 +29,6 @@ from mediagoblin.plugins.openid.models import OpenIDUserURL
from mediagoblin.tests.tools import get_app, fixture_add_user
from mediagoblin.tools import template
-
# App with plugin enabled
@pytest.fixture()
def openid_plugin_app(request):
@@ -41,7 +42,7 @@ def openid_plugin_app(request):
class TestOpenIDPlugin(object):
def _setup(self, openid_plugin_app, value=True, edit=False, delete=False):
if value:
- response = SuccessResponse(mock.Mock(), mock.Mock())
+ response = openid_consumer.SuccessResponse(mock.Mock(), mock.Mock())
if edit or delete:
response.identity_url = u'http://add.myopenid.com'
else:
@@ -186,8 +187,8 @@ class TestOpenIDPlugin(object):
openid_plugin_app.get('/auth/logout')
# Get user and detach from session
- test_user = mg_globals.database.User.find_one({
- 'username': u'chris'})
+ test_user = mg_globals.database.User.query.filter_by(
+ username=u'chris').first()
Session.expunge(test_user)
# Log back in
@@ -314,8 +315,8 @@ class TestOpenIDPlugin(object):
assert 'mediagoblin/edit/edit_account.html' in template.TEMPLATE_TEST_CONTEXT
# OpenID Added?
- new_openid = mg_globals.database.OpenIDUserURL.find_one(
- {'openid_url': u'http://add.myopenid.com'})
+ new_openid = mg_globals.database.OpenIDUserURL.query.filter_by(
+ openid_url=u'http://add.myopenid.com').first()
assert new_openid
_test_add()
@@ -365,8 +366,8 @@ class TestOpenIDPlugin(object):
assert 'mediagoblin/edit/edit_account.html' in template.TEMPLATE_TEST_CONTEXT
# OpenID deleted?
- new_openid = mg_globals.database.OpenIDUserURL.find_one(
- {'openid_url': u'http://add.myopenid.com'})
+ new_openid = mg_globals.database.OpenIDUserURL.query.filter_by(
+ openid_url=u'http://add.myopenid.com').first()
assert not new_openid
_test_delete(self, test_user)
diff --git a/mediagoblin/tests/test_persona.py b/mediagoblin/tests/test_persona.py
new file mode 100644
index 00000000..ce795258
--- /dev/null
+++ b/mediagoblin/tests/test_persona.py
@@ -0,0 +1,210 @@
+# 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 urlparse
+import pkg_resources
+import pytest
+import mock
+
+from mediagoblin import mg_globals
+from mediagoblin.db.base import Session
+from mediagoblin.tests.tools import get_app
+from mediagoblin.tools import template
+
+
+# App with plugin enabled
+@pytest.fixture()
+def persona_plugin_app(request):
+ return get_app(
+ request,
+ mgoblin_config=pkg_resources.resource_filename(
+ 'mediagoblin.tests.auth_configs',
+ 'persona_appconfig.ini'))
+
+
+class TestPersonaPlugin(object):
+ def test_authentication_views(self, persona_plugin_app):
+ res = persona_plugin_app.get('/auth/login/')
+
+ assert urlparse.urlsplit(res.location)[2] == '/'
+
+ res = persona_plugin_app.get('/auth/register/')
+
+ assert urlparse.urlsplit(res.location)[2] == '/'
+
+ res = persona_plugin_app.get('/auth/persona/login/')
+
+ assert urlparse.urlsplit(res.location)[2] == '/auth/login/'
+
+ res = persona_plugin_app.get('/auth/persona/register/')
+
+ assert urlparse.urlsplit(res.location)[2] == '/auth/login/'
+
+ @mock.patch('mediagoblin.plugins.persona.views._get_response', mock.Mock(return_value=u'test@example.com'))
+ def _test_registration():
+ # No register users
+ template.clear_test_template_context()
+ res = persona_plugin_app.post(
+ '/auth/persona/login/', {})
+
+ assert 'mediagoblin/auth/register.html' in template.TEMPLATE_TEST_CONTEXT
+ context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html']
+ register_form = context['register_form']
+
+ assert register_form.email.data == u'test@example.com'
+ assert register_form.persona_email.data == u'test@example.com'
+
+ template.clear_test_template_context()
+ res = persona_plugin_app.post(
+ '/auth/persona/register/', {})
+
+ assert 'mediagoblin/auth/register.html' in template.TEMPLATE_TEST_CONTEXT
+ context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html']
+ register_form = context['register_form']
+
+ assert register_form.username.errors == [u'This field is required.']
+ assert register_form.email.errors == [u'This field is required.']
+ assert register_form.persona_email.errors == [u'This field is required.']
+
+ # Successful register
+ template.clear_test_template_context()
+ res = persona_plugin_app.post(
+ '/auth/persona/register/',
+ {'username': 'chris',
+ 'email': 'chris@example.com',
+ 'persona_email': 'test@example.com'})
+ res.follow()
+
+ assert urlparse.urlsplit(res.location)[2] == '/u/chris/'
+ assert 'mediagoblin/user_pages/user.html' in template.TEMPLATE_TEST_CONTEXT
+
+ # Try to register same Persona email address
+ template.clear_test_template_context()
+ res = persona_plugin_app.post(
+ '/auth/persona/register/',
+ {'username': 'chris1',
+ 'email': 'chris1@example.com',
+ 'persona_email': 'test@example.com'})
+
+ assert 'mediagoblin/auth/register.html' in template.TEMPLATE_TEST_CONTEXT
+ context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html']
+ register_form = context['register_form']
+
+ assert register_form.persona_email.errors == [u'Sorry, an account is already registered to that Persona email.']
+
+ # Logout
+ persona_plugin_app.get('/auth/logout/')
+
+ # Get user and detach from session
+ test_user = mg_globals.database.User.query.filter_by(
+ username=u'chris').first()
+ test_user.email_verified = True
+ test_user.status = u'active'
+ test_user.save()
+ test_user = mg_globals.database.User.query.filter_by(
+ username=u'chris').first()
+ Session.expunge(test_user)
+
+ # Add another user for _test_edit_persona
+ persona_plugin_app.post(
+ '/auth/persona/register/',
+ {'username': 'chris1',
+ 'email': 'chris1@example.com',
+ 'persona_email': 'test1@example.com'})
+
+ # Log back in
+ template.clear_test_template_context()
+ res = persona_plugin_app.post(
+ '/auth/persona/login/')
+ res.follow()
+
+ assert urlparse.urlsplit(res.location)[2] == '/'
+ assert 'mediagoblin/root.html' in template.TEMPLATE_TEST_CONTEXT
+
+ # Make sure user is in the session
+ context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/root.html']
+ session = context['request'].session
+ assert session['user_id'] == unicode(test_user.id)
+
+ _test_registration()
+
+ @mock.patch('mediagoblin.plugins.persona.views._get_response', mock.Mock(return_value=u'new@example.com'))
+ def _test_edit_persona():
+ # Try and delete only Persona email address
+ template.clear_test_template_context()
+ res = persona_plugin_app.post(
+ '/edit/persona/',
+ {'email': 'test@example.com'})
+
+ assert 'mediagoblin/plugins/persona/edit.html' in template.TEMPLATE_TEST_CONTEXT
+ context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/plugins/persona/edit.html']
+ form = context['form']
+
+ assert form.email.errors == [u"You can't delete your only Persona email address unless you have a password set."]
+
+ template.clear_test_template_context()
+ res = persona_plugin_app.post(
+ '/edit/persona/', {})
+
+ assert 'mediagoblin/plugins/persona/edit.html' in template.TEMPLATE_TEST_CONTEXT
+ context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/plugins/persona/edit.html']
+ form = context['form']
+
+ assert form.email.errors == [u'This field is required.']
+
+ # Try and delete Persona not owned by the user
+ template.clear_test_template_context()
+ res = persona_plugin_app.post(
+ '/edit/persona/',
+ {'email': 'test1@example.com'})
+
+ assert 'mediagoblin/plugins/persona/edit.html' in template.TEMPLATE_TEST_CONTEXT
+ context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/plugins/persona/edit.html']
+ form = context['form']
+
+ assert form.email.errors == [u'That Persona email address is not registered to this account.']
+
+ res = persona_plugin_app.get('/edit/persona/add/')
+
+ assert urlparse.urlsplit(res.location)[2] == '/edit/persona/'
+
+ # Add Persona email address
+ template.clear_test_template_context()
+ res = persona_plugin_app.post(
+ '/edit/persona/add/')
+ res.follow()
+
+ assert urlparse.urlsplit(res.location)[2] == '/edit/account/'
+
+ # Delete a Persona
+ res = persona_plugin_app.post(
+ '/edit/persona/',
+ {'email': 'test@example.com'})
+ res.follow()
+
+ assert urlparse.urlsplit(res.location)[2] == '/edit/account/'
+
+ _test_edit_persona()
+
+ @mock.patch('mediagoblin.plugins.persona.views._get_response', mock.Mock(return_value=u'test1@example.com'))
+ def _test_add_existing():
+ template.clear_test_template_context()
+ res = persona_plugin_app.post(
+ '/edit/persona/add/')
+ res.follow()
+
+ assert urlparse.urlsplit(res.location)[2] == '/edit/persona/'
+
+ _test_add_existing()
diff --git a/mediagoblin/tests/test_sql_migrations.py b/mediagoblin/tests/test_sql_migrations.py
index 2fc4c043..3d67fdf6 100644
--- a/mediagoblin/tests/test_sql_migrations.py
+++ b/mediagoblin/tests/test_sql_migrations.py
@@ -58,6 +58,10 @@ class Level1(Base1):
SET1_MODELS = [Creature1, Level1]
+FOUNDATIONS = {Creature1:[{'name':u'goblin','num_legs':2,'is_demon':False},
+ {'name':u'cerberus','num_legs':4,'is_demon':True}]
+ }
+
SET1_MIGRATIONS = {}
#######################################################
@@ -542,7 +546,6 @@ def _insert_migration3_objects(session):
session.commit()
-
def create_test_engine():
from sqlalchemy import create_engine
engine = create_engine('sqlite:///:memory:', echo=False)
@@ -572,7 +575,7 @@ def test_set1_to_set3():
printer = CollectingPrinter()
migration_manager = MigrationManager(
- u'__main__', SET1_MODELS, SET1_MIGRATIONS, Session(),
+ u'__main__', SET1_MODELS, FOUNDATIONS, SET1_MIGRATIONS, Session(),
printer)
# Check latest migration and database current migration
@@ -585,11 +588,13 @@ def test_set1_to_set3():
assert result == u'inited'
# Check output
assert printer.combined_string == (
- "-> Initializing main mediagoblin tables... done.\n")
+ "-> Initializing main mediagoblin tables... done.\n" + \
+ " + Laying foundations for Creature1 table\n" )
# Check version in database
assert migration_manager.latest_migration == 0
assert migration_manager.database_current_migration == 0
+
# Install the initial set
# -----------------------
@@ -597,8 +602,8 @@ def test_set1_to_set3():
# Try to "re-migrate" with same manager settings... nothing should happen
migration_manager = MigrationManager(
- u'__main__', SET1_MODELS, SET1_MIGRATIONS, Session(),
- printer)
+ u'__main__', SET1_MODELS, FOUNDATIONS, SET1_MIGRATIONS,
+ Session(), printer)
assert migration_manager.init_or_migrate() == None
# Check version in database
@@ -639,6 +644,20 @@ def test_set1_to_set3():
# Now check to see if stuff seems to be in there.
session = Session()
+ # Check the creation of the foundation rows on the creature table
+ creature = session.query(Creature1).filter_by(
+ name=u'goblin').one()
+ assert creature.num_legs == 2
+ assert creature.is_demon == False
+
+ creature = session.query(Creature1).filter_by(
+ name=u'cerberus').one()
+ assert creature.num_legs == 4
+ assert creature.is_demon == True
+
+
+ # Check the creation of the inserted rows on the creature and levels tables
+
creature = session.query(Creature1).filter_by(
name=u'centipede').one()
assert creature.num_legs == 100
@@ -679,7 +698,7 @@ def test_set1_to_set3():
# isn't said to be updated yet
printer = CollectingPrinter()
migration_manager = MigrationManager(
- u'__main__', SET3_MODELS, SET3_MIGRATIONS, Session(),
+ u'__main__', SET3_MODELS, FOUNDATIONS, SET3_MIGRATIONS, Session(),
printer)
assert migration_manager.latest_migration == 8
@@ -706,7 +725,7 @@ def test_set1_to_set3():
# Make sure version matches expected
migration_manager = MigrationManager(
- u'__main__', SET3_MODELS, SET3_MIGRATIONS, Session(),
+ u'__main__', SET3_MODELS, FOUNDATIONS, SET3_MIGRATIONS, Session(),
printer)
assert migration_manager.latest_migration == 8
assert migration_manager.database_current_migration == 8
@@ -772,6 +791,15 @@ def test_set1_to_set3():
# Now check to see if stuff seems to be in there.
session = Session()
+
+
+ # Start with making sure that the foundations did not run again
+ assert session.query(Creature3).filter_by(
+ name=u'goblin').count() == 1
+ assert session.query(Creature3).filter_by(
+ name=u'cerberus').count() == 1
+
+ # Then make sure the models have been migrated correctly
creature = session.query(Creature3).filter_by(
name=u'centipede').one()
assert creature.num_limbs == 100.0
diff --git a/mediagoblin/tests/test_submission.py b/mediagoblin/tests/test_submission.py
index 162b2d19..ac941063 100644
--- a/mediagoblin/tests/test_submission.py
+++ b/mediagoblin/tests/test_submission.py
@@ -26,7 +26,7 @@ from mediagoblin.tests.tools import fixture_add_user
from mediagoblin import mg_globals
from mediagoblin.db.models import MediaEntry
from mediagoblin.tools import template
-from mediagoblin.media_types.image import MEDIA_MANAGER as img_MEDIA_MANAGER
+from mediagoblin.media_types.image import ImageMediaManager
from mediagoblin.media_types.pdf.processing import check_prerequisites as pdf_check_prerequisites
from .resources import GOOD_JPG, GOOD_PNG, EVIL_FILE, EVIL_JPG, EVIL_PNG, \
@@ -77,7 +77,7 @@ class TestSubmission:
return {'upload_files': [('file', filename)]}
def check_comments(self, request, media_id, count):
- comments = request.db.MediaComment.find({'media_entry': media_id})
+ comments = request.db.MediaComment.query.filter_by(media_entry=media_id)
assert count == len(list(comments))
def test_missing_fields(self):
@@ -122,7 +122,7 @@ class TestSubmission:
assert 'mediagoblin/user_pages/user.html' in context
def check_media(self, request, find_data, count=None):
- media = MediaEntry.find(find_data)
+ media = MediaEntry.query.filter_by(**find_data)
if count is not None:
assert media.count() == count
if count == 0:
@@ -219,7 +219,7 @@ class TestSubmission:
media = self.check_media(request, {'title': u'Balanced Goblin'}, 1)
assert media.media_type == u'mediagoblin.media_types.image'
- assert isinstance(media.media_manager, img_MEDIA_MANAGER)
+ assert isinstance(media.media_manager, ImageMediaManager)
assert media.media_manager.entry == media
@@ -240,8 +240,8 @@ class TestSubmission:
request = context['request']
- media = request.db.MediaEntry.find_one({
- u'title': u'UNIQUE_TITLE_PLS_DONT_CREATE_OTHER_MEDIA_WITH_THIS_TITLE'})
+ media = request.db.MediaEntry.query.filter_by(
+ title=u'UNIQUE_TITLE_PLS_DONT_CREATE_OTHER_MEDIA_WITH_THIS_TITLE').first()
assert media.media_type == 'mediagoblin.media_types.image'
@@ -252,7 +252,7 @@ class TestSubmission:
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))
- entry = mg_globals.database.MediaEntry.find_one({'title': title})
+ entry = mg_globals.database.MediaEntry.query.filter_by(title=title).first()
assert entry.state == 'failed'
assert entry.fail_error == u'mediagoblin.processing:BadMediaFail'
diff --git a/mediagoblin/tests/tools.py b/mediagoblin/tests/tools.py
index 2584c62f..98361adc 100644
--- a/mediagoblin/tests/tools.py
+++ b/mediagoblin/tests/tools.py
@@ -164,7 +164,7 @@ def assert_db_meets_expected(db, expected):
for collection_name, collection_data in expected.iteritems():
collection = db[collection_name]
for expected_document in collection_data:
- document = collection.find_one({'id': expected_document['id']})
+ document = collection.query.filter_by(id=expected_document['id']).first()
assert document is not None # make sure it exists
assert document == expected_document # make sure it matches
diff --git a/mediagoblin/tools/crypto.py b/mediagoblin/tools/crypto.py
index 1379d21b..917e674c 100644
--- a/mediagoblin/tools/crypto.py
+++ b/mediagoblin/tools/crypto.py
@@ -14,6 +14,8 @@
# 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 base64
+import string
import errno
import itsdangerous
import logging
@@ -24,6 +26,9 @@ from mediagoblin import mg_globals
_log = logging.getLogger(__name__)
+# produces base64 alphabet
+alphabet = string.ascii_letters + "-_"
+base = len(alphabet)
# Use the system (hardware-based) random number generator if it exists.
# -- this optimization is lifted from Django
@@ -111,3 +116,13 @@ def get_timed_signer_url(namespace):
assert __itsda_secret is not None
return itsdangerous.URLSafeTimedSerializer(__itsda_secret,
salt=namespace)
+
+def random_string(length):
+ """ Returns a URL safe base64 encoded crypographically strong string """
+ rstring = ""
+ for i in range(length):
+ n = getrandbits(6) # 6 bytes = 2^6 = 64
+ n = divmod(n, base)[1]
+ rstring += alphabet[n]
+
+ return rstring
diff --git a/mediagoblin/tools/request.py b/mediagoblin/tools/request.py
index ee342eae..d4739039 100644
--- a/mediagoblin/tools/request.py
+++ b/mediagoblin/tools/request.py
@@ -14,12 +14,18 @@
# 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 json
import logging
from mediagoblin.db.models import User
_log = logging.getLogger(__name__)
+# MIME-Types
+form_encoded = "application/x-www-form-urlencoded"
+json_encoded = "application/json"
+
+
def setup_user_in_request(request):
"""
Examine a request and tack on a request.user parameter if that's
@@ -36,3 +42,15 @@ def setup_user_in_request(request):
# this session.
_log.warn("Killing session for user id %r", request.session['user_id'])
request.session.delete()
+
+def decode_request(request):
+ """ Decodes a request based on MIME-Type """
+ data = request.get_data()
+
+ if request.content_type == json_encoded:
+ data = json.loads(data)
+ elif request.content_type == form_encoded or request.content_type == "":
+ data = request.form
+ else:
+ data = ""
+ return data
diff --git a/mediagoblin/tools/response.py b/mediagoblin/tools/response.py
index 0be1f835..b0401e08 100644
--- a/mediagoblin/tools/response.py
+++ b/mediagoblin/tools/response.py
@@ -14,6 +14,8 @@
# 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 json
+
import werkzeug.utils
from werkzeug.wrappers import Response as wz_Response
from mediagoblin.tools.template import render_template
@@ -31,7 +33,6 @@ def render_to_response(request, template, context, status=200):
render_template(request, template, context),
status=status)
-
def render_error(request, status=500, title=_('Oops!'),
err_msg=_('An error occured')):
"""Render any error page with a given error code, title and text body
@@ -44,6 +45,14 @@ def render_error(request, status=500, title=_('Oops!'),
{'err_code': status, 'title': title, 'err_msg': err_msg}),
status=status)
+def render_400(request, err_msg=None):
+ """ Render a standard 400 page"""
+ _ = pass_to_ugettext
+ title = _("Bad Request")
+ if err_msg is None:
+ err_msg = _("The request sent to the server is invalid, please double check it")
+
+ return render_error(request, 400, title, err_msg)
def render_403(request):
"""Render a standard 403 page"""
@@ -106,3 +115,45 @@ def redirect_obj(request, obj):
Requires obj to have a .url_for_self method."""
return redirect(request, location=obj.url_for_self(request.urlgen))
+
+def json_response(serializable, _disable_cors=False, *args, **kw):
+ '''
+ Serializes a json objects and returns a werkzeug Response object with the
+ serialized value as the response body and Content-Type: application/json.
+
+ :param serializable: A json-serializable object
+
+ Any extra arguments and keyword arguments are passed to the
+ Response.__init__ method.
+ '''
+
+ response = wz_Response(json.dumps(serializable), *args, content_type='application/json', **kw)
+
+ if not _disable_cors:
+ cors_headers = {
+ 'Access-Control-Allow-Origin': '*',
+ 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
+ 'Access-Control-Allow-Headers': 'Content-Type, X-Requested-With'}
+ for key, value in cors_headers.iteritems():
+ response.headers.set(key, value)
+
+ return response
+
+def form_response(data, *args, **kwargs):
+ """
+ Responds using application/x-www-form-urlencoded and returns a werkzeug
+ Response object with the data argument as the body
+ and 'application/x-www-form-urlencoded' as the Content-Type.
+
+ Any extra arguments and keyword arguments are passed to the
+ Response.__init__ method.
+ """
+
+ response = wz_Response(
+ data,
+ content_type="application/x-www-form-urlencoded",
+ *args,
+ **kwargs
+ )
+
+ return response
diff --git a/mediagoblin/tools/session.py b/mediagoblin/tools/session.py
index fdc32523..a57f69cc 100644
--- a/mediagoblin/tools/session.py
+++ b/mediagoblin/tools/session.py
@@ -17,10 +17,12 @@
import itsdangerous
import logging
-import crypto
+from mediagoblin.tools import crypto
_log = logging.getLogger(__name__)
+MAX_AGE = 30 * 24 * 60 * 60
+
class Session(dict):
def __init__(self, *args, **kwargs):
self.send_new_cookie = False
@@ -64,5 +66,10 @@ class SessionManager(object):
elif not session:
response.delete_cookie(self.cookie_name)
else:
+ if session.get('stay_logged_in', False):
+ max_age = MAX_AGE
+ else:
+ max_age = None
+
response.set_cookie(self.cookie_name, self.signer.dumps(session),
- httponly=True)
+ max_age=max_age, httponly=True)
diff --git a/mediagoblin/tools/template.py b/mediagoblin/tools/template.py
index 615ce129..fa290611 100644
--- a/mediagoblin/tools/template.py
+++ b/mediagoblin/tools/template.py
@@ -32,7 +32,6 @@ from mediagoblin.tools.timesince import timesince
from mediagoblin.meddleware.csrf import render_csrf_form_token
-
SETUP_JINJA_ENVS = {}
@@ -50,6 +49,12 @@ def get_jinja_env(template_loader, locale):
if locale in SETUP_JINJA_ENVS:
return SETUP_JINJA_ENVS[locale]
+ # The default config does not require a [jinja2] block.
+ # You may create one if you wish to enable additional jinja2 extensions,
+ # see example in config_spec.ini
+ jinja2_config = mg_globals.global_config.get('jinja2', {})
+ local_exts = jinja2_config.get('extensions', [])
+
# jinja2.StrictUndefined will give exceptions on references
# to undefined/unknown variables in templates.
template_env = jinja2.Environment(
@@ -57,7 +62,7 @@ def get_jinja_env(template_loader, locale):
undefined=jinja2.StrictUndefined,
extensions=[
'jinja2.ext.i18n', 'jinja2.ext.autoescape',
- TemplateHookExtension])
+ TemplateHookExtension] + local_exts)
template_env.install_gettext_callables(
mg_globals.thread_scope.translations.ugettext,
@@ -84,6 +89,16 @@ def get_jinja_env(template_loader, locale):
template_env.globals = hook_transform(
'template_global_context', template_env.globals)
+ #### THIS IS TEMPORARY, PLEASE FIX IT
+ ## Notifications stuff is not yet a plugin (and we're not sure it will be),
+ ## but it needs to add stuff to the context. This is THE WRONG WAY TO DO IT
+ from mediagoblin import notifications
+ template_env.globals['get_notifications'] = notifications.get_notifications
+ template_env.globals[
+ 'get_notification_count'] = notifications.get_notification_count
+ template_env.globals[
+ 'get_comment_subscription'] = notifications.get_comment_subscription
+
if exists(locale):
SETUP_JINJA_ENVS[locale] = template_env
diff --git a/mediagoblin/tools/validator.py b/mediagoblin/tools/validator.py
new file mode 100644
index 00000000..03598f9c
--- /dev/null
+++ b/mediagoblin/tools/validator.py
@@ -0,0 +1,46 @@
+# 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 wtforms.validators import Email, URL
+
+def validate_email(email):
+ """
+ Validates an email
+
+ Returns True if valid and False if invalid
+ """
+
+ email_re = Email().regex
+ result = email_re.match(email)
+ if result is None:
+ return False
+ else:
+ return result.string
+
+def validate_url(url):
+ """
+ Validates a url
+
+ Returns True if valid and False if invalid
+ """
+
+ url_re = URL().regex
+ result = url_re.match(url)
+ if result is None:
+ return False
+ else:
+ return result.string
+
diff --git a/mediagoblin/user_pages/forms.py b/mediagoblin/user_pages/forms.py
index 9a193680..ac8084c5 100644
--- a/mediagoblin/user_pages/forms.py
+++ b/mediagoblin/user_pages/forms.py
@@ -23,7 +23,7 @@ class MediaCommentForm(wtforms.Form):
_('Comment'),
[wtforms.validators.Required()],
description=_(u'You can use '
- u'<a href="http://daringfireball.net/projects/markdown/basics">'
+ u'<a href="http://daringfireball.net/projects/markdown/basics" target="_blank">'
u'Markdown</a> for formatting.'))
class ConfirmDeleteForm(wtforms.Form):
@@ -47,5 +47,5 @@ class MediaCollectForm(wtforms.Form):
collection_description = wtforms.TextAreaField(
_('Description of this collection'),
description=_("""You can use
- <a href="http://daringfireball.net/projects/markdown/basics">
+ <a href="http://daringfireball.net/projects/markdown/basics" target="_blank">
Markdown</a> for formatting."""))
diff --git a/mediagoblin/user_pages/routing.py b/mediagoblin/user_pages/routing.py
index 9cb665b5..b1dde397 100644
--- a/mediagoblin/user_pages/routing.py
+++ b/mediagoblin/user_pages/routing.py
@@ -32,6 +32,10 @@ add_route('mediagoblin.user_pages.media_post_comment',
'/u/<string:user>/m/<int:media_id>/comment/add/',
'mediagoblin.user_pages.views:media_post_comment')
+add_route('mediagoblin.user_pages.media_preview_comment',
+ '/ajax/comment/preview/',
+ 'mediagoblin.user_pages.views:media_preview_comment')
+
add_route('mediagoblin.user_pages.user_gallery',
'/u/<string:user>/gallery/',
'mediagoblin.user_pages.views:user_gallery')
diff --git a/mediagoblin/user_pages/views.py b/mediagoblin/user_pages/views.py
index 83a524ec..91ea04b8 100644
--- a/mediagoblin/user_pages/views.py
+++ b/mediagoblin/user_pages/views.py
@@ -16,19 +16,20 @@
import logging
import datetime
+import json
from mediagoblin import messages, mg_globals
from mediagoblin.db.models import (MediaEntry, MediaTag, Collection,
CollectionItem, User)
from mediagoblin.tools.response import render_to_response, render_404, \
redirect, redirect_obj
+from mediagoblin.tools.text import cleaned_markdown_conversion
from mediagoblin.tools.translate import pass_to_ugettext as _
from mediagoblin.tools.pagination import Pagination
from mediagoblin.user_pages import forms as user_forms
from mediagoblin.user_pages.lib import add_media_to_collection
from mediagoblin.notifications import trigger_notification, \
add_comment_subscription, mark_comment_notification_seen
-
from mediagoblin.decorators import (uses_pagination, get_user_media_entry,
get_media_entry_by_id,
require_active_login, user_may_delete_media, user_may_alter_collection,
@@ -36,6 +37,7 @@ from mediagoblin.decorators import (uses_pagination, get_user_media_entry,
from werkzeug.contrib.atom import AtomFeed
from werkzeug.exceptions import MethodNotAllowed
+from werkzeug.wrappers import Response
_log = logging.getLogger(__name__)
@@ -142,7 +144,7 @@ def media_home(request, media, page, **kwargs):
comment_form = user_forms.MediaCommentForm(request.form)
- media_template_name = media.media_manager['display_template']
+ media_template_name = media.media_manager.display_template
return render_to_response(
request,
@@ -166,6 +168,7 @@ def media_post_comment(request, media):
comment = request.db.MediaComment()
comment.media_entry = media.id
comment.author = request.user.id
+ print request.form['comment_content']
comment.content = unicode(request.form['comment_content'])
# Show error message if commenting is disabled.
@@ -193,6 +196,18 @@ def media_post_comment(request, media):
return redirect_obj(request, media)
+
+def media_preview_comment(request):
+ """Runs a comment through markdown so it can be previewed."""
+ # If this isn't an ajax request, render_404
+ if not request.is_xhr:
+ return render_404(request)
+
+ comment = unicode(request.form['comment_content'])
+ cleancomment = { "content":cleaned_markdown_conversion(comment)}
+
+ return Response(json.dumps(cleancomment))
+
@get_media_entry_by_id
@require_active_login
def media_collect(request, media):