Resolve merge conflicts

This commit is contained in:
Jef van Schendel
2012-02-21 17:43:05 +01:00
35 changed files with 1683 additions and 198 deletions

View File

@@ -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(

View File

@@ -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")

View File

@@ -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)

View File

@@ -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')

View File

@@ -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']

View File

@@ -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)

View File

@@ -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

View File

@@ -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 = {}

View File

@@ -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:'

271
mediagoblin/db/sql/util.py Normal file
View File

@@ -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__])

View File

@@ -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,

View File

@@ -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'},
}

View File

@@ -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()

View File

@@ -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,

View File

@@ -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 = {}

View File

@@ -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]

View File

@@ -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 = {}

View File

@@ -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]

View File

@@ -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 = {}

View File

@@ -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]

View File

@@ -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;

View File

@@ -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

View File

@@ -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 %}

View File

@@ -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>

View File

@@ -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;">

View File

@@ -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 %}

View File

@@ -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 %}
{#

View File

@@ -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 %}

View File

@@ -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>

View File

@@ -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

View File

@@ -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={