1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
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.
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.
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/>.
20 from sqlalchemy import (MetaData, Table, Column, Boolean, SmallInteger,
21 Integer, Unicode, UnicodeText, DateTime,
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
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, ReportBase,
38 @RegisterMigration(1, MIGRATIONS)
39 def ogg_to_webm_audio(db_conn):
40 metadata = MetaData(bind=db_conn.bind)
42 file_keynames = Table('core__file_keynames', metadata, autoload=True,
43 autoload_with=db_conn.bind)
46 file_keynames.update().where(file_keynames.c.name == 'ogg').
47 values(name='webm_audio')
52 @RegisterMigration(2, MIGRATIONS)
53 def add_wants_notification_column(db_conn):
54 metadata = MetaData(bind=db_conn.bind)
56 users = Table('core__users', metadata, autoload=True,
57 autoload_with=db_conn.bind)
59 col = Column('wants_comment_notification', Boolean,
60 default=True, nullable=True)
61 col.create(users, populate_defaults=True)
65 @RegisterMigration(3, MIGRATIONS)
66 def add_transcoding_progress(db_conn):
67 metadata = MetaData(bind=db_conn.bind)
69 media_entry = inspect_table(metadata, 'core__media_entries')
71 col = Column('transcoding_progress', SmallInteger)
72 col.create(media_entry)
76 class Collection_v0(declarative_base()):
77 __tablename__ = "core__collections"
79 id = Column(Integer, primary_key=True)
80 title = Column(Unicode, nullable=False)
81 slug = Column(Unicode)
82 created = Column(DateTime, nullable=False, default=datetime.datetime.now,
84 description = Column(UnicodeText)
85 creator = Column(Integer, ForeignKey(User.id), nullable=False)
86 items = Column(Integer, default=0)
88 class CollectionItem_v0(declarative_base()):
89 __tablename__ = "core__collection_items"
91 id = Column(Integer, primary_key=True)
93 Integer, ForeignKey(MediaEntry.id), nullable=False, index=True)
94 collection = Column(Integer, ForeignKey(Collection.id), nullable=False)
95 note = Column(UnicodeText, nullable=True)
96 added = Column(DateTime, nullable=False, default=datetime.datetime.now)
97 position = Column(Integer)
99 ## This should be activated, normally.
100 ## But this would change the way the next migration used to work.
101 ## So it's commented for now.
103 UniqueConstraint('collection', 'media_entry'),
106 collectionitem_unique_constraint_done = False
108 @RegisterMigration(4, MIGRATIONS)
109 def add_collection_tables(db_conn):
110 Collection_v0.__table__.create(db_conn.bind)
111 CollectionItem_v0.__table__.create(db_conn.bind)
113 global collectionitem_unique_constraint_done
114 collectionitem_unique_constraint_done = True
119 @RegisterMigration(5, MIGRATIONS)
120 def add_mediaentry_collected(db_conn):
121 metadata = MetaData(bind=db_conn.bind)
123 media_entry = inspect_table(metadata, 'core__media_entries')
125 col = Column('collected', Integer, default=0)
126 col.create(media_entry)
130 class ProcessingMetaData_v0(declarative_base()):
131 __tablename__ = 'core__processing_metadata'
133 id = Column(Integer, primary_key=True)
134 media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False,
136 callback_url = Column(Unicode)
138 @RegisterMigration(6, MIGRATIONS)
139 def create_processing_metadata_table(db):
140 ProcessingMetaData_v0.__table__.create(db.bind)
144 # Okay, problem being:
145 # Migration #4 forgot to add the uniqueconstraint for the
146 # new tables. While creating the tables from scratch had
147 # the constraint enabled.
149 # So we have four situations that should end up at the same
153 # Well, easy. Just uses the tables in models.py
154 # 2. Fresh install using a git version just before this migration
155 # The tables are all there, the unique constraint is also there.
156 # This migration should do nothing.
157 # But as we can't detect the uniqueconstraint easily,
158 # this migration just adds the constraint again.
159 # And possibly fails very loud. But ignores the failure.
160 # 3. old install, not using git, just releases.
161 # This one will get the new tables in #4 (now with constraint!)
162 # And this migration is just skipped silently.
163 # 4. old install, always on latest git.
164 # This one has the tables, but lacks the constraint.
165 # So this migration adds the constraint.
166 @RegisterMigration(7, MIGRATIONS)
167 def fix_CollectionItem_v0_constraint(db_conn):
168 """Add the forgotten Constraint on CollectionItem"""
170 global collectionitem_unique_constraint_done
171 if collectionitem_unique_constraint_done:
172 # Reset it. Maybe the whole thing gets run again
173 # For a different db?
174 collectionitem_unique_constraint_done = False
177 metadata = MetaData(bind=db_conn.bind)
179 CollectionItem_table = inspect_table(metadata, 'core__collection_items')
181 constraint = UniqueConstraint('collection', 'media_entry',
182 name='core__collection_items_collection_media_entry_key',
183 table=CollectionItem_table)
187 except ProgrammingError:
188 # User probably has an install that was run since the
189 # collection tables were added, so we don't need to run this migration.
195 @RegisterMigration(8, MIGRATIONS)
196 def add_license_preference(db):
197 metadata = MetaData(bind=db.bind)
199 user_table = inspect_table(metadata, 'core__users')
201 col = Column('license_preference', Unicode)
202 col.create(user_table)
206 @RegisterMigration(9, MIGRATIONS)
207 def mediaentry_new_slug_era(db):
209 Update for the new era for media type slugs.
211 Entries without slugs now display differently in the url like:
214 ... because of this, we should back-convert:
215 - entries without slugs should be converted to use the id, if possible, to
216 make old urls still work
217 - slugs with = (or also : which is now also not allowed) to have those
218 stripped out (small possibility of breakage here sadly)
221 def slug_and_user_combo_exists(slug, uploader):
224 and_(media_table.c.uploader==uploader,
225 media_table.c.slug==slug))).first() is not None
227 def append_garbage_till_unique(row, new_slug):
229 Attach junk to this row until it's unique, then save it
231 if slug_and_user_combo_exists(new_slug, row.uploader):
232 # okay, still no success;
233 # let's whack junk on there till it's unique.
234 new_slug += '-' + uuid.uuid4().hex[:4]
235 # keep going if necessary!
236 while slug_and_user_combo_exists(new_slug, row.uploader):
237 new_slug += uuid.uuid4().hex[:4]
240 media_table.update(). \
241 where(media_table.c.id==row.id). \
242 values(slug=new_slug))
244 metadata = MetaData(bind=db.bind)
246 media_table = inspect_table(metadata, 'core__media_entries')
248 for row in db.execute(media_table.select()):
249 # no slug, try setting to an id
251 append_garbage_till_unique(row, unicode(row.id))
252 # has "=" or ":" in it... we're getting rid of those
253 elif u"=" in row.slug or u":" in row.slug:
254 append_garbage_till_unique(
255 row, row.slug.replace(u"=", u"-").replace(u":", u"-"))
260 @RegisterMigration(10, MIGRATIONS)
261 def unique_collections_slug(db):
262 """Add unique constraint to collection slug"""
263 metadata = MetaData(bind=db.bind)
264 collection_table = inspect_table(metadata, "core__collections")
268 for row in db.execute(collection_table.select()):
269 # if duplicate slug, generate a unique slug
270 if row.creator in existing_slugs and row.slug in \
271 existing_slugs[row.creator]:
272 slugs_to_change.append(row.id)
274 if not row.creator in existing_slugs:
275 existing_slugs[row.creator] = [row.slug]
277 existing_slugs[row.creator].append(row.slug)
279 for row_id in slugs_to_change:
280 new_slug = unicode(uuid.uuid4())
281 db.execute(collection_table.update().
282 where(collection_table.c.id == row_id).
283 values(slug=new_slug))
284 # sqlite does not like to change the schema when a transaction(update) is
288 constraint = UniqueConstraint('creator', 'slug',
289 name='core__collection_creator_slug_key',
290 table=collection_table)
295 @RegisterMigration(11, MIGRATIONS)
296 def drop_token_related_User_columns(db):
298 Drop unneeded columns from the User table after switching to using
299 itsdangerous tokens for email and forgot password verification.
301 metadata = MetaData(bind=db.bind)
302 user_table = inspect_table(metadata, 'core__users')
304 verification_key = user_table.columns['verification_key']
305 fp_verification_key = user_table.columns['fp_verification_key']
306 fp_token_expire = user_table.columns['fp_token_expire']
308 verification_key.drop()
309 fp_verification_key.drop()
310 fp_token_expire.drop()
315 class CommentSubscription_v0(declarative_base()):
316 __tablename__ = 'core__comment_subscriptions'
317 id = Column(Integer, primary_key=True)
319 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
321 media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False)
323 user_id = Column(Integer, ForeignKey(User.id), nullable=False)
325 notify = Column(Boolean, nullable=False, default=True)
326 send_email = Column(Boolean, nullable=False, default=True)
329 class Notification_v0(declarative_base()):
330 __tablename__ = 'core__notifications'
331 id = Column(Integer, primary_key=True)
332 type = Column(Unicode)
334 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
336 user_id = Column(Integer, ForeignKey(User.id), nullable=False,
338 seen = Column(Boolean, default=lambda: False, index=True)
341 class CommentNotification_v0(Notification_v0):
342 __tablename__ = 'core__comment_notifications'
343 id = Column(Integer, ForeignKey(Notification_v0.id), primary_key=True)
345 subject_id = Column(Integer, ForeignKey(MediaComment.id))
348 class ProcessingNotification_v0(Notification_v0):
349 __tablename__ = 'core__processing_notifications'
351 id = Column(Integer, ForeignKey(Notification_v0.id), primary_key=True)
353 subject_id = Column(Integer, ForeignKey(MediaEntry.id))
356 @RegisterMigration(12, MIGRATIONS)
357 def add_new_notification_tables(db):
358 metadata = MetaData(bind=db.bind)
360 user_table = inspect_table(metadata, 'core__users')
361 mediaentry_table = inspect_table(metadata, 'core__media_entries')
362 mediacomment_table = inspect_table(metadata, 'core__media_comments')
364 CommentSubscription_v0.__table__.create(db.bind)
366 Notification_v0.__table__.create(db.bind)
367 CommentNotification_v0.__table__.create(db.bind)
368 ProcessingNotification_v0.__table__.create(db.bind)
373 @RegisterMigration(13, MIGRATIONS)
374 def pw_hash_nullable(db):
375 """Make pw_hash column nullable"""
376 metadata = MetaData(bind=db.bind)
377 user_table = inspect_table(metadata, "core__users")
379 user_table.c.pw_hash.alter(nullable=True)
381 # sqlite+sqlalchemy seems to drop this constraint during the
382 # migration, so we add it back here for now a bit manually.
383 if db.bind.url.drivername == 'sqlite':
384 constraint = UniqueConstraint('username', table=user_table)
391 class Client_v0(declarative_base()):
393 Model representing a client - Used for API Auth
395 __tablename__ = "core__clients"
397 id = Column(Unicode, nullable=True, primary_key=True)
398 secret = Column(Unicode, nullable=False)
399 expirey = Column(DateTime, nullable=True)
400 application_type = Column(Unicode, nullable=False)
401 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
402 updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
405 redirect_uri = Column(JSONEncoded, nullable=True)
406 logo_url = Column(Unicode, nullable=True)
407 application_name = Column(Unicode, nullable=True)
408 contacts = Column(JSONEncoded, nullable=True)
411 if self.application_name:
412 return "<Client {0} - {1}>".format(self.application_name, self.id)
414 return "<Client {0}>".format(self.id)
416 class RequestToken_v0(declarative_base()):
418 Model for representing the request tokens
420 __tablename__ = "core__request_tokens"
422 token = Column(Unicode, primary_key=True)
423 secret = Column(Unicode, nullable=False)
424 client = Column(Unicode, ForeignKey(Client_v0.id))
425 user = Column(Integer, ForeignKey(User.id), nullable=True)
426 used = Column(Boolean, default=False)
427 authenticated = Column(Boolean, default=False)
428 verifier = Column(Unicode, nullable=True)
429 callback = Column(Unicode, nullable=False, default=u"oob")
430 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
431 updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
433 class AccessToken_v0(declarative_base()):
435 Model for representing the access tokens
437 __tablename__ = "core__access_tokens"
439 token = Column(Unicode, nullable=False, primary_key=True)
440 secret = Column(Unicode, nullable=False)
441 user = Column(Integer, ForeignKey(User.id))
442 request_token = Column(Unicode, ForeignKey(RequestToken_v0.token))
443 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
444 updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
447 class NonceTimestamp_v0(declarative_base()):
449 A place the timestamp and nonce can be stored - this is for OAuth1
451 __tablename__ = "core__nonce_timestamps"
453 nonce = Column(Unicode, nullable=False, primary_key=True)
454 timestamp = Column(DateTime, nullable=False, primary_key=True)
457 @RegisterMigration(14, MIGRATIONS)
458 def create_oauth1_tables(db):
459 """ Creates the OAuth1 tables """
461 Client_v0.__table__.create(db.bind)
462 RequestToken_v0.__table__.create(db.bind)
463 AccessToken_v0.__table__.create(db.bind)
464 NonceTimestamp_v0.__table__.create(db.bind)
469 @RegisterMigration(15, MIGRATIONS)
470 def wants_notifications(db):
471 """Add a wants_notifications field to User model"""
472 metadata = MetaData(bind=db.bind)
473 user_table = inspect_table(metadata, "core__users")
474 col = Column('wants_notifications', Boolean, default=True)
475 col.create(user_table)
478 class ReportBase_v0(declarative_base()):
479 __tablename__ = 'core__reports'
480 id = Column(Integer, primary_key=True)
481 reporter_id = Column(Integer, ForeignKey(User.id), nullable=False)
482 report_content = Column(UnicodeText)
483 reported_user_id = Column(Integer, ForeignKey(User.id), nullable=False)
484 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
485 discriminator = Column('type', Unicode(50))
486 resolver_id = Column(Integer, ForeignKey(User.id))
487 resolved = Column(DateTime)
488 result = Column(UnicodeText)
489 __mapper_args__ = {'polymorphic_on': discriminator}
491 class CommentReport_v0(ReportBase_v0):
492 __tablename__ = 'core__reports_on_comments'
493 __mapper_args__ = {'polymorphic_identity': 'comment_report'}
495 id = Column('id',Integer, ForeignKey('core__reports.id'),
497 comment_id = Column(Integer, ForeignKey(MediaComment.id), nullable=True)
501 class MediaReport_v0(ReportBase_v0):
502 __tablename__ = 'core__reports_on_media'
503 __mapper_args__ = {'polymorphic_identity': 'media_report'}
505 id = Column('id',Integer, ForeignKey('core__reports.id'), primary_key=True)
506 media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=True)
508 class UserBan_v0(declarative_base()):
509 __tablename__ = 'core__user_bans'
510 user_id = Column('id',Integer, ForeignKey(User.id), nullable=False,
512 expiration_date = Column(Date)
513 reason = Column(UnicodeText, nullable=False)
515 class Privilege_v0(declarative_base()):
516 __tablename__ = 'core__privileges'
517 id = Column(Integer, nullable=False, primary_key=True, unique=True)
518 privilege_name = Column(Unicode, nullable=False, unique=True)
520 class PrivilegeUserAssociation_v0(declarative_base()):
521 __tablename__ = 'core__privileges_users'
523 'core__privilege_id',
530 ForeignKey(Privilege.id),
533 @RegisterMigration(16, MIGRATIONS)
534 def create_moderation_tables(db):
535 ReportBase_v0.__table__.create(db.bind)
536 CommentReport_v0.__table__.create(db.bind)
537 MediaReport_v0.__table__.create(db.bind)
538 UserBan_v0.__table__.create(db.bind)
539 Privilege_v0.__table__.create(db.bind)
540 PrivilegeUserAssociation_v0.__table__.create(db.bind)
544 for parameters in FOUNDATIONS[Privilege]:
545 p = Privilege(**parameters)
549 @RegisterMigration(17, MIGRATIONS)
550 def update_user_privilege_columns(db):
551 # first, create the privileges which would be created by foundations
552 default_privileges = Privilege.query.filter(
553 Privilege.privilege_name !=u'admin').filter(
554 Privilege.privilege_name !=u'moderator').filter(
555 Privilege.privilege_name !=u'active').all()
556 admin_privilege = Privilege.query.filter(
557 Privilege.privilege_name ==u'admin').first()
558 active_privilege = Privilege.query.filter(
559 Privilege.privilege_name ==u'active').first()
560 # then, assign them to the appropriate users
561 for inactive_user in User.query.filter(
562 User.status!=u'active').filter(
563 User.is_admin==False).all():
565 inactive_user.all_privileges = default_privileges
567 for user in User.query.filter(
568 User.status==u'active').filter(
569 User.is_admin==False).all():
571 user.all_privileges = default_privileges + [active_privilege]
573 for admin_user in User.query.filter(
574 User.is_admin==True).all():
576 admin_user.all_privileges = default_privileges + [
577 admin_privilege, active_privilege]
580 # and then drop the now vestigial status column
581 metadata = MetaData(bind=db.bind)
582 user_table = inspect_table(metadata, 'core__users')
583 status = user_table.columns['status']