diff options
40 files changed, 663 insertions, 545 deletions
diff --git a/mediagoblin/app.py b/mediagoblin/app.py index c1636693..10fbf4a3 100644 --- a/mediagoblin/app.py +++ b/mediagoblin/app.py @@ -22,6 +22,7 @@ from mediagoblin.tools.routing import endpoint_to_controller from werkzeug.wrappers import Request from werkzeug.exceptions import HTTPException, NotFound +from werkzeug.routing import RequestRedirect from mediagoblin import meddleware, __version__ from mediagoblin.tools import common, translate, template @@ -186,6 +187,9 @@ class MediaGoblinApp(object): try: found_rule, url_values = map_adapter.match(return_rule=True) request.matchdict = url_values + except RequestRedirect as response: + # Deal with 301 responses eg due to missing final slash + return response(environ, start_response) except HTTPException as exc: # Stop and render exception return render_http_exception( diff --git a/mediagoblin/config_spec.ini b/mediagoblin/config_spec.ini index 17df2819..bee67d46 100644 --- a/mediagoblin/config_spec.ini +++ b/mediagoblin/config_spec.ini @@ -9,9 +9,6 @@ source_link = string(default="https://gitorious.org/mediagoblin/mediagoblin") media_types = string_list(default=list("mediagoblin.media_types.image")) # database stuff -db_host = string() -db_name = string(default="mediagoblin") -db_port = integer() sql_engine = string(default="sqlite:///%(here)s/mediagoblin.db") # Where temporary files used in processing and etc are kept diff --git a/mediagoblin/db/migration_tools.py b/mediagoblin/db/migration_tools.py new file mode 100644 index 00000000..e5380a3b --- /dev/null +++ b/mediagoblin/db/migration_tools.py @@ -0,0 +1,270 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from mediagoblin.tools.common import simple_printer +from sqlalchemy import Table + + +class MigrationManager(object): + """ + Migration handling tool. + + Takes information about a database, lets you update the database + to the latest migrations, etc. + """ + + def __init__(self, name, models, migration_registry, session, + printer=simple_printer): + """ + Args: + - name: identifier of this section of the database + - session: session we're going to migrate + - migration_registry: where we should find all migrations to + run + """ + self.name = unicode(name) + self.models = models + self.session = session + self.migration_registry = migration_registry + self._sorted_migrations = None + self.printer = printer + + # For convenience + from mediagoblin.db.models import MigrationData + + self.migration_model = MigrationData + self.migration_table = MigrationData.__table__ + + @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 + + @property + def migration_data(self): + """ + Get the migration row associated with this object, if any. + """ + return self.session.query( + self.migration_model).filter_by(name=self.name).first() + + @property + 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 + + @property + def database_current_migration(self): + """ + Return the current migration in the database. + """ + # If the table doesn't even exist, return None. + if not self.migration_table.exists(self.session.bind): + return None + + # Also return None if self.migration_data is None. + if self.migration_data is None: + return None + + return self.migration_data.version + + def set_current_migration(self, migration_number=None): + """ + Set the migration in the database to migration_number + (or, the latest available) + """ + self.migration_data.version = migration_number or self.latest_migration + self.session.commit() + + def migrations_to_run(self): + """ + Get a list of migrations to run still, if any. + + Note that this will fail if there's no migration record for + this class! + """ + assert self.database_current_migration is not None + + 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 init_tables(self): + """ + Create all tables relative to this package + """ + # sanity check before we proceed, none of these should be created + for model in self.models: + # Maybe in the future just print out a "Yikes!" or something? + assert not model.__table__.exists(self.session.bind) + + self.migration_model.metadata.create_all( + self.session.bind, + tables=[model.__table__ for model in self.models]) + + def create_new_migration_record(self): + """ + Create a new migration record for this migration set + """ + migration_record = self.migration_model( + name=self.name, + version=self.latest_migration) + self.session.add(migration_record) + self.session.commit() + + def dry_run(self): + """ + Print out a dry run of what we would have upgraded. + """ + if self.database_current_migration is None: + self.printer( + u'~> Woulda initialized: %s\n' % self.name_for_printing()) + return u'inited' + + migrations_to_run = self.migrations_to_run() + if migrations_to_run: + self.printer( + u'~> Woulda updated %s:\n' % self.name_for_printing()) + + for migration_number, migration_func in migrations_to_run(): + self.printer( + u' + Would update %s, "%s"\n' % ( + migration_number, migration_func.func_name)) + + return u'migrated' + + def name_for_printing(self): + if self.name == u'__main__': + return u"main mediagoblin tables" + else: + # TODO: Use the friendlier media manager "human readable" name + return u'media type "%s"' % self.name + + def init_or_migrate(self): + """ + Initialize the database or migrate if appropriate. + + Returns information about whether or not we initialized + ('inited'), migrated ('migrated'), or did nothing (None) + """ + assure_migrations_table_setup(self.session) + + # Find out what migration number, if any, this database data is at, + # and what the latest is. + migration_number = self.database_current_migration + + # Is this our first time? Is there even a table entry for + # this identifier? + # If so: + # - create all tables + # - create record in migrations registry + # - print / inform the user + # - return 'inited' + if migration_number is None: + self.printer(u"-> Initializing %s... " % self.name_for_printing()) + + self.init_tables() + # auto-set at latest migration number + self.create_new_migration_record() + + self.printer(u"done.\n") + self.set_current_migration() + return u'inited' + + # Run migrations, if appropriate. + migrations_to_run = self.migrations_to_run() + if migrations_to_run: + self.printer( + u'-> Updating %s:\n' % self.name_for_printing()) + for migration_number, migration_func in migrations_to_run: + self.printer( + u' + Running migration %s, "%s"... ' % ( + migration_number, migration_func.func_name)) + migration_func(self.session) + self.set_current_migration(migration_number) + self.printer('done.\n') + + return u'migrated' + + # Otherwise return None. Well it would do this anyway, but + # for clarity... ;) + return None + + +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): + 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 + + +def assure_migrations_table_setup(db): + """ + Make sure the migrations table is set up in the database. + """ + from mediagoblin.db.models import MigrationData + + if not MigrationData.__table__.exists(db.bind): + MigrationData.metadata.create_all( + db.bind, tables=[MigrationData.__table__]) + + +def inspect_table(metadata, table_name): + """Simple helper to get a ref to an already existing table""" + return Table(table_name, metadata, autoload=True, + autoload_with=metadata.bind) diff --git a/mediagoblin/db/migrations.py b/mediagoblin/db/migrations.py index 476e2a06..80ec5269 100644 --- a/mediagoblin/db/migrations.py +++ b/mediagoblin/db/migrations.py @@ -23,7 +23,7 @@ from sqlalchemy.exc import ProgrammingError from sqlalchemy.ext.declarative import declarative_base from migrate.changeset.constraint import UniqueConstraint -from mediagoblin.db.util import RegisterMigration +from mediagoblin.db.migration_tools import RegisterMigration, inspect_table from mediagoblin.db.models import MediaEntry, Collection, User MIGRATIONS = {} @@ -60,8 +60,7 @@ def add_wants_notification_column(db_conn): def add_transcoding_progress(db_conn): metadata = MetaData(bind=db_conn.bind) - media_entry = Table('core__media_entries', metadata, autoload=True, - autoload_with=db_conn.bind) + media_entry = inspect_table(metadata, 'core__media_entries') col = Column('transcoding_progress', SmallInteger) col.create(media_entry) @@ -115,8 +114,7 @@ def add_collection_tables(db_conn): def add_mediaentry_collected(db_conn): metadata = MetaData(bind=db_conn.bind) - media_entry = Table('core__media_entries', metadata, autoload=True, - autoload_with=db_conn.bind) + media_entry = inspect_table(metadata, 'core__media_entries') col = Column('collected', Integer, default=0) col.create(media_entry) @@ -172,8 +170,7 @@ def fix_CollectionItem_v0_constraint(db_conn): metadata = MetaData(bind=db_conn.bind) - CollectionItem_table = Table('core__collection_items', - metadata, autoload=True, autoload_with=db_conn.bind) + CollectionItem_table = inspect_table(metadata, 'core__collection_items') constraint = UniqueConstraint('collection', 'media_entry', name='core__collection_items_collection_media_entry_key', diff --git a/mediagoblin/db/mixin.py b/mediagoblin/db/mixin.py index 9829bb6e..001b7826 100644 --- a/mediagoblin/db/mixin.py +++ b/mediagoblin/db/mixin.py @@ -58,8 +58,7 @@ class MediaEntryMixin(object): self.slug = slugify(self.title) - duplicate = check_media_slug_used(mg_globals.database, - self.uploader, self.slug, self.id) + duplicate = check_media_slug_used(self.uploader, self.slug, self.id) if duplicate: if self.id is not None: diff --git a/mediagoblin/db/util.py b/mediagoblin/db/util.py index 2017cfc0..529ef8b9 100644 --- a/mediagoblin/db/util.py +++ b/mediagoblin/db/util.py @@ -13,258 +13,10 @@ # # 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 sys + from mediagoblin.db.base import Session from mediagoblin.db.models import MediaEntry, Tag, MediaTag, Collection -from mediagoblin.tools.common import simple_printer - - -class MigrationManager(object): - """ - Migration handling tool. - - Takes information about a database, lets you update the database - to the latest migrations, etc. - """ - - def __init__(self, name, models, migration_registry, session, - printer=simple_printer): - """ - Args: - - name: identifier of this section of the database - - session: session we're going to migrate - - migration_registry: where we should find all migrations to - run - """ - self.name = unicode(name) - self.models = models - self.session = session - self.migration_registry = migration_registry - self._sorted_migrations = None - self.printer = printer - - # For convenience - from mediagoblin.db.models import MigrationData - - self.migration_model = MigrationData - self.migration_table = MigrationData.__table__ - - @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 - - @property - def migration_data(self): - """ - Get the migration row associated with this object, if any. - """ - return self.session.query( - self.migration_model).filter_by(name=self.name).first() - - @property - 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 - - @property - def database_current_migration(self): - """ - Return the current migration in the database. - """ - # If the table doesn't even exist, return None. - if not self.migration_table.exists(self.session.bind): - return None - - # Also return None if self.migration_data is None. - if self.migration_data is None: - return None - - return self.migration_data.version - - def set_current_migration(self, migration_number=None): - """ - Set the migration in the database to migration_number - (or, the latest available) - """ - self.migration_data.version = migration_number or self.latest_migration - self.session.commit() - - def migrations_to_run(self): - """ - Get a list of migrations to run still, if any. - - Note that this will fail if there's no migration record for - this class! - """ - assert self.database_current_migration is not None - - 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 init_tables(self): - """ - Create all tables relative to this package - """ - # sanity check before we proceed, none of these should be created - for model in self.models: - # Maybe in the future just print out a "Yikes!" or something? - assert not model.__table__.exists(self.session.bind) - - self.migration_model.metadata.create_all( - self.session.bind, - tables=[model.__table__ for model in self.models]) - - def create_new_migration_record(self): - """ - Create a new migration record for this migration set - """ - migration_record = self.migration_model( - name=self.name, - version=self.latest_migration) - self.session.add(migration_record) - self.session.commit() - - def dry_run(self): - """ - Print out a dry run of what we would have upgraded. - """ - if self.database_current_migration is None: - self.printer( - u'~> Woulda initialized: %s\n' % self.name_for_printing()) - return u'inited' - - migrations_to_run = self.migrations_to_run() - if migrations_to_run: - self.printer( - u'~> Woulda updated %s:\n' % self.name_for_printing()) - - for migration_number, migration_func in migrations_to_run(): - self.printer( - u' + Would update %s, "%s"\n' % ( - migration_number, migration_func.func_name)) - - return u'migrated' - - def name_for_printing(self): - if self.name == u'__main__': - return u"main mediagoblin tables" - else: - # TODO: Use the friendlier media manager "human readable" name - return u'media type "%s"' % self.name - - def init_or_migrate(self): - """ - Initialize the database or migrate if appropriate. - - Returns information about whether or not we initialized - ('inited'), migrated ('migrated'), or did nothing (None) - """ - assure_migrations_table_setup(self.session) - - # Find out what migration number, if any, this database data is at, - # and what the latest is. - migration_number = self.database_current_migration - - # Is this our first time? Is there even a table entry for - # this identifier? - # If so: - # - create all tables - # - create record in migrations registry - # - print / inform the user - # - return 'inited' - if migration_number is None: - self.printer(u"-> Initializing %s... " % self.name_for_printing()) - - self.init_tables() - # auto-set at latest migration number - self.create_new_migration_record() - - self.printer(u"done.\n") - self.set_current_migration() - return u'inited' - - # Run migrations, if appropriate. - migrations_to_run = self.migrations_to_run() - if migrations_to_run: - self.printer( - u'-> Updating %s:\n' % self.name_for_printing()) - for migration_number, migration_func in migrations_to_run: - self.printer( - u' + Running migration %s, "%s"... ' % ( - migration_number, migration_func.func_name)) - migration_func(self.session) - self.set_current_migration(migration_number) - self.printer('done.\n') - - return u'migrated' - - # Otherwise return None. Well it would do this anyway, but - # for clarity... ;) - return None - - -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): - 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 - - -def assure_migrations_table_setup(db): - """ - Make sure the migrations table is set up in the database. - """ - from mediagoblin.db.models import MigrationData - - if not MigrationData.__table__.exists(db.bind): - MigrationData.metadata.create_all( - db.bind, tables=[MigrationData.__table__]) - ########################## # Random utility functions @@ -277,12 +29,11 @@ def atomic_update(table, query_dict, update_values): Session.commit() -def check_media_slug_used(dummy_db, uploader_id, slug, ignore_m_id): - filt = (MediaEntry.uploader == uploader_id) \ - & (MediaEntry.slug == slug) +def check_media_slug_used(uploader_id, slug, ignore_m_id): + query = MediaEntry.query.filter_by(uploader=uploader_id, slug=slug) if ignore_m_id is not None: - filt = filt & (MediaEntry.id != ignore_m_id) - does_exist = Session.query(MediaEntry.id).filter(filt).first() is not None + query = query.filter(MediaEntry.id != ignore_m_id) + does_exist = query.first() is not None return does_exist diff --git a/mediagoblin/decorators.py b/mediagoblin/decorators.py index 5533e81d..a40f1d5a 100644 --- a/mediagoblin/decorators.py +++ b/mediagoblin/decorators.py @@ -69,7 +69,7 @@ def user_may_delete_media(controller): """ @wraps(controller) def wrapper(request, *args, **kwargs): - uploader_id = MediaEntry.query.get(request.matchdict['media']).uploader + uploader_id = kwargs['media'].uploader if not (request.user.is_admin or request.user.id == uploader_id): raise Forbidden() @@ -209,12 +209,16 @@ def get_media_entry_by_id(controller): @wraps(controller) def wrapper(request, *args, **kwargs): media = MediaEntry.query.filter_by( - id=request.matchdict['media'], + id=request.matchdict['media_id'], state=u'processed').first() # Still no media? Okay, 404. if not media: return render_404(request) + given_username = request.matchdict.get('user') + if given_username and (given_username != media.get_uploader.username): + return render_404(request) + return controller(request, media=media, *args, **kwargs) return wrapper diff --git a/mediagoblin/edit/routing.py b/mediagoblin/edit/routing.py index 3e6787d2..d382e549 100644 --- a/mediagoblin/edit/routing.py +++ b/mediagoblin/edit/routing.py @@ -16,7 +16,9 @@ from mediagoblin.tools.routing import add_route -add_route('mediagoblin.edit.profile', '/edit/profile/', +add_route('mediagoblin.edit.profile', '/u/<string:user>/edit/', 'mediagoblin.edit.views:edit_profile') +add_route('mediagoblin.edit.legacy_edit_profile', '/edit/profile/', + 'mediagoblin.edit.views:legacy_edit_profile') add_route('mediagoblin.edit.account', '/edit/account/', 'mediagoblin.edit.views:edit_account') diff --git a/mediagoblin/edit/views.py b/mediagoblin/edit/views.py index 9de034bb..9b7cab46 100644 --- a/mediagoblin/edit/views.py +++ b/mediagoblin/edit/views.py @@ -26,8 +26,9 @@ from mediagoblin import mg_globals from mediagoblin.auth import lib as auth_lib from mediagoblin.edit import forms from mediagoblin.edit.lib import may_edit_media -from mediagoblin.decorators import require_active_login, get_user_media_entry, \ - user_may_alter_collection, get_user_collection +from mediagoblin.decorators import (require_active_login, active_user_from_url, + get_media_entry_by_id, + get_user_media_entry, user_may_alter_collection, get_user_collection) from mediagoblin.tools.response import render_to_response, redirect from mediagoblin.tools.translate import pass_to_ugettext as _ from mediagoblin.tools.text import ( @@ -37,7 +38,7 @@ from mediagoblin.db.util import check_media_slug_used, check_collection_slug_use import mimetypes -@get_user_media_entry +@get_media_entry_by_id @require_active_login def edit_media(request, media): if not may_edit_media(request, media): @@ -57,8 +58,8 @@ def edit_media(request, media): if request.method == 'POST' and form.validate(): # Make sure there isn't already a MediaEntry with such a slug # and userid. - slug_used = check_media_slug_used(request.db, media.uploader, - request.form['slug'], media.id) + slug_used = check_media_slug_used(media.uploader, request.form['slug'], + media.id) if slug_used: form.slug.errors.append( @@ -167,20 +168,28 @@ def edit_attachments(request, media): else: raise Forbidden("Attachments are disabled") +@require_active_login +def legacy_edit_profile(request): + """redirect the old /edit/profile/?username=USER to /u/USER/edit/""" + username = request.GET.get('username') or request.user.username + return redirect(request, 'mediagoblin.edit.profile', user=username) + @require_active_login -def edit_profile(request): - # admins may edit any user profile given a username in the querystring - edit_username = request.GET.get('username') - if request.user.is_admin and request.user.username != edit_username: - user = request.db.User.find_one({'username': edit_username}) +@active_user_from_url +def edit_profile(request, url_user=None): + # admins may edit any user profile + if request.user.username != url_user.username: + if not request.user.is_admin: + raise Forbidden(_("You can only edit your own profile.")) + # No need to warn again if admin just submitted an edited profile if request.method != 'POST': messages.add_message( request, messages.WARNING, _("You are editing a user's profile. Proceed with caution.")) - else: - user = request.user + + user = url_user form = forms.EditProfileForm(request.form, url=user.get('url'), diff --git a/mediagoblin/gmg_commands/dbupdate.py b/mediagoblin/gmg_commands/dbupdate.py index 5151ba9d..65b3f922 100644 --- a/mediagoblin/gmg_commands/dbupdate.py +++ b/mediagoblin/gmg_commands/dbupdate.py @@ -19,7 +19,7 @@ import logging from sqlalchemy.orm import sessionmaker from mediagoblin.db.open import setup_connection_and_db_from_config -from mediagoblin.db.util import MigrationManager +from mediagoblin.db.migration_tools import MigrationManager from mediagoblin.init import setup_global_and_app_config from mediagoblin.tools.common import import_component diff --git a/mediagoblin/meddleware/csrf.py b/mediagoblin/meddleware/csrf.py index 2984ebb9..661f0ba2 100644 --- a/mediagoblin/meddleware/csrf.py +++ b/mediagoblin/meddleware/csrf.py @@ -22,6 +22,7 @@ from wtforms import Form, HiddenField, validators from mediagoblin import mg_globals from mediagoblin.meddleware import BaseMeddleware +from mediagoblin.tools.translate import lazy_pass_to_ugettext as _ _log = logging.getLogger(__name__) @@ -127,10 +128,13 @@ class CsrfMeddleware(BaseMeddleware): None) if cookie_token is None: - # the CSRF cookie must be present in the request - errstr = 'CSRF cookie not present' - _log.error(errstr) - raise Forbidden(errstr) + # the CSRF cookie must be present in the request, if not a + # cookie blocker might be in action (in the best case) + _log.error('CSRF cookie not present') + raise Forbidden(_('CSRF cookie not present. This is most likely ' + 'the result of a cookie blocker or somesuch.<br/>' + 'Make sure to permit the settings of cookies for ' + 'this domain.')) # get the form token and confirm it matches form = CsrfForm(request.form) diff --git a/mediagoblin/media_types/stl/models.py b/mediagoblin/media_types/stl/models.py index 0ed4a2e5..17091f0e 100644 --- a/mediagoblin/media_types/stl/models.py +++ b/mediagoblin/media_types/stl/models.py @@ -15,7 +15,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. -from mediagoblin.db.sql.base import Base +from mediagoblin.db.base import Base from sqlalchemy import ( Column, Integer, Float, String, ForeignKey) diff --git a/mediagoblin/plugins/oauth/migrations.py b/mediagoblin/plugins/oauth/migrations.py index 7b028faf..6aa0d7cb 100644 --- a/mediagoblin/plugins/oauth/migrations.py +++ b/mediagoblin/plugins/oauth/migrations.py @@ -19,7 +19,7 @@ from sqlalchemy import (MetaData, Table, Column, Integer, Unicode, Enum, DateTime, ForeignKey) from sqlalchemy.ext.declarative import declarative_base -from mediagoblin.db.util import RegisterMigration +from mediagoblin.db.migration_tools import RegisterMigration from mediagoblin.db.models import User diff --git a/mediagoblin/templates/mediagoblin/base.html b/mediagoblin/templates/mediagoblin/base.html index 3f5e2c79..0a9a56d3 100644 --- a/mediagoblin/templates/mediagoblin/base.html +++ b/mediagoblin/templates/mediagoblin/base.html @@ -41,13 +41,7 @@ {% block mediagoblin_body %} {% block mediagoblin_header %} <header> - {% block mediagoblin_logo %} - <a class="logo" - href="{{ request.urlgen('index') }}" - ><img src="{{ request.staticdirect('/images/logo.png') }}" - alt="{% trans %}MediaGoblin logo{% endtrans %}" /> - </a> - {% endblock mediagoblin_logo %} + {%- include "mediagoblin/bits/logo.html" -%} {% block mediagoblin_header_title %}{% endblock %} <div class="header_right"> {% if request.user %} diff --git a/mediagoblin/templates/mediagoblin/bits/logo.html b/mediagoblin/templates/mediagoblin/bits/logo.html new file mode 100644 index 00000000..5bd8edd8 --- /dev/null +++ b/mediagoblin/templates/mediagoblin/bits/logo.html @@ -0,0 +1,25 @@ +{# +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +-#} + +{% block mediagoblin_logo %} + <a class="logo" + href="{{ request.urlgen('index') }}" + ><img src="{{ request.staticdirect('/images/logo.png') }}" + alt="{% trans %}MediaGoblin logo{% endtrans %}" /> + </a> +{% endblock mediagoblin_logo -%} diff --git a/mediagoblin/templates/mediagoblin/edit/edit.html b/mediagoblin/templates/mediagoblin/edit/edit.html index 1f5b91f7..9a040095 100644 --- a/mediagoblin/templates/mediagoblin/edit/edit.html +++ b/mediagoblin/templates/mediagoblin/edit/edit.html @@ -29,7 +29,7 @@ <form action="{{ request.urlgen('mediagoblin.edit.edit_media', user= media.get_uploader.username, - media= media.id) }}" + media_id=media.id) }}" method="POST" enctype="multipart/form-data"> <div class="form_box_xl edit_box"> <h1>{% trans media_title=media.title %}Editing {{ media_title }}{% endtrans %}</h1> diff --git a/mediagoblin/templates/mediagoblin/edit/edit_profile.html b/mediagoblin/templates/mediagoblin/edit/edit_profile.html index 2b2fa4fa..163fe186 100644 --- a/mediagoblin/templates/mediagoblin/edit/edit_profile.html +++ b/mediagoblin/templates/mediagoblin/edit/edit_profile.html @@ -27,9 +27,8 @@ {% block mediagoblin_content %} - <form action="{{ request.urlgen('mediagoblin.edit.profile') }}?username={{ - user.username }}" - method="POST" enctype="multipart/form-data"> + <form action="{{ request.urlgen('mediagoblin.edit.profile', + user=user.username) }}" method="POST" enctype="multipart/form-data"> <div class="form_box edit_box"> <h1> {%- trans username=user.username -%} diff --git a/mediagoblin/templates/mediagoblin/root.html b/mediagoblin/templates/mediagoblin/root.html index 047dd2bb..11e8f2ac 100644 --- a/mediagoblin/templates/mediagoblin/root.html +++ b/mediagoblin/templates/mediagoblin/root.html @@ -27,9 +27,10 @@ <li><a href="{{ request.urlgen('mediagoblin.submit.start') }}"> {%- trans %}Add media{% endtrans -%} </a></li> - <li><a href="{{ request.urlgen('mediagoblin.submit.collection') }}"> - {%- trans %}Create new collection{% endtrans -%} - </a></li> + <li><a href="{{ request.urlgen('mediagoblin.user_pages.collection_list', + user=request.user.username) }}"> + {%- trans %}Browse collections{% endtrans -%} + </a></li> <li><a href="{{ request.urlgen('mediagoblin.edit.account') }}"> {%- trans %}Change account settings{% endtrans -%} </a></li> diff --git a/mediagoblin/templates/mediagoblin/user_pages/collection.html b/mediagoblin/templates/mediagoblin/user_pages/collection.html index f1ab7a42..5a7baadd 100644 --- a/mediagoblin/templates/mediagoblin/user_pages/collection.html +++ b/mediagoblin/templates/mediagoblin/user_pages/collection.html @@ -57,7 +57,9 @@ {% endif %} <p> - {{ collection.description }} + {% autoescape False %} + {{ collection.description_html }} + {% endautoescape %} </p> {{ collection_gallery(request, collection_items, pagination) }} diff --git a/mediagoblin/templates/mediagoblin/user_pages/collection_list.html b/mediagoblin/templates/mediagoblin/user_pages/collection_list.html new file mode 100644 index 00000000..abf22623 --- /dev/null +++ b/mediagoblin/templates/mediagoblin/user_pages/collection_list.html @@ -0,0 +1,56 @@ +{# +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +#} +{% extends "mediagoblin/base.html" %} + +{% block title %} + {%- trans username=user.username -%} + {{ username }}'s collections + {%- endtrans %} — {{ super() }} +{% endblock %} + +{% block mediagoblin_content -%} + <h1> + {%- trans username=user.username, + user_url=request.urlgen( + 'mediagoblin.user_pages.user_home', + user=user.username) -%} + <a href="{{ user_url }}">{{ username }}</a>'s collections + {%- endtrans %} + </h1> + + {% if request.user %} + {% if request.user.status == 'active' %} + <p> + <a href="{{ request.urlgen('mediagoblin.submit.collection', + user=user.username) }}"> + {%- trans %}Create new collection{% endtrans -%} + </p> + {% endif %} + {% endif %} + + <ul> + {% for coll in collections %} + {% set coll_url = request.urlgen( + 'mediagoblin.user_pages.user_collection', + user=user.username, + collection=coll.slug) %} + <li><a href="{{ coll_url }}">{{ coll.title }}</li> + {% endfor %} + </ul> + +{% endblock %} diff --git a/mediagoblin/templates/mediagoblin/user_pages/media.html b/mediagoblin/templates/mediagoblin/user_pages/media.html index 11f2a2a1..10b48296 100644 --- a/mediagoblin/templates/mediagoblin/user_pages/media.html +++ b/mediagoblin/templates/mediagoblin/user_pages/media.html @@ -83,11 +83,11 @@ request.user.is_admin) %} {% set edit_url = request.urlgen('mediagoblin.edit.edit_media', user= media.get_uploader.username, - media= media.id) %} + media_id=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, - media= media.id) %} + media_id=media.id) %} <a class="button_action" href="{{ delete_url }}">{% trans %}Delete{% endtrans %}</a> {% endif %} {% autoescape False %} @@ -104,7 +104,7 @@ {% if request.user %} <form action="{{ request.urlgen('mediagoblin.user_pages.media_post_comment', user= media.get_uploader.username, - media=media.id) }}" method="POST" id="form_comment"> + media_id=media.id) }}" method="POST" id="form_comment"> <p> {% trans %}You can use <a href="http://daringfireball.net/projects/markdown/basics">Markdown</a> for formatting.{% endtrans %} </p> diff --git a/mediagoblin/templates/mediagoblin/user_pages/media_confirm_delete.html b/mediagoblin/templates/mediagoblin/user_pages/media_confirm_delete.html index 833f500d..d2a5655e 100644 --- a/mediagoblin/templates/mediagoblin/user_pages/media_confirm_delete.html +++ b/mediagoblin/templates/mediagoblin/user_pages/media_confirm_delete.html @@ -23,7 +23,7 @@ <form action="{{ request.urlgen('mediagoblin.user_pages.media_confirm_delete', user=media.get_uploader.username, - media=media.id) }}" + media_id=media.id) }}" method="POST" enctype="multipart/form-data"> <div class="form_box"> <h1> diff --git a/mediagoblin/templates/mediagoblin/user_pages/user.html b/mediagoblin/templates/mediagoblin/user_pages/user.html index 65c636b9..76bce1e2 100644 --- a/mediagoblin/templates/mediagoblin/user_pages/user.html +++ b/mediagoblin/templates/mediagoblin/user_pages/user.html @@ -95,9 +95,8 @@ <p> {% trans %}Here's a spot to tell others about yourself.{% endtrans %} </p> - <a href="{{ request.urlgen('mediagoblin.edit.profile') }}?username={{ - user.username }}" - class="button_action"> + <a href="{{ request.urlgen('mediagoblin.edit.profile', + user=user.username) }}" class="button_action"> {%- trans %}Edit profile{% endtrans -%} </a> {% else %} @@ -113,8 +112,8 @@ {% include "mediagoblin/utils/profile.html" %} {% if request.user and (request.user.id == user.id or request.user.is_admin) %} - <a href="{{ request.urlgen('mediagoblin.edit.profile') }}?username={{ - user.username }}"> + <a href="{{ request.urlgen('mediagoblin.edit.profile', + user=user.username) }}"> {%- trans %}Edit profile{% endtrans -%} </a> {% endif %} diff --git a/mediagoblin/tests/test_api.py b/mediagoblin/tests/test_api.py index 188cdadb..4b784da3 100644 --- a/mediagoblin/tests/test_api.py +++ b/mediagoblin/tests/test_api.py @@ -44,7 +44,7 @@ BIG_BLUE = resource('bigblue.png') class TestAPI(object): def setUp(self): - self.app = get_test_app() + self.app = get_test_app(dump_old_app=False) self.db = mg_globals.database self.user_password = u'4cc355_70k3N' diff --git a/mediagoblin/tests/test_auth.py b/mediagoblin/tests/test_auth.py index f4a31a81..a40c9cbc 100644 --- a/mediagoblin/tests/test_auth.py +++ b/mediagoblin/tests/test_auth.py @@ -22,7 +22,7 @@ from nose.tools import assert_equal from mediagoblin import mg_globals from mediagoblin.auth import lib as auth_lib from mediagoblin.db.models import User -from mediagoblin.tests.tools import setup_fresh_app, fixture_add_user +from mediagoblin.tests.tools import get_test_app, fixture_add_user from mediagoblin.tools import template, mail @@ -67,11 +67,11 @@ def test_bcrypt_gen_password_hash(): 'notthepassword', hashed_pw, '3><7R45417') -@setup_fresh_app -def test_register_views(test_app): +def test_register_views(): """ Massive test function that all our registration-related views all work. """ + test_app = get_test_app(dump_old_app=False) # Test doing a simple GET on the page # ----------------------------------- @@ -311,11 +311,11 @@ def test_register_views(test_app): 'mediagoblin/root.html') -@setup_fresh_app -def test_authentication_views(test_app): +def test_authentication_views(): """ Test logging in and logging out """ + test_app = get_test_app(dump_old_app=False) # Make a new user test_user = fixture_add_user(active_user=False) diff --git a/mediagoblin/tests/test_csrf_middleware.py b/mediagoblin/tests/test_csrf_middleware.py index d730909f..22a0eb04 100644 --- a/mediagoblin/tests/test_csrf_middleware.py +++ b/mediagoblin/tests/test_csrf_middleware.py @@ -14,13 +14,12 @@ # 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.tests.tools import setup_fresh_app +from mediagoblin.tests.tools import get_test_app from mediagoblin import mg_globals -@setup_fresh_app -def test_csrf_cookie_set(test_app): - +def test_csrf_cookie_set(): + test_app = get_test_app(dump_old_app=False) cookie_name = mg_globals.app_config['csrf_cookie_name'] # get login page @@ -34,8 +33,11 @@ def test_csrf_cookie_set(test_app): assert response.headers.get('Vary', False) == 'Cookie' -@setup_fresh_app -def test_csrf_token_must_match(test_app): +def test_csrf_token_must_match(): + # We need a fresh app for this test on webtest < 1.3.6. + # We do not understand why, but it fixes the tests. + # If we require webtest >= 1.3.6, we can switch to a non fresh app here. + test_app = get_test_app(dump_old_app=True) # construct a request with no cookie or form token assert test_app.post('/auth/login/', @@ -65,9 +67,8 @@ def test_csrf_token_must_match(test_app): extra_environ={'gmg.verify_csrf': True}).\ status_int == 200 -@setup_fresh_app -def test_csrf_exempt(test_app): - +def test_csrf_exempt(): + test_app = get_test_app(dump_old_app=False) # monkey with the views to decorate a known endpoint import mediagoblin.auth.views from mediagoblin.meddleware.csrf import csrf_exempt diff --git a/mediagoblin/tests/test_edit.py b/mediagoblin/tests/test_edit.py index 353a7eb9..cbdad649 100644 --- a/mediagoblin/tests/test_edit.py +++ b/mediagoblin/tests/test_edit.py @@ -14,83 +14,104 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +from nose.tools import assert_equal + from mediagoblin import mg_globals -from mediagoblin.tests.tools import setup_fresh_app, fixture_add_user +from mediagoblin.db.models import User +from mediagoblin.tests.tools import get_test_app, fixture_add_user from mediagoblin.tools import template from mediagoblin.auth.lib import bcrypt_check_password - -@setup_fresh_app -def test_change_password(test_app): - """Test changing password correctly and incorrectly""" - # set up new user - test_user = fixture_add_user() - - test_app.post( - '/auth/login/', { - 'username': u'chris', - 'password': 'toast'}) - - # test that the password can be changed - # template.clear_test_template_context() - test_app.post( - '/edit/account/', { - 'old_password': 'toast', - 'new_password': '123456', - 'wants_comment_notification': 'y' - }) - - # test_user has to be fetched again in order to have the current values - test_user = mg_globals.database.User.one({'username': u'chris'}) - - assert bcrypt_check_password('123456', test_user.pw_hash) - - # test that the password cannot be changed if the given old_password - # is wrong - # template.clear_test_template_context() - test_app.post( - '/edit/account/', { - 'old_password': 'toast', - 'new_password': '098765', - }) - - test_user = mg_globals.database.User.one({'username': u'chris'}) - - assert not bcrypt_check_password('098765', test_user.pw_hash) - - -@setup_fresh_app -def change_bio_url(test_app): - """Test changing bio and URL""" - # set up new user - test_user = fixture_add_user() - - # test changing the bio and the URL properly - test_app.post( - '/edit/profile/', { - 'bio': u'I love toast!', - 'url': u'http://dustycloud.org/'}) - - test_user = mg_globals.database.User.one({'username': u'chris'}) - - assert test_user.bio == u'I love toast!' - assert test_user.url == u'http://dustycloud.org/' - - # test changing the bio and the URL inproperly - too_long_bio = 150 * 'T' + 150 * 'o' + 150 * 'a' + 150 * 's' + 150* 't' - - test_app.post( - '/edit/profile/', { - # more than 500 characters - 'bio': too_long_bio, - 'url': 'this-is-no-url'}) - - test_user = mg_globals.database.User.one({'username': u'chris'}) - - context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/edit/edit_profile.html'] - form = context['edit_profile_form'] - - assert form.bio.errors == [u'Field must be between 0 and 500 characters long.'] - assert form.url.errors == [u'Improperly formed URL'] - - # test changing the url inproperly +class TestUserEdit(object): + def setUp(self): + self.app = get_test_app(dump_old_app=False) + # set up new user + self.user_password = u'toast' + self.user = fixture_add_user(password = self.user_password) + self.login() + + def login(self): + self.app.post( + '/auth/login/', { + 'username': self.user.username, + 'password': self.user_password}) + + + def test_change_password(self): + """Test changing password correctly and incorrectly""" + # test that the password can be changed + # template.clear_test_template_context() + res = self.app.post( + '/edit/account/', { + 'old_password': 'toast', + 'new_password': '123456', + 'wants_comment_notification': 'y' + }) + + # Check for redirect on success + assert_equal(res.status_int, 302) + # test_user has to be fetched again in order to have the current values + test_user = User.query.filter_by(username=u'chris').first() + assert bcrypt_check_password('123456', test_user.pw_hash) + # Update current user passwd + self.user_password = '123456' + + # test that the password cannot be changed if the given + # old_password is wrong template.clear_test_template_context() + self.app.post( + '/edit/account/', { + 'old_password': 'toast', + 'new_password': '098765', + }) + + test_user = User.query.filter_by(username=u'chris').first() + assert not bcrypt_check_password('098765', test_user.pw_hash) + + + + def test_change_bio_url(self): + """Test changing bio and URL""" + # Test if legacy profile editing URL redirects correctly + res = self.app.post( + '/edit/profile/', { + 'bio': u'I love toast!', + 'url': u'http://dustycloud.org/'}, expect_errors=True) + + # Should redirect to /u/chris/edit/ + assert_equal (res.status_int, 302) + assert res.headers['Location'].endswith("/u/chris/edit/") + + res = self.app.post( + '/u/chris/edit/', { + 'bio': u'I love toast!', + 'url': u'http://dustycloud.org/'}) + + test_user = User.query.filter_by(username=u'chris').first() + assert_equal(test_user.bio, u'I love toast!') + assert_equal(test_user.url, u'http://dustycloud.org/') + + # change a different user than the logged in (should fail with 403) + fixture_add_user(username=u"foo") + res = self.app.post( + '/u/foo/edit/', { + 'bio': u'I love toast!', + 'url': u'http://dustycloud.org/'}, expect_errors=True) + assert_equal(res.status_int, 403) + + # test changing the bio and the URL inproperly + too_long_bio = 150 * 'T' + 150 * 'o' + 150 * 'a' + 150 * 's' + 150* 't' + + self.app.post( + '/u/chris/edit/', { + # more than 500 characters + 'bio': too_long_bio, + 'url': 'this-is-no-url'}) + + # Check form errors + context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/edit/edit_profile.html'] + form = context['form'] + + assert_equal(form.bio.errors, [u'Field must be between 0 and 500 characters long.']) + assert_equal(form.url.errors, [u'This address contains errors']) + +# test changing the url inproperly diff --git a/mediagoblin/tests/test_http_callback.py b/mediagoblin/tests/test_http_callback.py index 8b0a03b9..0f6e489f 100644 --- a/mediagoblin/tests/test_http_callback.py +++ b/mediagoblin/tests/test_http_callback.py @@ -27,7 +27,7 @@ from mediagoblin.tests import test_oauth as oauth class TestHTTPCallback(object): def setUp(self): - self.app = get_test_app() + self.app = get_test_app(dump_old_app=False) self.db = mg_globals.database self.user_password = u'secret' diff --git a/mediagoblin/tests/test_messages.py b/mediagoblin/tests/test_messages.py index d3b84828..c587e599 100644 --- a/mediagoblin/tests/test_messages.py +++ b/mediagoblin/tests/test_messages.py @@ -15,30 +15,31 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. from mediagoblin.messages import fetch_messages, add_message -from mediagoblin.tests.tools import setup_fresh_app +from mediagoblin.tests.tools import get_test_app from mediagoblin.tools import template -@setup_fresh_app -def test_messages(test_app): + +def test_messages(): """ Added messages should show up in the request.session, fetched messages should be the same as the added ones, and fetching should clear the message list. """ + test_app = get_test_app(dump_old_app=False) # Aquire a request object test_app.get('/') context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/root.html'] request = context['request'] - + # The message queue should be empty assert request.session.get('messages', []) == [] - + # Adding a message should modify the session accordingly add_message(request, 'herp_derp', 'First!') test_msg_queue = [{'text': 'First!', 'level': 'herp_derp'}] assert request.session['messages'] == test_msg_queue - + # fetch_messages should return and empty the queue assert fetch_messages(request) == test_msg_queue assert request.session.get('messages') == [] diff --git a/mediagoblin/tests/test_misc.py b/mediagoblin/tests/test_misc.py index 94ae5a51..8a96e7d0 100644 --- a/mediagoblin/tests/test_misc.py +++ b/mediagoblin/tests/test_misc.py @@ -16,11 +16,9 @@ from nose.tools import assert_equal -from mediagoblin.tests.tools import setup_fresh_app +from mediagoblin.tests.tools import get_test_app - -@setup_fresh_app -def test_404_for_non_existent(test_app): - assert_equal(test_app.get('/does-not-exist/', - expect_errors=True).status_int, - 404) +def test_404_for_non_existent(): + test_app = get_test_app(dump_old_app=False) + res = test_app.get('/does-not-exist/', expect_errors=True) + assert_equal(res.status_int, 404) diff --git a/mediagoblin/tests/test_sql_migrations.py b/mediagoblin/tests/test_sql_migrations.py index 26979bdf..2fc4c043 100644 --- a/mediagoblin/tests/test_sql_migrations.py +++ b/mediagoblin/tests/test_sql_migrations.py @@ -26,7 +26,7 @@ from sqlalchemy.sql import select, insert from migrate import changeset from mediagoblin.db.base import GMGTableBase -from mediagoblin.db.util import MigrationManager, RegisterMigration +from mediagoblin.db.migration_tools import MigrationManager, RegisterMigration from mediagoblin.tools.common import CollectingPrinter diff --git a/mediagoblin/tests/test_submission.py b/mediagoblin/tests/test_submission.py index 589ba7ed..53330c48 100644 --- a/mediagoblin/tests/test_submission.py +++ b/mediagoblin/tests/test_submission.py @@ -50,7 +50,7 @@ REQUEST_CONTEXT = ['mediagoblin/user_pages/user.html', 'request'] class TestSubmission: def setUp(self): - self.test_app = get_test_app() + self.test_app = get_test_app(dump_old_app=False) # TODO: Possibly abstract into a decorator like: # @as_authenticated_user('chris') @@ -132,11 +132,11 @@ class TestSubmission: def test_tags(self): # Good tag string # -------- - response, request = self.do_post({'title': u'Balanced Goblin', + response, request = self.do_post({'title': u'Balanced Goblin 2', 'tags': GOOD_TAG_STRING}, *REQUEST_CONTEXT, do_follow=True, **self.upload_data(GOOD_JPG)) - media = self.check_media(request, {'title': u'Balanced Goblin'}, 1) + media = self.check_media(request, {'title': u'Balanced Goblin 2'}, 1) assert media.tags[0]['name'] == u'yin' assert media.tags[0]['slug'] == u'yin' @@ -145,7 +145,7 @@ class TestSubmission: # Test tags that are too long # --------------- - response, form = self.do_post({'title': u'Balanced Goblin', + response, form = self.do_post({'title': u'Balanced Goblin 2', 'tags': BAD_TAG_STRING}, *FORM_CONTEXT, **self.upload_data(GOOD_JPG)) @@ -161,11 +161,17 @@ class TestSubmission: media = self.check_media(request, {'title': u'Balanced Goblin'}, 1) media_id = media.id + # At least render the edit page + edit_url = request.urlgen( + 'mediagoblin.edit.edit_media', + user=self.test_user.username, media_id=media_id) + self.test_app.get(edit_url) + # Add a comment, so we can test for its deletion later. self.check_comments(request, media_id, 0) comment_url = request.urlgen( 'mediagoblin.user_pages.media_post_comment', - user=self.test_user.username, media=media_id) + user=self.test_user.username, media_id=media_id) response = self.do_post({'comment_content': 'i love this test'}, url=comment_url, do_follow=True)[0] self.check_comments(request, media_id, 1) @@ -174,7 +180,7 @@ class TestSubmission: # --------------------------------------------------- delete_url = request.urlgen( 'mediagoblin.user_pages.media_confirm_delete', - user=self.test_user.username, media=media_id) + user=self.test_user.username, media_id=media_id) # Empty data means don't confirm response = self.do_post({}, do_follow=True, url=delete_url)[0] media = self.check_media(request, {'title': u'Balanced Goblin'}, 1) diff --git a/mediagoblin/tests/test_tags.py b/mediagoblin/tests/test_tags.py index bc657660..73af2eea 100644 --- a/mediagoblin/tests/test_tags.py +++ b/mediagoblin/tests/test_tags.py @@ -14,17 +14,17 @@ # 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.tests.tools import setup_fresh_app +from mediagoblin.tests.tools import get_test_app from mediagoblin.tools import text -@setup_fresh_app -def test_list_of_dicts_conversion(test_app): +def test_list_of_dicts_conversion(): """ When the user adds tags to a media entry, the string from the form is converted into a list of tags, where each tag is stored in the database as a dict. Each tag dict should contain the tag's name and slug. Another function performs the reverse operation when populating a form to edit tags. """ + test_app = get_test_app(dump_old_app=False) # Leading, trailing, and internal whitespace should be removed and slugified assert text.convert_to_tag_list_of_dicts('sleep , 6 AM, chainsaw! ') == [ {'name': u'sleep', 'slug': u'sleep'}, diff --git a/mediagoblin/tests/test_tests.py b/mediagoblin/tests/test_tests.py index b11dc730..d09e8f28 100644 --- a/mediagoblin/tests/test_tests.py +++ b/mediagoblin/tests/test_tests.py @@ -15,7 +15,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. from mediagoblin import mg_globals -from mediagoblin.tests.tools import get_test_app +from mediagoblin.tests.tools import get_test_app, fixture_add_user from mediagoblin.db.models import User @@ -23,16 +23,14 @@ def test_get_test_app_wipes_db(): """ Make sure we get a fresh database on every wipe :) """ - get_test_app() + get_test_app(dump_old_app=True) assert User.query.count() == 0 - new_user = mg_globals.database.User() - new_user.username = u'lolcat' - new_user.email = u'lol@cats.example.org' - new_user.pw_hash = u'pretend_this_is_a_hash' - new_user.save() + fixture_add_user() assert User.query.count() == 1 - get_test_app() + get_test_app(dump_old_app=False) + assert User.query.count() == 1 + get_test_app(dump_old_app=True) assert User.query.count() == 0 diff --git a/mediagoblin/tests/tools.py b/mediagoblin/tests/tools.py index 31afb08b..3e78b2e3 100644 --- a/mediagoblin/tests/tools.py +++ b/mediagoblin/tests/tools.py @@ -25,6 +25,7 @@ from paste.deploy import loadapp from webtest import TestApp from mediagoblin import mg_globals +from mediagoblin.db.models import User from mediagoblin.tools import testing from mediagoblin.init.config import read_mediagoblin_config from mediagoblin.db.open import setup_connection_and_db_from_config @@ -202,9 +203,12 @@ def assert_db_meets_expected(db, expected): assert document == expected_document # make sure it matches -def fixture_add_user(username=u'chris', password='toast', +def fixture_add_user(username=u'chris', password=u'toast', active_user=True): - test_user = mg_globals.database.User() + # Reuse existing user or create a new one + test_user = User.query.filter_by(username=username).first() + if test_user is None: + test_user = User() test_user.username = username test_user.email = username + u'@example.com' if password is not None: @@ -216,7 +220,7 @@ def fixture_add_user(username=u'chris', password='toast', test_user.save() # Reload - test_user = mg_globals.database.User.find_one({'username': username}) + test_user = User.query.filter_by(username=username).first() # ... and detach from session: Session.expunge(test_user) diff --git a/mediagoblin/themes/airy/templates/mediagoblin/base.html b/mediagoblin/themes/airy/templates/mediagoblin/base.html deleted file mode 100644 index 6e177ddb..00000000 --- a/mediagoblin/themes/airy/templates/mediagoblin/base.html +++ /dev/null @@ -1,98 +0,0 @@ -{# -# GNU MediaGoblin -- federated, autonomous media hosting -# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. --#} -<!doctype html> -<html> - <head> - <meta charset="utf-8"> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>{% block title %}{{ app_config['html_title'] }}{% endblock %}</title> - <link rel="stylesheet" type="text/css" - href="{{ request.staticdirect('/css/extlib/reset.css') }}"/> - <link rel="stylesheet" type="text/css" - href="{{ request.staticdirect('/css/base.css') }}"/> - <link rel="shortcut icon" - href="{{ request.staticdirect('/images/goblin.ico') }}" /> - <script src="{{ request.staticdirect('/js/extlib/jquery.js') }}"></script> - <!--[if lt IE 9]> - <script src="{{ request.staticdirect('/js/extlib/html5shiv.js') }}"></script> - <![endif]--> - - {% include "mediagoblin/extra_head.html" %} - - {% block mediagoblin_head %} - {% endblock mediagoblin_head %} - </head> - <body> - {% block mediagoblin_body %} - {% block mediagoblin_header %} - <header> - {% block mediagoblin_logo %} - <a class="logo" - href="{{ request.urlgen('index') }}"> - <img src="{{ request.staticdirect('/images/logo.png', 'theme') }}" - alt="{% trans %}MediaGoblin logo{% endtrans %}" /> - </a> - {% endblock mediagoblin_logo %} - {% block mediagoblin_header_title %}{% endblock %} - <div class="header_right"> - {% if request.user %} - {% trans - user_url=request.urlgen('mediagoblin.user_pages.user_home', - user= request.user.username), - user_name=request.user.username -%} - <a href="{{ user_url }}">{{ user_name }}</a>'s account - {%- endtrans %} - (<a href="{{ request.urlgen('mediagoblin.auth.logout') }}">{% trans %}log out{% endtrans %}</a>) - {% if request.user and request.user.status == 'active' %} - <a class="button_action" href="{{ request.urlgen('mediagoblin.submit.start') }}">{% trans %}Add media{% endtrans %}</a> - {% elif request.user and request.user.status == "needs_email_verification" %} - {# the following link should only appear when verification is needed #} - <a href="{{ request.urlgen('mediagoblin.user_pages.user_home', - user=request.user.username) }}" - class="button_action_highlight"> - {% trans %}Verify your email!{% endtrans %}</a> - {% endif %} - {% else %} - <a href="{{ request.urlgen('mediagoblin.auth.login') }}?next={{ - request.base_url|urlencode }}"> - {% trans %}Log in{% endtrans %}</a> - {% endif %} - </div> - <div class="clear"></div> - </header> - {% endblock %} - <div class="container"> - <div class="mediagoblin_content"> - {% include "mediagoblin/utils/messages.html" %} - {% block mediagoblin_content %} - {% endblock mediagoblin_content %} - </div> - {% block mediagoblin_footer %} - <footer> - {% trans -%} - Powered by <a href="http://mediagoblin.org">MediaGoblin</a>, a <a href="http://gnu.org/">GNU</a> project. - {%- endtrans %} - {% trans source_link=app_config['source_link'] -%} - Released under the <a href="http://www.fsf.org/licensing/licenses/agpl-3.0.html">AGPL</a>. <a href="{{ source_link }}">Source code</a> available. - {%- endtrans %} - </footer> - {% endblock mediagoblin_footer %} - {% endblock mediagoblin_body %} - </div> - </body> -</html> diff --git a/mediagoblin/themes/airy/templates/mediagoblin/bits/logo.html b/mediagoblin/themes/airy/templates/mediagoblin/bits/logo.html new file mode 100644 index 00000000..c8500159 --- /dev/null +++ b/mediagoblin/themes/airy/templates/mediagoblin/bits/logo.html @@ -0,0 +1,25 @@ +{# +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +-#} + +{% block mediagoblin_logo %} + <a class="logo" + href="{{ request.urlgen('index') }}"> + <img src="{{ request.staticdirect('/images/logo.png', 'theme') }}" + alt="{% trans %}MediaGoblin logo{% endtrans %}" /> + </a> +{% endblock mediagoblin_logo -%} diff --git a/mediagoblin/user_pages/routing.py b/mediagoblin/user_pages/routing.py index 63bf5c2a..a9431405 100644 --- a/mediagoblin/user_pages/routing.py +++ b/mediagoblin/user_pages/routing.py @@ -24,12 +24,12 @@ add_route('mediagoblin.user_pages.media_home', 'mediagoblin.user_pages.views:media_home') add_route('mediagoblin.user_pages.media_confirm_delete', - '/u/<string:user>/m/<string:media>/confirm-delete/', + '/u/<string:user>/m/<int:media_id>/confirm-delete/', 'mediagoblin.user_pages.views:media_confirm_delete') # Submission handling of new comments. TODO: only allow for POST methods add_route('mediagoblin.user_pages.media_post_comment', - '/u/<string:user>/m/<string:media>/comment/add/', + '/u/<string:user>/m/<int:media_id>/comment/add/', 'mediagoblin.user_pages.views:media_post_comment') add_route('mediagoblin.user_pages.user_gallery', @@ -48,6 +48,10 @@ add_route('mediagoblin.user_pages.media_collect', '/u/<string:user>/m/<string:media>/collect/', 'mediagoblin.user_pages.views:media_collect') +add_route('mediagoblin.user_pages.collection_list', + '/u/<string:user>/collections/', + 'mediagoblin.user_pages.views:collection_list') + add_route('mediagoblin.user_pages.user_collection', '/u/<string:user>/collection/<string:collection>/', 'mediagoblin.user_pages.views:user_collection') @@ -74,7 +78,7 @@ add_route('mediagoblin.user_pages.processing_panel', # Stray edit routes add_route('mediagoblin.edit.edit_media', - '/u/<string:user>/m/<string:media>/edit/', + '/u/<string:user>/m/<int:media_id>/edit/', 'mediagoblin.edit.views:edit_media') add_route('mediagoblin.edit.attachments', diff --git a/mediagoblin/user_pages/views.py b/mediagoblin/user_pages/views.py index f115c3b8..b9f03e8e 100644 --- a/mediagoblin/user_pages/views.py +++ b/mediagoblin/user_pages/views.py @@ -28,6 +28,7 @@ from mediagoblin.user_pages import forms as user_forms from mediagoblin.user_pages.lib import send_comment_email from mediagoblin.decorators import (uses_pagination, get_user_media_entry, + get_media_entry_by_id, require_active_login, user_may_delete_media, user_may_alter_collection, get_user_collection, get_user_collection_item, active_user_from_url) @@ -138,7 +139,7 @@ def media_home(request, media, page, **kwargs): 'app_config': mg_globals.app_config}) -@get_user_media_entry +@get_media_entry_by_id @require_active_login def media_post_comment(request, media): """ @@ -258,7 +259,7 @@ def media_collect(request, media): #TODO: Why does @user_may_delete_media not implicate @require_active_login? -@get_user_media_entry +@get_media_entry_by_id @require_active_login @user_may_delete_media def media_confirm_delete(request, media): @@ -337,6 +338,19 @@ def user_collection(request, page, url_user=None): 'pagination': pagination}) +@active_user_from_url +def collection_list(request, url_user=None): + """A User-defined Collection""" + collections = Collection.query.filter_by( + get_creator=url_user) + + return render_to_response( + request, + 'mediagoblin/user_pages/collection_list.html', + {'user': url_user, + 'collections': collections}) + + @get_user_collection_item @require_active_login @user_may_alter_collection diff --git a/runtests.sh b/runtests.sh index 94e77da2..a4ceec2e 100755 --- a/runtests.sh +++ b/runtests.sh @@ -16,9 +16,19 @@ # 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/>. -if [ -f ./bin/nosetests ]; then - echo "Using ./bin/nosetests"; - export NOSETESTS="./bin/nosetests"; +basedir="`dirname $0`" +# Directory to seaerch for: +subdir="mediagoblin/tests" +[ '!' -d "$basedir/$subdir" ] && basedir="." +if [ '!' -d "$basedir/$subdir" ] +then + echo "Could not find base directory" >&2 + exit 1 +fi + +if [ -x "$basedir/bin/nosetests" ]; then + export NOSETESTS="$basedir/bin/nosetests"; + echo "Using $NOSETESTS"; elif which nosetests > /dev/null; then echo "Using nosetests from \$PATH"; export NOSETESTS="nosetests"; @@ -28,4 +38,25 @@ else exit 1 fi -CELERY_CONFIG_MODULE=mediagoblin.init.celery.from_tests $NOSETESTS $@ +need_arg=1 +for i in "$@" +do + case "$i" in + -*) ;; + *) need_arg=0; break ;; + esac +done + +CELERY_CONFIG_MODULE=mediagoblin.init.celery.from_tests +export CELERY_CONFIG_MODULE +echo "+ CELERY_CONFIG_MODULE=$CELERY_CONFIG_MODULE" + +if [ "$need_arg" = 1 ] +then + testdir="$basedir/mediagoblin/tests" + set -x + exec "$NOSETESTS" "$@" "$testdir" +else + set -x + exec "$NOSETESTS" "$@" +fi |