aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--mediagoblin/app.py4
-rw-r--r--mediagoblin/config_spec.ini3
-rw-r--r--mediagoblin/db/migration_tools.py270
-rw-r--r--mediagoblin/db/migrations.py11
-rw-r--r--mediagoblin/db/mixin.py3
-rw-r--r--mediagoblin/db/util.py259
-rw-r--r--mediagoblin/decorators.py8
-rw-r--r--mediagoblin/edit/routing.py4
-rw-r--r--mediagoblin/edit/views.py33
-rw-r--r--mediagoblin/gmg_commands/dbupdate.py2
-rw-r--r--mediagoblin/meddleware/csrf.py12
-rw-r--r--mediagoblin/media_types/stl/models.py2
-rw-r--r--mediagoblin/plugins/oauth/migrations.py2
-rw-r--r--mediagoblin/templates/mediagoblin/base.html8
-rw-r--r--mediagoblin/templates/mediagoblin/bits/logo.html25
-rw-r--r--mediagoblin/templates/mediagoblin/edit/edit.html2
-rw-r--r--mediagoblin/templates/mediagoblin/edit/edit_profile.html5
-rw-r--r--mediagoblin/templates/mediagoblin/root.html7
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/collection.html4
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/collection_list.html56
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/media.html6
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/media_confirm_delete.html2
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/user.html9
-rw-r--r--mediagoblin/tests/test_api.py2
-rw-r--r--mediagoblin/tests/test_auth.py10
-rw-r--r--mediagoblin/tests/test_csrf_middleware.py19
-rw-r--r--mediagoblin/tests/test_edit.py173
-rw-r--r--mediagoblin/tests/test_http_callback.py2
-rw-r--r--mediagoblin/tests/test_messages.py13
-rw-r--r--mediagoblin/tests/test_misc.py12
-rw-r--r--mediagoblin/tests/test_sql_migrations.py2
-rw-r--r--mediagoblin/tests/test_submission.py18
-rw-r--r--mediagoblin/tests/test_tags.py6
-rw-r--r--mediagoblin/tests/test_tests.py14
-rw-r--r--mediagoblin/tests/tools.py10
-rw-r--r--mediagoblin/themes/airy/templates/mediagoblin/base.html98
-rw-r--r--mediagoblin/themes/airy/templates/mediagoblin/bits/logo.html25
-rw-r--r--mediagoblin/user_pages/routing.py10
-rw-r--r--mediagoblin/user_pages/views.py18
-rwxr-xr-xruntests.sh39
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 %} &mdash; {{ 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