Whew. This is a big update. I did some significant keeping work. I moved all of
[mediagoblin:mediagoblin.git] / mediagoblin / db / migrations.py
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 import uuid
19
20 from sqlalchemy import (MetaData, Table, Column, Boolean, SmallInteger,
21                         Integer, Unicode, UnicodeText, DateTime,
22                         ForeignKey)
23 from sqlalchemy.exc import ProgrammingError
24 from sqlalchemy.ext.declarative import declarative_base
25 from sqlalchemy.sql import and_
26 from migrate.changeset.constraint import UniqueConstraint
27
28 from mediagoblin.db.migration_tools import RegisterMigration, inspect_table
29 from mediagoblin.db.models import (MediaEntry, Collection, User, 
30                                    MediaComment, Privilege, ReportBase)
31
32 MIGRATIONS = {}
33
34
35 @RegisterMigration(1, MIGRATIONS)
36 def ogg_to_webm_audio(db_conn):
37     metadata = MetaData(bind=db_conn.bind)
38
39     file_keynames = Table('core__file_keynames', metadata, autoload=True,
40                           autoload_with=db_conn.bind)
41
42     db_conn.execute(
43         file_keynames.update().where(file_keynames.c.name == 'ogg').
44             values(name='webm_audio')
45     )
46     db_conn.commit()
47
48
49 @RegisterMigration(2, MIGRATIONS)
50 def add_wants_notification_column(db_conn):
51     metadata = MetaData(bind=db_conn.bind)
52
53     users = Table('core__users', metadata, autoload=True,
54             autoload_with=db_conn.bind)
55
56     col = Column('wants_comment_notification', Boolean,
57             default=True, nullable=True)
58     col.create(users, populate_defaults=True)
59     db_conn.commit()
60
61
62 @RegisterMigration(3, MIGRATIONS)
63 def add_transcoding_progress(db_conn):
64     metadata = MetaData(bind=db_conn.bind)
65
66     media_entry = inspect_table(metadata, 'core__media_entries')
67
68     col = Column('transcoding_progress', SmallInteger)
69     col.create(media_entry)
70     db_conn.commit()
71
72
73 class Collection_v0(declarative_base()):
74     __tablename__ = "core__collections"
75
76     id = Column(Integer, primary_key=True)
77     title = Column(Unicode, nullable=False)
78     slug = Column(Unicode)
79     created = Column(DateTime, nullable=False, default=datetime.datetime.now,
80         index=True)
81     description = Column(UnicodeText)
82     creator = Column(Integer, ForeignKey(User.id), nullable=False)
83     items = Column(Integer, default=0)
84
85 class CollectionItem_v0(declarative_base()):
86     __tablename__ = "core__collection_items"
87
88     id = Column(Integer, primary_key=True)
89     media_entry = Column(
90         Integer, ForeignKey(MediaEntry.id), nullable=False, index=True)
91     collection = Column(Integer, ForeignKey(Collection.id), nullable=False)
92     note = Column(UnicodeText, nullable=True)
93     added = Column(DateTime, nullable=False, default=datetime.datetime.now)
94     position = Column(Integer)
95
96     ## This should be activated, normally.
97     ## But this would change the way the next migration used to work.
98     ## So it's commented for now.
99     __table_args__ = (
100         UniqueConstraint('collection', 'media_entry'),
101         {})
102
103 collectionitem_unique_constraint_done = False
104
105 @RegisterMigration(4, MIGRATIONS)
106 def add_collection_tables(db_conn):
107     Collection_v0.__table__.create(db_conn.bind)
108     CollectionItem_v0.__table__.create(db_conn.bind)
109
110     global collectionitem_unique_constraint_done
111     collectionitem_unique_constraint_done = True
112
113     db_conn.commit()
114
115
116 @RegisterMigration(5, MIGRATIONS)
117 def add_mediaentry_collected(db_conn):
118     metadata = MetaData(bind=db_conn.bind)
119
120     media_entry = inspect_table(metadata, 'core__media_entries')
121
122     col = Column('collected', Integer, default=0)
123     col.create(media_entry)
124     db_conn.commit()
125
126
127 class ProcessingMetaData_v0(declarative_base()):
128     __tablename__ = 'core__processing_metadata'
129
130     id = Column(Integer, primary_key=True)
131     media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False,
132             index=True)
133     callback_url = Column(Unicode)
134
135 @RegisterMigration(6, MIGRATIONS)
136 def create_processing_metadata_table(db):
137     ProcessingMetaData_v0.__table__.create(db.bind)
138     db.commit()
139
140
141 # Okay, problem being:
142 #  Migration #4 forgot to add the uniqueconstraint for the
143 #  new tables. While creating the tables from scratch had
144 #  the constraint enabled.
145 #
146 # So we have four situations that should end up at the same
147 # db layout:
148 #
149 # 1. Fresh install.
150 #    Well, easy. Just uses the tables in models.py
151 # 2. Fresh install using a git version just before this migration
152 #    The tables are all there, the unique constraint is also there.
153 #    This migration should do nothing.
154 #    But as we can't detect the uniqueconstraint easily,
155 #    this migration just adds the constraint again.
156 #    And possibly fails very loud. But ignores the failure.
157 # 3. old install, not using git, just releases.
158 #    This one will get the new tables in #4 (now with constraint!)
159 #    And this migration is just skipped silently.
160 # 4. old install, always on latest git.
161 #    This one has the tables, but lacks the constraint.
162 #    So this migration adds the constraint.
163 @RegisterMigration(7, MIGRATIONS)
164 def fix_CollectionItem_v0_constraint(db_conn):
165     """Add the forgotten Constraint on CollectionItem"""
166
167     global collectionitem_unique_constraint_done
168     if collectionitem_unique_constraint_done:
169         # Reset it. Maybe the whole thing gets run again
170         # For a different db?
171         collectionitem_unique_constraint_done = False
172         return
173
174     metadata = MetaData(bind=db_conn.bind)
175
176     CollectionItem_table = inspect_table(metadata, 'core__collection_items')
177
178     constraint = UniqueConstraint('collection', 'media_entry',
179         name='core__collection_items_collection_media_entry_key',
180         table=CollectionItem_table)
181
182     try:
183         constraint.create()
184     except ProgrammingError:
185         # User probably has an install that was run since the
186         # collection tables were added, so we don't need to run this migration.
187         pass
188
189     db_conn.commit()
190
191
192 @RegisterMigration(8, MIGRATIONS)
193 def add_license_preference(db):
194     metadata = MetaData(bind=db.bind)
195
196     user_table = inspect_table(metadata, 'core__users')
197
198     col = Column('license_preference', Unicode)
199     col.create(user_table)
200     db.commit()
201
202
203 @RegisterMigration(9, MIGRATIONS)
204 def mediaentry_new_slug_era(db):
205     """
206     Update for the new era for media type slugs.
207
208     Entries without slugs now display differently in the url like:
209       /u/cwebber/m/id=251/
210
211     ... because of this, we should back-convert:
212      - entries without slugs should be converted to use the id, if possible, to
213        make old urls still work
214      - slugs with = (or also : which is now also not allowed) to have those
215        stripped out (small possibility of breakage here sadly)
216     """
217
218     def slug_and_user_combo_exists(slug, uploader):
219         return db.execute(
220             media_table.select(
221                 and_(media_table.c.uploader==uploader,
222                      media_table.c.slug==slug))).first() is not None
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
245     for row in db.execute(media_table.select()):
246         # no slug, try setting to an id
247         if not row.slug:
248             append_garbage_till_unique(row, unicode(row.id))
249         # has "=" or ":" in it... we're getting rid of those
250         elif u"=" in row.slug or u":" in row.slug:
251             append_garbage_till_unique(
252                 row, row.slug.replace(u"=", u"-").replace(u":", u"-"))
253
254     db.commit()
255
256
257 @RegisterMigration(10, MIGRATIONS)
258 def unique_collections_slug(db):
259     """Add unique constraint to collection slug"""
260     metadata = MetaData(bind=db.bind)
261     collection_table = inspect_table(metadata, "core__collections")
262     existing_slugs = {}
263     slugs_to_change = []
264
265     for row in db.execute(collection_table.select()):
266         # if duplicate slug, generate a unique slug
267         if row.creator in existing_slugs and row.slug in \
268            existing_slugs[row.creator]:
269             slugs_to_change.append(row.id)
270         else:
271             if not row.creator in existing_slugs:
272                 existing_slugs[row.creator] = [row.slug]
273             else:
274                 existing_slugs[row.creator].append(row.slug)
275
276     for row_id in slugs_to_change:
277         new_slug = unicode(uuid.uuid4())
278         db.execute(collection_table.update().
279                    where(collection_table.c.id == row_id).
280                    values(slug=new_slug))
281     # sqlite does not like to change the schema when a transaction(update) is
282     # not yet completed
283     db.commit()
284
285     constraint = UniqueConstraint('creator', 'slug',
286                                   name='core__collection_creator_slug_key',
287                                   table=collection_table)
288     constraint.create()
289
290     db.commit()
291
292 class ReportBase_v0(declarative_base()):
293     """
294
295     """
296     __tablename__ = 'core__reports'
297     id = Column(Integer, primary_key=True)
298     reporter_id = Column(Integer, ForeignKey(User.id), nullable=False)
299     report_content = Column(UnicodeText)
300     reported_user_id = Column(Integer, ForeignKey(User.id), nullable=False)
301     created = Column(DateTime, nullable=False, default=datetime.datetime.now) 
302     resolved = Column(DateTime)
303     result = Column(UnicodeText)
304     discriminator = Column('type', Unicode(50))
305     __mapper_args__ = {'polymorphic_on': discriminator}
306
307
308 class CommentReport_v0(ReportBase_v0):
309     __tablename__ = 'core__reports_on_comments'
310     __mapper_args__ = {'polymorphic_identity': 'comment_report'}
311
312     id = Column('id',Integer, ForeignKey('core__reports.id'),
313                                                 primary_key=True)
314     comment_id = Column(Integer, ForeignKey(MediaComment.id), nullable=False)
315
316 class MediaReport_v0(ReportBase_v0):
317     __tablename__ = 'core__reports_on_media'
318     __mapper_args__ = {'polymorphic_identity': 'media_report'}
319
320     id = Column(
321         'id',
322         Integer, 
323         ForeignKey('core__reports.id'),
324         primary_key=True)
325     media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False)
326
327
328 class UserBan_v0(declarative_base()):
329     __tablename__ = 'core__user_bans'
330     user_id = Column('id',Integer, ForeignKey(User.id), nullable=False,
331                                          primary_key=True)
332     expiration_date = Column(DateTime)
333     reason = Column(UnicodeText, nullable=False)
334
335 class Privilege_v0(declarative_base()):
336     __tablename__ = 'core__privileges'
337     id = Column(Integer, nullable=False, primary_key=True, unique=True)
338     privilege_name = Column(Unicode, nullable=False)
339
340 class PrivilegeUserAssociation_v0(declarative_base()):
341     __tablename__ = 'core__privileges_users'
342
343     group_id = Column(
344         'core__privilege_id', 
345         Integer, 
346         ForeignKey(User.id), 
347         primary_key=True)
348     user_id = Column(
349         'core__user_id', 
350         Integer, 
351         ForeignKey(Privilege.id), 
352         primary_key=True)
353
354 @RegisterMigration(11, MIGRATIONS)
355 def create_moderation_tables(db):
356     ReportBase_v0.__table__.create(db.bind)
357     CommentReport_v0.__table__.create(db.bind)
358     MediaReport_v0.__table__.create(db.bind)
359     UserBan_v0.__table__.create(db.bind)
360     Privilege_v0.__table__.create(db.bind)
361     PrivilegeUserAssociation_v0.__table__.create(db.bind)
362     db.commit()