Merge branch 'merge-python3-port'
[mediagoblin.git] / mediagoblin / db / migrations.py
... / ...
CommitLineData
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
17import datetime
18import uuid
19
20import six
21
22from sqlalchemy import (MetaData, Table, Column, Boolean, SmallInteger,
23 Integer, Unicode, UnicodeText, DateTime,
24 ForeignKey, Date, Index)
25from sqlalchemy.exc import ProgrammingError
26from sqlalchemy.ext.declarative import declarative_base
27from sqlalchemy.sql import and_
28from sqlalchemy.schema import UniqueConstraint
29
30from mediagoblin.db.extratypes import JSONEncoded, MutationDict
31from mediagoblin.db.migration_tools import (
32 RegisterMigration, inspect_table, replace_table_hack)
33from mediagoblin.db.models import (MediaEntry, Collection, MediaComment, User,
34 Privilege)
35from mediagoblin.db.extratypes import JSONEncoded, MutationDict
36
37
38MIGRATIONS = {}
39
40
41@RegisterMigration(1, MIGRATIONS)
42def ogg_to_webm_audio(db_conn):
43 metadata = MetaData(bind=db_conn.bind)
44
45 file_keynames = Table('core__file_keynames', metadata, autoload=True,
46 autoload_with=db_conn.bind)
47
48 db_conn.execute(
49 file_keynames.update().where(file_keynames.c.name == 'ogg').
50 values(name='webm_audio')
51 )
52 db_conn.commit()
53
54
55@RegisterMigration(2, MIGRATIONS)
56def add_wants_notification_column(db_conn):
57 metadata = MetaData(bind=db_conn.bind)
58
59 users = Table('core__users', metadata, autoload=True,
60 autoload_with=db_conn.bind)
61
62 col = Column('wants_comment_notification', Boolean,
63 default=True, nullable=True)
64 col.create(users, populate_defaults=True)
65 db_conn.commit()
66
67
68@RegisterMigration(3, MIGRATIONS)
69def add_transcoding_progress(db_conn):
70 metadata = MetaData(bind=db_conn.bind)
71
72 media_entry = inspect_table(metadata, 'core__media_entries')
73
74 col = Column('transcoding_progress', SmallInteger)
75 col.create(media_entry)
76 db_conn.commit()
77
78
79class Collection_v0(declarative_base()):
80 __tablename__ = "core__collections"
81
82 id = Column(Integer, primary_key=True)
83 title = Column(Unicode, nullable=False)
84 slug = Column(Unicode)
85 created = Column(DateTime, nullable=False, default=datetime.datetime.now,
86 index=True)
87 description = Column(UnicodeText)
88 creator = Column(Integer, ForeignKey(User.id), nullable=False)
89 items = Column(Integer, default=0)
90
91class CollectionItem_v0(declarative_base()):
92 __tablename__ = "core__collection_items"
93
94 id = Column(Integer, primary_key=True)
95 media_entry = Column(
96 Integer, ForeignKey(MediaEntry.id), nullable=False, index=True)
97 collection = Column(Integer, ForeignKey(Collection.id), nullable=False)
98 note = Column(UnicodeText, nullable=True)
99 added = Column(DateTime, nullable=False, default=datetime.datetime.now)
100 position = Column(Integer)
101
102 ## This should be activated, normally.
103 ## But this would change the way the next migration used to work.
104 ## So it's commented for now.
105 __table_args__ = (
106 UniqueConstraint('collection', 'media_entry'),
107 {})
108
109collectionitem_unique_constraint_done = False
110
111@RegisterMigration(4, MIGRATIONS)
112def add_collection_tables(db_conn):
113 Collection_v0.__table__.create(db_conn.bind)
114 CollectionItem_v0.__table__.create(db_conn.bind)
115
116 global collectionitem_unique_constraint_done
117 collectionitem_unique_constraint_done = True
118
119 db_conn.commit()
120
121
122@RegisterMigration(5, MIGRATIONS)
123def add_mediaentry_collected(db_conn):
124 metadata = MetaData(bind=db_conn.bind)
125
126 media_entry = inspect_table(metadata, 'core__media_entries')
127
128 col = Column('collected', Integer, default=0)
129 col.create(media_entry)
130 db_conn.commit()
131
132
133class ProcessingMetaData_v0(declarative_base()):
134 __tablename__ = 'core__processing_metadata'
135
136 id = Column(Integer, primary_key=True)
137 media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False,
138 index=True)
139 callback_url = Column(Unicode)
140
141@RegisterMigration(6, MIGRATIONS)
142def create_processing_metadata_table(db):
143 ProcessingMetaData_v0.__table__.create(db.bind)
144 db.commit()
145
146
147# Okay, problem being:
148# Migration #4 forgot to add the uniqueconstraint for the
149# new tables. While creating the tables from scratch had
150# the constraint enabled.
151#
152# So we have four situations that should end up at the same
153# db layout:
154#
155# 1. Fresh install.
156# Well, easy. Just uses the tables in models.py
157# 2. Fresh install using a git version just before this migration
158# The tables are all there, the unique constraint is also there.
159# This migration should do nothing.
160# But as we can't detect the uniqueconstraint easily,
161# this migration just adds the constraint again.
162# And possibly fails very loud. But ignores the failure.
163# 3. old install, not using git, just releases.
164# This one will get the new tables in #4 (now with constraint!)
165# And this migration is just skipped silently.
166# 4. old install, always on latest git.
167# This one has the tables, but lacks the constraint.
168# So this migration adds the constraint.
169@RegisterMigration(7, MIGRATIONS)
170def fix_CollectionItem_v0_constraint(db_conn):
171 """Add the forgotten Constraint on CollectionItem"""
172
173 global collectionitem_unique_constraint_done
174 if collectionitem_unique_constraint_done:
175 # Reset it. Maybe the whole thing gets run again
176 # For a different db?
177 collectionitem_unique_constraint_done = False
178 return
179
180 metadata = MetaData(bind=db_conn.bind)
181
182 CollectionItem_table = inspect_table(metadata, 'core__collection_items')
183
184 constraint = UniqueConstraint('collection', 'media_entry',
185 name='core__collection_items_collection_media_entry_key',
186 table=CollectionItem_table)
187
188 try:
189 constraint.create()
190 except ProgrammingError:
191 # User probably has an install that was run since the
192 # collection tables were added, so we don't need to run this migration.
193 pass
194
195 db_conn.commit()
196
197
198@RegisterMigration(8, MIGRATIONS)
199def add_license_preference(db):
200 metadata = MetaData(bind=db.bind)
201
202 user_table = inspect_table(metadata, 'core__users')
203
204 col = Column('license_preference', Unicode)
205 col.create(user_table)
206 db.commit()
207
208
209@RegisterMigration(9, MIGRATIONS)
210def mediaentry_new_slug_era(db):
211 """
212 Update for the new era for media type slugs.
213
214 Entries without slugs now display differently in the url like:
215 /u/cwebber/m/id=251/
216
217 ... because of this, we should back-convert:
218 - entries without slugs should be converted to use the id, if possible, to
219 make old urls still work
220 - slugs with = (or also : which is now also not allowed) to have those
221 stripped out (small possibility of breakage here sadly)
222 """
223
224 def slug_and_user_combo_exists(slug, uploader):
225 return db.execute(
226 media_table.select(
227 and_(media_table.c.uploader==uploader,
228 media_table.c.slug==slug))).first() is not None
229
230 def append_garbage_till_unique(row, new_slug):
231 """
232 Attach junk to this row until it's unique, then save it
233 """
234 if slug_and_user_combo_exists(new_slug, row.uploader):
235 # okay, still no success;
236 # let's whack junk on there till it's unique.
237 new_slug += '-' + uuid.uuid4().hex[:4]
238 # keep going if necessary!
239 while slug_and_user_combo_exists(new_slug, row.uploader):
240 new_slug += uuid.uuid4().hex[:4]
241
242 db.execute(
243 media_table.update(). \
244 where(media_table.c.id==row.id). \
245 values(slug=new_slug))
246
247 metadata = MetaData(bind=db.bind)
248
249 media_table = inspect_table(metadata, 'core__media_entries')
250
251 for row in db.execute(media_table.select()):
252 # no slug, try setting to an id
253 if not row.slug:
254 append_garbage_till_unique(row, six.text_type(row.id))
255 # has "=" or ":" in it... we're getting rid of those
256 elif u"=" in row.slug or u":" in row.slug:
257 append_garbage_till_unique(
258 row, row.slug.replace(u"=", u"-").replace(u":", u"-"))
259
260 db.commit()
261
262
263@RegisterMigration(10, MIGRATIONS)
264def unique_collections_slug(db):
265 """Add unique constraint to collection slug"""
266 metadata = MetaData(bind=db.bind)
267 collection_table = inspect_table(metadata, "core__collections")
268 existing_slugs = {}
269 slugs_to_change = []
270
271 for row in db.execute(collection_table.select()):
272 # if duplicate slug, generate a unique slug
273 if row.creator in existing_slugs and row.slug in \
274 existing_slugs[row.creator]:
275 slugs_to_change.append(row.id)
276 else:
277 if not row.creator in existing_slugs:
278 existing_slugs[row.creator] = [row.slug]
279 else:
280 existing_slugs[row.creator].append(row.slug)
281
282 for row_id in slugs_to_change:
283 new_slug = six.text_type(uuid.uuid4())
284 db.execute(collection_table.update().
285 where(collection_table.c.id == row_id).
286 values(slug=new_slug))
287 # sqlite does not like to change the schema when a transaction(update) is
288 # not yet completed
289 db.commit()
290
291 constraint = UniqueConstraint('creator', 'slug',
292 name='core__collection_creator_slug_key',
293 table=collection_table)
294 constraint.create()
295
296 db.commit()
297
298@RegisterMigration(11, MIGRATIONS)
299def drop_token_related_User_columns(db):
300 """
301 Drop unneeded columns from the User table after switching to using
302 itsdangerous tokens for email and forgot password verification.
303 """
304 metadata = MetaData(bind=db.bind)
305 user_table = inspect_table(metadata, 'core__users')
306
307 verification_key = user_table.columns['verification_key']
308 fp_verification_key = user_table.columns['fp_verification_key']
309 fp_token_expire = user_table.columns['fp_token_expire']
310
311 verification_key.drop()
312 fp_verification_key.drop()
313 fp_token_expire.drop()
314
315 db.commit()
316
317
318class CommentSubscription_v0(declarative_base()):
319 __tablename__ = 'core__comment_subscriptions'
320 id = Column(Integer, primary_key=True)
321
322 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
323
324 media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False)
325
326 user_id = Column(Integer, ForeignKey(User.id), nullable=False)
327
328 notify = Column(Boolean, nullable=False, default=True)
329 send_email = Column(Boolean, nullable=False, default=True)
330
331
332class Notification_v0(declarative_base()):
333 __tablename__ = 'core__notifications'
334 id = Column(Integer, primary_key=True)
335 type = Column(Unicode)
336
337 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
338
339 user_id = Column(Integer, ForeignKey(User.id), nullable=False,
340 index=True)
341 seen = Column(Boolean, default=lambda: False, index=True)
342
343
344class CommentNotification_v0(Notification_v0):
345 __tablename__ = 'core__comment_notifications'
346 id = Column(Integer, ForeignKey(Notification_v0.id), primary_key=True)
347
348 subject_id = Column(Integer, ForeignKey(MediaComment.id))
349
350
351class ProcessingNotification_v0(Notification_v0):
352 __tablename__ = 'core__processing_notifications'
353
354 id = Column(Integer, ForeignKey(Notification_v0.id), primary_key=True)
355
356 subject_id = Column(Integer, ForeignKey(MediaEntry.id))
357
358
359@RegisterMigration(12, MIGRATIONS)
360def add_new_notification_tables(db):
361 metadata = MetaData(bind=db.bind)
362
363 user_table = inspect_table(metadata, 'core__users')
364 mediaentry_table = inspect_table(metadata, 'core__media_entries')
365 mediacomment_table = inspect_table(metadata, 'core__media_comments')
366
367 CommentSubscription_v0.__table__.create(db.bind)
368
369 Notification_v0.__table__.create(db.bind)
370 CommentNotification_v0.__table__.create(db.bind)
371 ProcessingNotification_v0.__table__.create(db.bind)
372
373 db.commit()
374
375
376@RegisterMigration(13, MIGRATIONS)
377def pw_hash_nullable(db):
378 """Make pw_hash column nullable"""
379 metadata = MetaData(bind=db.bind)
380 user_table = inspect_table(metadata, "core__users")
381
382 user_table.c.pw_hash.alter(nullable=True)
383
384 # sqlite+sqlalchemy seems to drop this constraint during the
385 # migration, so we add it back here for now a bit manually.
386 if db.bind.url.drivername == 'sqlite':
387 constraint = UniqueConstraint('username', table=user_table)
388 constraint.create()
389
390 db.commit()
391
392
393# oauth1 migrations
394class Client_v0(declarative_base()):
395 """
396 Model representing a client - Used for API Auth
397 """
398 __tablename__ = "core__clients"
399
400 id = Column(Unicode, nullable=True, primary_key=True)
401 secret = Column(Unicode, nullable=False)
402 expirey = Column(DateTime, nullable=True)
403 application_type = Column(Unicode, nullable=False)
404 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
405 updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
406
407 # optional stuff
408 redirect_uri = Column(JSONEncoded, nullable=True)
409 logo_url = Column(Unicode, nullable=True)
410 application_name = Column(Unicode, nullable=True)
411 contacts = Column(JSONEncoded, nullable=True)
412
413 def __repr__(self):
414 if self.application_name:
415 return "<Client {0} - {1}>".format(self.application_name, self.id)
416 else:
417 return "<Client {0}>".format(self.id)
418
419class RequestToken_v0(declarative_base()):
420 """
421 Model for representing the request tokens
422 """
423 __tablename__ = "core__request_tokens"
424
425 token = Column(Unicode, primary_key=True)
426 secret = Column(Unicode, nullable=False)
427 client = Column(Unicode, ForeignKey(Client_v0.id))
428 user = Column(Integer, ForeignKey(User.id), nullable=True)
429 used = Column(Boolean, default=False)
430 authenticated = Column(Boolean, default=False)
431 verifier = Column(Unicode, nullable=True)
432 callback = Column(Unicode, nullable=False, default=u"oob")
433 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
434 updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
435
436class AccessToken_v0(declarative_base()):
437 """
438 Model for representing the access tokens
439 """
440 __tablename__ = "core__access_tokens"
441
442 token = Column(Unicode, nullable=False, primary_key=True)
443 secret = Column(Unicode, nullable=False)
444 user = Column(Integer, ForeignKey(User.id))
445 request_token = Column(Unicode, ForeignKey(RequestToken_v0.token))
446 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
447 updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
448
449
450class NonceTimestamp_v0(declarative_base()):
451 """
452 A place the timestamp and nonce can be stored - this is for OAuth1
453 """
454 __tablename__ = "core__nonce_timestamps"
455
456 nonce = Column(Unicode, nullable=False, primary_key=True)
457 timestamp = Column(DateTime, nullable=False, primary_key=True)
458
459
460@RegisterMigration(14, MIGRATIONS)
461def create_oauth1_tables(db):
462 """ Creates the OAuth1 tables """
463
464 Client_v0.__table__.create(db.bind)
465 RequestToken_v0.__table__.create(db.bind)
466 AccessToken_v0.__table__.create(db.bind)
467 NonceTimestamp_v0.__table__.create(db.bind)
468
469 db.commit()
470
471@RegisterMigration(15, MIGRATIONS)
472def wants_notifications(db):
473 """Add a wants_notifications field to User model"""
474 metadata = MetaData(bind=db.bind)
475 user_table = inspect_table(metadata, "core__users")
476 col = Column('wants_notifications', Boolean, default=True)
477 col.create(user_table)
478 db.commit()
479
480
481
482@RegisterMigration(16, MIGRATIONS)
483def upload_limits(db):
484 """Add user upload limit columns"""
485 metadata = MetaData(bind=db.bind)
486
487 user_table = inspect_table(metadata, 'core__users')
488 media_entry_table = inspect_table(metadata, 'core__media_entries')
489
490 col = Column('uploaded', Integer, default=0)
491 col.create(user_table)
492
493 col = Column('upload_limit', Integer)
494 col.create(user_table)
495
496 col = Column('file_size', Integer, default=0)
497 col.create(media_entry_table)
498
499 db.commit()
500
501
502@RegisterMigration(17, MIGRATIONS)
503def add_file_metadata(db):
504 """Add file_metadata to MediaFile"""
505 metadata = MetaData(bind=db.bind)
506 media_file_table = inspect_table(metadata, "core__mediafiles")
507
508 col = Column('file_metadata', MutationDict.as_mutable(JSONEncoded))
509 col.create(media_file_table)
510
511 db.commit()
512
513###################
514# Moderation tables
515###################
516
517class ReportBase_v0(declarative_base()):
518 __tablename__ = 'core__reports'
519 id = Column(Integer, primary_key=True)
520 reporter_id = Column(Integer, ForeignKey(User.id), nullable=False)
521 report_content = Column(UnicodeText)
522 reported_user_id = Column(Integer, ForeignKey(User.id), nullable=False)
523 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
524 discriminator = Column('type', Unicode(50))
525 resolver_id = Column(Integer, ForeignKey(User.id))
526 resolved = Column(DateTime)
527 result = Column(UnicodeText)
528 __mapper_args__ = {'polymorphic_on': discriminator}
529
530
531class CommentReport_v0(ReportBase_v0):
532 __tablename__ = 'core__reports_on_comments'
533 __mapper_args__ = {'polymorphic_identity': 'comment_report'}
534
535 id = Column('id',Integer, ForeignKey('core__reports.id'),
536 primary_key=True)
537 comment_id = Column(Integer, ForeignKey(MediaComment.id), nullable=True)
538
539
540class MediaReport_v0(ReportBase_v0):
541 __tablename__ = 'core__reports_on_media'
542 __mapper_args__ = {'polymorphic_identity': 'media_report'}
543
544 id = Column('id',Integer, ForeignKey('core__reports.id'), primary_key=True)
545 media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=True)
546
547
548class UserBan_v0(declarative_base()):
549 __tablename__ = 'core__user_bans'
550 user_id = Column(Integer, ForeignKey(User.id), nullable=False,
551 primary_key=True)
552 expiration_date = Column(Date)
553 reason = Column(UnicodeText, nullable=False)
554
555
556class Privilege_v0(declarative_base()):
557 __tablename__ = 'core__privileges'
558 id = Column(Integer, nullable=False, primary_key=True, unique=True)
559 privilege_name = Column(Unicode, nullable=False, unique=True)
560
561
562class PrivilegeUserAssociation_v0(declarative_base()):
563 __tablename__ = 'core__privileges_users'
564 privilege_id = Column(
565 'core__privilege_id',
566 Integer,
567 ForeignKey(User.id),
568 primary_key=True)
569 user_id = Column(
570 'core__user_id',
571 Integer,
572 ForeignKey(Privilege.id),
573 primary_key=True)
574
575
576PRIVILEGE_FOUNDATIONS_v0 = [{'privilege_name':u'admin'},
577 {'privilege_name':u'moderator'},
578 {'privilege_name':u'uploader'},
579 {'privilege_name':u'reporter'},
580 {'privilege_name':u'commenter'},
581 {'privilege_name':u'active'}]
582
583
584# vR1 stands for "version Rename 1". This only exists because we need
585# to deal with dropping some booleans and it's otherwise impossible
586# with sqlite.
587
588class User_vR1(declarative_base()):
589 __tablename__ = 'rename__users'
590 id = Column(Integer, primary_key=True)
591 username = Column(Unicode, nullable=False, unique=True)
592 email = Column(Unicode, nullable=False)
593 pw_hash = Column(Unicode)
594 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
595 wants_comment_notification = Column(Boolean, default=True)
596 wants_notifications = Column(Boolean, default=True)
597 license_preference = Column(Unicode)
598 url = Column(Unicode)
599 bio = Column(UnicodeText) # ??
600 uploaded = Column(Integer, default=0)
601 upload_limit = Column(Integer)
602
603
604@RegisterMigration(18, MIGRATIONS)
605def create_moderation_tables(db):
606
607 # First, we will create the new tables in the database.
608 #--------------------------------------------------------------------------
609 ReportBase_v0.__table__.create(db.bind)
610 CommentReport_v0.__table__.create(db.bind)
611 MediaReport_v0.__table__.create(db.bind)
612 UserBan_v0.__table__.create(db.bind)
613 Privilege_v0.__table__.create(db.bind)
614 PrivilegeUserAssociation_v0.__table__.create(db.bind)
615
616 db.commit()
617
618 # Then initialize the tables that we will later use
619 #--------------------------------------------------------------------------
620 metadata = MetaData(bind=db.bind)
621 privileges_table= inspect_table(metadata, "core__privileges")
622 user_table = inspect_table(metadata, 'core__users')
623 user_privilege_assoc = inspect_table(
624 metadata, 'core__privileges_users')
625
626 # This section initializes the default Privilege foundations, that
627 # would be created through the FOUNDATIONS system in a new instance
628 #--------------------------------------------------------------------------
629 for parameters in PRIVILEGE_FOUNDATIONS_v0:
630 db.execute(privileges_table.insert().values(**parameters))
631
632 db.commit()
633
634 # This next section takes the information from the old is_admin and status
635 # columns and converts those to the new privilege system
636 #--------------------------------------------------------------------------
637 admin_users_ids, active_users_ids, inactive_users_ids = (
638 db.execute(
639 user_table.select().where(
640 user_table.c.is_admin==True)).fetchall(),
641 db.execute(
642 user_table.select().where(
643 user_table.c.is_admin==False).where(
644 user_table.c.status==u"active")).fetchall(),
645 db.execute(
646 user_table.select().where(
647 user_table.c.is_admin==False).where(
648 user_table.c.status!=u"active")).fetchall())
649
650 # Get the ids for each of the privileges so we can reference them ~~~~~~~~~
651 (admin_privilege_id, uploader_privilege_id,
652 reporter_privilege_id, commenter_privilege_id,
653 active_privilege_id) = [
654 db.execute(privileges_table.select().where(
655 privileges_table.c.privilege_name==privilege_name)).first()['id']
656 for privilege_name in
657 [u"admin",u"uploader",u"reporter",u"commenter",u"active"]
658 ]
659
660 # Give each user the appopriate privileges depending whether they are an
661 # admin, an active user or an inactive user ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
662 for admin_user in admin_users_ids:
663 admin_user_id = admin_user['id']
664 for privilege_id in [admin_privilege_id, uploader_privilege_id,
665 reporter_privilege_id, commenter_privilege_id,
666 active_privilege_id]:
667 db.execute(user_privilege_assoc.insert().values(
668 core__privilege_id=admin_user_id,
669 core__user_id=privilege_id))
670
671 for active_user in active_users_ids:
672 active_user_id = active_user['id']
673 for privilege_id in [uploader_privilege_id, reporter_privilege_id,
674 commenter_privilege_id, active_privilege_id]:
675 db.execute(user_privilege_assoc.insert().values(
676 core__privilege_id=active_user_id,
677 core__user_id=privilege_id))
678
679 for inactive_user in inactive_users_ids:
680 inactive_user_id = inactive_user['id']
681 for privilege_id in [uploader_privilege_id, reporter_privilege_id,
682 commenter_privilege_id]:
683 db.execute(user_privilege_assoc.insert().values(
684 core__privilege_id=inactive_user_id,
685 core__user_id=privilege_id))
686
687 db.commit()
688
689 # And then, once the information is taken from is_admin & status columns
690 # we drop all of the vestigial columns from the User table.
691 #--------------------------------------------------------------------------
692 if db.bind.url.drivername == 'sqlite':
693 # SQLite has some issues that make it *impossible* to drop boolean
694 # columns. So, the following code is a very hacky workaround which
695 # makes it possible. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
696
697 User_vR1.__table__.create(db.bind)
698 db.commit()
699 new_user_table = inspect_table(metadata, 'rename__users')
700 replace_table_hack(db, user_table, new_user_table)
701 else:
702 # If the db is not run using SQLite, this process is much simpler ~~~~~
703
704 status = user_table.columns['status']
705 email_verified = user_table.columns['email_verified']
706 is_admin = user_table.columns['is_admin']
707 status.drop()
708 email_verified.drop()
709 is_admin.drop()
710
711 db.commit()
712
713
714@RegisterMigration(19, MIGRATIONS)
715def drop_MediaEntry_collected(db):
716 """
717 Drop unused MediaEntry.collected column
718 """
719 metadata = MetaData(bind=db.bind)
720
721 media_collected= inspect_table(metadata, 'core__media_entries')
722 media_collected = media_collected.columns['collected']
723
724 media_collected.drop()
725
726 db.commit()
727
728
729@RegisterMigration(20, MIGRATIONS)
730def add_metadata_column(db):
731 metadata = MetaData(bind=db.bind)
732
733 media_entry = inspect_table(metadata, 'core__media_entries')
734
735 col = Column('media_metadata', MutationDict.as_mutable(JSONEncoded),
736 default=MutationDict())
737 col.create(media_entry)
738
739 db.commit()
740
741
742class PrivilegeUserAssociation_R1(declarative_base()):
743 __tablename__ = 'rename__privileges_users'
744 user = Column(
745 "user",
746 Integer,
747 ForeignKey(User.id),
748 primary_key=True)
749 privilege = Column(
750 "privilege",
751 Integer,
752 ForeignKey(Privilege.id),
753 primary_key=True)
754
755@RegisterMigration(21, MIGRATIONS)
756def fix_privilege_user_association_table(db):
757 """
758 There was an error in the PrivilegeUserAssociation table that allowed for a
759 dangerous sql error. We need to the change the name of the columns to be
760 unique, and properly referenced.
761 """
762 metadata = MetaData(bind=db.bind)
763
764 privilege_user_assoc = inspect_table(
765 metadata, 'core__privileges_users')
766
767 # This whole process is more complex if we're dealing with sqlite
768 if db.bind.url.drivername == 'sqlite':
769 PrivilegeUserAssociation_R1.__table__.create(db.bind)
770 db.commit()
771
772 new_privilege_user_assoc = inspect_table(
773 metadata, 'rename__privileges_users')
774 result = db.execute(privilege_user_assoc.select())
775 for row in result:
776 # The columns were improperly named before, so we switch the columns
777 user_id, priv_id = row['core__privilege_id'], row['core__user_id']
778 db.execute(new_privilege_user_assoc.insert().values(
779 user=user_id,
780 privilege=priv_id))
781
782 db.commit()
783
784 privilege_user_assoc.drop()
785 new_privilege_user_assoc.rename('core__privileges_users')
786
787 # much simpler if postgres though!
788 else:
789 privilege_user_assoc.c.core__user_id.alter(name="privilege")
790 privilege_user_assoc.c.core__privilege_id.alter(name="user")
791
792 db.commit()
793
794
795@RegisterMigration(22, MIGRATIONS)
796def add_index_username_field(db):
797 """
798 This migration has been found to be doing the wrong thing. See
799 the documentation in migration 23 (revert_username_index) below
800 which undoes this for those databases that did run this migration.
801
802 Old description:
803 This indexes the User.username field which is frequently queried
804 for example a user logging in. This solves the issue #894
805 """
806 ## This code is left commented out *on purpose!*
807 ##
808 ## We do not normally allow commented out code like this in
809 ## MediaGoblin but this is a special case: since this migration has
810 ## been nullified but with great work to set things back below,
811 ## this is commented out for historical clarity.
812 #
813 # metadata = MetaData(bind=db.bind)
814 # user_table = inspect_table(metadata, "core__users")
815 #
816 # new_index = Index("ix_core__users_uploader", user_table.c.username)
817 # new_index.create()
818 #
819 # db.commit()
820 pass
821
822
823@RegisterMigration(23, MIGRATIONS)
824def revert_username_index(db):
825 """
826 Revert the stuff we did in migration 22 above.
827
828 There were a couple of problems with what we did:
829 - There was never a need for this migration! The unique
830 constraint had an implicit b-tree index, so it wasn't really
831 needed. (This is my (Chris Webber's) fault for suggesting it
832 needed to happen without knowing what's going on... my bad!)
833 - On top of that, databases created after the models.py was
834 changed weren't the same as those that had been run through
835 migration 22 above.
836
837 As such, we're setting things back to the way they were before,
838 but as it turns out, that's tricky to do!
839 """
840 metadata = MetaData(bind=db.bind)
841 user_table = inspect_table(metadata, "core__users")
842 indexes = dict(
843 [(index.name, index) for index in user_table.indexes])
844
845 # index from unnecessary migration
846 users_uploader_index = indexes.get(u'ix_core__users_uploader')
847 # index created from models.py after (unique=True, index=True)
848 # was set in models.py
849 users_username_index = indexes.get(u'ix_core__users_username')
850
851 if users_uploader_index is None and users_username_index is None:
852 # We don't need to do anything.
853 # The database isn't in a state where it needs fixing
854 #
855 # (ie, either went through the previous borked migration or
856 # was initialized with a models.py where core__users was both
857 # unique=True and index=True)
858 return
859
860 if db.bind.url.drivername == 'sqlite':
861 # Again, sqlite has problems. So this is tricky.
862
863 # Yes, this is correct to use User_vR1! Nothing has changed
864 # between the *correct* version of this table and migration 18.
865 User_vR1.__table__.create(db.bind)
866 db.commit()
867 new_user_table = inspect_table(metadata, 'rename__users')
868 replace_table_hack(db, user_table, new_user_table)
869
870 else:
871 # If the db is not run using SQLite, we don't need to do crazy
872 # table copying.
873
874 # Remove whichever of the not-used indexes are in place
875 if users_uploader_index is not None:
876 users_uploader_index.drop()
877 if users_username_index is not None:
878 users_username_index.drop()
879
880 # Given we're removing indexes then adding a unique constraint
881 # which *we know might fail*, thus probably rolling back the
882 # session, let's commit here.
883 db.commit()
884
885 try:
886 # Add the unique constraint
887 constraint = UniqueConstraint(
888 'username', table=user_table)
889 constraint.create()
890 except ProgrammingError:
891 # constraint already exists, no need to add
892 db.rollback()
893
894 db.commit()