aboutsummaryrefslogtreecommitdiffstats
path: root/mediagoblin/db
diff options
context:
space:
mode:
Diffstat (limited to 'mediagoblin/db')
-rw-r--r--mediagoblin/db/mixin.py23
-rw-r--r--mediagoblin/db/mongo/migrations.py38
-rw-r--r--mediagoblin/db/mongo/models.py32
-rw-r--r--mediagoblin/db/mongo/util.py11
-rw-r--r--mediagoblin/db/sql/base.py10
-rw-r--r--mediagoblin/db/sql/convert.py37
-rw-r--r--mediagoblin/db/sql/extratypes.py28
-rw-r--r--mediagoblin/db/sql/migrations.py17
-rw-r--r--mediagoblin/db/sql/models.py82
-rw-r--r--mediagoblin/db/sql/open.py1
-rw-r--r--mediagoblin/db/sql/util.py284
-rw-r--r--mediagoblin/db/util.py4
12 files changed, 529 insertions, 38 deletions
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..c5766b0d 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,31 @@ 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')
+
+@RegisterMigration(10)
+def convert_video_media_data(database):
+ """
+ Move media_data["video"] directly into media_data
+ """
+ collection = database['media_entries']
+ target = collection.find(
+ {'media_data.video': {'$exists': True}})
+
+ for document in target:
+ assert len(document['media_data']) == 1
+ document['media_data'] = document['media_data']['video']
+ collection.save(document)
diff --git a/mediagoblin/db/mongo/models.py b/mediagoblin/db/mongo/models.py
index 541086bc..c86adbb6 100644
--- a/mediagoblin/db/mongo/models.py
+++ b/mediagoblin/db/mongo/models.py
@@ -23,7 +23,18 @@ 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
+
+
+class MongoPK(object):
+ """An alias for the _id primary key"""
+ def __get__(self, instance, cls):
+ return instance['_id']
+ def __set__(self, instance, val):
+ instance['_id'] = val
+ def __delete__(self, instance):
+ del instance['_id']
+
###################
# Custom validators
@@ -59,7 +70,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 +86,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,
}
@@ -89,6 +98,8 @@ class User(Document, UserMixin):
'status': u'needs_email_verification',
'is_admin': False}
+ id = MongoPK()
+
class MediaEntry(Document, MediaEntryMixin):
"""
@@ -112,9 +123,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 +187,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.
@@ -211,6 +218,11 @@ class MediaEntry(Document, MediaEntryMixin):
'created': datetime.datetime.utcnow,
'state': u'unprocessed'}
+ id = MongoPK()
+
+ def media_data_init(self, **kwargs):
+ self.media_data.update(kwargs)
+
def get_comments(self, ascending=False):
if ascending:
order = ASCENDING
@@ -257,7 +269,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 +278,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 +288,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/mongo/util.py b/mediagoblin/db/mongo/util.py
index 4daf616a..89348d98 100644
--- a/mediagoblin/db/mongo/util.py
+++ b/mediagoblin/db/mongo/util.py
@@ -290,3 +290,14 @@ class MigrationManager(object):
self.set_current_migration(migration_number)
if post_callback:
post_callback(migration_number, migration_func)
+
+
+##########################
+# Random utility functions
+##########################
+
+
+def atomic_update(table, query_dict, update_values):
+ table.collection.update(
+ query_dict,
+ {"$set": update_values})
diff --git a/mediagoblin/db/sql/base.py b/mediagoblin/db/sql/base.py
index 6ed24a03..838080b0 100644
--- a/mediagoblin/db/sql/base.py
+++ b/mediagoblin/db/sql/base.py
@@ -67,6 +67,10 @@ class GMGTableBase(object):
def get(self, key):
return getattr(self, key)
+ def setdefault(self, key, defaultvalue):
+ # The key *has* to exist on sql.
+ return getattr(self, key)
+
def save(self, validate=True):
assert validate
sess = object_session(self)
@@ -75,6 +79,12 @@ class GMGTableBase(object):
sess.add(self)
sess.commit()
+ def delete(self):
+ sess = object_session(self)
+ assert sess is not None, "Not going to delete detached %r" % self
+ sess.delete(self)
+ sess.commit()
+
Base = declarative_base(cls=GMGTableBase)
diff --git a/mediagoblin/db/sql/convert.py b/mediagoblin/db/sql/convert.py
index f6575be9..250c559b 100644
--- a/mediagoblin/db/sql/convert.py
+++ b/mediagoblin/db/sql/convert.py
@@ -19,7 +19,8 @@ from mediagoblin.init import setup_global_and_app_config, setup_database
from mediagoblin.db.mongo.util import ObjectId
from mediagoblin.db.sql.models import (Base, User, MediaEntry, MediaComment,
- Tag, MediaTag, MediaFile)
+ Tag, MediaTag, MediaFile, MediaAttachmentFile)
+from mediagoblin.media_types.video.models import VideoData
from mediagoblin.db.sql.open import setup_connection_and_db_from_config as \
sql_connect
from mediagoblin.db.mongo.open import setup_connection_and_db_from_config as \
@@ -49,14 +50,14 @@ def copy_reference_attr(entry, new_entry, ref_attr):
def convert_users(mk_db):
session = Session()
- for entry in mk_db.User.find():
+ for entry in mk_db.User.find().sort('created'):
print entry.username
new_entry = User()
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
@@ -71,15 +72,15 @@ def convert_users(mk_db):
def convert_media_entries(mk_db):
session = Session()
- for entry in mk_db.MediaEntry.find():
+ for entry in mk_db.MediaEntry.find().sort('created'):
print repr(entry.title)
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")
@@ -92,6 +93,15 @@ def convert_media_entries(mk_db):
new_file.media_entry = new_entry.id
Session.add(new_file)
+ for attachment in entry.attachment_files:
+ new_attach = MediaAttachmentFile(
+ name=attachment["name"],
+ filepath=attachment["filepath"],
+ created=attachment["created"]
+ )
+ new_attach.media_entry = new_entry.id
+ Session.add(new_attach)
+
session.commit()
session.close()
@@ -100,7 +110,7 @@ def convert_media_tags(mk_db):
session = Session()
session.autoflush = False
- for media in mk_db.MediaEntry.find():
+ for media in mk_db.MediaEntry.find().sort('created'):
print repr(media.title)
for otag in media.tags:
@@ -127,13 +137,13 @@ def convert_media_tags(mk_db):
def convert_media_comments(mk_db):
session = Session()
- for entry in mk_db.MediaComment.find():
+ for entry in mk_db.MediaComment.find().sort('created'):
print repr(entry.content)
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")
@@ -145,11 +155,10 @@ def convert_media_comments(mk_db):
session.close()
-def main():
- global_config, app_config = setup_global_and_app_config("mediagoblin.ini")
-
- sql_conn, sql_db = sql_connect({'sql_engine': 'sqlite:///mediagoblin.db'})
+def run_conversion(config_name):
+ global_config, app_config = setup_global_and_app_config(config_name)
+ sql_conn, sql_db = sql_connect(app_config)
mk_conn, mk_db = mongo_connect(app_config)
Base.metadata.create_all(sql_db.engine)
@@ -165,4 +174,4 @@ def main():
if __name__ == '__main__':
- main()
+ run_conversion("mediagoblin.ini")
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..dbc9ca05 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)
@@ -113,6 +118,15 @@ class MediaEntry(Base, MediaEntryMixin):
creator=lambda k, v: MediaFile(name=k, file_path=v)
)
+ attachment_files_helper = relationship("MediaAttachmentFile",
+ cascade="all, delete-orphan",
+ order_by="MediaAttachmentFile.created"
+ )
+ attachment_files = association_proxy("attachment_files_helper", "dict_view",
+ creator=lambda v: MediaAttachmentFile(
+ name=v["name"], filepath=v["filepath"])
+ )
+
tags_helper = relationship("MediaTag",
cascade="all, delete-orphan"
)
@@ -122,7 +136,6 @@ class MediaEntry(Base, MediaEntryMixin):
## TODO
# media_data
- # attachment_files
# fail_error
_id = SimpleFieldAlias("id")
@@ -154,6 +167,15 @@ class MediaEntry(Base, MediaEntryMixin):
if media is not None:
return media.url_for_self(urlgen)
+ @property
+ def media_data(self):
+ # TODO: Replace with proper code to read the correct table
+ return {}
+
+ def media_data_init(self, **kwargs):
+ # TODO: Implement this
+ pass
+
class MediaFile(Base):
"""
@@ -172,6 +194,23 @@ class MediaFile(Base):
return "<MediaFile %s: %r>" % (self.name, self.file_path)
+class MediaAttachmentFile(Base):
+ __tablename__ = "core__attachment_files"
+
+ id = Column(Integer, primary_key=True)
+ media_entry = Column(
+ Integer, ForeignKey(MediaEntry.id),
+ nullable=False)
+ name = Column(Unicode, nullable=False)
+ filepath = Column(PathTupleWithSlashes)
+ created = Column(DateTime, nullable=False, default=datetime.datetime.now)
+
+ @property
+ def dict_view(self):
+ """A dict like view on this object"""
+ return DictReadAttrProxy(self)
+
+
class Tag(Base):
__tablename__ = "tags"
@@ -209,10 +248,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 +261,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 +270,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/open.py b/mediagoblin/db/sql/open.py
index 1bfc5538..a8677bcb 100644
--- a/mediagoblin/db/sql/open.py
+++ b/mediagoblin/db/sql/open.py
@@ -36,6 +36,7 @@ class DatabaseMaster(object):
Session.flush()
def reset_after_request(self):
+ Session.rollback()
Session.remove()
diff --git a/mediagoblin/db/sql/util.py b/mediagoblin/db/sql/util.py
new file mode 100644
index 00000000..13bc97e1
--- /dev/null
+++ b/mediagoblin/db/sql/util.py
@@ -0,0 +1,284 @@
+# 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/>.
+
+
+import sys
+from mediagoblin.db.sql.base import Session
+
+
+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__])
+
+
+##########################
+# Random utility functions
+##########################
+
+
+def atomic_update(table, query_dict, update_values):
+ table.find(query_dict).update(update_values,
+ synchronize_session=False)
+ Session.commit()
diff --git a/mediagoblin/db/util.py b/mediagoblin/db/util.py
index 1fc949a6..73aee238 100644
--- a/mediagoblin/db/util.py
+++ b/mediagoblin/db/util.py
@@ -21,5 +21,7 @@ except ImportError:
if use_sql:
from mediagoblin.db.sql.fake import ObjectId, InvalidId, DESCENDING
+ from mediagoblin.db.sql.util import atomic_update
else:
- from mediagoblin.db.mongo.util import ObjectId, InvalidId, DESCENDING
+ from mediagoblin.db.mongo.util import \
+ ObjectId, InvalidId, DESCENDING, atomic_update