diff options
Diffstat (limited to 'mediagoblin')
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="vp8, vorbis"" /> + <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 }} — {{ 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 |