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
28 from mediagoblin
.db
.migration_tools
import RegisterMigration
, inspect_table
29 from mediagoblin
.db
.models
import (MediaEntry
, Collection
, User
,
30 MediaComment
, Privilege
, ReportBase
)
35 @RegisterMigration(1, MIGRATIONS
)
36 def ogg_to_webm_audio(db_conn
):
37 metadata
= MetaData(bind
=db_conn
.bind
)
39 file_keynames
= Table('core__file_keynames', metadata
, autoload
=True,
40 autoload_with
=db_conn
.bind
)
43 file_keynames
.update().where(file_keynames
.c
.name
== 'ogg').
44 values(name
='webm_audio')
49 @RegisterMigration(2, MIGRATIONS
)
50 def add_wants_notification_column(db_conn
):
51 metadata
= MetaData(bind
=db_conn
.bind
)
53 users
= Table('core__users', metadata
, autoload
=True,
54 autoload_with
=db_conn
.bind
)
56 col
= Column('wants_comment_notification', Boolean
,
57 default
=True, nullable
=True)
58 col
.create(users
, populate_defaults
=True)
62 @RegisterMigration(3, MIGRATIONS
)
63 def add_transcoding_progress(db_conn
):
64 metadata
= MetaData(bind
=db_conn
.bind
)
66 media_entry
= inspect_table(metadata
, 'core__media_entries')
68 col
= Column('transcoding_progress', SmallInteger
)
69 col
.create(media_entry
)
73 class Collection_v0(declarative_base()):
74 __tablename__
= "core__collections"
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
,
81 description
= Column(UnicodeText
)
82 creator
= Column(Integer
, ForeignKey(User
.id), nullable
=False)
83 items
= Column(Integer
, default
=0)
85 class CollectionItem_v0(declarative_base()):
86 __tablename__
= "core__collection_items"
88 id = Column(Integer
, primary_key
=True)
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
)
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.
100 UniqueConstraint('collection', 'media_entry'),
103 collectionitem_unique_constraint_done
= False
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
)
110 global collectionitem_unique_constraint_done
111 collectionitem_unique_constraint_done
= True
116 @RegisterMigration(5, MIGRATIONS
)
117 def add_mediaentry_collected(db_conn
):
118 metadata
= MetaData(bind
=db_conn
.bind
)
120 media_entry
= inspect_table(metadata
, 'core__media_entries')
122 col
= Column('collected', Integer
, default
=0)
123 col
.create(media_entry
)
127 class ProcessingMetaData_v0(declarative_base()):
128 __tablename__
= 'core__processing_metadata'
130 id = Column(Integer
, primary_key
=True)
131 media_entry_id
= Column(Integer
, ForeignKey(MediaEntry
.id), nullable
=False,
133 callback_url
= Column(Unicode
)
135 @RegisterMigration(6, MIGRATIONS
)
136 def create_processing_metadata_table(db
):
137 ProcessingMetaData_v0
.__table__
.create(db
.bind
)
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.
146 # So we have four situations that should end up at the same
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"""
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
174 metadata
= MetaData(bind
=db_conn
.bind
)
176 CollectionItem_table
= inspect_table(metadata
, 'core__collection_items')
178 constraint
= UniqueConstraint('collection', 'media_entry',
179 name
='core__collection_items_collection_media_entry_key',
180 table
=CollectionItem_table
)
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.
192 @RegisterMigration(8, MIGRATIONS
)
193 def add_license_preference(db
):
194 metadata
= MetaData(bind
=db
.bind
)
196 user_table
= inspect_table(metadata
, 'core__users')
198 col
= Column('license_preference', Unicode
)
199 col
.create(user_table
)
203 @RegisterMigration(9, MIGRATIONS
)
204 def mediaentry_new_slug_era(db
):
206 Update for the new era for media type slugs.
208 Entries without slugs now display differently in the url like:
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)
218 def slug_and_user_combo_exists(slug
, uploader
):
221 and_(media_table
.c
.uploader
==uploader
,
222 media_table
.c
.slug
==slug
))).first() is not None
224 def append_garbage_till_unique(row
, new_slug
):
226 Attach junk to this row until it's unique, then save it
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]
237 media_table
.update(). \
238 where(media_table
.c
.id==row
.id). \
239 values(slug
=new_slug
))
241 metadata
= MetaData(bind
=db
.bind
)
243 media_table
= inspect_table(metadata
, 'core__media_entries')
245 for row
in db
.execute(media_table
.select()):
246 # no slug, try setting to an id
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
"-"))
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")
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)
271 if not row
.creator
in existing_slugs
:
272 existing_slugs
[row
.creator
] = [row
.slug
]
274 existing_slugs
[row
.creator
].append(row
.slug
)
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
285 constraint
= UniqueConstraint('creator', 'slug',
286 name
='core__collection_creator_slug_key',
287 table
=collection_table
)
292 @RegisterMigration(11, MIGRATIONS
)
293 def drop_token_related_User_columns(db
):
295 Drop unneeded columns from the User table after switching to using
296 itsdangerous tokens for email and forgot password verification.
298 metadata
= MetaData(bind
=db
.bind
)
299 user_table
= inspect_table(metadata
, 'core__users')
302 verification_key
= user_table
.columns
['verification_key']
303 fp_verification_key
= user_table
.columns
['fp_verification_key']
304 fp_token_expire
= user_table
.columns
['fp_token_expire']
306 verification_key
.drop()
307 fp_verification_key
.drop()
308 fp_token_expire
.drop()
313 class CommentSubscription_v0(declarative_base()):
314 __tablename__
= 'core__comment_subscriptions'
315 id = Column(Integer
, primary_key
=True)
317 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
319 media_entry_id
= Column(Integer
, ForeignKey(MediaEntry
.id), nullable
=False)
321 user_id
= Column(Integer
, ForeignKey(User
.id), nullable
=False)
324 notify
= Column(Boolean
, nullable
=False, default
=True)
325 send_email
= Column(Boolean
, nullable
=False, default
=True)
328 class Notification_v0(declarative_base()):
329 __tablename__
= 'core__notifications'
330 id = Column(Integer
, primary_key
=True)
331 type = Column(Unicode
)
333 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
335 user_id
= Column(Integer
, ForeignKey(User
.id), nullable
=False,
337 seen
= Column(Boolean
, default
=lambda: False, index
=True)
340 class CommentNotification_v0(Notification_v0
):
341 __tablename__
= 'core__comment_notifications'
342 id = Column(Integer
, ForeignKey(Notification_v0
.id), primary_key
=True)
344 subject_id
= Column(Integer
, ForeignKey(MediaComment
.id))
347 class ProcessingNotification_v0(Notification_v0
):
348 __tablename__
= 'core__processing_notifications'
350 id = Column(Integer
, ForeignKey(Notification_v0
.id), primary_key
=True)
352 subject_id
= Column(Integer
, ForeignKey(MediaEntry
.id))
355 @RegisterMigration(12, MIGRATIONS
)
356 def add_new_notification_tables(db
):
357 metadata
= MetaData(bind
=db
.bind
)
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')
363 CommentSubscription_v0
.__table__
.create(db
.bind
)
365 Notification_v0
.__table__
.create(db
.bind
)
366 CommentNotification_v0
.__table__
.create(db
.bind
)
367 ProcessingNotification_v0
.__table__
.create(db
.bind
)
370 @RegisterMigration(13, MIGRATIONS
)
371 def pw_hash_nullable(db
):
372 """Make pw_hash column nullable"""
373 metadata
= MetaData(bind
=db
.bind
)
374 user_table
= inspect_table(metadata
, "core__users")
376 user_table
.c
.pw_hash
.alter(nullable
=True)
378 # sqlite+sqlalchemy seems to drop this constraint during the
379 # migration, so we add it back here for now a bit manually.
380 if db
.bind
.url
.drivername
== 'sqlite':
381 constraint
= UniqueConstraint('username', table
=user_table
)
384 class ReportBase_v0(declarative_base()):
385 __tablename__
= 'core__reports'
386 id = Column(Integer
, primary_key
=True)
387 reporter_id
= Column(Integer
, ForeignKey(User
.id), nullable
=False)
388 report_content
= Column(UnicodeText
)
389 reported_user_id
= Column(Integer
, ForeignKey(User
.id), nullable
=False)
390 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
391 discriminator
= Column('type', Unicode(50))
392 __mapper_args__
= {'polymorphic_on': discriminator
}
394 class CommentReport_v0(ReportBase_v0
):
395 __tablename__
= 'core__reports_on_comments'
396 __mapper_args__
= {'polymorphic_identity': 'comment_report'}
398 id = Column('id',Integer
, ForeignKey('core__reports.id'),
400 comment_id
= Column(Integer
, ForeignKey(MediaComment
.id), nullable
=False)
402 class MediaReport_v0(ReportBase_v0
):
403 __tablename__
= 'core__reports_on_media'
404 __mapper_args__
= {'polymorphic_identity': 'media_report'}
406 id = Column('id',Integer
, ForeignKey('core__reports.id'), primary_key
=True)
407 media_entry_id
= Column(Integer
, ForeignKey(MediaEntry
.i
409 class ArchivedReport_v0(ReportBase_v0
):
410 __tablename__
= 'core__reports_archived'
411 __mapper_args__
= {'polymorphic_identity': 'archived_report'}
413 id = Column('id',Integer
, ForeignKey('core__reports.id'))
414 media_entry_id
= Column(Integer
, ForeignKey(MediaEntry
.id))
415 comment_id
= Column(Integer
, ForeignKey(MediaComment
.id))
416 resolver_id
= Column(Integer
, ForeignKey(User
.id), nullable
=False)
417 resolved_time
= Column(DateTime
)
418 result
= Column(UnicodeText
)
420 class UserBan_v0(declarative_base()):
421 __tablename__
= 'core__user_bans'
422 user_id
= Column('id',Integer
, ForeignKey(User
.id), nullable
=False,
424 expiration_date
= Column(DateTime
)
425 reason
= Column(UnicodeText
, nullable
=False)
427 class Privilege_v0(declarative_base()):
428 __tablename__
= 'core__privileges'
429 id = Column(Integer
, nullable
=False, primary_key
=True, unique
=True)
430 privilege_name
= Column(Unicode
, nullable
=False)
432 class PrivilegeUserAssociation_v0(declarative_base()):
433 __tablename__
= 'core__privileges_users'
435 'core__privilege_id',
442 ForeignKey(Privilege
.id),
445 @RegisterMigration(14, MIGRATIONS
)
446 def create_moderation_tables(db
):
447 ReportBase_v0
.__table__
.create(db
.bind
)
448 CommentReport_v0
.__table__
.create(db
.bind
)
449 MediaReport_v0
.__table__
.create(db
.bind
)
450 ArchivedReport_v0
.__table__
.create(db
.bind
)
451 UserBan_v0
.__table__
.create(db
.bind
)
452 Privilege_v0
.__table__
.create(db
.bind
)
453 PrivilegeUserAssociation_v0
.__table__
.create(db
.bind
)