aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--COPYING2
-rw-r--r--mediagoblin.ini4
-rw-r--r--mediagoblin/auth/views.py4
-rw-r--r--mediagoblin/config_spec.ini1
-rw-r--r--mediagoblin/db/mixin.py23
-rw-r--r--mediagoblin/db/mongo/migrations.py24
-rw-r--r--mediagoblin/db/mongo/models.py14
-rw-r--r--mediagoblin/db/sql/convert.py11
-rw-r--r--mediagoblin/db/sql/extratypes.py28
-rw-r--r--mediagoblin/db/sql/migrations.py17
-rw-r--r--mediagoblin/db/sql/models.py46
-rw-r--r--mediagoblin/db/sql/util.py271
-rw-r--r--mediagoblin/edit/views.py7
-rw-r--r--mediagoblin/gmg_commands/__init__.py4
-rw-r--r--mediagoblin/gmg_commands/dbupdate.py89
-rw-r--r--mediagoblin/listings/views.py2
-rw-r--r--mediagoblin/media_types/ascii/migrations.py17
-rw-r--r--mediagoblin/media_types/ascii/models.py34
-rw-r--r--mediagoblin/media_types/image/migrations.py17
-rw-r--r--mediagoblin/media_types/image/models.py19
-rw-r--r--mediagoblin/media_types/video/migrations.py17
-rw-r--r--mediagoblin/media_types/video/models.py34
-rw-r--r--mediagoblin/static/css/base.css63
-rw-r--r--mediagoblin/submit/views.py4
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/media.html141
-rw-r--r--mediagoblin/templates/mediagoblin/utils/exif.html2
-rw-r--r--mediagoblin/templates/mediagoblin/utils/geolocation_map.html2
-rw-r--r--mediagoblin/templates/mediagoblin/utils/license.html2
-rw-r--r--mediagoblin/templates/mediagoblin/utils/object_gallery.html36
-rw-r--r--mediagoblin/templates/mediagoblin/utils/prev_next.html46
-rw-r--r--mediagoblin/templates/mediagoblin/utils/tags.html9
-rw-r--r--mediagoblin/tests/test_sql_migrations.py884
-rw-r--r--mediagoblin/user_pages/views.py4
-rw-r--r--setup.py2
35 files changed, 1683 insertions, 198 deletions
diff --git a/.gitignore b/.gitignore
index b46ec38a..e3a83822 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,7 @@
/user_dev/
/paste_local.ini
/mediagoblin_local.ini
+/mediagoblin.db
/server-log.txt
# Tests
diff --git a/COPYING b/COPYING
index f8d93cf6..23e8db70 100644
--- a/COPYING
+++ b/COPYING
@@ -30,7 +30,7 @@ If not, see <http://www.gnu.org/licenses/>.
JavaScript files located in the ``mediagoblin/`` directory tree
are free software: you can redistribute and/or modify them under the
-terms of the GNU Lesser General Public License as published by 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.
diff --git a/mediagoblin.ini b/mediagoblin.ini
index dbde6e51..223f0f4a 100644
--- a/mediagoblin.ini
+++ b/mediagoblin.ini
@@ -5,6 +5,10 @@
direct_remote_path = /mgoblin_static/
email_sender_address = "notice@mediagoblin.example.org"
+## Uncomment and change to your DB's appropiate setting.
+## Default is a local sqlite db "mediagoblin.db".
+# sql_engine = postgresql:///gmg
+
# set to false to enable sending notices
email_debug_mode = true
diff --git a/mediagoblin/auth/views.py b/mediagoblin/auth/views.py
index 9af89c2a..e18469b9 100644
--- a/mediagoblin/auth/views.py
+++ b/mediagoblin/auth/views.py
@@ -60,7 +60,9 @@ def register(request):
if request.method == 'POST' and register_form.validate():
# TODO: Make sure the user doesn't exist already
username = unicode(request.POST['username'].lower())
- email = unicode(request.POST['email'].lower())
+ em_user, em_dom = unicode(request.POST['email']).split("@", 1)
+ em_dom = em_dom.lower()
+ email = em_user + "@" + em_dom
users_with_username = request.db.User.find(
{'username': username}).count()
users_with_email = request.db.User.find(
diff --git a/mediagoblin/config_spec.ini b/mediagoblin/config_spec.ini
index 2d410899..2b4ba2f9 100644
--- a/mediagoblin/config_spec.ini
+++ b/mediagoblin/config_spec.ini
@@ -9,6 +9,7 @@ media_types = string_list(default=list("mediagoblin.media_types.image"))
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
workbench_path = string(default="%(here)s/user_dev/media/workbench")
diff --git a/mediagoblin/db/mixin.py b/mediagoblin/db/mixin.py
index beaff9b0..758f7e72 100644
--- a/mediagoblin/db/mixin.py
+++ b/mediagoblin/db/mixin.py
@@ -29,6 +29,7 @@ real objects.
from mediagoblin.auth import lib as auth_lib
from mediagoblin.tools import common, licenses
+from mediagoblin.tools.text import cleaned_markdown_conversion
class UserMixin(object):
@@ -39,8 +40,20 @@ class UserMixin(object):
return auth_lib.bcrypt_check_password(
password, self.pw_hash)
+ @property
+ def bio_html(self):
+ return cleaned_markdown_conversion(self.bio)
+
class MediaEntryMixin(object):
+ @property
+ def description_html(self):
+ """
+ Rendered version of the description, run through
+ Markdown and cleaned with our cleaning tool.
+ """
+ return cleaned_markdown_conversion(self.description)
+
def get_display_media(self, media_map,
fetch_order=common.DISPLAY_IMAGE_FETCHING_ORDER):
"""
@@ -91,3 +104,13 @@ class MediaEntryMixin(object):
def get_license_data(self):
"""Return license dict for requested license"""
return licenses.SUPPORTED_LICENSES[self.license or ""]
+
+
+class MediaCommentMixin(object):
+ @property
+ def content_html(self):
+ """
+ the actual html-rendered version of the comment displayed.
+ Run through Markdown and the HTML cleaner.
+ """
+ return cleaned_markdown_conversion(self.content)
diff --git a/mediagoblin/db/mongo/migrations.py b/mediagoblin/db/mongo/migrations.py
index 261e21a5..59035f3b 100644
--- a/mediagoblin/db/mongo/migrations.py
+++ b/mediagoblin/db/mongo/migrations.py
@@ -29,6 +29,16 @@ def add_table_field(db, table_name, field_name, default_value):
multi=True)
+def drop_table_field(db, table_name, field_name):
+ """
+ Drop an old field from a table/collection
+ """
+ db[table_name].update(
+ {field_name: {'$exists': True}},
+ {'$unset': {field_name: 1}},
+ multi=True)
+
+
# Please see mediagoblin/tests/test_migrations.py for some examples of
# basic migrations.
@@ -115,3 +125,17 @@ def mediaentry_add_license(database):
Add the 'license' field for entries that don't have it.
"""
add_table_field(database, 'media_entries', 'license', None)
+
+
+@RegisterMigration(9)
+def remove_calculated_html(database):
+ """
+ Drop pre-rendered html again and calculate things
+ on the fly (and cache):
+ - User.bio_html
+ - MediaEntry.description_html
+ - MediaComment.content_html
+ """
+ drop_table_field(database, 'users', 'bio_html')
+ drop_table_field(database, 'media_entries', 'description_html')
+ drop_table_field(database, 'media_comments', 'content_html')
diff --git a/mediagoblin/db/mongo/models.py b/mediagoblin/db/mongo/models.py
index 541086bc..57af137d 100644
--- a/mediagoblin/db/mongo/models.py
+++ b/mediagoblin/db/mongo/models.py
@@ -23,7 +23,7 @@ from mediagoblin.db.mongo import migrations
from mediagoblin.db.mongo.util import ASCENDING, DESCENDING, ObjectId
from mediagoblin.tools.pagination import Pagination
from mediagoblin.tools import url
-from mediagoblin.db.mixin import UserMixin, MediaEntryMixin
+from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, MediaCommentMixin
###################
# Custom validators
@@ -59,7 +59,6 @@ class User(Document, UserMixin):
- is_admin: Whether or not this user is an administrator or not.
- url: this user's personal webpage/website, if appropriate.
- bio: biography of this user (plaintext, in markdown)
- - bio_html: biography of the user converted to proper HTML.
"""
__collection__ = 'users'
use_dot_notation = True
@@ -76,7 +75,6 @@ class User(Document, UserMixin):
'is_admin': bool,
'url': unicode,
'bio': unicode, # May contain markdown
- 'bio_html': unicode, # May contain plaintext, or HTML
'fp_verification_key': unicode, # forgotten password verification key
'fp_token_expire': datetime.datetime,
}
@@ -112,9 +110,6 @@ class MediaEntry(Document, MediaEntryMixin):
up with MarkDown for slight fanciness (links, boldness, italics,
paragraphs...)
- - description_html: Rendered version of the description, run through
- Markdown and cleaned with our cleaning tool.
-
- media_type: What type of media is this? Currently we only support
'image' ;)
@@ -179,7 +174,6 @@ class MediaEntry(Document, MediaEntryMixin):
'slug': unicode,
'created': datetime.datetime,
'description': unicode, # May contain markdown/up
- 'description_html': unicode, # May contain plaintext, or HTML
'media_type': unicode,
'media_data': dict, # extra data relevant to this media_type
'plugin_data': dict, # plugins can dump stuff here.
@@ -257,7 +251,7 @@ class MediaEntry(Document, MediaEntryMixin):
return self.db.User.find_one({'_id': self.uploader})
-class MediaComment(Document):
+class MediaComment(Document, MediaCommentMixin):
"""
A comment on a MediaEntry.
@@ -266,8 +260,6 @@ class MediaComment(Document):
- author: user who posted this comment
- created: when the comment was created
- content: plaintext (but markdown'able) version of the comment's content.
- - content_html: the actual html-rendered version of the comment displayed.
- Run through Markdown and the HTML cleaner.
"""
__collection__ = 'media_comments'
@@ -278,7 +270,7 @@ class MediaComment(Document):
'author': ObjectId,
'created': datetime.datetime,
'content': unicode,
- 'content_html': unicode}
+ }
required_fields = [
'media_entry', 'author', 'created', 'content']
diff --git a/mediagoblin/db/sql/convert.py b/mediagoblin/db/sql/convert.py
index f6575be9..36d6fc7f 100644
--- a/mediagoblin/db/sql/convert.py
+++ b/mediagoblin/db/sql/convert.py
@@ -56,7 +56,7 @@ def convert_users(mk_db):
copy_attrs(entry, new_entry,
('username', 'email', 'created', 'pw_hash', 'email_verified',
'status', 'verification_key', 'is_admin', 'url',
- 'bio', 'bio_html',
+ 'bio',
'fp_verification_key', 'fp_token_expire',))
# new_entry.fp_verification_expire = entry.fp_token_expire
@@ -77,9 +77,9 @@ def convert_media_entries(mk_db):
new_entry = MediaEntry()
copy_attrs(entry, new_entry,
('title', 'slug', 'created',
- 'description', 'description_html',
+ 'description',
'media_type', 'state', 'license',
- 'fail_error',
+ 'fail_error', 'fail_metadata',
'queued_task_id',))
copy_reference_attr(entry, new_entry, "uploader")
@@ -133,7 +133,7 @@ def convert_media_comments(mk_db):
new_entry = MediaComment()
copy_attrs(entry, new_entry,
('created',
- 'content', 'content_html',))
+ 'content',))
copy_reference_attr(entry, new_entry, "media_entry")
copy_reference_attr(entry, new_entry, "author")
@@ -148,8 +148,7 @@ def convert_media_comments(mk_db):
def main():
global_config, app_config = setup_global_and_app_config("mediagoblin.ini")
- sql_conn, sql_db = sql_connect({'sql_engine': 'sqlite:///mediagoblin.db'})
-
+ sql_conn, sql_db = sql_connect(app_config)
mk_conn, mk_db = mongo_connect(app_config)
Base.metadata.create_all(sql_db.engine)
diff --git a/mediagoblin/db/sql/extratypes.py b/mediagoblin/db/sql/extratypes.py
index 3a594728..8e078f14 100644
--- a/mediagoblin/db/sql/extratypes.py
+++ b/mediagoblin/db/sql/extratypes.py
@@ -15,7 +15,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-from sqlalchemy.types import TypeDecorator, Unicode
+from sqlalchemy.types import TypeDecorator, Unicode, VARCHAR
+import json
class PathTupleWithSlashes(TypeDecorator):
@@ -35,3 +36,28 @@ class PathTupleWithSlashes(TypeDecorator):
if value is not None:
value = tuple(value.split('/'))
return value
+
+
+# The following class and only this one class is in very
+# large parts based on example code from sqlalchemy.
+#
+# The original copyright notice and license follows:
+# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+#
+class JSONEncoded(TypeDecorator):
+ "Represents an immutable structure as a json-encoded string."
+
+ impl = VARCHAR
+
+ def process_bind_param(self, value, dialect):
+ if value is not None:
+ value = json.dumps(value)
+ return value
+
+ def process_result_value(self, value, dialect):
+ if value is not None:
+ value = json.loads(value)
+ return value
diff --git a/mediagoblin/db/sql/migrations.py b/mediagoblin/db/sql/migrations.py
new file mode 100644
index 00000000..98d0d0aa
--- /dev/null
+++ b/mediagoblin/db/sql/migrations.py
@@ -0,0 +1,17 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011 MediaGoblin contributors. See AUTHORS.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+MIGRATIONS = {}
diff --git a/mediagoblin/db/sql/models.py b/mediagoblin/db/sql/models.py
index 9d06f79c..699dbf33 100644
--- a/mediagoblin/db/sql/models.py
+++ b/mediagoblin/db/sql/models.py
@@ -29,9 +29,16 @@ from sqlalchemy.orm.collections import attribute_mapped_collection
from sqlalchemy.sql.expression import desc
from sqlalchemy.ext.associationproxy import association_proxy
-from mediagoblin.db.sql.extratypes import PathTupleWithSlashes
+from mediagoblin.db.sql.extratypes import PathTupleWithSlashes, JSONEncoded
from mediagoblin.db.sql.base import Base, DictReadAttrProxy
-from mediagoblin.db.mixin import UserMixin, MediaEntryMixin
+from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, MediaCommentMixin
+
+# It's actually kind of annoying how sqlalchemy-migrate does this, if
+# I understand it right, but whatever. Anyway, don't remove this :P
+#
+# We could do migration calls more manually instead of relying on
+# this import-based meddling...
+from migrate import changeset
class SimpleFieldAlias(object):
@@ -64,7 +71,6 @@ class User(Base, UserMixin):
is_admin = Column(Boolean, default=False, nullable=False)
url = Column(Unicode)
bio = Column(UnicodeText) # ??
- bio_html = Column(UnicodeText) # ??
fp_verification_key = Column(Unicode)
fp_token_expire = Column(DateTime)
@@ -86,14 +92,13 @@ class MediaEntry(Base, MediaEntryMixin):
slug = Column(Unicode)
created = Column(DateTime, nullable=False, default=datetime.datetime.now)
description = Column(UnicodeText) # ??
- description_html = Column(UnicodeText) # ??
media_type = Column(Unicode, nullable=False)
state = Column(Unicode, default=u'unprocessed', nullable=False)
# or use sqlalchemy.types.Enum?
license = Column(Unicode)
fail_error = Column(Unicode)
- fail_metadata = Column(UnicodeText)
+ fail_metadata = Column(JSONEncoded)
queued_media_file = Column(PathTupleWithSlashes)
@@ -209,10 +214,12 @@ class MediaTag(Base):
creator=Tag.find_or_new
)
- def __init__(self, name, slug):
+ def __init__(self, name=None, slug=None):
Base.__init__(self)
- self.name = name
- self.tag_helper = Tag.find_or_new(slug)
+ if name is not None:
+ self.name = name
+ if slug is not None:
+ self.tag_helper = Tag.find_or_new(slug)
@property
def dict_view(self):
@@ -220,7 +227,7 @@ class MediaTag(Base):
return DictReadAttrProxy(self)
-class MediaComment(Base):
+class MediaComment(Base, MediaCommentMixin):
__tablename__ = "media_comments"
id = Column(Integer, primary_key=True)
@@ -229,13 +236,32 @@ class MediaComment(Base):
author = Column(Integer, ForeignKey('users.id'), nullable=False)
created = Column(DateTime, nullable=False, default=datetime.datetime.now)
content = Column(UnicodeText, nullable=False)
- content_html = Column(UnicodeText)
get_author = relationship(User)
_id = SimpleFieldAlias("id")
+MODELS = [
+ User, MediaEntry, Tag, MediaTag, MediaComment]
+
+
+######################################################
+# Special, migrations-tracking table
+#
+# Not listed in MODELS because this is special and not
+# really migrated, but used for migrations (for now)
+######################################################
+
+class MigrationData(Base):
+ __tablename__ = "migrations"
+
+ name = Column(Unicode, primary_key=True)
+ version = Column(Integer, nullable=False, default=0)
+
+######################################################
+
+
def show_table_init(engine_uri):
if engine_uri is None:
engine_uri = 'sqlite:///:memory:'
diff --git a/mediagoblin/db/sql/util.py b/mediagoblin/db/sql/util.py
new file mode 100644
index 00000000..08602414
--- /dev/null
+++ b/mediagoblin/db/sql/util.py
@@ -0,0 +1,271 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011 MediaGoblin contributors. See AUTHORS.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+import sys
+
+def _simple_printer(string):
+ """
+ Prints a string, but without an auto \n at the end.
+ """
+ sys.stdout.write(string)
+ sys.stdout.flush()
+
+
+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 = name
+ self.models = models
+ self.session = session
+ self.migration_registry = migration_registry
+ self._sorted_migrations = None
+ self.printer = printer
+
+ # For convenience
+ from mediagoblin.db.sql.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.printer('done.\n')
+
+ self.set_current_migration()
+ 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.sql.models import MigrationData
+
+ if not MigrationData.__table__.exists(db.bind):
+ MigrationData.metadata.create_all(
+ db.bind, tables=[MigrationData.__table__])
diff --git a/mediagoblin/edit/views.py b/mediagoblin/edit/views.py
index a7245517..3df36e8e 100644
--- a/mediagoblin/edit/views.py
+++ b/mediagoblin/edit/views.py
@@ -34,7 +34,7 @@ from mediagoblin.tools.response import render_to_response, redirect
from mediagoblin.tools.translate import pass_to_ugettext as _
from mediagoblin.tools.text import (
clean_html, convert_to_tag_list_of_dicts,
- media_tags_as_string, cleaned_markdown_conversion)
+ media_tags_as_string)
from mediagoblin.tools.licenses import SUPPORTED_LICENSES
@@ -72,9 +72,6 @@ def edit_media(request, media):
media.tags = convert_to_tag_list_of_dicts(
request.POST.get('tags'))
- media.description_html = cleaned_markdown_conversion(
- media.description)
-
media.license = unicode(request.POST.get('license', '')) or None
media.slug = unicode(request.POST['slug'])
@@ -171,8 +168,6 @@ def edit_profile(request):
user.url = unicode(request.POST['url'])
user.bio = unicode(request.POST['bio'])
- user.bio_html = cleaned_markdown_conversion(user.bio)
-
user.save()
messages.add_message(request,
diff --git a/mediagoblin/gmg_commands/__init__.py b/mediagoblin/gmg_commands/__init__.py
index db944b3c..d804376b 100644
--- a/mediagoblin/gmg_commands/__init__.py
+++ b/mediagoblin/gmg_commands/__init__.py
@@ -53,6 +53,10 @@ SUBCOMMAND_MAP = {
'setup': 'mediagoblin.gmg_commands.import_export:import_export_parse_setup',
'func': 'mediagoblin.gmg_commands.import_export:env_import',
'help': 'Exports the data for this MediaGoblin instance'},
+ 'dbupdate': {
+ 'setup': 'mediagoblin.gmg_commands.dbupdate:dbupdate_parse_setup',
+ 'func': 'mediagoblin.gmg_commands.dbupdate:dbupdate',
+ 'help': 'Set up or update the SQL database'},
}
diff --git a/mediagoblin/gmg_commands/dbupdate.py b/mediagoblin/gmg_commands/dbupdate.py
new file mode 100644
index 00000000..27698170
--- /dev/null
+++ b/mediagoblin/gmg_commands/dbupdate.py
@@ -0,0 +1,89 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from sqlalchemy.orm import sessionmaker
+
+from mediagoblin.db.sql.open import setup_connection_and_db_from_config
+from mediagoblin.db.sql.util import (
+ MigrationManager, assure_migrations_table_setup)
+from mediagoblin.init import setup_global_and_app_config
+from mediagoblin.tools.common import import_component
+
+
+def dbupdate_parse_setup(subparser):
+ pass
+
+
+class DatabaseData(object):
+ def __init__(self, name, models, migrations):
+ self.name = name
+ self.models = models
+ self.migrations = migrations
+
+ def make_migration_manager(self, session):
+ return MigrationManager(
+ self.name, self.models, self.migrations, session)
+
+
+def gather_database_data(media_types):
+ """
+ Gather all database data relevant to the extensions we have
+ installed so we can do migrations and table initialization.
+
+ Returns a list of DatabaseData objects.
+ """
+ managed_dbdata = []
+
+ # Add main first
+ from mediagoblin.db.sql.models import MODELS as MAIN_MODELS
+ from mediagoblin.db.sql.migrations import MIGRATIONS as MAIN_MIGRATIONS
+
+ managed_dbdata.append(
+ DatabaseData(
+ '__main__', MAIN_MODELS, MAIN_MIGRATIONS))
+
+ # Then get all registered media managers (eventually, plugins)
+ for media_type in media_types:
+ models = import_component('%s.models:MODELS' % media_type)
+ migrations = import_component('%s.migrations:MIGRATIONS' % media_type)
+ managed_dbdata.append(
+ DatabaseData(media_type, models, migrations))
+
+ return managed_dbdata
+
+
+def dbupdate(args):
+ """
+ Initialize or migrate the database as specified by the config file.
+
+ Will also initialize or migrate all extensions (media types, and
+ in the future, plugins)
+ """
+ globa_config, app_config = setup_global_and_app_config(args.conf_file)
+
+ # Gather information from all media managers / projects
+ dbdatas = gather_database_data(app_config['media_types'])
+
+ # Set up the database
+ connection, db = setup_connection_and_db_from_config(app_config)
+
+ Session = sessionmaker(bind=db.engine)
+
+ # Setup media managers for all dbdata, run init/migrate and print info
+ # For each component, create/migrate tables
+ for dbdata in dbdatas:
+ migration_manager = dbdata.make_migration_manager(Session())
+ migration_manager.init_or_migrate()
diff --git a/mediagoblin/listings/views.py b/mediagoblin/listings/views.py
index 48320cb2..ba23fc46 100644
--- a/mediagoblin/listings/views.py
+++ b/mediagoblin/listings/views.py
@@ -91,7 +91,7 @@ def tag_atom_feed(request):
'type': 'text/html'}])
for entry in cursor:
feed.add(entry.get('title'),
- entry.get('description_html'),
+ entry.description_html,
id=entry.url_for_self(request.urlgen,qualified=True),
content_type='html',
author={'name': entry.get_uploader.username,
diff --git a/mediagoblin/media_types/ascii/migrations.py b/mediagoblin/media_types/ascii/migrations.py
new file mode 100644
index 00000000..f54c23ea
--- /dev/null
+++ b/mediagoblin/media_types/ascii/migrations.py
@@ -0,0 +1,17 @@
+# 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/>.
+
+MIGRATIONS = {}
diff --git a/mediagoblin/media_types/ascii/models.py b/mediagoblin/media_types/ascii/models.py
new file mode 100644
index 00000000..324794b9
--- /dev/null
+++ b/mediagoblin/media_types/ascii/models.py
@@ -0,0 +1,34 @@
+# 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.db.sql.models import Base
+
+from sqlalchemy import (
+ Column, Integer, Unicode, UnicodeText, DateTime, Boolean, ForeignKey,
+ UniqueConstraint)
+
+
+class AsciiData(Base):
+ __tablename__ = "ascii_data"
+
+ id = Column(Integer, primary_key=True)
+ media_entry = Column(
+ Integer, ForeignKey('media_entries.id'), nullable=False)
+
+
+DATA_MODEL = AsciiData
+MODELS = [AsciiData]
diff --git a/mediagoblin/media_types/image/migrations.py b/mediagoblin/media_types/image/migrations.py
new file mode 100644
index 00000000..f54c23ea
--- /dev/null
+++ b/mediagoblin/media_types/image/migrations.py
@@ -0,0 +1,17 @@
+# 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/>.
+
+MIGRATIONS = {}
diff --git a/mediagoblin/media_types/image/models.py b/mediagoblin/media_types/image/models.py
new file mode 100644
index 00000000..296eca0a
--- /dev/null
+++ b/mediagoblin/media_types/image/models.py
@@ -0,0 +1,19 @@
+from mediagoblin.db.sql.models import Base
+
+from sqlalchemy import (
+ Column, Integer, Unicode, UnicodeText, DateTime, Boolean, ForeignKey,
+ UniqueConstraint)
+
+
+class ImageData(Base):
+ __tablename__ = "image_data"
+
+ id = Column(Integer, primary_key=True)
+ width = Column(Integer)
+ height = Column(Integer)
+ media_entry = Column(
+ Integer, ForeignKey('media_entries.id'), nullable=False)
+
+
+DATA_MODEL = ImageData
+MODELS = [ImageData]
diff --git a/mediagoblin/media_types/video/migrations.py b/mediagoblin/media_types/video/migrations.py
new file mode 100644
index 00000000..f54c23ea
--- /dev/null
+++ b/mediagoblin/media_types/video/migrations.py
@@ -0,0 +1,17 @@
+# 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/>.
+
+MIGRATIONS = {}
diff --git a/mediagoblin/media_types/video/models.py b/mediagoblin/media_types/video/models.py
new file mode 100644
index 00000000..741c329b
--- /dev/null
+++ b/mediagoblin/media_types/video/models.py
@@ -0,0 +1,34 @@
+# 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.db.sql.models import Base
+
+from sqlalchemy import (
+ Column, Integer, Unicode, UnicodeText, DateTime, Boolean, ForeignKey,
+ UniqueConstraint)
+
+
+class VideoData(Base):
+ __tablename__ = "video_data"
+
+ id = Column(Integer, primary_key=True)
+ media_entry = Column(
+ Integer, ForeignKey('media_entries.id'), nullable=False)
+
+
+DATA_MODEL = VideoData
+MODELS = [VideoData]
diff --git a/mediagoblin/static/css/base.css b/mediagoblin/static/css/base.css
index dc0f3b85..cb321c85 100644
--- a/mediagoblin/static/css/base.css
+++ b/mediagoblin/static/css/base.css
@@ -109,7 +109,7 @@ input, textarea {
.container {
margin: auto;
width: 96%;
- max-width: 820px;
+ max-width: 940px;
}
header {
@@ -151,7 +151,7 @@ footer {
}
.media_pane {
- width: 560px;
+ width: 640px;
margin-left: 0px;
margin-right: 10px;
float: left;
@@ -164,8 +164,9 @@ img.media_image {
}
.media_sidebar {
- width: 240px;
+ width: 280px;
margin-left: 10px;
+ margin-right: 0px;
float: left;
}
@@ -231,6 +232,15 @@ text-align: center;
float: right;
}
+textarea#comment_content {
+ resize: vertical;
+ width: 634px;
+ height: 90px;
+ border: none;
+ background-color: #f1f1f1;
+ padding: 3px;
+}
+
.clear {
clear: both;
display: block;
@@ -240,15 +250,6 @@ text-align: center;
height: 0;
}
-h3.sidedata {
- border: none;
- background-color: #212121;
- border-radius: 4px 4px 0 0;
- padding: 3px 8px;
- margin: 20px 0 5px 0;
- font-size: 1em;
-}
-
/* forms */
.form_box,.form_box_xl {
@@ -330,18 +331,13 @@ textarea#description, textarea#bio {
}
textarea#comment_content {
- resize: vertical;
- width: 100%;
+ width: 634px;
height: 90px;
border: none;
background-color: #f1f1f1;
padding: 3px;
}
-#form_comment .form_field_input {
- padding-right: 6px;
-}
-
/* media galleries */
.media_thumbnail {
@@ -352,7 +348,6 @@ textarea#comment_content {
margin: 0px 5px 10px 5px;
text-align: center;
font-size: 0.875em;
- list-style: none;
}
.media_thumbnail a {
@@ -364,12 +359,6 @@ textarea#comment_content {
h2.media_title {
margin-bottom: 0px;
- display: inline-block;
-}
-
-p.context {
- display: inline-block;
- padding-top: 4px;
}
p.media_specs {
@@ -396,21 +385,19 @@ img.media_icon {
/* navigation */
-.navigation {
- float: right;
-}
-
.navigation_button {
width: 135px;
- display: inline-block;
+ display: block;
+ float: left;
text-align: center;
background-color: #1d1d1d;
border: 1px solid;
border-color: #2c2c2c #232323 #1a1a1a;
border-radius: 4px;
text-decoration: none;
- padding: 4px 0 8px;
- margin: 0 0 6px
+ padding: 12px 0 16px;
+ font-size: 1.4em;
+ margin: 0 0 20px
}
.navigation_left {
@@ -495,18 +482,11 @@ table.media_panel th {
}
/* Media queries and other responsivisivity */
-
-@media screen and (max-width: 820px) {
+@media screen and (max-width: 680px) {
.media_pane {
width: 100%;
margin: 0px;
}
-
- .media_sidebar {
- width: 100%;
- margin: 0px;
- }
-
img.media_image {
width: 100%;
display: inline;
@@ -515,12 +495,13 @@ table.media_panel th {
.media_thumbnail {
width: 21%;
}
+}
+@media screen and (max-width: 960px) {
.profile_sidebar {
width: 100%;
margin: 0px;
}
-
.profile_showcase {
width: 100%;
margin: 0px;
diff --git a/mediagoblin/submit/views.py b/mediagoblin/submit/views.py
index cdd097ec..845400ca 100644
--- a/mediagoblin/submit/views.py
+++ b/mediagoblin/submit/views.py
@@ -28,7 +28,7 @@ _log = logging.getLogger(__name__)
from werkzeug.utils import secure_filename
from mediagoblin.db.util import ObjectId
-from mediagoblin.tools.text import cleaned_markdown_conversion, convert_to_tag_list_of_dicts
+from mediagoblin.tools.text import convert_to_tag_list_of_dicts
from mediagoblin.tools.translate import pass_to_ugettext as _
from mediagoblin.tools.response import render_to_response, redirect
from mediagoblin.decorators import require_active_login
@@ -66,8 +66,6 @@ def submit_start(request):
or unicode(splitext(filename)[0]))
entry.description = unicode(request.POST.get('description'))
- entry.description_html = cleaned_markdown_conversion(
- entry.description)
entry.license = unicode(request.POST.get('license', "")) or None
diff --git a/mediagoblin/templates/mediagoblin/user_pages/media.html b/mediagoblin/templates/mediagoblin/user_pages/media.html
index b8172eaa..fb79e4dc 100644
--- a/mediagoblin/templates/mediagoblin/user_pages/media.html
+++ b/mediagoblin/templates/mediagoblin/user_pages/media.html
@@ -40,84 +40,67 @@
{% endblock mediagoblin_head %}
{% block mediagoblin_content %}
- {% trans user_url=request.urlgen(
- 'mediagoblin.user_pages.user_home',
- user=media.get_uploader.username),
- username=media.get_uploader.username -%}
- <p class="context">❖ Browsing media by <a href="{{ user_url }}">{{ username }}</a></p>
- {%- endtrans %}
- {% include "mediagoblin/utils/prev_next.html" %}
- <div class="media_image_container">
- {% block mediagoblin_media %}
- {% set display_media = request.app.public_store.file_url(
- media.get_display_media(media.media_files)) %}
- {# if there's a medium file size, that means the medium size
- # isn't the original... so link to the original!
- #}
- {% if media.media_files.has_key('medium') %}
- <a href="{{ request.app.public_store.file_url(
- media.media_files['original']) }}">
+ <div class="media_pane">
+ <div class="media_image_container">
+ {% block mediagoblin_media %}
+ {% set display_media = request.app.public_store.file_url(
+ media.get_display_media(media.media_files)) %}
+ {# if there's a medium file size, that means the medium size
+ # isn't the original... so link to the original!
+ #}
+ {% if media.media_files.has_key('medium') %}
+ <a href="{{ request.app.public_store.file_url(
+ media.media_files['original']) }}">
+ <img class="media_image"
+ src="{{ display_media }}"
+ alt="Image for {{ media.title }}" />
+ </a>
+ {% else %}
<img class="media_image"
src="{{ display_media }}"
alt="Image for {{ media.title }}" />
- </a>
- {% else %}
- <img class="media_image"
- src="{{ display_media }}"
- alt="Image for {{ media.title }}" />
- {% endif %}
- {% endblock %}
- </div>
- <div class="media_pane">
+ {% endif %}
+ {% endblock %}
+ </div>
<h2 class="media_title">
{{ media.title }}
</h2>
- {% if request.user and
- (media.uploader == request.user._id or
- request.user.is_admin) %}
- {% set edit_url = request.urlgen('mediagoblin.edit.edit_media',
- user= media.get_uploader.username,
- media= media._id) %}
- <a class="button_action" href="{{ edit_url }}">{% trans %}Edit{% endtrans %}</a>
- {% set delete_url = request.urlgen('mediagoblin.user_pages.media_confirm_delete',
- user= media.get_uploader.username,
- media= media._id) %}
- <a class="button_action" href="{{ delete_url }}">{% trans %}Delete{% endtrans %}</a>
- {% endif %}
{% autoescape False %}
<p>{{ media.description_html }}</p>
- {% endautoescape %}
- {% if media.attachment_files|count %}
- <h3>Attachments</h3>
- <ul>
- {% for attachment in media.attachment_files %}
- <li>
- <a href="{{ request.app.public_store.file_url(attachment.filepath) }}">
- {{ attachment.name }}
- </a>
- </li>
- {% endfor %}
- </ul>
- {% endif %}
- {% if app_config['allow_attachments']
- and request.user
- and (media.uploader == request.user._id
- or request.user.is_admin) %}
- <p>
- <a href="{{ request.urlgen('mediagoblin.edit.attachments',
- user=media.get_uploader.username,
- media=media._id) }}">Add attachment</a>
- </p>
- {% endif %}
+ {% endautoescape %}
+ <p class="media_specs">
+ {% trans date=media.created.strftime("%Y-%m-%d") -%}
+ Added on {{ date }}.
+ {%- endtrans %}
+ {% if request.user and
+ (media.uploader == request.user._id or
+ request.user.is_admin) %}
+ {% set edit_url = request.urlgen('mediagoblin.edit.edit_media',
+ user= media.get_uploader.username,
+ media= media._id) %}
+ <a class="button_action" href="{{ edit_url }}">{% trans %}Edit{% endtrans %}</a>
+ {% set delete_url = request.urlgen('mediagoblin.user_pages.media_confirm_delete',
+ user= media.get_uploader.username,
+ media= media._id) %}
+ <a class="button_action" href="{{ delete_url }}">{% trans %}Delete{% endtrans %}</a>
+ {% endif %}
+ </p>
{% if comments %}
<h3>
+ {% if comments.count()==1 %}
+ {% trans comment_count=comments.count() -%}{{ comment_count }} comment{%- endtrans %}
+ {% elif comments.count()>1 %}
+ {% trans comment_count=comments.count() -%}{{ comment_count }} comments{%- endtrans %}
+ {% else %}
+ {% trans %}No comments yet.{% endtrans %}
+ {% endif %}
<div class="right_align">
<a
{% if not request.user %}
href="{{ request.urlgen('mediagoblin.auth.login') }}"
{% endif %}
class="button_action" id="button_addcomment" title="Add a comment">
- {% trans %}Add a comment{% endtrans %}
+ {% trans %}Add one{% endtrans %}
</a>
</div>
</h3>
@@ -167,11 +150,35 @@
{% endif %}
</div>
<div class="media_sidebar">
- {% trans date=media.created.strftime("%Y-%m-%d") -%}
- <h3 class="sidedata">Added on</h3>
- <p>{{ date }}</p>
- {%- endtrans %}
-
+ {% trans user_url=request.urlgen(
+ 'mediagoblin.user_pages.user_home',
+ user=media.get_uploader.username),
+ username=media.get_uploader.username -%}
+ <p>❖ Browsing media by <a href="{{ user_url }}">{{ username }}</a></p>
+ {%- endtrans %}
+ {% include "mediagoblin/utils/prev_next.html" %}
+ {% if media.attachment_files|count %}
+ <h3>Attachments</h3>
+ <ul>
+ {% for attachment in media.attachment_files %}
+ <li>
+ <a href="{{ request.app.public_store.file_url(attachment.filepath) }}">
+ {{ attachment.name }}
+ </a>
+ </li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+ {% if app_config['allow_attachments']
+ and request.user
+ and (media.uploader == request.user._id
+ or request.user.is_admin) %}
+ <p>
+ <a href="{{ request.urlgen('mediagoblin.edit.attachments',
+ user=media.get_uploader.username,
+ media=media._id) }}">Add attachment</a>
+ </p>
+ {% endif %}
{% if media.tags %}
{% include "mediagoblin/utils/tags.html" %}
{% endif %}
diff --git a/mediagoblin/templates/mediagoblin/utils/exif.html b/mediagoblin/templates/mediagoblin/utils/exif.html
index bd2e3307..0dd187f2 100644
--- a/mediagoblin/templates/mediagoblin/utils/exif.html
+++ b/mediagoblin/templates/mediagoblin/utils/exif.html
@@ -20,7 +20,7 @@
{% if media.media_data.has_key('exif')
and app_config['exif_visible']
and media.media_data.exif.has_key('useful') %}
- <h3 class="sidedata">EXIF</h3>
+ <h4>EXIF</h4>
<table>
{% for key, tag in media.media_data.exif.useful.items() %}
<tr>
diff --git a/mediagoblin/templates/mediagoblin/utils/geolocation_map.html b/mediagoblin/templates/mediagoblin/utils/geolocation_map.html
index 118d0e62..c1909ae5 100644
--- a/mediagoblin/templates/mediagoblin/utils/geolocation_map.html
+++ b/mediagoblin/templates/mediagoblin/utils/geolocation_map.html
@@ -20,7 +20,7 @@
{% if media.media_data.has_key('gps')
and app_config['geolocation_map_visible']
and media.media_data.gps %}
- <h3 class="sidedata">Location</h3>
+ <h4>Map</h4>
<div>
{% set gps = media.media_data.gps %}
<div id="tile-map" style="width: 100%; height: 196px;">
diff --git a/mediagoblin/templates/mediagoblin/utils/license.html b/mediagoblin/templates/mediagoblin/utils/license.html
index ab157508..2438ed4e 100644
--- a/mediagoblin/templates/mediagoblin/utils/license.html
+++ b/mediagoblin/templates/mediagoblin/utils/license.html
@@ -17,8 +17,8 @@
#}
{% block license_content -%}
- <h3 class="sidedata">{% trans %}License{% endtrans %}</h3>
<p>
+ {% trans %}License:{% endtrans %}
{% if media.license %}
<a href="{{ media.license }}">{{ media.get_license_data().abbreviation }}</a>
{% else %}
diff --git a/mediagoblin/templates/mediagoblin/utils/object_gallery.html b/mediagoblin/templates/mediagoblin/utils/object_gallery.html
index 6b5988fb..81506a84 100644
--- a/mediagoblin/templates/mediagoblin/utils/object_gallery.html
+++ b/mediagoblin/templates/mediagoblin/utils/object_gallery.html
@@ -19,23 +19,29 @@
{% from "mediagoblin/utils/pagination.html" import render_pagination %}
{% macro media_grid(request, media_entries, col_number=5) %}
- <ul class="thumb_gallery">
+ <table class="thumb_gallery">
{% for row in gridify_cursor(media_entries, col_number) %}
- {% for entry in row %}
- {% set entry_url = entry.url_for_self(request.urlgen) %}
- <li class="media_thumbnail">
- <a href="{{ entry_url }}">
- <img src="{{ request.app.public_store.file_url(
- entry.media_files['thumb']) }}" />
- </a>
- {% if entry.title %}
- <br />
- <a href="{{ entry_url }}">{{ entry.title }}</a>
- {% endif %}
- </li>
- {% endfor %}
+ <tr class="thumb_row
+ {%- if loop.first %} thumb_row_first
+ {%- elif loop.last %} thumb_row_last{% endif %}">
+ {% for entry in row %}
+ {% set entry_url = entry.url_for_self(request.urlgen) %}
+ <td class="media_thumbnail thumb_entry
+ {%- if loop.first %} thumb_entry_first
+ {%- elif loop.last %} thumb_entry_last{% endif %}">
+ <a href="{{ entry_url }}">
+ <img src="{{ request.app.public_store.file_url(
+ entry.media_files['thumb']) }}" />
+ </a>
+ {% if entry.title %}
+ <br />
+ <a href="{{ entry_url }}">{{ entry.title }}</a>
+ {% endif %}
+ </td>
+ {% endfor %}
+ </tr>
{% endfor %}
- </ul>
+ </table>
{%- endmacro %}
{#
diff --git a/mediagoblin/templates/mediagoblin/utils/prev_next.html b/mediagoblin/templates/mediagoblin/utils/prev_next.html
index f1175ce4..d0cf3f8c 100644
--- a/mediagoblin/templates/mediagoblin/utils/prev_next.html
+++ b/mediagoblin/templates/mediagoblin/utils/prev_next.html
@@ -21,28 +21,26 @@
{% set next_entry_url = media.url_to_next(request.urlgen) %}
{% if prev_entry_url or next_entry_url %}
- <div class="navigation">
- {# There are no previous entries for the very first media entry #}
- {% if prev_entry_url %}
- <a class="navigation_button navigation_left" href="{{ prev_entry_url }}">
- &larr; {% trans %}newer{% endtrans %}
- </a>
- {% else %}
- {# This is the first entry. display greyed-out 'previous' image #}
- <p class="navigation_button navigation_left">
- &larr; {% trans %}newer{% endtrans %}
- </p>
- {% endif %}
- {# Likewise, this could be the very last media entry #}
- {% if next_entry_url %}
- <a class="navigation_button" href="{{ next_entry_url }}">
- {% trans %}older{% endtrans %} &rarr;
- </a>
- {% else %}
- {# This is the last entry. display greyed-out 'next' image #}
- <p class="navigation_button">
- {% trans %}older{% endtrans %} &rarr;
- </p>
- {% endif %}
- </div>
+ {# There are no previous entries for the very first media entry #}
+ {% if prev_entry_url %}
+ <a class="navigation_button navigation_left" href="{{ prev_entry_url }}">
+ &larr; {% trans %}newer{% endtrans %}
+ </a>
+ {% else %}
+ {# This is the first entry. display greyed-out 'previous' image #}
+ <p class="navigation_button navigation_left">
+ &larr; {% trans %}newer{% endtrans %}
+ </p>
+ {% endif %}
+ {# Likewise, this could be the very last media entry #}
+ {% if next_entry_url %}
+ <a class="navigation_button" href="{{ next_entry_url }}">
+ {% trans %}older{% endtrans %} &rarr;
+ </a>
+ {% else %}
+ {# This is the last entry. display greyed-out 'next' image #}
+ <p class="navigation_button">
+ {% trans %}older{% endtrans %} &rarr;
+ </p>
+ {% endif %}
{% endif %}
diff --git a/mediagoblin/templates/mediagoblin/utils/tags.html b/mediagoblin/templates/mediagoblin/utils/tags.html
index bcf3b5fd..6408102d 100644
--- a/mediagoblin/templates/mediagoblin/utils/tags.html
+++ b/mediagoblin/templates/mediagoblin/utils/tags.html
@@ -17,17 +17,16 @@
#}
{% block tags_content -%}
- <h3 class="sidedata">Tagged with</h3>
- <p>
+ <p>{% trans %}View more media tagged with{% endtrans %}
{% for tag in media.tags %}
{% if loop.last %}
{# the 'and' should only appear if there is more than one tag #}
{% if media.tags|length > 1 %}
- &middot;
+ {% trans %}or{% endtrans %}
{% endif %}
<a href="{{ request.urlgen(
'mediagoblin.listings.tags_listing',
- tag=tag['slug']) }}">{{ tag['name'] }}</a>
+ tag=tag['slug']) }}">{{ tag['name'] }}</a>.
{% elif loop.revindex == 2 %}
<a href="{{ request.urlgen(
'mediagoblin.listings.tags_listing',
@@ -35,7 +34,7 @@
{% else %}
<a href="{{ request.urlgen(
'mediagoblin.listings.tags_listing',
- tag=tag['slug']) }}">{{ tag['name'] }}</a> &middot;
+ tag=tag['slug']) }}">{{ tag['name'] }}</a>,
{% endif %}
{% endfor %}
</p>
diff --git a/mediagoblin/tests/test_sql_migrations.py b/mediagoblin/tests/test_sql_migrations.py
new file mode 100644
index 00000000..507a7725
--- /dev/null
+++ b/mediagoblin/tests/test_sql_migrations.py
@@ -0,0 +1,884 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2012, 2012 MediaGoblin contributors. See AUTHORS.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import copy
+
+from sqlalchemy import (
+ Table, Column, MetaData, Index,
+ Integer, Float, Unicode, UnicodeText, DateTime, Boolean,
+ ForeignKey, UniqueConstraint, PickleType, VARCHAR)
+from sqlalchemy.orm import sessionmaker, relationship
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.sql import select, insert
+from migrate import changeset
+
+from mediagoblin.db.sql.base import GMGTableBase
+from mediagoblin.db.sql.util import MigrationManager, RegisterMigration
+
+
+# This one will get filled with local migrations
+FULL_MIGRATIONS = {}
+
+
+#######################################################
+# Migration set 1: Define initial models, no migrations
+#######################################################
+
+Base1 = declarative_base(cls=GMGTableBase)
+
+class Creature1(Base1):
+ __tablename__ = "creature"
+
+ id = Column(Integer, primary_key=True)
+ name = Column(Unicode, unique=True, nullable=False, index=True)
+ num_legs = Column(Integer, nullable=False)
+ is_demon = Column(Boolean)
+
+class Level1(Base1):
+ __tablename__ = "level"
+
+ id = Column(Unicode, primary_key=True)
+ name = Column(Unicode)
+ description = Column(Unicode)
+ exits = Column(PickleType)
+
+SET1_MODELS = [Creature1, Level1]
+
+SET1_MIGRATIONS = {}
+
+#######################################################
+# Migration set 2: A few migrations and new model
+#######################################################
+
+Base2 = declarative_base(cls=GMGTableBase)
+
+class Creature2(Base2):
+ __tablename__ = "creature"
+
+ id = Column(Integer, primary_key=True)
+ name = Column(Unicode, unique=True, nullable=False, index=True)
+ num_legs = Column(Integer, nullable=False)
+ magical_powers = relationship("CreaturePower2")
+
+class CreaturePower2(Base2):
+ __tablename__ = "creature_power"
+
+ id = Column(Integer, primary_key=True)
+ creature = Column(
+ Integer, ForeignKey('creature.id'), nullable=False)
+ name = Column(Unicode)
+ description = Column(Unicode)
+ hitpower = Column(Integer, nullable=False)
+
+class Level2(Base2):
+ __tablename__ = "level"
+
+ id = Column(Unicode, primary_key=True)
+ name = Column(Unicode)
+ description = Column(Unicode)
+
+class LevelExit2(Base2):
+ __tablename__ = "level_exit"
+
+ id = Column(Integer, primary_key=True)
+ name = Column(Unicode)
+ from_level = Column(
+ Unicode, ForeignKey('level.id'), nullable=False)
+ to_level = Column(
+ Unicode, ForeignKey('level.id'), nullable=False)
+
+SET2_MODELS = [Creature2, CreaturePower2, Level2, LevelExit2]
+
+
+@RegisterMigration(1, FULL_MIGRATIONS)
+def creature_remove_is_demon(db_conn):
+ """
+ Remove the is_demon field from the creature model. We don't need
+ it!
+ """
+ # :( Commented out 'cuz of:
+ # http://code.google.com/p/sqlalchemy-migrate/issues/detail?id=143&thanks=143&ts=1327882242
+
+ # metadata = MetaData(bind=db_conn.bind)
+ # creature_table = Table(
+ # 'creature', metadata,
+ # autoload=True, autoload_with=db_conn.bind)
+ # creature_table.drop_column('is_demon')
+ pass
+
+
+@RegisterMigration(2, FULL_MIGRATIONS)
+def creature_powers_new_table(db_conn):
+ """
+ Add a new table for creature powers. Nothing needs to go in it
+ yet though as there wasn't anything that previously held this
+ information
+ """
+ metadata = MetaData(bind=db_conn.bind)
+
+ # We have to access the creature table so sqlalchemy can make the
+ # foreign key relationship
+ creature_table = Table(
+ 'creature', metadata,
+ autoload=True, autoload_with=db_conn.bind)
+
+ creature_powers = Table(
+ 'creature_power', metadata,
+ Column('id', Integer, primary_key=True),
+ Column('creature',
+ Integer, ForeignKey('creature.id'), nullable=False),
+ Column('name', Unicode),
+ Column('description', Unicode),
+ Column('hitpower', Integer, nullable=False))
+ metadata.create_all(db_conn.bind)
+
+
+@RegisterMigration(3, FULL_MIGRATIONS)
+def level_exits_new_table(db_conn):
+ """
+ Make a new table for level exits and move the previously pickled
+ stuff over to here (then drop the old unneeded table)
+ """
+ # First, create the table
+ # -----------------------
+ metadata = MetaData(bind=db_conn.bind)
+
+ # Minimal representation of level table.
+ # Not auto-introspecting here because of pickle table. I'm not
+ # sure sqlalchemy can auto-introspect pickle columns.
+ levels = Table(
+ 'level', metadata,
+ Column('id', Unicode, primary_key=True),
+ Column('name', Unicode),
+ Column('description', Unicode),
+ Column('exits', PickleType))
+
+ level_exits = Table(
+ 'level_exit', metadata,
+ Column('id', Integer, primary_key=True),
+ Column('name', Unicode),
+ Column('from_level',
+ Unicode, ForeignKey('level.id'), nullable=False),
+ Column('to_level',
+ Unicode, ForeignKey('level.id'), nullable=False))
+ metadata.create_all(db_conn.bind)
+
+ # And now, convert all the old exit pickles to new level exits
+ # ------------------------------------------------------------
+
+ # query over and insert
+ result = db_conn.execute(
+ select([levels], levels.c.exits!=None))
+
+ for level in result:
+
+ for exit_name, to_level in level['exits'].iteritems():
+ # Insert the level exit
+ db_conn.execute(
+ level_exits.insert().values(
+ name=exit_name,
+ from_level=level.id,
+ to_level=to_level))
+
+ # Finally, drop the old level exits pickle table
+ # ----------------------------------------------
+ levels.drop_column('exits')
+
+
+# A hack! At this point we freeze-fame and get just a partial list of
+# migrations
+
+SET2_MIGRATIONS = copy.copy(FULL_MIGRATIONS)
+
+#######################################################
+# Migration set 3: Final migrations
+#######################################################
+
+Base3 = declarative_base(cls=GMGTableBase)
+
+class Creature3(Base3):
+ __tablename__ = "creature"
+
+ id = Column(Integer, primary_key=True)
+ name = Column(Unicode, unique=True, nullable=False, index=True)
+ num_limbs= Column(Integer, nullable=False)
+ magical_powers = relationship("CreaturePower3")
+
+class CreaturePower3(Base3):
+ __tablename__ = "creature_power"
+
+ id = Column(Integer, primary_key=True)
+ creature = Column(
+ Integer, ForeignKey('creature.id'), nullable=False, index=True)
+ name = Column(Unicode)
+ description = Column(Unicode)
+ hitpower = Column(Float, nullable=False)
+
+class Level3(Base3):
+ __tablename__ = "level"
+
+ id = Column(Unicode, primary_key=True)
+ name = Column(Unicode)
+ description = Column(Unicode)
+
+class LevelExit3(Base3):
+ __tablename__ = "level_exit"
+
+ id = Column(Integer, primary_key=True)
+ name = Column(Unicode)
+ from_level = Column(
+ Unicode, ForeignKey('level.id'), nullable=False, index=True)
+ to_level = Column(
+ Unicode, ForeignKey('level.id'), nullable=False, index=True)
+
+
+SET3_MODELS = [Creature3, CreaturePower3, Level3, LevelExit3]
+SET3_MIGRATIONS = FULL_MIGRATIONS
+
+
+@RegisterMigration(4, FULL_MIGRATIONS)
+def creature_num_legs_to_num_limbs(db_conn):
+ """
+ Turns out we're tracking all sorts of limbs, not "legs"
+ specifically. Humans would be 4 here, for instance. So we
+ renamed the column.
+ """
+ metadata = MetaData(bind=db_conn.bind)
+ creature_table = Table(
+ 'creature', metadata,
+ autoload=True, autoload_with=db_conn.bind)
+ creature_table.c.num_legs.alter(name=u"num_limbs")
+
+
+@RegisterMigration(5, FULL_MIGRATIONS)
+def level_exit_index_from_and_to_level(db_conn):
+ """
+ Index the from and to levels of the level exit table.
+ """
+ metadata = MetaData(bind=db_conn.bind)
+ level_exit = Table(
+ 'level_exit', metadata,
+ autoload=True, autoload_with=db_conn.bind)
+ Index('ix_level_exit_from_level',
+ level_exit.c.from_level).create(db_conn.bind)
+ Index('ix_level_exit_to_level',
+ level_exit.c.to_level).create(db_conn.bind)
+
+
+@RegisterMigration(6, FULL_MIGRATIONS)
+def creature_power_index_creature(db_conn):
+ """
+ Index our foreign key relationship to the creatures
+ """
+ metadata = MetaData(bind=db_conn.bind)
+ creature_power = Table(
+ 'creature_power', metadata,
+ autoload=True, autoload_with=db_conn.bind)
+ Index('ix_creature_power_creature',
+ creature_power.c.creature).create(db_conn.bind)
+
+
+@RegisterMigration(7, FULL_MIGRATIONS)
+def creature_power_hitpower_to_float(db_conn):
+ """
+ Convert hitpower column on creature power table from integer to
+ float.
+
+ Turns out we want super precise values of how much hitpower there
+ really is.
+ """
+ metadata = MetaData(bind=db_conn.bind)
+
+ # We have to access the creature table so sqlalchemy can make the
+ # foreign key relationship
+ creature_table = Table(
+ 'creature', metadata,
+ autoload=True, autoload_with=db_conn.bind)
+
+ creature_power = Table(
+ 'creature_power', metadata,
+ Column('id', Integer, primary_key=True),
+ Column('creature', Integer,
+ ForeignKey('creature.id'), nullable=False,
+ index=True),
+ Column('name', Unicode),
+ Column('description', Unicode),
+ Column('hitpower', Integer, nullable=False))
+
+ creature_power.c.hitpower.alter(type=Float)
+
+
+def _insert_migration1_objects(session):
+ """
+ Test objects to insert for the first set of things
+ """
+ # Insert creatures
+ session.add_all(
+ [Creature1(name=u'centipede',
+ num_legs=100,
+ is_demon=False),
+ Creature1(name=u'wolf',
+ num_legs=4,
+ is_demon=False),
+ # don't ask me what a wizardsnake is.
+ Creature1(name=u'wizardsnake',
+ num_legs=0,
+ is_demon=True)])
+
+ # Insert levels
+ session.add_all(
+ [Level1(id=u'necroplex',
+ name=u'The Necroplex',
+ description=u'A complex full of pure deathzone.',
+ exits={
+ 'deathwell': 'evilstorm',
+ 'portal': 'central_park'}),
+ Level1(id=u'evilstorm',
+ name=u'Evil Storm',
+ description=u'A storm full of pure evil.',
+ exits={}), # you can't escape the evilstorm
+ Level1(id=u'central_park',
+ name=u'Central Park, NY, NY',
+ description=u"New York's friendly Central Park.",
+ exits={
+ 'portal': 'necroplex'})])
+
+ session.commit()
+
+
+def _insert_migration2_objects(session):
+ """
+ Test objects to insert for the second set of things
+ """
+ # Insert creatures
+ session.add_all(
+ [Creature2(
+ name=u'centipede',
+ num_legs=100),
+ Creature2(
+ name=u'wolf',
+ num_legs=4,
+ magical_powers = [
+ CreaturePower2(
+ name=u"ice breath",
+ description=u"A blast of icy breath!",
+ hitpower=20),
+ CreaturePower2(
+ name=u"death stare",
+ description=u"A frightening stare, for sure!",
+ hitpower=45)]),
+ Creature2(
+ name=u'wizardsnake',
+ num_legs=0,
+ magical_powers=[
+ CreaturePower2(
+ name=u'death_rattle',
+ description=u'A rattle... of DEATH!',
+ hitpower=1000),
+ CreaturePower2(
+ name=u'sneaky_stare',
+ description=u"The sneakiest stare you've ever seen!",
+ hitpower=300),
+ CreaturePower2(
+ name=u'slithery_smoke',
+ description=u"A blast of slithery, slithery smoke.",
+ hitpower=10),
+ CreaturePower2(
+ name=u'treacherous_tremors',
+ description=u"The ground shakes beneath footed animals!",
+ hitpower=0)])])
+
+ # Insert levels
+ session.add_all(
+ [Level2(id=u'necroplex',
+ name=u'The Necroplex',
+ description=u'A complex full of pure deathzone.'),
+ Level2(id=u'evilstorm',
+ name=u'Evil Storm',
+ description=u'A storm full of pure evil.',
+ exits=[]), # you can't escape the evilstorm
+ Level2(id=u'central_park',
+ name=u'Central Park, NY, NY',
+ description=u"New York's friendly Central Park.")])
+
+ # necroplex exits
+ session.add_all(
+ [LevelExit2(name=u'deathwell',
+ from_level=u'necroplex',
+ to_level=u'evilstorm'),
+ LevelExit2(name=u'portal',
+ from_level=u'necroplex',
+ to_level=u'central_park')])
+
+ # there are no evilstorm exits because there is no exit from the
+ # evilstorm
+
+ # central park exits
+ session.add_all(
+ [LevelExit2(name=u'portal',
+ from_level=u'central_park',
+ to_level=u'necroplex')])
+
+ session.commit()
+
+
+def _insert_migration3_objects(session):
+ """
+ Test objects to insert for the third set of things
+ """
+ # Insert creatures
+ session.add_all(
+ [Creature3(
+ name=u'centipede',
+ num_limbs=100),
+ Creature3(
+ name=u'wolf',
+ num_limbs=4,
+ magical_powers = [
+ CreaturePower3(
+ name=u"ice breath",
+ description=u"A blast of icy breath!",
+ hitpower=20.0),
+ CreaturePower3(
+ name=u"death stare",
+ description=u"A frightening stare, for sure!",
+ hitpower=45.0)]),
+ Creature3(
+ name=u'wizardsnake',
+ num_limbs=0,
+ magical_powers=[
+ CreaturePower3(
+ name=u'death_rattle',
+ description=u'A rattle... of DEATH!',
+ hitpower=1000.0),
+ CreaturePower3(
+ name=u'sneaky_stare',
+ description=u"The sneakiest stare you've ever seen!",
+ hitpower=300.0),
+ CreaturePower3(
+ name=u'slithery_smoke',
+ description=u"A blast of slithery, slithery smoke.",
+ hitpower=10.0),
+ CreaturePower3(
+ name=u'treacherous_tremors',
+ description=u"The ground shakes beneath footed animals!",
+ hitpower=0.0)])],
+ # annnnnd one more to test a floating point hitpower
+ Creature3(
+ name=u'deity',
+ numb_limbs=30,
+ magical_powers=[
+ CreaturePower3(
+ name=u'smite',
+ description=u'Smitten by holy wrath!',
+ hitpower=9999.9)]))
+
+ # Insert levels
+ session.add_all(
+ [Level3(id=u'necroplex',
+ name=u'The Necroplex',
+ description=u'A complex full of pure deathzone.'),
+ Level3(id=u'evilstorm',
+ name=u'Evil Storm',
+ description=u'A storm full of pure evil.',
+ exits=[]), # you can't escape the evilstorm
+ Level3(id=u'central_park',
+ name=u'Central Park, NY, NY',
+ description=u"New York's friendly Central Park.")])
+
+ # necroplex exits
+ session.add_all(
+ [LevelExit3(name=u'deathwell',
+ from_level=u'necroplex',
+ to_level=u'evilstorm'),
+ LevelExit3(name=u'portal',
+ from_level=u'necroplex',
+ to_level=u'central_park')])
+
+ # there are no evilstorm exits because there is no exit from the
+ # evilstorm
+
+ # central park exits
+ session.add_all(
+ [LevelExit3(name=u'portal',
+ from_level=u'central_park',
+ to_level=u'necroplex')])
+
+ session.commit()
+
+
+class CollectingPrinter(object):
+ def __init__(self):
+ self.collection = []
+
+ def __call__(self, string):
+ self.collection.append(string)
+
+ @property
+ def combined_string(self):
+ return u''.join(self.collection)
+
+
+def create_test_engine():
+ from sqlalchemy import create_engine
+ engine = create_engine('sqlite:///:memory:', echo=False)
+ Session = sessionmaker(bind=engine)
+ return engine, Session
+
+
+def assert_col_type(column, this_class):
+ assert isinstance(column.type, this_class)
+
+
+def _get_level3_exits(session, level):
+ return dict(
+ [(level_exit.name, level_exit.to_level)
+ for level_exit in
+ session.query(LevelExit3).filter_by(from_level=level.id)])
+
+
+def test_set1_to_set3():
+ # Create / connect to database
+ # ----------------------------
+
+ engine, Session = create_test_engine()
+
+ # Create tables by migrating on empty initial set
+ # -----------------------------------------------
+
+ printer = CollectingPrinter()
+ migration_manager = MigrationManager(
+ '__main__', SET1_MODELS, SET1_MIGRATIONS, Session(),
+ printer)
+
+ # Check latest migration and database current migration
+ assert migration_manager.latest_migration == 0
+ assert migration_manager.database_current_migration == None
+
+ result = migration_manager.init_or_migrate()
+
+ # Make sure output was "inited"
+ assert result == u'inited'
+ # Check output
+ assert printer.combined_string == (
+ "-> Initializing main mediagoblin tables... done.\n")
+ # Check version in database
+ assert migration_manager.latest_migration == 0
+ assert migration_manager.database_current_migration == 0
+
+ # Install the initial set
+ # -----------------------
+
+ _insert_migration1_objects(Session())
+
+ # Try to "re-migrate" with same manager settings... nothing should happen
+ migration_manager = MigrationManager(
+ '__main__', SET1_MODELS, SET1_MIGRATIONS, Session(),
+ printer)
+ assert migration_manager.init_or_migrate() == None
+
+ # Check version in database
+ assert migration_manager.latest_migration == 0
+ assert migration_manager.database_current_migration == 0
+
+ # Sanity check a few things in the database...
+ metadata = MetaData(bind=engine)
+
+ # Check the structure of the creature table
+ creature_table = Table(
+ 'creature', metadata,
+ autoload=True, autoload_with=engine)
+ assert set(creature_table.c.keys()) == set(
+ ['id', 'name', 'num_legs', 'is_demon'])
+ assert_col_type(creature_table.c.id, Integer)
+ assert_col_type(creature_table.c.name, VARCHAR)
+ assert creature_table.c.name.nullable is False
+ #assert creature_table.c.name.index is True
+ #assert creature_table.c.name.unique is True
+ assert_col_type(creature_table.c.num_legs, Integer)
+ assert creature_table.c.num_legs.nullable is False
+ assert_col_type(creature_table.c.is_demon, Boolean)
+
+ # Check the structure of the level table
+ level_table = Table(
+ 'level', metadata,
+ autoload=True, autoload_with=engine)
+ assert set(level_table.c.keys()) == set(
+ ['id', 'name', 'description', 'exits'])
+ assert_col_type(level_table.c.id, VARCHAR)
+ assert level_table.c.id.primary_key is True
+ assert_col_type(level_table.c.name, VARCHAR)
+ assert_col_type(level_table.c.description, VARCHAR)
+ # Skipping exits... Not sure if we can detect pickletype, not a
+ # big deal regardless.
+
+ # Now check to see if stuff seems to be in there.
+ session = Session()
+
+ creature = session.query(Creature1).filter_by(
+ name=u'centipede').one()
+ assert creature.num_legs == 100
+ assert creature.is_demon == False
+
+ creature = session.query(Creature1).filter_by(
+ name=u'wolf').one()
+ assert creature.num_legs == 4
+ assert creature.is_demon == False
+
+ creature = session.query(Creature1).filter_by(
+ name=u'wizardsnake').one()
+ assert creature.num_legs == 0
+ assert creature.is_demon == True
+
+ level = session.query(Level1).filter_by(
+ id=u'necroplex').one()
+ assert level.name == u'The Necroplex'
+ assert level.description == u'A complex full of pure deathzone.'
+ assert level.exits == {
+ 'deathwell': 'evilstorm',
+ 'portal': 'central_park'}
+
+ level = session.query(Level1).filter_by(
+ id=u'evilstorm').one()
+ assert level.name == u'Evil Storm'
+ assert level.description == u'A storm full of pure evil.'
+ assert level.exits == {} # You still can't escape the evilstorm!
+
+ level = session.query(Level1).filter_by(
+ id=u'central_park').one()
+ assert level.name == u'Central Park, NY, NY'
+ assert level.description == u"New York's friendly Central Park."
+ assert level.exits == {
+ 'portal': 'necroplex'}
+
+ # Create new migration manager, but make sure the db migration
+ # isn't said to be updated yet
+ printer = CollectingPrinter()
+ migration_manager = MigrationManager(
+ '__main__', SET3_MODELS, SET3_MIGRATIONS, Session(),
+ printer)
+
+ assert migration_manager.latest_migration == 7
+ assert migration_manager.database_current_migration == 0
+
+ # Migrate
+ result = migration_manager.init_or_migrate()
+
+ # Make sure result was "migrated"
+ assert result == u'migrated'
+
+ # TODO: Check output to user
+ assert printer.combined_string == """\
+-> Updating main mediagoblin tables:
+ + Running migration 1, "creature_remove_is_demon"... done.
+ + Running migration 2, "creature_powers_new_table"... done.
+ + Running migration 3, "level_exits_new_table"... done.
+ + Running migration 4, "creature_num_legs_to_num_limbs"... done.
+ + Running migration 5, "level_exit_index_from_and_to_level"... done.
+ + Running migration 6, "creature_power_index_creature"... done.
+ + Running migration 7, "creature_power_hitpower_to_float"... done.
+"""
+
+ # Make sure version matches expected
+ migration_manager = MigrationManager(
+ '__main__', SET3_MODELS, SET3_MIGRATIONS, Session(),
+ printer)
+ assert migration_manager.latest_migration == 7
+ assert migration_manager.database_current_migration == 7
+
+ # Check all things in database match expected
+
+ # Check the creature table
+ metadata = MetaData(bind=engine)
+ creature_table = Table(
+ 'creature', metadata,
+ autoload=True, autoload_with=engine)
+ # assert set(creature_table.c.keys()) == set(
+ # ['id', 'name', 'num_limbs'])
+ assert set(creature_table.c.keys()) == set(
+ [u'id', 'name', u'num_limbs', u'is_demon'])
+ assert_col_type(creature_table.c.id, Integer)
+ assert_col_type(creature_table.c.name, VARCHAR)
+ assert creature_table.c.name.nullable is False
+ #assert creature_table.c.name.index is True
+ #assert creature_table.c.name.unique is True
+ assert_col_type(creature_table.c.num_limbs, Integer)
+ assert creature_table.c.num_limbs.nullable is False
+
+ # Check the CreaturePower table
+ creature_power_table = Table(
+ 'creature_power', metadata,
+ autoload=True, autoload_with=engine)
+ assert set(creature_power_table.c.keys()) == set(
+ ['id', 'creature', 'name', 'description', 'hitpower'])
+ assert_col_type(creature_power_table.c.id, Integer)
+ assert_col_type(creature_power_table.c.creature, Integer)
+ assert creature_power_table.c.creature.nullable is False
+ assert_col_type(creature_power_table.c.name, VARCHAR)
+ assert_col_type(creature_power_table.c.description, VARCHAR)
+ assert_col_type(creature_power_table.c.hitpower, Float)
+ assert creature_power_table.c.hitpower.nullable is False
+
+ # Check the structure of the level table
+ level_table = Table(
+ 'level', metadata,
+ autoload=True, autoload_with=engine)
+ assert set(level_table.c.keys()) == set(
+ ['id', 'name', 'description'])
+ assert_col_type(level_table.c.id, VARCHAR)
+ assert level_table.c.id.primary_key is True
+ assert_col_type(level_table.c.name, VARCHAR)
+ assert_col_type(level_table.c.description, VARCHAR)
+
+ # Check the structure of the level_exits table
+ level_exit_table = Table(
+ 'level_exit', metadata,
+ autoload=True, autoload_with=engine)
+ assert set(level_exit_table.c.keys()) == set(
+ ['id', 'name', 'from_level', 'to_level'])
+ assert_col_type(level_exit_table.c.id, Integer)
+ assert_col_type(level_exit_table.c.name, VARCHAR)
+ assert_col_type(level_exit_table.c.from_level, VARCHAR)
+ assert level_exit_table.c.from_level.nullable is False
+ #assert level_exit_table.c.from_level.index is True
+ assert_col_type(level_exit_table.c.to_level, VARCHAR)
+ assert level_exit_table.c.to_level.nullable is False
+ #assert level_exit_table.c.to_level.index is True
+
+ # Now check to see if stuff seems to be in there.
+ session = Session()
+ creature = session.query(Creature3).filter_by(
+ name=u'centipede').one()
+ assert creature.num_limbs == 100.0
+ assert creature.magical_powers == []
+
+ creature = session.query(Creature3).filter_by(
+ name=u'wolf').one()
+ assert creature.num_limbs == 4.0
+ assert creature.magical_powers == []
+
+ creature = session.query(Creature3).filter_by(
+ name=u'wizardsnake').one()
+ assert creature.num_limbs == 0.0
+ assert creature.magical_powers == []
+
+ level = session.query(Level3).filter_by(
+ id=u'necroplex').one()
+ assert level.name == u'The Necroplex'
+ assert level.description == u'A complex full of pure deathzone.'
+ level_exits = _get_level3_exits(session, level)
+ assert level_exits == {
+ u'deathwell': u'evilstorm',
+ u'portal': u'central_park'}
+
+ level = session.query(Level3).filter_by(
+ id=u'evilstorm').one()
+ assert level.name == u'Evil Storm'
+ assert level.description == u'A storm full of pure evil.'
+ level_exits = _get_level3_exits(session, level)
+ assert level_exits == {} # You still can't escape the evilstorm!
+
+ level = session.query(Level3).filter_by(
+ id=u'central_park').one()
+ assert level.name == u'Central Park, NY, NY'
+ assert level.description == u"New York's friendly Central Park."
+ level_exits = _get_level3_exits(session, level)
+ assert level_exits == {
+ 'portal': 'necroplex'}
+
+
+#def test_set2_to_set3():
+ # Create / connect to database
+ # Create tables by migrating on empty initial set
+
+ # Install the initial set
+ # Check version in database
+ # Sanity check a few things in the database
+
+ # Migrate
+ # Make sure version matches expected
+ # Check all things in database match expected
+ # pass
+
+
+#def test_set1_to_set2_to_set3():
+ # Create / connect to database
+ # Create tables by migrating on empty initial set
+
+ # Install the initial set
+ # Check version in database
+ # Sanity check a few things in the database
+
+ # Migrate
+ # Make sure version matches expected
+ # Check all things in database match expected
+
+ # Migrate again
+ # Make sure version matches expected again
+ # Check all things in database match expected again
+
+ ##### Set2
+ # creature_table = Table(
+ # 'creature', metadata,
+ # autoload=True, autoload_with=db_conn.bind)
+ # assert set(creature_table.c.keys()) == set(
+ # ['id', 'name', 'num_legs'])
+ # assert_col_type(creature_table.c.id, Integer)
+ # assert_col_type(creature_table.c.name, VARCHAR)
+ # assert creature_table.c.name.nullable is False
+ # assert creature_table.c.name.index is True
+ # assert creature_table.c.name.unique is True
+ # assert_col_type(creature_table.c.num_legs, Integer)
+ # assert creature_table.c.num_legs.nullable is False
+
+ # # Check the CreaturePower table
+ # creature_power_table = Table(
+ # 'creature_power', metadata,
+ # autoload=True, autoload_with=db_conn.bind)
+ # assert set(creature_power_table.c.keys()) == set(
+ # ['id', 'creature', 'name', 'description', 'hitpower'])
+ # assert_col_type(creature_power_table.c.id, Integer)
+ # assert_col_type(creature_power_table.c.creature, Integer)
+ # assert creature_power_table.c.creature.nullable is False
+ # assert_col_type(creature_power_table.c.name, VARCHAR)
+ # assert_col_type(creature_power_table.c.description, VARCHAR)
+ # assert_col_type(creature_power_table.c.hitpower, Integer)
+ # assert creature_power_table.c.hitpower.nullable is False
+
+ # # Check the structure of the level table
+ # level_table = Table(
+ # 'level', metadata,
+ # autoload=True, autoload_with=db_conn.bind)
+ # assert set(level_table.c.keys()) == set(
+ # ['id', 'name', 'description'])
+ # assert_col_type(level_table.c.id, VARCHAR)
+ # assert level_table.c.id.primary_key is True
+ # assert_col_type(level_table.c.name, VARCHAR)
+ # assert_col_type(level_table.c.description, VARCHAR)
+
+ # # Check the structure of the level_exits table
+ # level_exit_table = Table(
+ # 'level_exit', metadata,
+ # autoload=True, autoload_with=db_conn.bind)
+ # assert set(level_exit_table.c.keys()) == set(
+ # ['id', 'name', 'from_level', 'to_level'])
+ # assert_col_type(level_exit_table.c.id, Integer)
+ # assert_col_type(level_exit_table.c.name, VARCHAR)
+ # assert_col_type(level_exit_table.c.from_level, VARCHAR)
+ # assert level_exit_table.c.from_level.nullable is False
+ # assert_col_type(level_exit_table.c.to_level, VARCHAR)
+
+ # pass
diff --git a/mediagoblin/user_pages/views.py b/mediagoblin/user_pages/views.py
index 82791278..05d07b1b 100644
--- a/mediagoblin/user_pages/views.py
+++ b/mediagoblin/user_pages/views.py
@@ -18,7 +18,6 @@ from webob import exc
from mediagoblin import messages, mg_globals
from mediagoblin.db.util import DESCENDING, ObjectId
-from mediagoblin.tools.text import cleaned_markdown_conversion
from mediagoblin.tools.response import render_to_response, render_404, redirect
from mediagoblin.tools.translate import pass_to_ugettext as _
from mediagoblin.tools.pagination import Pagination
@@ -146,7 +145,6 @@ def media_post_comment(request, media):
comment['media_entry'] = media._id
comment['author'] = request.user._id
comment['content'] = unicode(request.POST['comment_content'])
- comment['content_html'] = cleaned_markdown_conversion(comment['content'])
if not comment['content'].strip():
messages.add_message(
@@ -250,7 +248,7 @@ def atom_feed(request):
for entry in cursor:
feed.add(entry.get('title'),
- entry.get('description_html'),
+ entry.description_html,
id=entry.url_for_self(request.urlgen,qualified=True),
content_type='html',
author={
diff --git a/setup.py b/setup.py
index 9dd8964a..1c7caf96 100644
--- a/setup.py
+++ b/setup.py
@@ -62,6 +62,8 @@ setup(
'webtest',
'ConfigObj',
'Markdown',
+ 'sqlalchemy',
+ 'sqlalchemy-migrate',
## For now we're expecting that users will install this from
## their package managers.
# 'lxml',