| 1 | # GNU MediaGoblin -- federated, autonomous media hosting |
| 2 | # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. |
| 3 | # |
| 4 | # This program is free software: you can redistribute it and/or modify |
| 5 | # it under the terms of the GNU Affero General Public License as published by |
| 6 | # the Free Software Foundation, either version 3 of the License, or |
| 7 | # (at your option) any later version. |
| 8 | # |
| 9 | # This program is distributed in the hope that it will be useful, |
| 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | # GNU Affero General Public License for more details. |
| 13 | # |
| 14 | # You should have received a copy of the GNU Affero General Public License |
| 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 16 | |
| 17 | import datetime |
| 18 | |
| 19 | from sqlalchemy import (MetaData, Table, Column, Boolean, SmallInteger, |
| 20 | Integer, Unicode, UnicodeText, DateTime, |
| 21 | ForeignKey) |
| 22 | from sqlalchemy.exc import ProgrammingError |
| 23 | from sqlalchemy.ext.declarative import declarative_base |
| 24 | from migrate.changeset.constraint import UniqueConstraint |
| 25 | |
| 26 | from mediagoblin.db.migration_tools import RegisterMigration, inspect_table |
| 27 | from mediagoblin.db.models import MediaEntry, Collection, User |
| 28 | |
| 29 | MIGRATIONS = {} |
| 30 | |
| 31 | |
| 32 | @RegisterMigration(1, MIGRATIONS) |
| 33 | def ogg_to_webm_audio(db_conn): |
| 34 | metadata = MetaData(bind=db_conn.bind) |
| 35 | |
| 36 | file_keynames = Table('core__file_keynames', metadata, autoload=True, |
| 37 | autoload_with=db_conn.bind) |
| 38 | |
| 39 | db_conn.execute( |
| 40 | file_keynames.update().where(file_keynames.c.name == 'ogg'). |
| 41 | values(name='webm_audio') |
| 42 | ) |
| 43 | db_conn.commit() |
| 44 | |
| 45 | |
| 46 | @RegisterMigration(2, MIGRATIONS) |
| 47 | def add_wants_notification_column(db_conn): |
| 48 | metadata = MetaData(bind=db_conn.bind) |
| 49 | |
| 50 | users = Table('core__users', metadata, autoload=True, |
| 51 | autoload_with=db_conn.bind) |
| 52 | |
| 53 | col = Column('wants_comment_notification', Boolean, |
| 54 | default=True, nullable=True) |
| 55 | col.create(users, populate_defaults=True) |
| 56 | db_conn.commit() |
| 57 | |
| 58 | |
| 59 | @RegisterMigration(3, MIGRATIONS) |
| 60 | def add_transcoding_progress(db_conn): |
| 61 | metadata = MetaData(bind=db_conn.bind) |
| 62 | |
| 63 | media_entry = inspect_table(metadata, 'core__media_entries') |
| 64 | |
| 65 | col = Column('transcoding_progress', SmallInteger) |
| 66 | col.create(media_entry) |
| 67 | db_conn.commit() |
| 68 | |
| 69 | |
| 70 | class Collection_v0(declarative_base()): |
| 71 | __tablename__ = "core__collections" |
| 72 | |
| 73 | id = Column(Integer, primary_key=True) |
| 74 | title = Column(Unicode, nullable=False) |
| 75 | slug = Column(Unicode) |
| 76 | created = Column(DateTime, nullable=False, default=datetime.datetime.now, |
| 77 | index=True) |
| 78 | description = Column(UnicodeText) |
| 79 | creator = Column(Integer, ForeignKey(User.id), nullable=False) |
| 80 | items = Column(Integer, default=0) |
| 81 | |
| 82 | class CollectionItem_v0(declarative_base()): |
| 83 | __tablename__ = "core__collection_items" |
| 84 | |
| 85 | id = Column(Integer, primary_key=True) |
| 86 | media_entry = Column( |
| 87 | Integer, ForeignKey(MediaEntry.id), nullable=False, index=True) |
| 88 | collection = Column(Integer, ForeignKey(Collection.id), nullable=False) |
| 89 | note = Column(UnicodeText, nullable=True) |
| 90 | added = Column(DateTime, nullable=False, default=datetime.datetime.now) |
| 91 | position = Column(Integer) |
| 92 | |
| 93 | ## This should be activated, normally. |
| 94 | ## But this would change the way the next migration used to work. |
| 95 | ## So it's commented for now. |
| 96 | __table_args__ = ( |
| 97 | UniqueConstraint('collection', 'media_entry'), |
| 98 | {}) |
| 99 | |
| 100 | collectionitem_unique_constraint_done = False |
| 101 | |
| 102 | @RegisterMigration(4, MIGRATIONS) |
| 103 | def add_collection_tables(db_conn): |
| 104 | Collection_v0.__table__.create(db_conn.bind) |
| 105 | CollectionItem_v0.__table__.create(db_conn.bind) |
| 106 | |
| 107 | global collectionitem_unique_constraint_done |
| 108 | collectionitem_unique_constraint_done = True |
| 109 | |
| 110 | db_conn.commit() |
| 111 | |
| 112 | |
| 113 | @RegisterMigration(5, MIGRATIONS) |
| 114 | def add_mediaentry_collected(db_conn): |
| 115 | metadata = MetaData(bind=db_conn.bind) |
| 116 | |
| 117 | media_entry = inspect_table(metadata, 'core__media_entries') |
| 118 | |
| 119 | col = Column('collected', Integer, default=0) |
| 120 | col.create(media_entry) |
| 121 | db_conn.commit() |
| 122 | |
| 123 | |
| 124 | class ProcessingMetaData_v0(declarative_base()): |
| 125 | __tablename__ = 'core__processing_metadata' |
| 126 | |
| 127 | id = Column(Integer, primary_key=True) |
| 128 | media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False, |
| 129 | index=True) |
| 130 | callback_url = Column(Unicode) |
| 131 | |
| 132 | @RegisterMigration(6, MIGRATIONS) |
| 133 | def create_processing_metadata_table(db): |
| 134 | ProcessingMetaData_v0.__table__.create(db.bind) |
| 135 | db.commit() |
| 136 | |
| 137 | |
| 138 | # Okay, problem being: |
| 139 | # Migration #4 forgot to add the uniqueconstraint for the |
| 140 | # new tables. While creating the tables from scratch had |
| 141 | # the constraint enabled. |
| 142 | # |
| 143 | # So we have four situations that should end up at the same |
| 144 | # db layout: |
| 145 | # |
| 146 | # 1. Fresh install. |
| 147 | # Well, easy. Just uses the tables in models.py |
| 148 | # 2. Fresh install using a git version just before this migration |
| 149 | # The tables are all there, the unique constraint is also there. |
| 150 | # This migration should do nothing. |
| 151 | # But as we can't detect the uniqueconstraint easily, |
| 152 | # this migration just adds the constraint again. |
| 153 | # And possibly fails very loud. But ignores the failure. |
| 154 | # 3. old install, not using git, just releases. |
| 155 | # This one will get the new tables in #4 (now with constraint!) |
| 156 | # And this migration is just skipped silently. |
| 157 | # 4. old install, always on latest git. |
| 158 | # This one has the tables, but lacks the constraint. |
| 159 | # So this migration adds the constraint. |
| 160 | @RegisterMigration(7, MIGRATIONS) |
| 161 | def fix_CollectionItem_v0_constraint(db_conn): |
| 162 | """Add the forgotten Constraint on CollectionItem""" |
| 163 | |
| 164 | global collectionitem_unique_constraint_done |
| 165 | if collectionitem_unique_constraint_done: |
| 166 | # Reset it. Maybe the whole thing gets run again |
| 167 | # For a different db? |
| 168 | collectionitem_unique_constraint_done = False |
| 169 | return |
| 170 | |
| 171 | metadata = MetaData(bind=db_conn.bind) |
| 172 | |
| 173 | CollectionItem_table = inspect_table(metadata, 'core__collection_items') |
| 174 | |
| 175 | constraint = UniqueConstraint('collection', 'media_entry', |
| 176 | name='core__collection_items_collection_media_entry_key', |
| 177 | table=CollectionItem_table) |
| 178 | |
| 179 | try: |
| 180 | constraint.create() |
| 181 | except ProgrammingError: |
| 182 | # User probably has an install that was run since the |
| 183 | # collection tables were added, so we don't need to run this migration. |
| 184 | pass |
| 185 | |
| 186 | db_conn.commit() |
| 187 | |
| 188 | |
| 189 | @RegisterMigration(8, MIGRATIONS) |
| 190 | def add_license_preference(db): |
| 191 | metadata = MetaData(bind=db.bind) |
| 192 | |
| 193 | user_table = inspect_table(metadata, 'core__users') |
| 194 | |
| 195 | col = Column('license_preference', Unicode) |
| 196 | col.create(user_table) |
| 197 | db.commit() |
| 198 | |
| 199 | |
| 200 | @RegisterMigration(9, MIGRATIONS) |
| 201 | def mediaentry_new_slug_era(db): |
| 202 | """ |
| 203 | Update for the new era for media type slugs. |
| 204 | |
| 205 | Entries without slugs now display differently in the url like: |
| 206 | /u/cwebber/m/id=251/ |
| 207 | |
| 208 | ... because of this, we should back-convert: |
| 209 | - entries without slugs should be converted to use the id, if possible, to |
| 210 | make old urls still work |
| 211 | - slugs with = (or also : which is now also not allowed) to have those |
| 212 | stripped out (small possibility of breakage here sadly) |
| 213 | """ |
| 214 | import uuid |
| 215 | |
| 216 | def slug_and_user_combo_exists(slug, uploader): |
| 217 | # Technically returns the number of entries with this slug and user |
| 218 | # that already exist |
| 219 | return db.execute( |
| 220 | media_table.select( |
| 221 | media_table.c.uploader==uploader, |
| 222 | media_table.c.slug==slug).count()).first().tbl_row_count |
| 223 | |
| 224 | def append_garbage_till_unique(row, new_slug): |
| 225 | """ |
| 226 | Attach junk to this row until it's unique, then save it |
| 227 | """ |
| 228 | if slug_and_user_combo_exists(new_slug, row.uploader): |
| 229 | # okay, still no success; |
| 230 | # let's whack junk on there till it's unique. |
| 231 | new_slug += '-' + uuid.uuid4().hex[:4] |
| 232 | # keep going if necessary! |
| 233 | while slug_and_user_combo_exists(new_slug, row.uploader): |
| 234 | new_slug += uuid.uuid4().hex[:4] |
| 235 | |
| 236 | db.execute( |
| 237 | media_table.update(). \ |
| 238 | where(media_table.c.id==row.id). \ |
| 239 | values(slug=new_slug)) |
| 240 | |
| 241 | metadata = MetaData(bind=db.bind) |
| 242 | |
| 243 | media_table = inspect_table(metadata, 'core__media_entries') |
| 244 | for row in db.execute(media_table.select()): |
| 245 | # no slug, try setting to an id |
| 246 | if not row.slug: |
| 247 | append_garbage_till_unique(row, unicode(row.id)) |
| 248 | # has "=" or ":" in it... we're getting rid of those |
| 249 | elif u"=" in row.slug or u":" in row.slug: |
| 250 | append_garbage_till_unique( |
| 251 | row, row.slug.replace(u"=", u"-").replace(u":", u"-")) |