This commit was solely to remove unused imports in the code that I have written
[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, Date)
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
29 from mediagoblin.db.extratypes import JSONEncoded
30 from mediagoblin.db.migration_tools import RegisterMigration, inspect_table
31 from mediagoblin.db.models import (MediaEntry, Collection, User,
32                                    MediaComment, Privilege, FOUNDATIONS)
33
34 MIGRATIONS = {}
35
36
37 @RegisterMigration(1, MIGRATIONS)
38 def ogg_to_webm_audio(db_conn):
39     metadata = MetaData(bind=db_conn.bind)
40
41     file_keynames = Table('core__file_keynames', metadata, autoload=True,
42                           autoload_with=db_conn.bind)
43
44     db_conn.execute(
45         file_keynames.update().where(file_keynames.c.name == 'ogg').
46             values(name='webm_audio')
47     )
48     db_conn.commit()
49
50
51 @RegisterMigration(2, MIGRATIONS)
52 def add_wants_notification_column(db_conn):
53     metadata = MetaData(bind=db_conn.bind)
54
55     users = Table('core__users', metadata, autoload=True,
56             autoload_with=db_conn.bind)
57
58     col = Column('wants_comment_notification', Boolean,
59             default=True, nullable=True)
60     col.create(users, populate_defaults=True)
61     db_conn.commit()
62
63
64 @RegisterMigration(3, MIGRATIONS)
65 def add_transcoding_progress(db_conn):
66     metadata = MetaData(bind=db_conn.bind)
67
68     media_entry = inspect_table(metadata, 'core__media_entries')
69
70     col = Column('transcoding_progress', SmallInteger)
71     col.create(media_entry)
72     db_conn.commit()
73
74
75 class Collection_v0(declarative_base()):
76     __tablename__ = "core__collections"
77
78     id = Column(Integer, primary_key=True)
79     title = Column(Unicode, nullable=False)
80     slug = Column(Unicode)
81     created = Column(DateTime, nullable=False, default=datetime.datetime.now,
82         index=True)
83     description = Column(UnicodeText)
84     creator = Column(Integer, ForeignKey(User.id), nullable=False)
85     items = Column(Integer, default=0)
86
87 class CollectionItem_v0(declarative_base()):
88     __tablename__ = "core__collection_items"
89
90     id = Column(Integer, primary_key=True)
91     media_entry = Column(
92         Integer, ForeignKey(MediaEntry.id), nullable=False, index=True)
93     collection = Column(Integer, ForeignKey(Collection.id), nullable=False)
94     note = Column(UnicodeText, nullable=True)
95     added = Column(DateTime, nullable=False, default=datetime.datetime.now)
96     position = Column(Integer)
97
98     ## This should be activated, normally.
99     ## But this would change the way the next migration used to work.
100     ## So it's commented for now.
101     __table_args__ = (
102         UniqueConstraint('collection', 'media_entry'),
103         {})
104
105 collectionitem_unique_constraint_done = False
106
107 @RegisterMigration(4, MIGRATIONS)
108 def add_collection_tables(db_conn):
109     Collection_v0.__table__.create(db_conn.bind)
110     CollectionItem_v0.__table__.create(db_conn.bind)
111
112     global collectionitem_unique_constraint_done
113     collectionitem_unique_constraint_done = True
114
115     db_conn.commit()
116
117
118 @RegisterMigration(5, MIGRATIONS)
119 def add_mediaentry_collected(db_conn):
120     metadata = MetaData(bind=db_conn.bind)
121
122     media_entry = inspect_table(metadata, 'core__media_entries')
123
124     col = Column('collected', Integer, default=0)
125     col.create(media_entry)
126     db_conn.commit()
127
128
129 class ProcessingMetaData_v0(declarative_base()):
130     __tablename__ = 'core__processing_metadata'
131
132     id = Column(Integer, primary_key=True)
133     media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False,
134             index=True)
135     callback_url = Column(Unicode)
136
137 @RegisterMigration(6, MIGRATIONS)
138 def create_processing_metadata_table(db):
139     ProcessingMetaData_v0.__table__.create(db.bind)
140     db.commit()
141
142
143 # Okay, problem being:
144 #  Migration #4 forgot to add the uniqueconstraint for the
145 #  new tables. While creating the tables from scratch had
146 #  the constraint enabled.
147 #
148 # So we have four situations that should end up at the same
149 # db layout:
150 #
151 # 1. Fresh install.
152 #    Well, easy. Just uses the tables in models.py
153 # 2. Fresh install using a git version just before this migration
154 #    The tables are all there, the unique constraint is also there.
155 #    This migration should do nothing.
156 #    But as we can't detect the uniqueconstraint easily,
157 #    this migration just adds the constraint again.
158 #    And possibly fails very loud. But ignores the failure.
159 # 3. old install, not using git, just releases.
160 #    This one will get the new tables in #4 (now with constraint!)
161 #    And this migration is just skipped silently.
162 # 4. old install, always on latest git.
163 #    This one has the tables, but lacks the constraint.
164 #    So this migration adds the constraint.
165 @RegisterMigration(7, MIGRATIONS)
166 def fix_CollectionItem_v0_constraint(db_conn):
167     """Add the forgotten Constraint on CollectionItem"""
168
169     global collectionitem_unique_constraint_done
170     if collectionitem_unique_constraint_done:
171         # Reset it. Maybe the whole thing gets run again
172         # For a different db?
173         collectionitem_unique_constraint_done = False
174         return
175
176     metadata = MetaData(bind=db_conn.bind)
177
178     CollectionItem_table = inspect_table(metadata, 'core__collection_items')
179
180     constraint = UniqueConstraint('collection', 'media_entry',
181         name='core__collection_items_collection_media_entry_key',
182         table=CollectionItem_table)
183
184     try:
185         constraint.create()
186     except ProgrammingError:
187         # User probably has an install that was run since the
188         # collection tables were added, so we don't need to run this migration.
189         pass
190
191     db_conn.commit()
192
193
194 @RegisterMigration(8, MIGRATIONS)
195 def add_license_preference(db):
196     metadata = MetaData(bind=db.bind)
197
198     user_table = inspect_table(metadata, 'core__users')
199
200     col = Column('license_preference', Unicode)
201     col.create(user_table)
202     db.commit()
203
204
205 @RegisterMigration(9, MIGRATIONS)
206 def mediaentry_new_slug_era(db):
207     """
208     Update for the new era for media type slugs.
209
210     Entries without slugs now display differently in the url like:
211       /u/cwebber/m/id=251/
212
213     ... because of this, we should back-convert:
214      - entries without slugs should be converted to use the id, if possible, to
215        make old urls still work
216      - slugs with = (or also : which is now also not allowed) to have those
217        stripped out (small possibility of breakage here sadly)
218     """
219
220     def slug_and_user_combo_exists(slug, uploader):
221         return db.execute(
222             media_table.select(
223                 and_(media_table.c.uploader==uploader,
224                      media_table.c.slug==slug))).first() is not None
225
226     def append_garbage_till_unique(row, new_slug):
227         """
228         Attach junk to this row until it's unique, then save it
229         """
230         if slug_and_user_combo_exists(new_slug, row.uploader):
231             # okay, still no success;
232             # let's whack junk on there till it's unique.
233             new_slug += '-' + uuid.uuid4().hex[:4]
234             # keep going if necessary!
235             while slug_and_user_combo_exists(new_slug, row.uploader):
236                 new_slug += uuid.uuid4().hex[:4]
237
238         db.execute(
239             media_table.update(). \
240             where(media_table.c.id==row.id). \
241             values(slug=new_slug))
242
243     metadata = MetaData(bind=db.bind)
244
245     media_table = inspect_table(metadata, 'core__media_entries')
246
247     for row in db.execute(media_table.select()):
248         # no slug, try setting to an id
249         if not row.slug:
250             append_garbage_till_unique(row, unicode(row.id))
251         # has "=" or ":" in it... we're getting rid of those
252         elif u"=" in row.slug or u":" in row.slug:
253             append_garbage_till_unique(
254                 row, row.slug.replace(u"=", u"-").replace(u":", u"-"))
255
256     db.commit()
257
258
259 @RegisterMigration(10, MIGRATIONS)
260 def unique_collections_slug(db):
261     """Add unique constraint to collection slug"""
262     metadata = MetaData(bind=db.bind)
263     collection_table = inspect_table(metadata, "core__collections")
264     existing_slugs = {}
265     slugs_to_change = []
266
267     for row in db.execute(collection_table.select()):
268         # if duplicate slug, generate a unique slug
269         if row.creator in existing_slugs and row.slug in \
270            existing_slugs[row.creator]:
271             slugs_to_change.append(row.id)
272         else:
273             if not row.creator in existing_slugs:
274                 existing_slugs[row.creator] = [row.slug]
275             else:
276                 existing_slugs[row.creator].append(row.slug)
277
278     for row_id in slugs_to_change:
279         new_slug = unicode(uuid.uuid4())
280         db.execute(collection_table.update().
281                    where(collection_table.c.id == row_id).
282                    values(slug=new_slug))
283     # sqlite does not like to change the schema when a transaction(update) is
284     # not yet completed
285     db.commit()
286
287     constraint = UniqueConstraint('creator', 'slug',
288                                   name='core__collection_creator_slug_key',
289                                   table=collection_table)
290     constraint.create()
291
292     db.commit()
293
294 @RegisterMigration(11, MIGRATIONS)
295 def drop_token_related_User_columns(db):
296     """
297     Drop unneeded columns from the User table after switching to using
298     itsdangerous tokens for email and forgot password verification.
299     """
300     metadata = MetaData(bind=db.bind)
301     user_table = inspect_table(metadata, 'core__users')
302
303     verification_key = user_table.columns['verification_key']
304     fp_verification_key = user_table.columns['fp_verification_key']
305     fp_token_expire = user_table.columns['fp_token_expire']
306
307     verification_key.drop()
308     fp_verification_key.drop()
309     fp_token_expire.drop()
310
311     db.commit()
312
313
314 class CommentSubscription_v0(declarative_base()):
315     __tablename__ = 'core__comment_subscriptions'
316     id = Column(Integer, primary_key=True)
317
318     created = Column(DateTime, nullable=False, default=datetime.datetime.now)
319
320     media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False)
321
322     user_id = Column(Integer, ForeignKey(User.id), nullable=False)
323
324     notify = Column(Boolean, nullable=False, default=True)
325     send_email = Column(Boolean, nullable=False, default=True)
326
327
328 class Notification_v0(declarative_base()):
329     __tablename__ = 'core__notifications'
330     id = Column(Integer, primary_key=True)
331     type = Column(Unicode)
332
333     created = Column(DateTime, nullable=False, default=datetime.datetime.now)
334
335     user_id = Column(Integer, ForeignKey(User.id), nullable=False,
336                      index=True)
337     seen = Column(Boolean, default=lambda: False, index=True)
338
339
340 class CommentNotification_v0(Notification_v0):
341     __tablename__ = 'core__comment_notifications'
342     id = Column(Integer, ForeignKey(Notification_v0.id), primary_key=True)
343
344     subject_id = Column(Integer, ForeignKey(MediaComment.id))
345
346
347 class ProcessingNotification_v0(Notification_v0):
348     __tablename__ = 'core__processing_notifications'
349
350     id = Column(Integer, ForeignKey(Notification_v0.id), primary_key=True)
351
352     subject_id = Column(Integer, ForeignKey(MediaEntry.id))
353
354
355 @RegisterMigration(12, MIGRATIONS)
356 def add_new_notification_tables(db):
357     metadata = MetaData(bind=db.bind)
358
359     user_table = inspect_table(metadata, 'core__users')
360     mediaentry_table = inspect_table(metadata, 'core__media_entries')
361     mediacomment_table = inspect_table(metadata, 'core__media_comments')
362
363     CommentSubscription_v0.__table__.create(db.bind)
364
365     Notification_v0.__table__.create(db.bind)
366     CommentNotification_v0.__table__.create(db.bind)
367     ProcessingNotification_v0.__table__.create(db.bind)
368
369     db.commit()
370
371
372 @RegisterMigration(13, MIGRATIONS)
373 def pw_hash_nullable(db):
374     """Make pw_hash column nullable"""
375     metadata = MetaData(bind=db.bind)
376     user_table = inspect_table(metadata, "core__users")
377
378     user_table.c.pw_hash.alter(nullable=True)
379
380     # sqlite+sqlalchemy seems to drop this constraint during the
381     # migration, so we add it back here for now a bit manually.
382     if db.bind.url.drivername == 'sqlite':
383         constraint = UniqueConstraint('username', table=user_table)
384         constraint.create()
385
386     db.commit()
387
388
389 # oauth1 migrations
390 class Client_v0(declarative_base()):
391     """
392         Model representing a client - Used for API Auth
393     """
394     __tablename__ = "core__clients"
395
396     id = Column(Unicode, nullable=True, primary_key=True)
397     secret = Column(Unicode, nullable=False)
398     expirey = Column(DateTime, nullable=True)
399     application_type = Column(Unicode, nullable=False)
400     created = Column(DateTime, nullable=False, default=datetime.datetime.now)
401     updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
402
403     # optional stuff
404     redirect_uri = Column(JSONEncoded, nullable=True)
405     logo_url = Column(Unicode, nullable=True)
406     application_name = Column(Unicode, nullable=True)
407     contacts = Column(JSONEncoded, nullable=True)
408
409     def __repr__(self):
410         if self.application_name:
411             return "<Client {0} - {1}>".format(self.application_name, self.id)
412         else:
413             return "<Client {0}>".format(self.id)
414
415 class RequestToken_v0(declarative_base()):
416     """
417         Model for representing the request tokens
418     """
419     __tablename__ = "core__request_tokens"
420
421     token = Column(Unicode, primary_key=True)
422     secret = Column(Unicode, nullable=False)
423     client = Column(Unicode, ForeignKey(Client_v0.id))
424     user = Column(Integer, ForeignKey(User.id), nullable=True)
425     used = Column(Boolean, default=False)
426     authenticated = Column(Boolean, default=False)
427     verifier = Column(Unicode, nullable=True)
428     callback = Column(Unicode, nullable=False, default=u"oob")
429     created = Column(DateTime, nullable=False, default=datetime.datetime.now)
430     updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
431
432 class AccessToken_v0(declarative_base()):
433     """
434         Model for representing the access tokens
435     """
436     __tablename__ = "core__access_tokens"
437
438     token = Column(Unicode, nullable=False, primary_key=True)
439     secret = Column(Unicode, nullable=False)
440     user = Column(Integer, ForeignKey(User.id))
441     request_token = Column(Unicode, ForeignKey(RequestToken_v0.token))
442     created = Column(DateTime, nullable=False, default=datetime.datetime.now)
443     updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
444
445
446 class NonceTimestamp_v0(declarative_base()):
447     """
448         A place the timestamp and nonce can be stored - this is for OAuth1
449     """
450     __tablename__ = "core__nonce_timestamps"
451
452     nonce = Column(Unicode, nullable=False, primary_key=True)
453     timestamp = Column(DateTime, nullable=False, primary_key=True)
454
455
456 @RegisterMigration(14, MIGRATIONS)
457 def create_oauth1_tables(db):
458     """ Creates the OAuth1 tables """
459
460     Client_v0.__table__.create(db.bind)
461     RequestToken_v0.__table__.create(db.bind)
462     AccessToken_v0.__table__.create(db.bind)
463     NonceTimestamp_v0.__table__.create(db.bind)
464
465     db.commit()
466
467
468 @RegisterMigration(15, MIGRATIONS)
469 def wants_notifications(db):
470     """Add a wants_notifications field to User model"""
471     metadata = MetaData(bind=db.bind)
472     user_table = inspect_table(metadata, "core__users")
473     col = Column('wants_notifications', Boolean, default=True)
474     col.create(user_table)
475     db.commit()
476
477 class ReportBase_v0(declarative_base()):
478     __tablename__ = 'core__reports'
479     id = Column(Integer, primary_key=True)
480     reporter_id = Column(Integer, ForeignKey(User.id), nullable=False)
481     report_content = Column(UnicodeText)
482     reported_user_id = Column(Integer, ForeignKey(User.id), nullable=False)
483     created = Column(DateTime, nullable=False, default=datetime.datetime.now)
484     discriminator = Column('type', Unicode(50))
485     resolver_id = Column(Integer, ForeignKey(User.id))
486     resolved = Column(DateTime)
487     result = Column(UnicodeText)
488     __mapper_args__ = {'polymorphic_on': discriminator}
489
490 class CommentReport_v0(ReportBase_v0):
491     __tablename__ = 'core__reports_on_comments'
492     __mapper_args__ = {'polymorphic_identity': 'comment_report'}
493
494     id = Column('id',Integer, ForeignKey('core__reports.id'),
495                                                 primary_key=True)
496     comment_id = Column(Integer, ForeignKey(MediaComment.id), nullable=True)
497
498
499
500 class MediaReport_v0(ReportBase_v0):
501     __tablename__ = 'core__reports_on_media'
502     __mapper_args__ = {'polymorphic_identity': 'media_report'}
503
504     id = Column('id',Integer, ForeignKey('core__reports.id'), primary_key=True)
505     media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=True)
506
507 class UserBan_v0(declarative_base()):
508     __tablename__ = 'core__user_bans'
509     user_id = Column('id',Integer, ForeignKey(User.id), nullable=False,
510                                          primary_key=True)
511     expiration_date = Column(Date)
512     reason = Column(UnicodeText, nullable=False)
513
514 class Privilege_v0(declarative_base()):
515     __tablename__ = 'core__privileges'
516     id = Column(Integer, nullable=False, primary_key=True, unique=True)
517     privilege_name = Column(Unicode, nullable=False, unique=True)
518
519 class PrivilegeUserAssociation_v0(declarative_base()):
520     __tablename__ = 'core__privileges_users'
521     group_id = Column(
522         'core__privilege_id',
523         Integer,
524         ForeignKey(User.id),
525         primary_key=True)
526     user_id = Column(
527         'core__user_id',
528         Integer,
529         ForeignKey(Privilege.id),
530         primary_key=True)
531
532 @RegisterMigration(16, MIGRATIONS)
533 def create_moderation_tables(db):
534     ReportBase_v0.__table__.create(db.bind)
535     CommentReport_v0.__table__.create(db.bind)
536     MediaReport_v0.__table__.create(db.bind)
537     UserBan_v0.__table__.create(db.bind)
538     Privilege_v0.__table__.create(db.bind)
539     PrivilegeUserAssociation_v0.__table__.create(db.bind)
540
541     db.commit()
542
543     for parameters in FOUNDATIONS[Privilege]:
544         p = Privilege(**parameters)
545         p.save()
546
547
548 @RegisterMigration(17, MIGRATIONS)
549 def update_user_privilege_columns(db):
550     # first, create the privileges which would be created by foundations
551     default_privileges = Privilege.query.filter(
552         Privilege.privilege_name !=u'admin').filter(
553         Privilege.privilege_name !=u'moderator').filter(
554         Privilege.privilege_name !=u'active').all()
555     admin_privilege = Privilege.query.filter(
556         Privilege.privilege_name ==u'admin').first()
557     active_privilege = Privilege.query.filter(
558         Privilege.privilege_name ==u'active').first()
559     # then, assign them to the appropriate users
560     for inactive_user in User.query.filter(
561         User.status!=u'active').filter(
562         User.is_admin==False).all():
563
564         inactive_user.all_privileges = default_privileges
565         inactive_user.save()
566     for user in User.query.filter(
567         User.status==u'active').filter(
568         User.is_admin==False).all():
569
570         user.all_privileges = default_privileges + [active_privilege]
571         user.save()
572     for admin_user in User.query.filter(
573         User.is_admin==True).all():
574
575         admin_user.all_privileges = default_privileges + [
576             admin_privilege, active_privilege]
577         admin_user.save()
578
579     # and then drop the now vestigial status column
580     metadata = MetaData(bind=db.bind)
581     user_table = inspect_table(metadata, 'core__users')
582     status = user_table.columns['status']
583     status.drop()
584     db.commit()
585