aboutsummaryrefslogtreecommitdiffstats
path: root/mediagoblin
diff options
context:
space:
mode:
Diffstat (limited to 'mediagoblin')
-rw-r--r--mediagoblin/app.py5
-rw-r--r--mediagoblin/auth/forms.py8
-rw-r--r--mediagoblin/auth/routing.py10
-rw-r--r--mediagoblin/auth/views.py31
-rw-r--r--mediagoblin/db/mongo/__init__.py (renamed from mediagoblin/templates/mediagoblin/auth/fp_changed_success.html)14
-rw-r--r--mediagoblin/db/mongo/indexes.py (renamed from mediagoblin/db/indexes.py)0
-rw-r--r--mediagoblin/db/mongo/migrations.py (renamed from mediagoblin/db/migrations.py)2
-rw-r--r--mediagoblin/db/mongo/models.py (renamed from mediagoblin/db/models.py)24
-rw-r--r--mediagoblin/db/mongo/open.py78
-rw-r--r--mediagoblin/db/mongo/util.py292
-rw-r--r--mediagoblin/db/open.py41
-rw-r--r--mediagoblin/db/sql/base.py38
-rw-r--r--mediagoblin/db/sql/convert.py27
-rw-r--r--mediagoblin/db/sql/models.py24
-rw-r--r--mediagoblin/db/sql/open.py33
-rw-r--r--mediagoblin/db/util.py278
-rw-r--r--mediagoblin/decorators.py6
-rw-r--r--mediagoblin/edit/forms.py38
-rw-r--r--mediagoblin/edit/routing.py5
-rw-r--r--mediagoblin/edit/views.py40
-rw-r--r--mediagoblin/gmg_commands/import_export.py4
-rw-r--r--mediagoblin/gmg_commands/migrate.py4
-rw-r--r--mediagoblin/init/__init__.py24
-rw-r--r--mediagoblin/listings/views.py2
-rw-r--r--mediagoblin/media_types/__init__.py20
-rw-r--r--mediagoblin/media_types/ascii/__init__.py27
-rw-r--r--mediagoblin/media_types/ascii/asciitoimage.py172
l---------mediagoblin/media_types/ascii/fonts/Inconsolata.otf1
-rw-r--r--mediagoblin/media_types/ascii/processing.py93
-rw-r--r--mediagoblin/media_types/image/processing.py4
-rw-r--r--mediagoblin/media_types/video/processing.py8
-rw-r--r--mediagoblin/processing.py2
-rw-r--r--mediagoblin/routing.py4
-rw-r--r--mediagoblin/static/css/base.css53
l---------mediagoblin/static/fonts/Inconsolata.otf1
-rw-r--r--mediagoblin/static/js/comment_show.js9
-rw-r--r--mediagoblin/static/js/show_password.js19
-rw-r--r--mediagoblin/submit/forms.py7
-rw-r--r--mediagoblin/submit/views.py20
-rw-r--r--mediagoblin/templates/mediagoblin/auth/change_fp.html5
-rw-r--r--mediagoblin/templates/mediagoblin/auth/register.html19
-rw-r--r--mediagoblin/templates/mediagoblin/edit/attachments.html4
-rw-r--r--mediagoblin/templates/mediagoblin/edit/edit.html4
-rw-r--r--mediagoblin/templates/mediagoblin/edit/edit_account.html45
-rw-r--r--mediagoblin/templates/mediagoblin/media_displays/ascii.html40
-rw-r--r--mediagoblin/templates/mediagoblin/media_displays/image.html18
-rw-r--r--mediagoblin/templates/mediagoblin/media_displays/video.html31
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/media.html128
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/media_confirm_delete.html4
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/user.html13
-rw-r--r--mediagoblin/templates/mediagoblin/utils/object_gallery.html8
-rw-r--r--mediagoblin/templates/mediagoblin/utils/wtforms.html4
-rw-r--r--mediagoblin/templates/mediagoblin/webfinger/host-meta.xml27
-rw-r--r--mediagoblin/templates/mediagoblin/webfinger/xrd.xml27
-rw-r--r--mediagoblin/tests/test_auth.py23
-rw-r--r--mediagoblin/tests/test_edit.py12
-rw-r--r--mediagoblin/tests/test_migrations.py4
-rw-r--r--mediagoblin/tests/test_submission.py9
-rw-r--r--mediagoblin/tools/files.py2
-rw-r--r--mediagoblin/tools/template.py3
-rw-r--r--mediagoblin/user_pages/views.py4
-rw-r--r--mediagoblin/webfinger/__init__.py (renamed from mediagoblin/templates/mediagoblin/auth/fp_email_sent.html)23
-rw-r--r--mediagoblin/webfinger/routing.py25
-rw-r--r--mediagoblin/webfinger/views.py117
64 files changed, 1444 insertions, 623 deletions
diff --git a/mediagoblin/app.py b/mediagoblin/app.py
index 04eb2acc..96b2c8ab 100644
--- a/mediagoblin/app.py
+++ b/mediagoblin/app.py
@@ -122,6 +122,11 @@ class MediaGoblinApp(object):
# The other option would be:
# request.full_path = environ["SCRIPT_URL"]
+ # Fix up environ for urlgen
+ # See bug: https://bitbucket.org/bbangert/routes/issue/55/cache_hostinfo-breaks-on-https-off
+ if environ.get('HTTPS', '').lower() == 'off':
+ environ.pop('HTTPS')
+
## Attach utilities to the request object
request.matchdict = route_match
request.urlgen = routes.URLGenerator(self.routing, environ)
diff --git a/mediagoblin/auth/forms.py b/mediagoblin/auth/forms.py
index 4cd3e9d8..5a707c7b 100644
--- a/mediagoblin/auth/forms.py
+++ b/mediagoblin/auth/forms.py
@@ -62,13 +62,7 @@ class ChangePassForm(wtforms.Form):
password = wtforms.PasswordField(
'Password',
[wtforms.validators.Required(),
- wtforms.validators.Length(min=6, max=30),
- wtforms.validators.EqualTo(
- 'confirm_password',
- 'Passwords must match.')])
- confirm_password = wtforms.PasswordField(
- 'Confirm password',
- [wtforms.validators.Required()])
+ wtforms.validators.Length(min=6, max=30)])
userid = wtforms.HiddenField(
'',
[wtforms.validators.Required()])
diff --git a/mediagoblin/auth/routing.py b/mediagoblin/auth/routing.py
index 365ccfaa..ea9388c5 100644
--- a/mediagoblin/auth/routing.py
+++ b/mediagoblin/auth/routing.py
@@ -35,12 +35,4 @@ auth_routes = [
controller='mediagoblin.auth.views:forgot_password'),
Route('mediagoblin.auth.verify_forgot_password',
'/forgot_password/verify/',
- controller='mediagoblin.auth.views:verify_forgot_password'),
- Route('mediagoblin.auth.fp_changed_success',
- '/forgot_password/changed_success/',
- template='mediagoblin/auth/fp_changed_success.html',
- controller='mediagoblin.views:simple_template_render'),
- Route('mediagoblin.auth.fp_email_sent',
- '/forgot_password/email_sent/',
- template='mediagoblin/auth/fp_email_sent.html',
- controller='mediagoblin.views:simple_template_render')]
+ controller='mediagoblin.auth.views:verify_forgot_password')]
diff --git a/mediagoblin/auth/views.py b/mediagoblin/auth/views.py
index 919aa3cd..88dc40ad 100644
--- a/mediagoblin/auth/views.py
+++ b/mediagoblin/auth/views.py
@@ -84,6 +84,7 @@ def register(request):
user.email = email
user.pw_hash = auth_lib.bcrypt_gen_password_hash(
request.POST['password'])
+ user.verification_key = unicode(uuid.uuid4())
user.save(validate=True)
# log the user in
@@ -231,16 +232,12 @@ def forgot_password(request):
"""
Forgot password view
- Sends an email whit an url to renew forgoten password
+ Sends an email with an url to renew forgotten password
"""
fp_form = auth_forms.ForgotPassForm(request.POST)
if request.method == 'POST' and fp_form.validate():
- # Here, so it doesn't depend on the actual mail being sent
- # and thus doesn't reveal, wether mail was sent.
- email_debug_message(request)
-
# '$or' not available till mongodb 1.5.3
user = request.db.User.find_one(
{'username': request.POST['username']})
@@ -256,6 +253,14 @@ def forgot_password(request):
user.save()
send_fp_verification_email(user, request)
+
+ messages.add_message(
+ request,
+ messages.INFO,
+ _("An email has been sent with instructions on how to "
+ "change your password."))
+ email_debug_message(request)
+
else:
# special case... we can't send the email because the
# username is inactive / hasn't verified their email
@@ -269,9 +274,13 @@ def forgot_password(request):
return redirect(
request, 'mediagoblin.user_pages.user_home',
user=user.username)
-
- # do not reveal whether or not there is a matching user
- return redirect(request, 'mediagoblin.auth.fp_email_sent')
+ return redirect(request, 'mediagoblin.auth.login')
+ else:
+ messages.add_message(
+ request,
+ messages.WARNING,
+ _("Couldn't find someone with that username or email."))
+ return redirect(request, 'mediagoblin.auth.forgot_password')
return render_to_response(
request,
@@ -315,7 +324,11 @@ def verify_forgot_password(request):
user.fp_token_expire = None
user.save()
- return redirect(request, 'mediagoblin.auth.fp_changed_success')
+ messages.add_message(
+ request,
+ messages.INFO,
+ _("You can now log in using your new password."))
+ return redirect(request, 'mediagoblin.auth.login')
else:
return render_to_response(
request,
diff --git a/mediagoblin/templates/mediagoblin/auth/fp_changed_success.html b/mediagoblin/db/mongo/__init__.py
index 7cea312d..ba347c69 100644
--- a/mediagoblin/templates/mediagoblin/auth/fp_changed_success.html
+++ b/mediagoblin/db/mongo/__init__.py
@@ -1,6 +1,5 @@
-{#
# GNU MediaGoblin -- federated, autonomous media hosting
-# Copyright (C) 2011 Free Software Foundation, Inc
+# Copyright (C) 2011 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
@@ -14,14 +13,3 @@
#
# 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 mediagoblin_content %}
- <p>
- {% trans -%}
- Your password has been changed. Try to log in now.
- {%- endtrans %}
- </p>
-{% endblock %}
-
diff --git a/mediagoblin/db/indexes.py b/mediagoblin/db/mongo/indexes.py
index 1dd73f2b..1dd73f2b 100644
--- a/mediagoblin/db/indexes.py
+++ b/mediagoblin/db/mongo/indexes.py
diff --git a/mediagoblin/db/migrations.py b/mediagoblin/db/mongo/migrations.py
index cfc01287..cf4e94ae 100644
--- a/mediagoblin/db/migrations.py
+++ b/mediagoblin/db/mongo/migrations.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 mediagoblin.db.util import RegisterMigration
+from mediagoblin.db.mongo.util import RegisterMigration
from mediagoblin.tools.text import cleaned_markdown_conversion
diff --git a/mediagoblin/db/models.py b/mediagoblin/db/mongo/models.py
index 569c3600..5de59c12 100644
--- a/mediagoblin/db/models.py
+++ b/mediagoblin/db/mongo/models.py
@@ -15,14 +15,13 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import datetime
-import uuid
from mongokit import Document
from mediagoblin.auth import lib as auth_lib
from mediagoblin import mg_globals
-from mediagoblin.db import migrations
-from mediagoblin.db.util import ASCENDING, DESCENDING, ObjectId
+from mediagoblin.db.mongo import migrations
+from mediagoblin.db.mongo.util import ASCENDING, DESCENDING, ObjectId
from mediagoblin.tools.pagination import Pagination
from mediagoblin.tools import url, common
@@ -88,7 +87,6 @@ class User(Document):
'created': datetime.datetime.utcnow,
'email_verified': False,
'status': u'needs_email_verification',
- 'verification_key': lambda: unicode(uuid.uuid4()),
'is_admin': False}
def check_login(self, password):
@@ -263,7 +261,7 @@ class MediaEntry(Document):
Use a slug if we have one, else use our '_id'.
"""
- uploader = self.get_uploader()
+ uploader = self.get_uploader
if self.get('slug'):
return urlgen(
@@ -284,10 +282,8 @@ class MediaEntry(Document):
'uploader': self.uploader,
'state': 'processed'}).sort(
'_id', ASCENDING).limit(1)
- if cursor.count():
- return urlgen('mediagoblin.user_pages.media_home',
- user=self.get_uploader().username,
- media=unicode(cursor[0].slug))
+ for media in cursor:
+ return media.url_for_self(urlgen)
def url_to_next(self, urlgen):
"""
@@ -298,11 +294,10 @@ class MediaEntry(Document):
'state': 'processed'}).sort(
'_id', DESCENDING).limit(1)
- if cursor.count():
- return urlgen('mediagoblin.user_pages.media_home',
- user=self.get_uploader().username,
- media=unicode(cursor[0].slug))
+ for media in cursor:
+ return media.url_for_self(urlgen)
+ @property
def get_uploader(self):
return self.db.User.find_one({'_id': self.uploader})
@@ -346,7 +341,8 @@ class MediaComment(Document):
def media_entry(self):
return self.db.MediaEntry.find_one({'_id': self['media_entry']})
- def author(self):
+ @property
+ def get_author(self):
return self.db.User.find_one({'_id': self['author']})
diff --git a/mediagoblin/db/mongo/open.py b/mediagoblin/db/mongo/open.py
new file mode 100644
index 00000000..48c909d9
--- /dev/null
+++ b/mediagoblin/db/mongo/open.py
@@ -0,0 +1,78 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011 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 pymongo
+import mongokit
+from paste.deploy.converters import asint
+from mediagoblin.db.mongo import models
+from mediagoblin.db.mongo.util import MigrationManager
+
+
+def connect_database_from_config(app_config, use_pymongo=False):
+ """
+ Connect to the main database, take config from app_config
+
+ Optionally use pymongo instead of mongokit for the connection.
+ """
+ port = app_config.get('db_port')
+ if port:
+ port = asint(port)
+
+ if use_pymongo:
+ connection = pymongo.Connection(
+ app_config.get('db_host'), port)
+ else:
+ connection = mongokit.Connection(
+ app_config.get('db_host'), port)
+ return connection
+
+
+def setup_connection_and_db_from_config(app_config, use_pymongo=False):
+ """
+ Setup connection and database from config.
+
+ Optionally use pymongo instead of mongokit.
+ """
+ connection = connect_database_from_config(app_config, use_pymongo)
+ database_path = app_config['db_name']
+ db = connection[database_path]
+
+ if not use_pymongo:
+ models.register_models(connection)
+
+ return (connection, db)
+
+
+def check_db_migrations_current(db):
+ # This MUST be imported so as to set up the appropriate migrations!
+ from mediagoblin.db.mongo import migrations
+
+ # Init the migration number if necessary
+ migration_manager = MigrationManager(db)
+ migration_manager.install_migration_version_if_missing()
+
+ # Tiny hack to warn user if our migration is out of date
+ if not migration_manager.database_at_latest_migration():
+ db_migration_num = migration_manager.database_current_migration()
+ latest_migration_num = migration_manager.latest_migration()
+ if db_migration_num < latest_migration_num:
+ print (
+ "*WARNING:* Your migrations are out of date, "
+ "maybe run ./bin/gmg migrate?")
+ elif db_migration_num > latest_migration_num:
+ print (
+ "*WARNING:* Your migrations are out of date... "
+ "in fact they appear to be from the future?!")
diff --git a/mediagoblin/db/mongo/util.py b/mediagoblin/db/mongo/util.py
new file mode 100644
index 00000000..e2065693
--- /dev/null
+++ b/mediagoblin/db/mongo/util.py
@@ -0,0 +1,292 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011 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/>.
+
+"""
+Utilities for database operations.
+
+Some note on migration and indexing tools:
+
+We store information about what the state of the database is in the
+'mediagoblin' document of the 'app_metadata' collection. Keys in that
+document relevant to here:
+
+ - 'migration_number': The integer representing the current state of
+ the migrations
+"""
+
+import copy
+
+# Imports that other modules might use
+from pymongo import ASCENDING, DESCENDING
+from pymongo.errors import InvalidId
+from mongokit import ObjectId
+
+from mediagoblin.db.mongo.indexes import ACTIVE_INDEXES, DEPRECATED_INDEXES
+
+
+################
+# Indexing tools
+################
+
+
+def add_new_indexes(database, active_indexes=ACTIVE_INDEXES):
+ """
+ Add any new indexes to the database.
+
+ Args:
+ - database: pymongo or mongokit database instance.
+ - active_indexes: indexes to possibly add in the pattern of:
+ {'collection_name': {
+ 'identifier': {
+ 'index': [index_foo_goes_here],
+ 'unique': True}}
+ where 'index' is the index to add and all other options are
+ arguments for collection.create_index.
+
+ Returns:
+ A list of indexes added in form ('collection', 'index_name')
+ """
+ indexes_added = []
+
+ for collection_name, indexes in active_indexes.iteritems():
+ collection = database[collection_name]
+ collection_indexes = collection.index_information().keys()
+
+ for index_name, index_data in indexes.iteritems():
+ if not index_name in collection_indexes:
+ # Get a copy actually so we don't modify the actual
+ # structure
+ index_data = copy.copy(index_data)
+ index = index_data.pop('index')
+ collection.create_index(
+ index, name=index_name, **index_data)
+
+ indexes_added.append((collection_name, index_name))
+
+ return indexes_added
+
+
+def remove_deprecated_indexes(database, deprecated_indexes=DEPRECATED_INDEXES):
+ """
+ Remove any deprecated indexes from the database.
+
+ Args:
+ - database: pymongo or mongokit database instance.
+ - deprecated_indexes: the indexes to deprecate in the pattern of:
+ {'collection_name': {
+ 'identifier': {
+ 'index': [index_foo_goes_here],
+ 'unique': True}}
+
+ (... although we really only need the 'identifier' here, as the
+ rest of the information isn't used in this case. But it's kept
+ around so we can remember what it was)
+
+ Returns:
+ A list of indexes removed in form ('collection', 'index_name')
+ """
+ indexes_removed = []
+
+ for collection_name, indexes in deprecated_indexes.iteritems():
+ collection = database[collection_name]
+ collection_indexes = collection.index_information().keys()
+
+ for index_name, index_data in indexes.iteritems():
+ if index_name in collection_indexes:
+ collection.drop_index(index_name)
+
+ indexes_removed.append((collection_name, index_name))
+
+ return indexes_removed
+
+
+#################
+# Migration tools
+#################
+
+# The default migration registry...
+#
+# Don't set this yourself! RegisterMigration will automatically fill
+# this with stuff via decorating methods in migrations.py
+
+class MissingCurrentMigration(Exception):
+ pass
+
+
+MIGRATIONS = {}
+
+
+class RegisterMigration(object):
+ """
+ Tool for registering migrations
+
+ Call like:
+
+ @RegisterMigration(33)
+ def update_dwarves(database):
+ [...]
+
+ This will register your migration with the default migration
+ registry. Alternately, to specify a very specific
+ migration_registry, you can pass in that as the second argument.
+
+ Note, the number of your migration should NEVER be 0 or less than
+ 0. 0 is the default "no migrations" state!
+ """
+ def __init__(self, migration_number, migration_registry=MIGRATIONS):
+ assert migration_number > 0, "Migration number must be > 0!"
+ assert migration_number not in migration_registry, \
+ "Duplicate migration numbers detected! That's not allowed!"
+
+ self.migration_number = migration_number
+ self.migration_registry = migration_registry
+
+ def __call__(self, migration):
+ self.migration_registry[self.migration_number] = migration
+ return migration
+
+
+class MigrationManager(object):
+ """
+ Migration handling tool.
+
+ Takes information about a database, lets you update the database
+ to the latest migrations, etc.
+ """
+ def __init__(self, database, migration_registry=MIGRATIONS):
+ """
+ Args:
+ - database: database we're going to migrate
+ - migration_registry: where we should find all migrations to
+ run
+ """
+ self.database = database
+ self.migration_registry = migration_registry
+ self._sorted_migrations = None
+
+ def _ensure_current_migration_record(self):
+ """
+ If there isn't a database[u'app_metadata'] mediagoblin entry
+ with the 'current_migration', throw an error.
+ """
+ if self.database_current_migration() is None:
+ raise MissingCurrentMigration(
+ "Tried to call function which requires "
+ "'current_migration' set in database")
+
+ @property
+ def sorted_migrations(self):
+ """
+ Sort migrations if necessary and store in self._sorted_migrations
+ """
+ if not self._sorted_migrations:
+ self._sorted_migrations = sorted(
+ self.migration_registry.items(),
+ # sort on the key... the migration number
+ key=lambda migration_tuple: migration_tuple[0])
+
+ return self._sorted_migrations
+
+ def latest_migration(self):
+ """
+ Return a migration number for the latest migration, or 0 if
+ there are no migrations.
+ """
+ if self.sorted_migrations:
+ return self.sorted_migrations[-1][0]
+ else:
+ # If no migrations have been set, we start at 0.
+ return 0
+
+ def set_current_migration(self, migration_number):
+ """
+ Set the migration in the database to migration_number
+ """
+ # Add the mediagoblin migration if necessary
+ self.database[u'app_metadata'].update(
+ {u'_id': u'mediagoblin'},
+ {u'$set': {u'current_migration': migration_number}},
+ upsert=True)
+
+ def install_migration_version_if_missing(self):
+ """
+ Sets the migration to the latest version if no migration
+ version at all is set.
+ """
+ mgoblin_metadata = self.database[u'app_metadata'].find_one(
+ {u'_id': u'mediagoblin'})
+ if not mgoblin_metadata:
+ latest_migration = self.latest_migration()
+ self.set_current_migration(latest_migration)
+
+ def database_current_migration(self):
+ """
+ Return the current migration in the database.
+ """
+ mgoblin_metadata = self.database[u'app_metadata'].find_one(
+ {u'_id': u'mediagoblin'})
+ if not mgoblin_metadata:
+ return None
+ else:
+ return mgoblin_metadata[u'current_migration']
+
+ def database_at_latest_migration(self):
+ """
+ See if the database is at the latest migration.
+ Returns a boolean.
+ """
+ current_migration = self.database_current_migration()
+ return current_migration == self.latest_migration()
+
+ def migrations_to_run(self):
+ """
+ Get a list of migrations to run still, if any.
+
+ Note that calling this will set your migration version to the
+ latest version if it isn't installed to anything yet!
+ """
+ self._ensure_current_migration_record()
+
+ db_current_migration = self.database_current_migration()
+
+ return [
+ (migration_number, migration_func)
+ for migration_number, migration_func in self.sorted_migrations
+ if migration_number > db_current_migration]
+
+ def migrate_new(self, pre_callback=None, post_callback=None):
+ """
+ Run all migrations.
+
+ Includes two optional args:
+ - pre_callback: if called, this is a callback on something to
+ run pre-migration. Takes (migration_number, migration_func)
+ as arguments
+ - pre_callback: if called, this is a callback on something to
+ run post-migration. Takes (migration_number, migration_func)
+ as arguments
+ """
+ # If we aren't set to any version number, presume we're at the
+ # latest (which means we'll do nothing here...)
+ self.install_migration_version_if_missing()
+
+ for migration_number, migration_func in self.migrations_to_run():
+ if pre_callback:
+ pre_callback(migration_number, migration_func)
+ migration_func(self.database)
+ self.set_current_migration(migration_number)
+ if post_callback:
+ post_callback(migration_number, migration_func)
diff --git a/mediagoblin/db/open.py b/mediagoblin/db/open.py
index e677ba12..32827fcb 100644
--- a/mediagoblin/db/open.py
+++ b/mediagoblin/db/open.py
@@ -14,42 +14,5 @@
# 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 pymongo
-import mongokit
-from paste.deploy.converters import asint
-from mediagoblin.db import models
-
-
-def connect_database_from_config(app_config, use_pymongo=False):
- """
- Connect to the main database, take config from app_config
-
- Optionally use pymongo instead of mongokit for the connection.
- """
- port = app_config.get('db_port')
- if port:
- port = asint(port)
-
- if use_pymongo:
- connection = pymongo.Connection(
- app_config.get('db_host'), port)
- else:
- connection = mongokit.Connection(
- app_config.get('db_host'), port)
- return connection
-
-
-def setup_connection_and_db_from_config(app_config, use_pymongo=False):
- """
- Setup connection and database from config.
-
- Optionally use pymongo instead of mongokit.
- """
- connection = connect_database_from_config(app_config, use_pymongo)
- database_path = app_config['db_name']
- db = connection[database_path]
-
- if not use_pymongo:
- models.register_models(connection)
-
- return (connection, db)
+from mediagoblin.db.mongo.open import \
+ setup_connection_and_db_from_config, check_db_migrations_current
diff --git a/mediagoblin/db/sql/base.py b/mediagoblin/db/sql/base.py
new file mode 100644
index 00000000..40140327
--- /dev/null
+++ b/mediagoblin/db/sql/base.py
@@ -0,0 +1,38 @@
+from sqlalchemy.orm import scoped_session, sessionmaker, object_session
+
+
+Session = scoped_session(sessionmaker())
+
+
+def _fix_query_dict(query_dict):
+ if '_id' in query_dict:
+ query_dict['id'] = query_dict.pop('_id')
+
+
+class GMGTableBase(object):
+ query = Session.query_property()
+
+ @classmethod
+ def find(cls, query_dict={}):
+ _fix_query_dict(query_dict)
+ return cls.query.filter_by(**query_dict)
+
+ @classmethod
+ def find_one(cls, query_dict={}):
+ _fix_query_dict(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)
+
+ def save(self, validate = True):
+ assert validate
+ sess = object_session(self)
+ if sess is None:
+ sess = Session()
+ sess.add(self)
+ sess.commit()
diff --git a/mediagoblin/db/sql/convert.py b/mediagoblin/db/sql/convert.py
index 2ffa9fd7..6698b767 100644
--- a/mediagoblin/db/sql/convert.py
+++ b/mediagoblin/db/sql/convert.py
@@ -1,13 +1,13 @@
-from sqlalchemy import create_engine
-from sqlalchemy.orm import sessionmaker
-
from mediagoblin.init import setup_global_and_app_config, setup_database
-from mediagoblin.db.util import ObjectId
+from mediagoblin.db.mongo.util import ObjectId
from mediagoblin.db.sql.models import (Base, User, MediaEntry, MediaComment,
Tag, MediaTag)
-
-Session = sessionmaker()
+from mediagoblin.db.sql.open import setup_connection_and_db_from_config as \
+ sql_connect
+from mediagoblin.db.mongo.open import setup_connection_and_db_from_config as \
+ mongo_connect
+from mediagoblin.db.sql.base import Session
obj_id_table = dict()
@@ -61,7 +61,7 @@ def convert_media_entries(mk_db):
copy_attrs(entry, new_entry,
('title', 'slug', 'created',
'description', 'description_html',
- 'media_type',
+ 'media_type', 'state',
'fail_error',
'queued_task_id',))
copy_reference_attr(entry, new_entry, "uploader")
@@ -124,19 +124,22 @@ def convert_media_comments(mk_db):
def main():
- engine = create_engine('sqlite:///mediagoblin.db', echo=True)
- Session.configure(bind=engine)
+ global_config, app_config = setup_global_and_app_config("mediagoblin.ini")
- setup_global_and_app_config("mediagoblin.ini")
+ sql_conn, sql_db = sql_connect({'sql_engine': 'sqlite:///mediagoblin.db'})
- mk_conn, mk_db = setup_database()
+ mk_conn, mk_db = mongo_connect(app_config)
- Base.metadata.create_all(engine)
+ Base.metadata.create_all(sql_db.engine)
convert_users(mk_db)
+ Session.remove()
convert_media_entries(mk_db)
+ Session.remove()
convert_media_tags(mk_db)
+ Session.remove()
convert_media_comments(mk_db)
+ Session.remove()
if __name__ == '__main__':
diff --git a/mediagoblin/db/sql/models.py b/mediagoblin/db/sql/models.py
index 7723a753..31a6ed3b 100644
--- a/mediagoblin/db/sql/models.py
+++ b/mediagoblin/db/sql/models.py
@@ -4,9 +4,24 @@ from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import (
Column, Integer, Unicode, UnicodeText, DateTime, Boolean, ForeignKey,
UniqueConstraint)
+from sqlalchemy.orm import relationship
+from mediagoblin.db.sql.base import GMGTableBase
-Base = declarative_base()
+
+Base = declarative_base(cls=GMGTableBase)
+
+
+class SimpleFieldAlias(object):
+ """An alias for any field"""
+ def __init__(self, fieldname):
+ self.fieldname = fieldname
+
+ def __get__(self, instance, cls):
+ return getattr(instance, self.fieldname)
+
+ def __set__(self, instance, val):
+ setattr(instance, self.fieldname, val)
class User(Base):
@@ -30,6 +45,8 @@ class User(Base):
## TODO
# plugin data would be in a separate model
+ _id = SimpleFieldAlias("id")
+
class MediaEntry(Base):
__tablename__ = "media_entries"
@@ -42,6 +59,7 @@ class MediaEntry(Base):
description = Column(UnicodeText) # ??
description_html = Column(UnicodeText) # ??
media_type = Column(Unicode, nullable=False)
+ state = Column(Unicode, nullable=False) # or use sqlalchemy.types.Enum?
fail_error = Column(Unicode)
fail_metadata = Column(UnicodeText)
@@ -54,6 +72,8 @@ class MediaEntry(Base):
UniqueConstraint('uploader', 'slug'),
{})
+ get_uploader = relationship(User)
+
## TODO
# media_files
# media_data
@@ -95,6 +115,8 @@ class MediaComment(Base):
content = Column(UnicodeText, nullable=False)
content_html = Column(UnicodeText)
+ get_author = relationship(User)
+
def show_table_init():
from sqlalchemy import create_engine
diff --git a/mediagoblin/db/sql/open.py b/mediagoblin/db/sql/open.py
new file mode 100644
index 00000000..c682bd3b
--- /dev/null
+++ b/mediagoblin/db/sql/open.py
@@ -0,0 +1,33 @@
+from sqlalchemy import create_engine
+
+from mediagoblin.db.sql.base import Session
+from mediagoblin.db.sql.models import Base
+
+
+class DatabaseMaster(object):
+ def __init__(self, engine):
+ self.engine = engine
+
+ for k,v in Base._decl_class_registry.iteritems():
+ setattr(self, k, v)
+
+ def commit(self):
+ Session.commit()
+
+ def save(self, obj):
+ Session.add(obj)
+ Session.flush()
+
+ def reset_after_request(self):
+ Session.remove()
+
+
+def setup_connection_and_db_from_config(app_config):
+ engine = create_engine(app_config['sql_engine'], echo=True)
+ Session.configure(bind=engine)
+
+ return "dummy conn", DatabaseMaster(engine)
+
+
+def check_db_migrations_current(db):
+ pass
diff --git a/mediagoblin/db/util.py b/mediagoblin/db/util.py
index 52e97f6d..1df9494c 100644
--- a/mediagoblin/db/util.py
+++ b/mediagoblin/db/util.py
@@ -14,279 +14,5 @@
# 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/>.
-"""
-Utilities for database operations.
-
-Some note on migration and indexing tools:
-
-We store information about what the state of the database is in the
-'mediagoblin' document of the 'app_metadata' collection. Keys in that
-document relevant to here:
-
- - 'migration_number': The integer representing the current state of
- the migrations
-"""
-
-import copy
-
-# Imports that other modules might use
-from pymongo import ASCENDING, DESCENDING
-from pymongo.errors import InvalidId
-from mongokit import ObjectId
-
-from mediagoblin.db.indexes import ACTIVE_INDEXES, DEPRECATED_INDEXES
-
-
-################
-# Indexing tools
-################
-
-
-def add_new_indexes(database, active_indexes=ACTIVE_INDEXES):
- """
- Add any new indexes to the database.
-
- Args:
- - database: pymongo or mongokit database instance.
- - active_indexes: indexes to possibly add in the pattern of:
- {'collection_name': {
- 'identifier': {
- 'index': [index_foo_goes_here],
- 'unique': True}}
- where 'index' is the index to add and all other options are
- arguments for collection.create_index.
-
- Returns:
- A list of indexes added in form ('collection', 'index_name')
- """
- indexes_added = []
-
- for collection_name, indexes in active_indexes.iteritems():
- collection = database[collection_name]
- collection_indexes = collection.index_information().keys()
-
- for index_name, index_data in indexes.iteritems():
- if not index_name in collection_indexes:
- # Get a copy actually so we don't modify the actual
- # structure
- index_data = copy.copy(index_data)
- index = index_data.pop('index')
- collection.create_index(
- index, name=index_name, **index_data)
-
- indexes_added.append((collection_name, index_name))
-
- return indexes_added
-
-
-def remove_deprecated_indexes(database, deprecated_indexes=DEPRECATED_INDEXES):
- """
- Remove any deprecated indexes from the database.
-
- Args:
- - database: pymongo or mongokit database instance.
- - deprecated_indexes: the indexes to deprecate in the pattern of:
- {'collection_name': {
- 'identifier': {
- 'index': [index_foo_goes_here],
- 'unique': True}}
-
- (... although we really only need the 'identifier' here, as the
- rest of the information isn't used in this case. But it's kept
- around so we can remember what it was)
-
- Returns:
- A list of indexes removed in form ('collection', 'index_name')
- """
- indexes_removed = []
-
- for collection_name, indexes in deprecated_indexes.iteritems():
- collection = database[collection_name]
- collection_indexes = collection.index_information().keys()
-
- for index_name, index_data in indexes.iteritems():
- if index_name in collection_indexes:
- collection.drop_index(index_name)
-
- indexes_removed.append((collection_name, index_name))
-
- return indexes_removed
-
-
-#################
-# Migration tools
-#################
-
-# The default migration registry...
-#
-# Don't set this yourself! RegisterMigration will automatically fill
-# this with stuff via decorating methods in migrations.py
-
-class MissingCurrentMigration(Exception):
- pass
-
-
-MIGRATIONS = {}
-
-
-class RegisterMigration(object):
- """
- Tool for registering migrations
-
- Call like:
-
- @RegisterMigration(33)
- def update_dwarves(database):
- [...]
-
- This will register your migration with the default migration
- registry. Alternately, to specify a very specific
- migration_registry, you can pass in that as the second argument.
-
- Note, the number of your migration should NEVER be 0 or less than
- 0. 0 is the default "no migrations" state!
- """
- def __init__(self, migration_number, migration_registry=MIGRATIONS):
- assert migration_number > 0, "Migration number must be > 0!"
- assert migration_number not in migration_registry, \
- "Duplicate migration numbers detected! That's not allowed!"
-
- self.migration_number = migration_number
- self.migration_registry = migration_registry
-
- def __call__(self, migration):
- self.migration_registry[self.migration_number] = migration
- return migration
-
-
-class MigrationManager(object):
- """
- Migration handling tool.
-
- Takes information about a database, lets you update the database
- to the latest migrations, etc.
- """
- def __init__(self, database, migration_registry=MIGRATIONS):
- """
- Args:
- - database: database we're going to migrate
- - migration_registry: where we should find all migrations to
- run
- """
- self.database = database
- self.migration_registry = migration_registry
- self._sorted_migrations = None
-
- def _ensure_current_migration_record(self):
- """
- If there isn't a database[u'app_metadata'] mediagoblin entry
- with the 'current_migration', throw an error.
- """
- if self.database_current_migration() is None:
- raise MissingCurrentMigration(
- "Tried to call function which requires "
- "'current_migration' set in database")
-
- @property
- def sorted_migrations(self):
- """
- Sort migrations if necessary and store in self._sorted_migrations
- """
- if not self._sorted_migrations:
- self._sorted_migrations = sorted(
- self.migration_registry.items(),
- # sort on the key... the migration number
- key=lambda migration_tuple: migration_tuple[0])
-
- return self._sorted_migrations
-
- def latest_migration(self):
- """
- Return a migration number for the latest migration, or 0 if
- there are no migrations.
- """
- if self.sorted_migrations:
- return self.sorted_migrations[-1][0]
- else:
- # If no migrations have been set, we start at 0.
- return 0
-
- def set_current_migration(self, migration_number):
- """
- Set the migration in the database to migration_number
- """
- # Add the mediagoblin migration if necessary
- self.database[u'app_metadata'].update(
- {u'_id': u'mediagoblin'},
- {u'$set': {u'current_migration': migration_number}},
- upsert=True)
-
- def install_migration_version_if_missing(self):
- """
- Sets the migration to the latest version if no migration
- version at all is set.
- """
- mgoblin_metadata = self.database[u'app_metadata'].find_one(
- {u'_id': u'mediagoblin'})
- if not mgoblin_metadata:
- latest_migration = self.latest_migration()
- self.set_current_migration(latest_migration)
-
- def database_current_migration(self):
- """
- Return the current migration in the database.
- """
- mgoblin_metadata = self.database[u'app_metadata'].find_one(
- {u'_id': u'mediagoblin'})
- if not mgoblin_metadata:
- return None
- else:
- return mgoblin_metadata[u'current_migration']
-
- def database_at_latest_migration(self):
- """
- See if the database is at the latest migration.
- Returns a boolean.
- """
- current_migration = self.database_current_migration()
- return current_migration == self.latest_migration()
-
- def migrations_to_run(self):
- """
- Get a list of migrations to run still, if any.
-
- Note that calling this will set your migration version to the
- latest version if it isn't installed to anything yet!
- """
- self._ensure_current_migration_record()
-
- db_current_migration = self.database_current_migration()
-
- return [
- (migration_number, migration_func)
- for migration_number, migration_func in self.sorted_migrations
- if migration_number > db_current_migration]
-
- def migrate_new(self, pre_callback=None, post_callback=None):
- """
- Run all migrations.
-
- Includes two optional args:
- - pre_callback: if called, this is a callback on something to
- run pre-migration. Takes (migration_number, migration_func)
- as arguments
- - pre_callback: if called, this is a callback on something to
- run post-migration. Takes (migration_number, migration_func)
- as arguments
- """
- # If we aren't set to any version number, presume we're at the
- # latest (which means we'll do nothing here...)
- self.install_migration_version_if_missing()
-
- for migration_number, migration_func in self.migrations_to_run():
- if pre_callback:
- pre_callback(migration_number, migration_func)
- migration_func(self.database)
- self.set_current_migration(migration_number)
- if post_callback:
- post_callback(migration_number, migration_func)
+from mediagoblin.db.mongo.util import (ObjectId, InvalidId,
+ DESCENDING)
diff --git a/mediagoblin/decorators.py b/mediagoblin/decorators.py
index 229664d7..4cf14a70 100644
--- a/mediagoblin/decorators.py
+++ b/mediagoblin/decorators.py
@@ -57,10 +57,10 @@ def user_may_delete_media(controller):
Require user ownership of the MediaEntry to delete.
"""
def wrapper(request, *args, **kwargs):
- uploader = request.db.MediaEntry.find_one(
- {'_id': ObjectId(request.matchdict['media'])}).get_uploader()
+ uploader_id = request.db.MediaEntry.find_one(
+ {'_id': ObjectId(request.matchdict['media'])}).uploader
if not (request.user.is_admin or
- request.user._id == uploader._id):
+ request.user._id == uploader_id):
return exc.HTTPForbidden()
return controller(request, *args, **kwargs)
diff --git a/mediagoblin/edit/forms.py b/mediagoblin/edit/forms.py
index dd339e08..09955874 100644
--- a/mediagoblin/edit/forms.py
+++ b/mediagoblin/edit/forms.py
@@ -23,41 +23,49 @@ class EditForm(wtforms.Form):
title = wtforms.TextField(
_('Title'),
[wtforms.validators.Length(min=0, max=500)])
- description = wtforms.TextAreaField('Description of this work')
+ description = wtforms.TextAreaField(
+ _('Description of this work'),
+ description=_("""You can use
+ <a href="http://daringfireball.net/projects/markdown/basics">
+ Markdown</a> for formatting."""))
tags = wtforms.TextField(
_('Tags'),
[tag_length_validator],
description=_(
- "Seperate tags by commas."))
+ "Separate tags by commas."))
slug = wtforms.TextField(
_('Slug'),
[wtforms.validators.Required(message=_("The slug can't be empty"))],
description=_(
- "The title part of this media's URL. "
+ "The title part of this media's address. "
"You usually don't need to change this."))
class EditProfileForm(wtforms.Form):
bio = wtforms.TextAreaField(
_('Bio'),
- [wtforms.validators.Length(min=0, max=500)])
+ [wtforms.validators.Length(min=0, max=500)],
+ description=_(
+ """You can use
+ <a href="http://daringfireball.net/projects/markdown/basics">
+ Markdown</a> for formatting."""))
url = wtforms.TextField(
_('Website'),
[wtforms.validators.Optional(),
- wtforms.validators.URL(message='Improperly formed URL')])
+ wtforms.validators.URL(message="""This address contains errors""")])
+
+
+class EditAccountForm(wtforms.Form):
old_password = wtforms.PasswordField(
_('Old password'),
- [wtforms.validators.Optional()])
+ [wtforms.validators.Required()],
+ description=_(
+ "Enter your old password to prove you own this account."))
new_password = wtforms.PasswordField(
- _('New Password'),
- [wtforms.validators.Optional(),
- wtforms.validators.Length(min=6, max=30),
- wtforms.validators.EqualTo(
- 'confirm_password',
- 'Passwords must match.')])
- confirm_password = wtforms.PasswordField(
- 'Confirm password',
- [wtforms.validators.Optional()])
+ _('New password'),
+ [wtforms.validators.Required(),
+ wtforms.validators.Length(min=6, max=30)],
+ id="password")
class EditAttachmentsForm(wtforms.Form):
diff --git a/mediagoblin/edit/routing.py b/mediagoblin/edit/routing.py
index 34e9fd80..5216f7ca 100644
--- a/mediagoblin/edit/routing.py
+++ b/mediagoblin/edit/routing.py
@@ -20,4 +20,7 @@ from routes.route import Route
edit_routes = [
# Media editing view handled in user_pages/routing.py
Route('mediagoblin.edit.profile', '/profile/',
- controller="mediagoblin.edit.views:edit_profile")]
+ controller="mediagoblin.edit.views:edit_profile"),
+ Route('mediagoblin.edit.account', '/account/',
+ controller="mediagoblin.edit.views:edit_account")
+ ]
diff --git a/mediagoblin/edit/views.py b/mediagoblin/edit/views.py
index 4cb98c15..bae85c5d 100644
--- a/mediagoblin/edit/views.py
+++ b/mediagoblin/edit/views.py
@@ -162,6 +162,35 @@ def edit_profile(request):
bio=user.get('bio'))
if request.method == 'POST' and form.validate():
+ user.url = unicode(request.POST['url'])
+ user.bio = unicode(request.POST['bio'])
+
+ user.bio_html = cleaned_markdown_conversion(user['bio'])
+
+ user.save()
+
+ messages.add_message(request,
+ messages.SUCCESS,
+ _("Profile changes saved"))
+ return redirect(request,
+ 'mediagoblin.user_pages.user_home',
+ user=user['username'])
+
+ return render_to_response(
+ request,
+ 'mediagoblin/edit/edit_profile.html',
+ {'user': user,
+ 'form': form})
+
+
+@require_active_login
+def edit_account(request):
+ edit_username = request.GET.get('username')
+ user = request.user
+
+ form = forms.EditAccountForm(request.POST)
+
+ if request.method == 'POST' and form.validate():
password_matches = auth_lib.bcrypt_check_password(
request.POST['old_password'],
user['pw_hash'])
@@ -172,30 +201,25 @@ def edit_profile(request):
return render_to_response(
request,
- 'mediagoblin/edit/edit_profile.html',
+ 'mediagoblin/edit/edit_account.html',
{'user': user,
'form': form})
- user.url = unicode(request.POST['url'])
- user.bio = unicode(request.POST['bio'])
-
if password_matches:
user['pw_hash'] = auth_lib.bcrypt_gen_password_hash(
request.POST['new_password'])
- user.bio_html = cleaned_markdown_conversion(user['bio'])
-
user.save()
messages.add_message(request,
messages.SUCCESS,
- _("Profile edited!"))
+ _("Account settings saved"))
return redirect(request,
'mediagoblin.user_pages.user_home',
user=user['username'])
return render_to_response(
request,
- 'mediagoblin/edit/edit_profile.html',
+ 'mediagoblin/edit/edit_account.html',
{'user': user,
'form': form})
diff --git a/mediagoblin/gmg_commands/import_export.py b/mediagoblin/gmg_commands/import_export.py
index eda41f4c..7f699429 100644
--- a/mediagoblin/gmg_commands/import_export.py
+++ b/mediagoblin/gmg_commands/import_export.py
@@ -65,7 +65,7 @@ def _import_media(db, args):
args._cache_path['queue'])
for entry in db.MediaEntry.find():
- for name, path in entry['media_files'].items():
+ for name, path in entry.media_files.items():
_log.info('Importing: {0} - {1}'.format(
entry.title,
name))
@@ -207,7 +207,7 @@ def _export_media(db, args):
args._cache_path['queue'])
for entry in db.MediaEntry.find():
- for name, path in entry['media_files'].items():
+ for name, path in entry.media_files.items():
_log.info(u'Exporting {0} - {1}'.format(
entry.title,
name))
diff --git a/mediagoblin/gmg_commands/migrate.py b/mediagoblin/gmg_commands/migrate.py
index bd3bcb20..0a8ee7dc 100644
--- a/mediagoblin/gmg_commands/migrate.py
+++ b/mediagoblin/gmg_commands/migrate.py
@@ -16,12 +16,12 @@
import sys
-from mediagoblin.db import util as db_util
+from mediagoblin.db.mongo import util as db_util
from mediagoblin.db.open import setup_connection_and_db_from_config
from mediagoblin.init import setup_global_and_app_config
# This MUST be imported so as to set up the appropriate migrations!
-from mediagoblin.db import migrations
+from mediagoblin.db.mongo import migrations
def migrate_parser_setup(subparser):
diff --git a/mediagoblin/init/__init__.py b/mediagoblin/init/__init__.py
index 08a0618d..23c1c26d 100644
--- a/mediagoblin/init/__init__.py
+++ b/mediagoblin/init/__init__.py
@@ -23,8 +23,8 @@ from mediagoblin.init.config import (
read_mediagoblin_config, generate_validation_report)
from mediagoblin import mg_globals
from mediagoblin.mg_globals import setup_globals
-from mediagoblin.db.open import setup_connection_and_db_from_config
-from mediagoblin.db.util import MigrationManager
+from mediagoblin.db.open import setup_connection_and_db_from_config, \
+ check_db_migrations_current
from mediagoblin.workbench import WorkbenchManager
from mediagoblin.storage import storage_system_from_config
@@ -56,28 +56,10 @@ def setup_global_and_app_config(config_path):
def setup_database():
app_config = mg_globals.app_config
- # This MUST be imported so as to set up the appropriate migrations!
- from mediagoblin.db import migrations
-
# Set up the database
connection, db = setup_connection_and_db_from_config(app_config)
- # Init the migration number if necessary
- migration_manager = MigrationManager(db)
- migration_manager.install_migration_version_if_missing()
-
- # Tiny hack to warn user if our migration is out of date
- if not migration_manager.database_at_latest_migration():
- db_migration_num = migration_manager.database_current_migration()
- latest_migration_num = migration_manager.latest_migration()
- if db_migration_num < latest_migration_num:
- print (
- "*WARNING:* Your migrations are out of date, "
- "maybe run ./bin/gmg migrate?")
- elif db_migration_num > latest_migration_num:
- print (
- "*WARNING:* Your migrations are out of date... "
- "in fact they appear to be from the future?!")
+ check_db_migrations_current(db)
setup_globals(
db_connection=connection,
diff --git a/mediagoblin/listings/views.py b/mediagoblin/listings/views.py
index 6b83ffcf..3ecf06f4 100644
--- a/mediagoblin/listings/views.py
+++ b/mediagoblin/listings/views.py
@@ -86,7 +86,7 @@ def tag_atom_feed(request):
feed.add(entry.get('title'),
entry.get('description_html'),
content_type='html',
- author=entry.get_uploader().username,
+ author=entry.get_uploader.username,
updated=entry.get('created'),
url=entry.url_for_self(request.urlgen))
diff --git a/mediagoblin/media_types/__init__.py b/mediagoblin/media_types/__init__.py
index 6f94c714..e7eb1dde 100644
--- a/mediagoblin/media_types/__init__.py
+++ b/mediagoblin/media_types/__init__.py
@@ -69,16 +69,20 @@ def get_media_type_and_manager(filename):
'''
Get the media type and manager based on a filename
'''
- for media_type, manager in get_media_managers():
- if filename.find('.') > 0:
- # Get the file extension
- ext = os.path.splitext(filename)[1].lower()
- else:
- raise InvalidFileType(
- _('Could not find any file extension in "{filename}"').format(
- filename=filename))
+ if filename.find('.') > 0:
+ # Get the file extension
+ ext = os.path.splitext(filename)[1].lower()
+ else:
+ raise InvalidFileType(
+ _(u'Could not extract any file extension from "{filename}"').format(
+ filename=filename))
+ for media_type, manager in get_media_managers():
# Omit the dot from the extension and match it against
# the media manager
if ext[1:] in manager['accepted_extensions']:
return media_type, manager
+ else:
+ raise FileTypeNotSupported(
+ # TODO: Provide information on which file types are supported
+ _(u'Sorry, I don\'t support that file type :('))
diff --git a/mediagoblin/media_types/ascii/__init__.py b/mediagoblin/media_types/ascii/__init__.py
new file mode 100644
index 00000000..21b31d0e
--- /dev/null
+++ b/mediagoblin/media_types/ascii/__init__.py
@@ -0,0 +1,27 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011 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.media_types.ascii.processing import process_ascii
+
+
+MEDIA_MANAGER = {
+ "human_readable": "ASCII",
+ "processor": process_ascii, # alternately a string,
+ # 'mediagoblin.media_types.image.processing'?
+ "display_template": "mediagoblin/media_displays/ascii.html",
+ "default_thumb": "images/media_thumbs/ascii.jpg",
+ "accepted_extensions": [
+ "txt"]}
diff --git a/mediagoblin/media_types/ascii/asciitoimage.py b/mediagoblin/media_types/ascii/asciitoimage.py
new file mode 100644
index 00000000..39c75a19
--- /dev/null
+++ b/mediagoblin/media_types/ascii/asciitoimage.py
@@ -0,0 +1,172 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011 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 Image
+import ImageFont
+import ImageDraw
+import logging
+import pkg_resources
+import os
+
+_log = logging.getLogger(__name__)
+
+class AsciiToImage(object):
+ '''
+ Converter of ASCII art into image files, preserving whitespace
+
+ kwargs:
+ - font: Path to font file
+ default: fonts/Inconsolata.otf
+ - font_size: Font size, ``int``
+ default: 11
+ '''
+
+ # Font file path
+ _font = None
+
+ _font_size = 11
+
+ # ImageFont instance
+ _if = None
+
+ # ImageFont
+ _if_dims = None
+
+ # Image instance
+ _im = None
+
+ def __init__(self, **kw):
+ if kw.get('font'):
+ self._font = kw.get('font')
+ else:
+ self._font = pkg_resources.resource_filename(
+ 'mediagoblin.media_types.ascii',
+ os.path.join('fonts', 'Inconsolata.otf'))
+
+ if kw.get('font_size'):
+ self._font_size = kw.get('font_size')
+
+ _log.info('Setting font to {0}, size {1}'.format(
+ self._font,
+ self._font_size))
+
+ self._if = ImageFont.truetype(
+ self._font,
+ self._font_size)
+
+ # ,-,-^-'-^'^-^'^-'^-.
+ # ( I am a wall socket )Oo, ___
+ # `-.,.-.,.-.-.,.-.--' ' `
+ # Get the size, in pixels of the '.' character
+ self._if_dims = self._if.getsize('.')
+ # `---'
+
+ def convert(self, text, destination):
+ # TODO: Detect if text is a file-like, if so, act accordingly
+ im = self._create_image(text)
+
+ # PIL's Image.save will handle both file-likes and paths
+ if im.save(destination):
+ _log.info('Saved image in {0}'.format(
+ destination))
+
+ def _create_image(self, text):
+ '''
+ Write characters to a PIL image canvas.
+
+ TODO:
+ - Character set detection and decoding,
+ http://pypi.python.org/pypi/chardet
+ '''
+ # TODO: Account for alternative line endings
+ lines = text.split('\n')
+
+ line_lengths = [len(i) for i in lines]
+
+ # Calculate destination size based on text input and character size
+ im_dims = (
+ max(line_lengths) * self._if_dims[0],
+ len(line_lengths) * self._if_dims[1])
+
+ _log.info('Destination image dimensions will be {0}'.format(
+ im_dims))
+
+ im = Image.new(
+ 'RGBA',
+ im_dims,
+ (255, 255, 255, 0))
+
+ draw = ImageDraw.Draw(im)
+
+ char_pos = [0, 0]
+
+ for line in lines:
+ line_length = len(line)
+
+ _log.debug('Writing line at {0}'.format(char_pos))
+
+ for _pos in range(0, line_length):
+ char = line[_pos]
+
+ px_pos = self._px_pos(char_pos)
+
+ _log.debug('Writing character "{0}" at {1} (px pos {2}'.format(
+ char,
+ char_pos,
+ px_pos))
+
+ draw.text(
+ px_pos,
+ char,
+ font=self._if,
+ fill=(0, 0, 0, 255))
+
+ char_pos[0] += 1
+
+ # Reset X position, increment Y position
+ char_pos[0] = 0
+ char_pos[1] += 1
+
+ return im
+
+ def _px_pos(self, char_pos):
+ '''
+ Helper function to calculate the pixel position based on
+ character position and character dimensions
+ '''
+ px_pos = [0, 0]
+ for index, val in zip(range(0, len(char_pos)), char_pos):
+ px_pos[index] = char_pos[index] * self._if_dims[index]
+
+ return px_pos
+
+
+if __name__ == "__main__":
+ import urllib
+ txt = urllib.urlopen('file:///home/joar/Dropbox/ascii/install-all-the-dependencies.txt')
+
+ _log.setLevel(logging.DEBUG)
+ logging.basicConfig()
+
+ converter = AsciiToImage()
+
+ converter.convert(txt.read(), '/tmp/test.png')
+
+ '''
+ im, x, y, duration = renderImage(h, 10)
+ print "Rendered image in %.5f seconds" % duration
+ im.save('tldr.png', "PNG")
+ '''
diff --git a/mediagoblin/media_types/ascii/fonts/Inconsolata.otf b/mediagoblin/media_types/ascii/fonts/Inconsolata.otf
new file mode 120000
index 00000000..4e742b5e
--- /dev/null
+++ b/mediagoblin/media_types/ascii/fonts/Inconsolata.otf
@@ -0,0 +1 @@
+../../../../extlib/inconsolata/Inconsolata.otf \ No newline at end of file
diff --git a/mediagoblin/media_types/ascii/processing.py b/mediagoblin/media_types/ascii/processing.py
new file mode 100644
index 00000000..a74690c1
--- /dev/null
+++ b/mediagoblin/media_types/ascii/processing.py
@@ -0,0 +1,93 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011 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 asciitoimage
+import chardet
+import os
+import Image
+
+from mediagoblin import mg_globals as mgg
+from mediagoblin.processing import create_pub_filepath, THUMB_SIZE
+
+
+def process_ascii(entry):
+ '''
+ Code to process a txt file
+ '''
+ workbench = mgg.workbench_manager.create_workbench()
+ # Conversions subdirectory to avoid collisions
+ conversions_subdir = os.path.join(
+ workbench.dir, 'conversions')
+ os.mkdir(conversions_subdir)
+
+ queued_filepath = entry['queued_media_file']
+ queued_filename = workbench.localized_file(
+ mgg.queue_store, queued_filepath,
+ 'source')
+
+ queued_file = file(queued_filename, 'rb')
+
+ with queued_file:
+ queued_file_charset = chardet.detect(queued_file.read())
+
+ queued_file.seek(0) # Rewind the queued file
+
+ thumb_filepath = create_pub_filepath(
+ entry, 'thumbnail.png')
+
+ tmp_thumb_filename = os.path.join(
+ conversions_subdir, thumb_filepath[-1])
+
+ converter = asciitoimage.AsciiToImage()
+
+ thumb = converter._create_image(
+ queued_file.read())
+
+ with file(tmp_thumb_filename, 'w') as thumb_file:
+ thumb.thumbnail(THUMB_SIZE, Image.ANTIALIAS)
+ thumb.save(thumb_file)
+
+ mgg.public_store.copy_local_to_storage(
+ tmp_thumb_filename, thumb_filepath)
+
+ queued_file.seek(0)
+
+ original_filepath = create_pub_filepath(entry, queued_filepath[-1])
+
+ with mgg.public_store.get_file(original_filepath, 'wb') \
+ as original_file:
+ original_file.write(queued_file.read())
+
+
+ queued_file.seek(0) # Rewind *again*
+
+ unicode_filepath = create_pub_filepath(entry, 'unicode.txt')
+
+ with mgg.public_store.get_file(unicode_filepath, 'wb') \
+ as unicode_file:
+ unicode_file.write(
+ unicode(queued_file.read().decode(
+ queued_file_charset['encoding'])).encode(
+ 'ascii',
+ 'xmlcharrefreplace'))
+
+ mgg.queue_store.delete_file(queued_filepath)
+ entry['queued_media_file'] = []
+ media_files_dict = entry.setdefault('media_files', {})
+ media_files_dict['thumb'] = thumb_filepath
+ media_files_dict['unicode'] = unicode_filepath
+ media_files_dict['original'] = original_filepath
+
+ entry.save()
diff --git a/mediagoblin/media_types/image/processing.py b/mediagoblin/media_types/image/processing.py
index e493eb2b..cf90388f 100644
--- a/mediagoblin/media_types/image/processing.py
+++ b/mediagoblin/media_types/image/processing.py
@@ -37,7 +37,7 @@ def process_image(entry):
workbench.dir, 'conversions')
os.mkdir(conversions_subdir)
- queued_filepath = entry['queued_media_file']
+ queued_filepath = entry.queued_media_file
queued_filename = workbench.localized_file(
mgg.queue_store, queued_filepath,
'source')
@@ -98,7 +98,7 @@ def process_image(entry):
original_file.write(queued_file.read())
mgg.queue_store.delete_file(queued_filepath)
- entry['queued_media_file'] = []
+ entry.queued_media_file = []
media_files_dict = entry.setdefault('media_files', {})
media_files_dict['thumb'] = thumb_filepath
media_files_dict['original'] = original_filepath
diff --git a/mediagoblin/media_types/video/processing.py b/mediagoblin/media_types/video/processing.py
index 7d261226..49a50647 100644
--- a/mediagoblin/media_types/video/processing.py
+++ b/mediagoblin/media_types/video/processing.py
@@ -45,7 +45,7 @@ def process_video(entry):
workbench = mgg.workbench_manager.create_workbench()
- queued_filepath = entry['queued_media_file']
+ queued_filepath = entry.queued_media_file
queued_filename = workbench.localized_file(
mgg.queue_store, queued_filepath,
'source')
@@ -74,7 +74,7 @@ def process_video(entry):
tmp_dst.read())
_log.debug('Saved medium')
- entry['media_files']['webm_640'] = medium_filepath
+ entry.media_files['webm_640'] = medium_filepath
# Save the width and height of the transcoded video
entry.media_data['video'] = {
@@ -94,7 +94,7 @@ def process_video(entry):
tmp_thumb.read())
_log.debug('Saved thumbnail')
- entry['media_files']['thumb'] = thumbnail_filepath
+ entry.media_files['thumb'] = thumbnail_filepath
if video_config['keep_original']:
# Push original file to public storage
@@ -111,7 +111,7 @@ def process_video(entry):
original_file.write(queued_file.read())
_log.debug('Saved original')
- entry['media_files']['original'] = original_filepath
+ entry.media_files['original'] = original_filepath
mgg.queue_store.delete_file(queued_filepath)
diff --git a/mediagoblin/processing.py b/mediagoblin/processing.py
index 7dd5cc7d..cbac8030 100644
--- a/mediagoblin/processing.py
+++ b/mediagoblin/processing.py
@@ -64,7 +64,7 @@ class ProcessMedia(Task):
except ImportError, exc:
mark_entry_failed(entry[u'_id'], exc)
- entry['state'] = u'processed'
+ entry.state = u'processed'
entry.save()
def on_failure(self, exc, task_id, args, kwargs, einfo):
diff --git a/mediagoblin/routing.py b/mediagoblin/routing.py
index ae56f8cb..bd727db5 100644
--- a/mediagoblin/routing.py
+++ b/mediagoblin/routing.py
@@ -21,6 +21,8 @@ from mediagoblin.submit.routing import submit_routes
from mediagoblin.user_pages.routing import user_routes
from mediagoblin.edit.routing import edit_routes
from mediagoblin.listings.routing import tag_routes
+from mediagoblin.webfinger.routing import webfinger_well_known_routes, \
+ webfinger_routes
def get_mapper():
@@ -36,5 +38,7 @@ def get_mapper():
mapping.extend(user_routes, '/u')
mapping.extend(edit_routes, '/edit')
mapping.extend(tag_routes, '/tag')
+ mapping.extend(webfinger_well_known_routes, '/.well-known')
+ mapping.extend(webfinger_routes, '/api/webfinger')
return mapping
diff --git a/mediagoblin/static/css/base.css b/mediagoblin/static/css/base.css
index e89ce8a2..f4359791 100644
--- a/mediagoblin/static/css/base.css
+++ b/mediagoblin/static/css/base.css
@@ -230,6 +230,14 @@ text-align: center;
float: right;
}
+textarea#comment_content {
+ resize: vertical;
+ width: 634px;
+ height: 90px;
+ border: none;
+ background-color: #f1f1f1;
+ padding: 3px;
+
.clear {
clear: both;
display: block;
@@ -295,6 +303,15 @@ text-align: center;
text-align: right;
}
+#password_boolean {
+ margin-top: 4px;
+ width: 20px;
+}
+
+textarea#description, textarea#bio {
+ resize: vertical;
+}
+
/* comments */
.comment_author {
@@ -345,11 +362,18 @@ h2.media_title {
p.media_specs {
font-size: 0.9em;
border-top: 1px solid #222;
- border-bottom: 1px solid #222;
padding: 10px 0px;
color: #888;
}
+.no_html5 {
+ background: black;
+ color: white;
+ text-align: center;
+ height: 160px;
+ padding: 130px 10px 20px 10px;
+}
+
/* icons */
img.media_icon {
@@ -443,23 +467,14 @@ table.media_panel th {
margin-left: 10px;
}
-@media screen and (max-width: 960px) {
- .mediagoblin_body {
- }
- .mediagoblin_footer {
- }
-}
+/* ASCII art */
+@font-face {
+ font-family: Inconsolata;
+ src: local('Inconsolata'), url('../fonts/Inconsolata.otf') format('opentype')
+}
- /* old code
- .navigation_button {
- position: fixed;
- bottom: 0px;
- right: 0px;
- width: 50%;
- margin: 0;
- }
- .navigation_left {
- left: 0px;
- }
- */
+.ascii-wrapper pre {
+ font-family: Inconsolata, monospace;
+ line-height: 1em;
+}
diff --git a/mediagoblin/static/fonts/Inconsolata.otf b/mediagoblin/static/fonts/Inconsolata.otf
new file mode 120000
index 00000000..777be657
--- /dev/null
+++ b/mediagoblin/static/fonts/Inconsolata.otf
@@ -0,0 +1 @@
+../../../extlib/inconsolata/Inconsolata.otf \ No newline at end of file
diff --git a/mediagoblin/static/js/comment_show.js b/mediagoblin/static/js/comment_show.js
new file mode 100644
index 00000000..2212b9ad
--- /dev/null
+++ b/mediagoblin/static/js/comment_show.js
@@ -0,0 +1,9 @@
+$(document).ready(function(){
+ $('#form_comment').hide();
+ $('#button_addcomment').click(function(){
+ $(this).fadeOut('fast');
+ $('#form_comment').slideDown(function(){
+ $('#comment_content').focus();
+ });
+ });
+});
diff --git a/mediagoblin/static/js/show_password.js b/mediagoblin/static/js/show_password.js
new file mode 100644
index 00000000..519b29c1
--- /dev/null
+++ b/mediagoblin/static/js/show_password.js
@@ -0,0 +1,19 @@
+$(document).ready(function(){
+ $("#password").after('<input type="text" value="" name="password_clear" id="password_clear" /><label><input type="checkbox" id="password_boolean" />Show password</label>');
+ $('#password_clear').hide();
+ $('#password_boolean').click(function(){
+ if($('#password_boolean').prop("checked")) {
+ $('#password_clear').val($('#password').val());
+ $('#password').hide();
+ $('#password_clear').show();
+ } else {
+ $('#password').val($('#password_clear').val());
+ $('#password_clear').hide();
+ $('#password').show();
+ };
+ });
+ $('#password,#password_clear').keyup(function(){
+ $('#password').val($(this).val());
+ $('#password_clear').val($(this).val());
+ });
+});
diff --git a/mediagoblin/submit/forms.py b/mediagoblin/submit/forms.py
index ad420771..7ef3638f 100644
--- a/mediagoblin/submit/forms.py
+++ b/mediagoblin/submit/forms.py
@@ -27,9 +27,12 @@ class SubmitStartForm(wtforms.Form):
_('Title'),
[wtforms.validators.Length(min=0, max=500)])
description = wtforms.TextAreaField(
- _('Description of this work'))
+ _('Description of this work'),
+ description=_("""You can use
+ <a href="http://daringfireball.net/projects/markdown/basics">
+ Markdown</a> for formatting."""))
tags = wtforms.TextField(
_('Tags'),
[tag_length_validator],
description=_(
- "Seperate tags by commas."))
+ "Separate tags by commas."))
diff --git a/mediagoblin/submit/views.py b/mediagoblin/submit/views.py
index 4e4c7c43..dd273c7f 100644
--- a/mediagoblin/submit/views.py
+++ b/mediagoblin/submit/views.py
@@ -31,7 +31,8 @@ from mediagoblin.decorators import require_active_login
from mediagoblin.submit import forms as submit_forms, security
from mediagoblin.processing import mark_entry_failed, ProcessMedia
from mediagoblin.messages import add_message, SUCCESS
-from mediagoblin.media_types import get_media_type_and_manager, InvalidFileType
+from mediagoblin.media_types import get_media_type_and_manager, \
+ InvalidFileType, FileTypeNotSupported
@require_active_login
@@ -88,7 +89,7 @@ def submit_start(request):
queue_file.write(request.POST['file'].file.read())
# Add queued filename to the entry
- entry['queued_media_file'] = queue_filepath
+ entry.queued_media_file = queue_filepath
# We generate this ourselves so we know what the taks id is for
# retrieval later.
@@ -128,9 +129,18 @@ def submit_start(request):
return redirect(request, "mediagoblin.user_pages.user_home",
user=request.user.username)
- except InvalidFileType, exc:
- submit_form.file.errors.append(
- _(u'Invalid file type.'))
+ except Exception as e:
+ '''
+ This section is intended to catch exceptions raised in
+ mediagobling.media_types
+ '''
+
+ if isinstance(e, InvalidFileType) or \
+ isinstance(e, FileTypeNotSupported):
+ submit_form.file.errors.append(
+ e)
+ else:
+ raise
return render_to_response(
request,
diff --git a/mediagoblin/templates/mediagoblin/auth/change_fp.html b/mediagoblin/templates/mediagoblin/auth/change_fp.html
index 9c8c79bf..d95516e8 100644
--- a/mediagoblin/templates/mediagoblin/auth/change_fp.html
+++ b/mediagoblin/templates/mediagoblin/auth/change_fp.html
@@ -19,6 +19,11 @@
{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
+{% block mediagoblin_head %}
+ <script type="text/javascript"
+ src="{{ request.staticdirect('/js/show_password.js') }}"></script>
+{% endblock mediagoblin_head %}
+
{% block mediagoblin_content %}
<form action="{{ request.urlgen('mediagoblin.auth.verify_forgot_password') }}"
method="POST" enctype="multipart/form-data">
diff --git a/mediagoblin/templates/mediagoblin/auth/register.html b/mediagoblin/templates/mediagoblin/auth/register.html
index 2520ca9b..afcfcda9 100644
--- a/mediagoblin/templates/mediagoblin/auth/register.html
+++ b/mediagoblin/templates/mediagoblin/auth/register.html
@@ -20,23 +20,8 @@
{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
{% block mediagoblin_head %}
- <script>
- $(document).ready(function(){
- $("#password").after('<input type="text" value="" name="password_clear" id="password_clear" /><input type="checkbox" id="password_boolean" />Show password');
- $('#password_clear').hide();
- $('#password_boolean').click(function(){
- if($('#password_boolean').prop("checked")) {
- $('#password_clear').val($('#password').val());
- $('#password').hide();
- $('#password_clear').show();
- } else {
- $('#password').val($('#password_clear').val());
- $('#password_clear').hide();
- $('#password').show();
- };
- });
- });
- </script>
+ <script type="text/javascript"
+ src="{{ request.staticdirect('/js/show_password.js') }}"></script>
{% endblock mediagoblin_head %}
{% block mediagoblin_content %}
diff --git a/mediagoblin/templates/mediagoblin/edit/attachments.html b/mediagoblin/templates/mediagoblin/edit/attachments.html
index ff357a8c..bd972b2a 100644
--- a/mediagoblin/templates/mediagoblin/edit/attachments.html
+++ b/mediagoblin/templates/mediagoblin/edit/attachments.html
@@ -20,14 +20,14 @@
{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
{% block mediagoblin_content %}
<form action="{{ request.urlgen('mediagoblin.edit.attachments',
- user= media.get_uploader().username,
+ user= media.get_uploader.username,
media= media._id) }}"
method="POST" enctype="multipart/form-data">
<div class="form_box">
<h1>Editing attachments for {{ media.title }}</h1>
<div style="text-align: center;" >
<img src="{{ request.app.public_store.file_url(
- media['media_files']['thumb']) }}" />
+ media.media_files['thumb']) }}" />
</div>
{% if media.attachment_files|count %}
diff --git a/mediagoblin/templates/mediagoblin/edit/edit.html b/mediagoblin/templates/mediagoblin/edit/edit.html
index d0341600..14200466 100644
--- a/mediagoblin/templates/mediagoblin/edit/edit.html
+++ b/mediagoblin/templates/mediagoblin/edit/edit.html
@@ -22,14 +22,14 @@
{% block mediagoblin_content %}
<form action="{{ request.urlgen('mediagoblin.edit.edit_media',
- user= media.get_uploader().username,
+ user= media.get_uploader.username,
media= media._id) }}"
method="POST" enctype="multipart/form-data">
<div class="form_box_xl">
<h1>{% trans media_title=media.title %}Editing {{ media_title }}{% endtrans %}</h1>
<div style="text-align: center;" >
<img src="{{ request.app.public_store.file_url(
- media['media_files']['thumb']) }}" />
+ media.media_files['thumb']) }}" />
</div>
{{ wtforms_util.render_divs(form) }}
<div class="form_submit_buttons">
diff --git a/mediagoblin/templates/mediagoblin/edit/edit_account.html b/mediagoblin/templates/mediagoblin/edit/edit_account.html
new file mode 100644
index 00000000..0a564161
--- /dev/null
+++ b/mediagoblin/templates/mediagoblin/edit/edit_account.html
@@ -0,0 +1,45 @@
+{#
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011 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 mediagoblin_head %}
+ <script type="text/javascript"
+ src="{{ request.staticdirect('/js/show_password.js') }}"></script>
+{% endblock mediagoblin_head %}
+
+{% block mediagoblin_content %}
+
+ <form action="{{ request.urlgen('mediagoblin.edit.account') }}?username={{
+ user.username }}"
+ method="POST" enctype="multipart/form-data">
+ <div class="grid_8 prefix_1 suffix_1 edit_box form_box">
+ <h1>
+ {%- trans username=user.username -%}
+ Changing {{ username }}'s account settings
+ {%- endtrans %}
+ </h1>
+ {{ wtforms_util.render_divs(form) }}
+ <div class="form_submit_buttons">
+ <input type="submit" value="{% trans %}Save changes{% endtrans %}" class="button_form" />
+ {{ csrf_token }}
+ </div>
+ </div>
+ </form>
+{% endblock %}
diff --git a/mediagoblin/templates/mediagoblin/media_displays/ascii.html b/mediagoblin/templates/mediagoblin/media_displays/ascii.html
new file mode 100644
index 00000000..6b40bf08
--- /dev/null
+++ b/mediagoblin/templates/mediagoblin/media_displays/ascii.html
@@ -0,0 +1,40 @@
+{#
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011 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/user_pages/media.html' %}
+
+{% block mediagoblin_media %}
+ <div class="ascii-wrapper">
+ <pre>
+ {%- autoescape False -%}
+ {{- request.app.public_store.get_file(
+ media.media_files['unicode']).read()|string -}}
+ {%- endautoescape -%}
+ </pre>
+ </div>
+ {% if 'original' in media.media_files %}
+ <p>
+ <a href="{{ request.app.public_store.file_url(
+ media.media_files['original']) }}">
+ {%- trans -%}
+ Original
+ {%- endtrans -%}
+ </a>
+ </p>
+ {% endif %}
+{% endblock %}
diff --git a/mediagoblin/templates/mediagoblin/media_displays/image.html b/mediagoblin/templates/mediagoblin/media_displays/image.html
index ad60fa94..94420e89 100644
--- a/mediagoblin/templates/mediagoblin/media_displays/image.html
+++ b/mediagoblin/templates/mediagoblin/media_displays/image.html
@@ -1 +1,19 @@
+{#
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011 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/user_pages/media.html' %}
diff --git a/mediagoblin/templates/mediagoblin/media_displays/video.html b/mediagoblin/templates/mediagoblin/media_displays/video.html
index 5ef1a782..6b5e7a0e 100644
--- a/mediagoblin/templates/mediagoblin/media_displays/video.html
+++ b/mediagoblin/templates/mediagoblin/media_displays/video.html
@@ -1,4 +1,23 @@
+{#
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011 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/user_pages/media.html' %}
+
{% block mediagoblin_media %}
<div class="video-player" style="position: relative;">
<video class="video-js vjs-default-skin"
@@ -8,14 +27,22 @@
preload="auto"
data-setup="">
<source src="{{ request.app.public_store.file_url(
- media['media_files']['webm_640']) }}"
+ media.media_files['webm_640']) }}"
type="video/webm; codecs=&quot;vp8, vorbis&quot;" />
+ <div class="no_html5">
+ {%- trans -%}Sorry, this video will not work because
+ your web browser does not support HTML5
+ video.{%- endtrans -%}<br/>
+ {%- trans -%}You can get a modern web browser that
+ can play this video at <a href="http://getfirefox.com">
+ http://getfirefox.com</a>!{%- endtrans -%}
+ </div>
</video>
</div>
{% if 'original' in media.media_files %}
<p>
<a href="{{ request.app.public_store.file_url(
- media['media_files']['original']) }}">
+ media.media_files['original']) }}">
{%- trans -%}
Original
{%- endtrans -%}
diff --git a/mediagoblin/templates/mediagoblin/user_pages/media.html b/mediagoblin/templates/mediagoblin/user_pages/media.html
index 0c3f373e..10525f4c 100644
--- a/mediagoblin/templates/mediagoblin/user_pages/media.html
+++ b/mediagoblin/templates/mediagoblin/user_pages/media.html
@@ -23,17 +23,8 @@
{% block title %}{{ media.title }} &mdash; {{ super() }}{% endblock %}
{% block mediagoblin_head %}
- <script>
- $(document).ready(function(){
- $('#form_comment').hide();
- $('#button_addcomment').click(function(){
- $(this).fadeOut('fast');
- $('#form_comment').slideDown(function(){
- $('#comment_content').focus();
- });
- });
- });
- </script>
+ <script type="text/javascript"
+ src="{{ request.staticdirect('/js/comment_show.js') }}"></script>
{% endblock mediagoblin_head %}
{% block mediagoblin_content %}
@@ -45,9 +36,9 @@
{# if there's a medium file size, that means the medium size
# isn't the original... so link to the original!
#}
- {% if media['media_files'].has_key('medium') %}
+ {% if media.media_files.has_key('medium') %}
<a href="{{ request.app.public_store.file_url(
- media['media_files']['original']) }}">
+ media.media_files['original']) }}">
<img class="media_image"
src="{{ display_media }}"
alt="Image for {{ media.title }}" />
@@ -69,36 +60,44 @@
{% trans date=media.created.strftime("%Y-%m-%d") -%}
Added on {{ date }}.
{%- endtrans %}
- {% if media['uploader'] == request.user._id or
- request.user['is_admin'] %}
+ {% if request.user and
+ (media.uploader == request.user._id or
+ request.user.is_admin) %}
{% set edit_url = request.urlgen('mediagoblin.edit.edit_media',
- user= media.get_uploader().username,
+ user= media.get_uploader.username,
media= media._id) %}
<a class="button_action" href="{{ edit_url }}">{% trans %}Edit{% endtrans %}</a>
{% set delete_url = request.urlgen('mediagoblin.user_pages.media_confirm_delete',
- user= media.get_uploader().username,
+ user= media.get_uploader.username,
media= media._id) %}
<a class="button_action" href="{{ delete_url }}">{% trans %}Delete{% endtrans %}</a>
{% endif %}
</p>
- <h3>{% trans comment_count=comments.count() -%}{{ comment_count }} comments{%- endtrans %}
- <div class="right_align">
- <a
- {% if not request.user %}
- href="{{ request.urlgen('mediagoblin.auth.login') }}"
- {% endif %}
- class="button_action" id="button_addcomment" title="Add a comment">
- {% trans %}Add one{% endtrans %}
- </a>
- </div>
- </h3>
- {# 0 comments. Be the first to add one! #}
+ {% if comments %}
+ <h3>
+ {% if comments.count()==1 %}
+ {% trans comment_count=comments.count() -%}{{ comment_count }} comment{%- endtrans %}
+ {% elif comments.count()>1 %}
+ {% trans comment_count=comments.count() -%}{{ comment_count }} comments{%- endtrans %}
+ {% else %}
+ {% trans %}No comments yet.{% endtrans %}
+ {% endif %}
+ <div class="right_align">
+ <a
+ {% if not request.user %}
+ href="{{ request.urlgen('mediagoblin.auth.login') }}"
+ {% endif %}
+ class="button_action" id="button_addcomment" title="Add a comment">
+ {% trans %}Add one{% endtrans %}
+ </a>
+ </div>
+ </h3>
{% if request.user %}
<form action="{{ request.urlgen('mediagoblin.user_pages.media_post_comment',
- user= media.get_uploader().username,
+ user= media.get_uploader.username,
media=media._id) }}" method="POST" id="form_comment">
<p>
- {% trans %}Type your comment here. You can use <a href="http://daringfireball.net/projects/markdown/basics" target="_blank">Markdown</a> for formatting.{% endtrans %}
+ {% trans %}Type your comment here. You can use <a href="http://daringfireball.net/projects/markdown/basics">Markdown</a> for formatting.{% endtrans %}
</p>
{{ wtforms_util.render_divs(comment_form) }}
<div class="form_submit_buttons">
@@ -107,45 +106,42 @@
</div>
</form>
{% endif %}
- {% if comments %}
- {% for comment in comments %}
- {% set comment_author = comment.author() %}
- {% if pagination.active_id == comment._id %}
- <div class="comment_wrapper comment_active" id="comment-{{ comment._id }}">
- <a name="comment" id="comment"></a>
- {% else %}
- <div class="comment_wrapper" id="comment-{{ comment._id }}">
- {% endif %}
- <div class="comment_content">
- {% autoescape False %}
- {{ comment.content_html }}
- {% endautoescape %}
- <img src="{{ request.staticdirect('/images/icon_comment.png') }}" />
- <a href="{{ request.urlgen('mediagoblin.user_pages.user_home',
- user = comment_author.username) }}">
- {{ comment_author.username }}
- </a>
- {% trans %}at{% endtrans %}
- <a href="{{ request.urlgen('mediagoblin.user_pages.media_home.view_comment',
- comment = comment._id,
- user = media.get_uploader().username,
- media = media.slug) }}#comment">
- {{ comment.created.strftime("%I:%M%p %Y-%m-%d") }}
- </a>
- </div>
- </div>
- {% endfor %}
- {{ render_pagination(request, pagination,
- request.urlgen('mediagoblin.user_pages.media_home',
- user = media.get_uploader().username,
- media = media._id)) }}
+ {% for comment in comments %}
+ {% set comment_author = comment.get_author %}
+ {% if pagination.active_id == comment._id %}
+ <div class="comment_wrapper comment_active" id="comment-{{ comment._id }}">
+ <a name="comment" id="comment"></a>
+ {% else %}
+ <div class="comment_wrapper" id="comment-{{ comment._id }}">
+ {% endif %}
+ <div class="comment_content">
+ {% autoescape False %}
+ {{ comment.content_html }}
+ {% endautoescape %}
+ <img src="{{ request.staticdirect('/images/icon_comment.png') }}" />
+ <a href="{{ request.urlgen('mediagoblin.user_pages.user_home',
+ user = comment_author.username) }}">
+ {{ comment_author.username }}
+ </a>
+ {% trans %}at{% endtrans %}
+ <a href="{{ request.urlgen('mediagoblin.user_pages.media_home.view_comment',
+ comment = comment._id,
+ user = media.get_uploader.username,
+ media = media.slug) }}#comment">
+ {{ comment.created.strftime("%I:%M%p %Y-%m-%d") }}
+ </a>
+ </div>
+ </div>
+ {% endfor %}
+ {{ render_pagination(request, pagination,
+ media.url_for_self(request.urlgen)) }}
{% endif %}
</div>
<div class="media_sidebar">
{% trans user_url=request.urlgen(
'mediagoblin.user_pages.user_home',
- user=media.get_uploader().username),
- username=media.get_uploader().username -%}
+ user=media.get_uploader.username),
+ username=media.get_uploader.username -%}
<p>❖ Browsing media by <a href="{{ user_url }}">{{ username }}</a></p>
{%- endtrans %}
{% include "mediagoblin/utils/prev_next.html" %}
@@ -166,7 +162,7 @@
or request.user.is_admin) %}
<p>
<a href="{{ request.urlgen('mediagoblin.edit.attachments',
- user=media.get_uploader().username,
+ user=media.get_uploader.username,
media=media._id) }}">Add attachment</a>
</p>
{% endif %}
diff --git a/mediagoblin/templates/mediagoblin/user_pages/media_confirm_delete.html b/mediagoblin/templates/mediagoblin/user_pages/media_confirm_delete.html
index 8e0f2904..dcb148e0 100644
--- a/mediagoblin/templates/mediagoblin/user_pages/media_confirm_delete.html
+++ b/mediagoblin/templates/mediagoblin/user_pages/media_confirm_delete.html
@@ -22,7 +22,7 @@
{% block mediagoblin_content %}
<form action="{{ request.urlgen('mediagoblin.user_pages.media_confirm_delete',
- user=media.get_uploader().username,
+ user=media.get_uploader.username,
media=media._id) }}"
method="POST" enctype="multipart/form-data">
<div class="form_box">
@@ -34,7 +34,7 @@
<div style="text-align: center;" >
<img src="{{ request.app.public_store.file_url(
- media['media_files']['thumb']) }}" />
+ media.media_files['thumb']) }}" />
</div>
<br />
diff --git a/mediagoblin/templates/mediagoblin/user_pages/user.html b/mediagoblin/templates/mediagoblin/user_pages/user.html
index 8a1d3a76..c8eb9026 100644
--- a/mediagoblin/templates/mediagoblin/user_pages/user.html
+++ b/mediagoblin/templates/mediagoblin/user_pages/user.html
@@ -90,7 +90,7 @@
</h1>
{% if not user.url and not user.bio %}
- {% if request.user._id == user._id %}
+ {% if request.user and (request.user._id == user._id) %}
<div class="profile_sidebar empty_space">
<p>
{% trans %}Here's a spot to tell others about yourself.{% endtrans %}
@@ -100,7 +100,6 @@
class="button_action">
{%- trans %}Edit profile{% endtrans -%}
</a>
- </div>
{% else %}
<div class="profile_sidebar empty_space">
<p>
@@ -108,17 +107,23 @@
This user hasn't filled in their profile (yet).
{%- endtrans %}
</p>
- </div>
{% endif %}
{% else %}
<div class="profile_sidebar">
{% include "mediagoblin/utils/profile.html" %}
- {% if request.user._id == user._id or request.user.is_admin %}
+ {% if request.user and
+ (request.user._id == user._id or request.user.is_admin) %}
<a href="{{ request.urlgen('mediagoblin.edit.profile') }}?username={{
user.username }}">
{%- trans %}Edit profile{% endtrans -%}
</a>
{% endif %}
+ {% endif %}
+
+ {% if request.user and (request.user._id == user._id) %}
+ <a href="{{ request.urlgen('mediagoblin.edit.account') }}">
+ {%- trans %}Change account settings{% endtrans -%}
+ </a>
</div>
{% endif %}
diff --git a/mediagoblin/templates/mediagoblin/utils/object_gallery.html b/mediagoblin/templates/mediagoblin/utils/object_gallery.html
index 65ff09a4..5f628dc7 100644
--- a/mediagoblin/templates/mediagoblin/utils/object_gallery.html
+++ b/mediagoblin/templates/mediagoblin/utils/object_gallery.html
@@ -31,7 +31,7 @@
{%- elif loop.last %} thumb_entry_last{% endif %}">
<a href="{{ entry_url }}">
<img src="{{ request.app.public_store.file_url(
- entry['media_files']['thumb']) }}" />
+ entry.media_files['thumb']) }}" />
</a>
{% if entry.title %}
<br />
@@ -68,7 +68,11 @@
{% endif %}
{% else %}
<p>
- <i>There doesn't seem to be any media here yet...</i>
+ <i>
+ {%- trans -%}
+ There doesn't seem to be any media here yet...
+ {%- endtrans -%}
+ </i>
</p>
{% endif %}
{% endmacro %}
diff --git a/mediagoblin/templates/mediagoblin/utils/wtforms.html b/mediagoblin/templates/mediagoblin/utils/wtforms.html
index cc30388f..44b27bb8 100644
--- a/mediagoblin/templates/mediagoblin/utils/wtforms.html
+++ b/mediagoblin/templates/mediagoblin/utils/wtforms.html
@@ -19,7 +19,7 @@
{# Generically render a field #}
{% macro render_field_div(field) %}
{% if field.label.text -%}
- <p class="form_field_label"><label for="{{ field.name }}">{{ _(field.label.text) }}</label></p>
+ <p class="form_field_label"><label for="{{ field.label.field_id }}">{{ _(field.label.text) }}</label></p>
{%- endif %}
<div class="form_field_input">
{{ field }}
@@ -29,7 +29,7 @@
{% endfor %}
{%- endif %}
{% if field.description -%}
- <p class="form_field_description">{{ _(field.description) }}</p>
+ <p class="form_field_description">{{ _(field.description)|safe }}</p>
{%- endif %}
</div>
{%- endmacro %}
diff --git a/mediagoblin/templates/mediagoblin/webfinger/host-meta.xml b/mediagoblin/templates/mediagoblin/webfinger/host-meta.xml
new file mode 100644
index 00000000..95a1a176
--- /dev/null
+++ b/mediagoblin/templates/mediagoblin/webfinger/host-meta.xml
@@ -0,0 +1,27 @@
+{# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011 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/>.
+-#}
+<?xml version="1.0" encoding="UTF-8"?>
+<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"
+ xmlns:hm="http://host-meta.net/xrd/1.0">
+
+ <hm:Host>{{ request.host }}</hm:Host>
+
+ <Link rel="lrdd"
+ template="{{ lrdd_template|replace(placeholder, '{uri}') }}">
+ <Title>{{ lrdd_title }}</Title>
+ </Link>
+</XRD>
diff --git a/mediagoblin/templates/mediagoblin/webfinger/xrd.xml b/mediagoblin/templates/mediagoblin/webfinger/xrd.xml
new file mode 100644
index 00000000..1fe34577
--- /dev/null
+++ b/mediagoblin/templates/mediagoblin/webfinger/xrd.xml
@@ -0,0 +1,27 @@
+{# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011 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/>.
+-#}
+<?xml version="1.0" encoding="UTF-8"?>
+<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
+
+ <Subject>{{ subject }}</Subject>
+ <Alias>{{ alias }}</Alias>
+ {% for link in links %}
+ <Link
+ {%- for attr, value in link.attrs.items() %} {{ attr }}="{{ value}}"
+ {%- endfor %} />
+ {%- endfor %}
+</XRD>
diff --git a/mediagoblin/tests/test_auth.py b/mediagoblin/tests/test_auth.py
index d3b8caf1..411b4539 100644
--- a/mediagoblin/tests/test_auth.py
+++ b/mediagoblin/tests/test_auth.py
@@ -89,7 +89,6 @@ def test_register_views(test_app):
form = context['register_form']
assert form.username.errors == [u'This field is required.']
assert form.password.errors == [u'This field is required.']
- assert form.confirm_password.errors == [u'This field is required.']
assert form.email.errors == [u'This field is required.']
# Try to register with fields that are known to be invalid
@@ -101,7 +100,6 @@ def test_register_views(test_app):
'/auth/register/', {
'username': 'l',
'password': 'o',
- 'confirm_password': 'o',
'email': 'l'})
context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html']
form = context['register_form']
@@ -125,18 +123,6 @@ def test_register_views(test_app):
assert form.email.errors == [
u'Invalid email address.']
- ## mismatching passwords
- template.clear_test_template_context()
- test_app.post(
- '/auth/register/', {
- 'password': 'herpderp',
- 'confirm_password': 'derpherp'})
- context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html']
- form = context['register_form']
-
- assert form.password.errors == [
- u'Passwords must match.']
-
## At this point there should be no users in the database ;)
assert not mg_globals.database.User.find().count()
@@ -147,7 +133,6 @@ def test_register_views(test_app):
'/auth/register/', {
'username': 'happygirl',
'password': 'iamsohappy',
- 'confirm_password': 'iamsohappy',
'email': 'happygrrl@example.org'})
response.follow()
@@ -227,7 +212,6 @@ def test_register_views(test_app):
'/auth/register/', {
'username': 'happygirl',
'password': 'iamsohappy2',
- 'confirm_password': 'iamsohappy2',
'email': 'happygrrl2@example.org'})
context = template.TEMPLATE_TEST_CONTEXT[
@@ -249,9 +233,9 @@ def test_register_views(test_app):
## Did we redirect to the proper page? Use the right template?
assert_equal(
urlparse.urlsplit(response.location)[2],
- '/auth/forgot_password/email_sent/')
+ '/auth/login/')
assert template.TEMPLATE_TEST_CONTEXT.has_key(
- 'mediagoblin/auth/fp_email_sent.html')
+ 'mediagoblin/auth/login.html')
## Make sure link to change password is sent by email
assert len(mail.EMAIL_TEST_INBOX) == 1
@@ -304,11 +288,10 @@ def test_register_views(test_app):
'/auth/forgot_password/verify/', {
'userid': parsed_get_params['userid'],
'password': 'iamveryveryhappy',
- 'confirm_password': 'iamveryveryhappy',
'token': parsed_get_params['token']})
response.follow()
assert template.TEMPLATE_TEST_CONTEXT.has_key(
- 'mediagoblin/auth/fp_changed_success.html')
+ 'mediagoblin/auth/login.html')
## Verify step 2.2 of password-change works -- login w/ new password success
template.clear_test_template_context()
diff --git a/mediagoblin/tests/test_edit.py b/mediagoblin/tests/test_edit.py
index 0cf71e9b..55f34b42 100644
--- a/mediagoblin/tests/test_edit.py
+++ b/mediagoblin/tests/test_edit.py
@@ -34,12 +34,10 @@ def test_change_password(test_app):
# test that the password can be changed
# template.clear_test_template_context()
test_app.post(
- '/edit/profile/', {
- 'bio': u'',
- 'url': u'',
+ '/edit/account/', {
'old_password': 'toast',
'new_password': '123456',
- 'confirm_password': '123456'})
+ })
# test_user has to be fetched again in order to have the current values
test_user = mg_globals.database.User.one({'username': 'chris'})
@@ -50,12 +48,10 @@ def test_change_password(test_app):
# is wrong
# template.clear_test_template_context()
test_app.post(
- '/edit/profile/', {
- 'bio': u'',
- 'url': u'',
+ '/edit/account/', {
'old_password': 'toast',
'new_password': '098765',
- 'confirm_password': '098765'})
+ })
test_user = mg_globals.database.User.one({'username': 'chris'})
diff --git a/mediagoblin/tests/test_migrations.py b/mediagoblin/tests/test_migrations.py
index e7cef0a1..8e573f5a 100644
--- a/mediagoblin/tests/test_migrations.py
+++ b/mediagoblin/tests/test_migrations.py
@@ -20,10 +20,10 @@ from pymongo import Connection
from mediagoblin.tests.tools import (
install_fixtures_simple, assert_db_meets_expected)
-from mediagoblin.db.util import (
+from mediagoblin.db.mongo.util import (
RegisterMigration, MigrationManager, ObjectId,
MissingCurrentMigration)
-from mediagoblin.db.migrations import add_table_field
+from mediagoblin.db.mongo.migrations import add_table_field
# This one will get filled with local migrations
TEST_MIGRATION_REGISTRY = {}
diff --git a/mediagoblin/tests/test_submission.py b/mediagoblin/tests/test_submission.py
index 7c372745..2b17c515 100644
--- a/mediagoblin/tests/test_submission.py
+++ b/mediagoblin/tests/test_submission.py
@@ -1,3 +1,4 @@
+
# GNU MediaGoblin -- federated, autonomous media hosting
# Copyright (C) 2011 MediaGoblin contributors. See AUTHORS.
#
@@ -16,6 +17,7 @@
import urlparse
import pkg_resources
+import re
from nose.tools import assert_equal, assert_true, assert_false
@@ -216,7 +218,8 @@ class TestSubmission:
context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/submit/start.html']
form = context['submit_form']
- assert form.file.errors == [u'Invalid file type.']
+ assert re.match(r'^Could not extract any file extension from ".*?"$', str(form.file.errors[0]))
+ assert len(form.file.errors) == 1
# NOTE: The following 2 tests will ultimately fail, but they
# *will* pass the initial form submission step. Instead,
@@ -237,7 +240,7 @@ class TestSubmission:
entry = mg_globals.database.MediaEntry.find_one(
{'title': 'Malicious Upload 2'})
- assert_equal(entry['state'], 'failed')
+ assert_equal(entry.state, 'failed')
assert_equal(
entry['fail_error'],
u'mediagoblin.processing:BadMediaFail')
@@ -257,7 +260,7 @@ class TestSubmission:
entry = mg_globals.database.MediaEntry.find_one(
{'title': 'Malicious Upload 3'})
- assert_equal(entry['state'], 'failed')
+ assert_equal(entry.state, 'failed')
assert_equal(
entry['fail_error'],
u'mediagoblin.processing:BadMediaFail')
diff --git a/mediagoblin/tools/files.py b/mediagoblin/tools/files.py
index e0bf0569..10f1d994 100644
--- a/mediagoblin/tools/files.py
+++ b/mediagoblin/tools/files.py
@@ -23,7 +23,7 @@ def delete_media_files(media):
Arguments:
- media: A MediaEntry document
"""
- for listpath in media['media_files'].itervalues():
+ for listpath in media.media_files.itervalues():
mg_globals.public_store.delete_file(
listpath)
diff --git a/mediagoblin/tools/template.py b/mediagoblin/tools/template.py
index d0400347..54a40de6 100644
--- a/mediagoblin/tools/template.py
+++ b/mediagoblin/tools/template.py
@@ -41,8 +41,11 @@ def get_jinja_env(template_loader, locale):
if SETUP_JINJA_ENVS.has_key(locale):
return SETUP_JINJA_ENVS[locale]
+ # jinja2.StrictUndefined will give exceptions on references
+ # to undefined/unknown variables in templates.
template_env = jinja2.Environment(
loader=template_loader, autoescape=True,
+ undefined=jinja2.StrictUndefined,
extensions=['jinja2.ext.i18n', 'jinja2.ext.autoescape'])
template_env.install_gettext_callables(
diff --git a/mediagoblin/user_pages/views.py b/mediagoblin/user_pages/views.py
index 87b82c74..f721f012 100644
--- a/mediagoblin/user_pages/views.py
+++ b/mediagoblin/user_pages/views.py
@@ -173,7 +173,7 @@ def media_confirm_delete(request, media):
if request.method == 'POST' and form.validate():
if form.confirm.data is True:
- username = media.get_uploader().username
+ username = media.get_uploader.username
# Delete all files on the public storage
delete_media_files(media)
@@ -192,7 +192,7 @@ def media_confirm_delete(request, media):
location=media.url_for_self(request.urlgen))
if ((request.user.is_admin and
- request.user._id != media.get_uploader()._id)):
+ request.user._id != media.uploader)):
messages.add_message(
request, messages.WARNING,
_("You are about to delete another user's media. "
diff --git a/mediagoblin/templates/mediagoblin/auth/fp_email_sent.html b/mediagoblin/webfinger/__init__.py
index 69aac6b3..ec7ec884 100644
--- a/mediagoblin/templates/mediagoblin/auth/fp_email_sent.html
+++ b/mediagoblin/webfinger/__init__.py
@@ -1,6 +1,5 @@
-{#
# GNU MediaGoblin -- federated, autonomous media hosting
-# Copyright (C) 2011 Free Software Foundation, Inc
+# Copyright (C) 2011 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
@@ -14,15 +13,13 @@
#
# 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 mediagoblin_content %}
- <p>
- {% trans -%}
- Check your inbox. We sent an email with a URL for changing your password.
- {%- endtrans %}
- </p>
-
-{% endblock %}
+'''
+mediagoblin.webfinger_ provides an LRDD discovery service and
+a web host meta information file
+Links:
+- `LRDD Discovery Draft
+ <http://tools.ietf.org/html/draft-hammer-discovery-06>`_.
+- `RFC 6415 - Web Host Metadata
+ <http://tools.ietf.org/html/rfc6415>`_.
+'''
diff --git a/mediagoblin/webfinger/routing.py b/mediagoblin/webfinger/routing.py
new file mode 100644
index 00000000..effb2bf2
--- /dev/null
+++ b/mediagoblin/webfinger/routing.py
@@ -0,0 +1,25 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011 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 routes.route import Route
+
+webfinger_well_known_routes = [
+ Route('mediagoblin.webfinger.host_meta', '/host-meta',
+ controller='mediagoblin.webfinger.views:host_meta')]
+
+webfinger_routes = [
+ Route('mediagoblin.webfinger.xrd', '/xrd',
+ controller='mediagoblin.webfinger.views:xrd')]
diff --git a/mediagoblin/webfinger/views.py b/mediagoblin/webfinger/views.py
new file mode 100644
index 00000000..22086396
--- /dev/null
+++ b/mediagoblin/webfinger/views.py
@@ -0,0 +1,117 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011 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/>.
+'''
+For references, see docstring in mediagoblin/webfinger/__init__.py
+'''
+
+import re
+
+from urlparse import urlparse
+
+from mediagoblin.tools.response import render_to_response, render_404
+
+def host_meta(request):
+ '''
+ Webfinger host-meta
+ '''
+
+ placeholder = 'MG_LRDD_PLACEHOLDER'
+
+ lrdd_title = 'GNU MediaGoblin - User lookup'
+
+ lrdd_template = request.urlgen(
+ 'mediagoblin.webfinger.xrd',
+ uri=placeholder,
+ qualified=True)
+
+ return render_to_response(
+ request,
+ 'mediagoblin/webfinger/host-meta.xml',
+ {'request': request,
+ 'lrdd_template': lrdd_template,
+ 'lrdd_title': lrdd_title,
+ 'placeholder': placeholder})
+
+MATCH_SCHEME_PATTERN = re.compile(r'^acct:')
+
+def xrd(request):
+ '''
+ Find user data based on a webfinger URI
+ '''
+ param_uri = request.GET.get('uri')
+
+ if not param_uri:
+ return render_404(request)
+
+ '''
+ :py:module:`urlparse` does not recognize usernames in URIs of the
+ form ``acct:user@example.org`` or ``user@example.org``.
+ '''
+ if not MATCH_SCHEME_PATTERN.search(param_uri):
+ # Assume the URI is in the form ``user@example.org``
+ uri = 'acct://' + param_uri
+ else:
+ # Assumes the URI looks like ``acct:user@example.org
+ uri = MATCH_SCHEME_PATTERN.sub(
+ 'acct://', param_uri)
+
+ parsed = urlparse(uri)
+
+ xrd_subject = param_uri
+
+ # TODO: Verify that the user exists
+ # Q: Does webfinger support error handling in this case?
+ # Returning 404 seems intuitive, need to check.
+ if parsed.username:
+ # The user object
+ # TODO: Fetch from database instead of using the MockUser
+ user = MockUser()
+ user.username = parsed.username
+
+ xrd_links = [
+ {'attrs': {
+ 'rel': 'http://microformats.org/profile/hcard',
+ 'href': request.urlgen(
+ 'mediagoblin.user_pages.user_home',
+ user=user.username,
+ qualified=True)}},
+ {'attrs': {
+ 'rel': 'http://schemas.google.com/g/2010#updates-from',
+ 'href': request.urlgen(
+ 'mediagoblin.user_pages.atom_feed',
+ user=user.username,
+ qualified=True)}}]
+
+ xrd_alias = request.urlgen(
+ 'mediagoblin.user_pages.user_home',
+ user=user.username,
+ qualified=True)
+
+ return render_to_response(
+ request,
+ 'mediagoblin/webfinger/xrd.xml',
+ {'request': request,
+ 'subject': xrd_subject,
+ 'alias': xrd_alias,
+ 'links': xrd_links })
+ else:
+ return render_404(request)
+
+class MockUser(object):
+ '''
+ TEMPORARY user object
+ '''
+ username = None