diff options
35 files changed, 1683 insertions, 198 deletions
@@ -15,6 +15,7 @@ /user_dev/ /paste_local.ini /mediagoblin_local.ini +/mediagoblin.db /server-log.txt # Tests @@ -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 }}"> - ← {% trans %}newer{% endtrans %} - </a> - {% else %} - {# This is the first entry. display greyed-out 'previous' image #} - <p class="navigation_button navigation_left"> - ← {% 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 %} → - </a> - {% else %} - {# This is the last entry. display greyed-out 'next' image #} - <p class="navigation_button"> - {% trans %}older{% endtrans %} → - </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 }}"> + ← {% trans %}newer{% endtrans %} + </a> + {% else %} + {# This is the first entry. display greyed-out 'previous' image #} + <p class="navigation_button navigation_left"> + ← {% 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 %} → + </a> + {% else %} + {# This is the last entry. display greyed-out 'next' image #} + <p class="navigation_button"> + {% trans %}older{% endtrans %} → + </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 %} - · + {% 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> · + 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={ @@ -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', |