Move db.sql.models* to db.models*
[mediagoblin.git] / mediagoblin / db / models.py
CommitLineData
fbad3a9f 1# GNU MediaGoblin -- federated, autonomous media hosting
7f4ebeed 2# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
fbad3a9f
E
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
eea6d276
E
17"""
18TODO: indexes on foreignkeys, where useful.
19"""
20
fbad3a9f 21
ccca0fbf 22import datetime
007ac2e7 23import sys
ccca0fbf 24
942084fb
JW
25from sqlalchemy import Column, Integer, Unicode, UnicodeText, DateTime, \
26 Boolean, ForeignKey, UniqueConstraint, PrimaryKeyConstraint, \
27 SmallInteger
28from sqlalchemy.orm import relationship, backref
02db7e0a 29from sqlalchemy.orm.collections import attribute_mapped_collection
c47a03b9 30from sqlalchemy.sql.expression import desc
02db7e0a 31from sqlalchemy.ext.associationproxy import association_proxy
007ac2e7 32from sqlalchemy.util import memoized_property
ccca0fbf 33
cf27accc 34from mediagoblin.db.sql.extratypes import PathTupleWithSlashes, JSONEncoded
de917303 35from mediagoblin.db.sql.base import Base, DictReadAttrProxy
be5be115 36from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, MediaCommentMixin, CollectionMixin, CollectionItemMixin
007ac2e7 37from mediagoblin.db.sql.base import Session
ccca0fbf 38
780fdd7b
CAW
39# It's actually kind of annoying how sqlalchemy-migrate does this, if
40# I understand it right, but whatever. Anyway, don't remove this :P
c4869eff 41#
780fdd7b
CAW
42# We could do migration calls more manually instead of relying on
43# this import-based meddling...
44from migrate import changeset
45
7b194a79 46
f42e49c3 47class User(Base, UserMixin):
eea6d276
E
48 """
49 TODO: We should consider moving some rarely used fields
50 into some sort of "shadow" table.
51 """
2f5ce68c 52 __tablename__ = "core__users"
ccca0fbf
CAW
53
54 id = Column(Integer, primary_key=True)
55 username = Column(Unicode, nullable=False, unique=True)
56 email = Column(Unicode, nullable=False)
57 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
58 pw_hash = Column(Unicode, nullable=False)
51fba991 59 email_verified = Column(Boolean, default=False)
e365f980 60 status = Column(Unicode, default=u"needs_email_verification", nullable=False)
c4869eff
JW
61 # Intented to be nullable=False, but migrations would not work for it
62 # set to nullable=True implicitly.
63 wants_comment_notification = Column(Boolean, default=True)
ccca0fbf
CAW
64 verification_key = Column(Unicode)
65 is_admin = Column(Boolean, default=False, nullable=False)
66 url = Column(Unicode)
fbad3a9f 67 bio = Column(UnicodeText) # ??
ccca0fbf 68 fp_verification_key = Column(Unicode)
7c2c56a5 69 fp_token_expire = Column(DateTime)
ccca0fbf
CAW
70
71 ## TODO
72 # plugin data would be in a separate model
73
88a9662b
JW
74 def __repr__(self):
75 return '<{0} #{1} {2} {3} "{4}">'.format(
76 self.__class__.__name__,
77 self.id,
78 'verified' if self.email_verified else 'non-verified',
79 'admin' if self.is_admin else 'user',
80 self.username)
81
ccca0fbf 82
f42e49c3 83class MediaEntry(Base, MediaEntryMixin):
eea6d276
E
84 """
85 TODO: Consider fetching the media_files using join
86 """
2f5ce68c 87 __tablename__ = "core__media_entries"
ccca0fbf
CAW
88
89 id = Column(Integer, primary_key=True)
ecd538bb 90 uploader = Column(Integer, ForeignKey(User.id), nullable=False, index=True)
7c2c56a5 91 title = Column(Unicode, nullable=False)
3e907d55 92 slug = Column(Unicode)
ecd538bb
E
93 created = Column(DateTime, nullable=False, default=datetime.datetime.now,
94 index=True)
ccca0fbf 95 description = Column(UnicodeText) # ??
ccca0fbf 96 media_type = Column(Unicode, nullable=False)
51fba991
E
97 state = Column(Unicode, default=u'unprocessed', nullable=False)
98 # or use sqlalchemy.types.Enum?
2788e6a1 99 license = Column(Unicode)
be5be115 100 collected = Column(Integer, default=0)
fbad3a9f 101
ccca0fbf 102 fail_error = Column(Unicode)
cf27accc 103 fail_metadata = Column(JSONEncoded)
ccca0fbf 104
64712915
JW
105 transcoding_progress = Column(SmallInteger)
106
02db7e0a 107 queued_media_file = Column(PathTupleWithSlashes)
ccca0fbf
CAW
108
109 queued_task_id = Column(Unicode)
110
111 __table_args__ = (
112 UniqueConstraint('uploader', 'slug'),
113 {})
114
88e90f41
E
115 get_uploader = relationship(User)
116
02db7e0a
E
117 media_files_helper = relationship("MediaFile",
118 collection_class=attribute_mapped_collection("name"),
119 cascade="all, delete-orphan"
120 )
121 media_files = association_proxy('media_files_helper', 'file_path',
fbad3a9f 122 creator=lambda k, v: MediaFile(name=k, file_path=v)
02db7e0a
E
123 )
124
35029581
E
125 attachment_files_helper = relationship("MediaAttachmentFile",
126 cascade="all, delete-orphan",
127 order_by="MediaAttachmentFile.created"
128 )
129 attachment_files = association_proxy("attachment_files_helper", "dict_view",
130 creator=lambda v: MediaAttachmentFile(
131 name=v["name"], filepath=v["filepath"])
132 )
133
de917303
E
134 tags_helper = relationship("MediaTag",
135 cascade="all, delete-orphan"
136 )
137 tags = association_proxy("tags_helper", "dict_view",
138 creator=lambda v: MediaTag(name=v["name"], slug=v["slug"])
139 )
140
be5be115
AW
141 collections_helper = relationship("CollectionItem",
142 cascade="all, delete-orphan"
143 )
144 collections = association_proxy("collections_helper", "in_collection")
145
ccca0fbf 146 ## TODO
ccca0fbf 147 # media_data
ccca0fbf
CAW
148 # fail_error
149
02ede858
E
150 def get_comments(self, ascending=False):
151 order_col = MediaComment.created
152 if not ascending:
153 order_col = desc(order_col)
154 return MediaComment.query.filter_by(
155 media_entry=self.id).order_by(order_col)
156
c47a03b9
E
157 def url_to_prev(self, urlgen):
158 """get the next 'newer' entry by this user"""
159 media = MediaEntry.query.filter(
160 (MediaEntry.uploader == self.uploader)
5bd0adeb 161 & (MediaEntry.state == u'processed')
c47a03b9
E
162 & (MediaEntry.id > self.id)).order_by(MediaEntry.id).first()
163
164 if media is not None:
165 return media.url_for_self(urlgen)
166
167 def url_to_next(self, urlgen):
168 """get the next 'older' entry by this user"""
169 media = MediaEntry.query.filter(
170 (MediaEntry.uploader == self.uploader)
5bd0adeb 171 & (MediaEntry.state == u'processed')
c47a03b9
E
172 & (MediaEntry.id < self.id)).order_by(desc(MediaEntry.id)).first()
173
174 if media is not None:
175 return media.url_for_self(urlgen)
176
007ac2e7 177 #@memoized_property
5fe1fd07
E
178 @property
179 def media_data(self):
007ac2e7
CAW
180 session = Session()
181
182 return session.query(self.media_data_table).filter_by(
729424be 183 media_entry=self.id).first()
5fe1fd07 184
acb21949 185 def media_data_init(self, **kwargs):
007ac2e7
CAW
186 """
187 Initialize or update the contents of a media entry's media_data row
188 """
189 session = Session()
190
191 media_data = session.query(self.media_data_table).filter_by(
192 media_entry=self.id).first()
193
194 # No media data, so actually add a new one
99c2f9f0 195 if media_data is None:
007ac2e7 196 media_data = self.media_data_table(
99c2f9f0 197 media_entry=self.id,
007ac2e7
CAW
198 **kwargs)
199 session.add(media_data)
200 # Update old media data
201 else:
202 for field, value in kwargs.iteritems():
203 setattr(media_data, field, value)
204
205 @memoized_property
206 def media_data_table(self):
207 # TODO: memoize this
208 models_module = self.media_type + '.models'
209 __import__(models_module)
210 return sys.modules[models_module].DATA_MODEL
acb21949 211
64712915 212 def __repr__(self):
79f28e0b
JW
213 safe_title = self.title.encode('ascii', 'replace')
214
64712915
JW
215 return '<{classname} {id}: {title}>'.format(
216 classname=self.__class__.__name__,
217 id=self.id,
79f28e0b 218 title=safe_title)
64712915 219
ccca0fbf 220
a9dac7c8
E
221class FileKeynames(Base):
222 """
223 keywords for various places.
224 currently the MediaFile keys
225 """
226 __tablename__ = "core__file_keynames"
227 id = Column(Integer, primary_key=True)
228 name = Column(Unicode, unique=True)
229
230 def __repr__(self):
231 return "<FileKeyname %r: %r>" % (self.id, self.name)
232
233 @classmethod
234 def find_or_new(cls, name):
235 t = cls.query.filter_by(name=name).first()
236 if t is not None:
237 return t
238 return cls(name=name)
239
240
02db7e0a 241class MediaFile(Base):
eea6d276
E
242 """
243 TODO: Highly consider moving "name" into a new table.
244 TODO: Consider preloading said table in software
245 """
2f5ce68c 246 __tablename__ = "core__mediafiles"
02db7e0a
E
247
248 media_entry = Column(
249 Integer, ForeignKey(MediaEntry.id),
a9dac7c8
E
250 nullable=False)
251 name_id = Column(SmallInteger, ForeignKey(FileKeynames.id), nullable=False)
02db7e0a
E
252 file_path = Column(PathTupleWithSlashes)
253
a9dac7c8
E
254 __table_args__ = (
255 PrimaryKeyConstraint('media_entry', 'name_id'),
256 {})
257
02db7e0a
E
258 def __repr__(self):
259 return "<MediaFile %s: %r>" % (self.name, self.file_path)
260
a9dac7c8
E
261 name_helper = relationship(FileKeynames, lazy="joined", innerjoin=True)
262 name = association_proxy('name_helper', 'name',
263 creator=FileKeynames.find_or_new
264 )
265
02db7e0a 266
35029581
E
267class MediaAttachmentFile(Base):
268 __tablename__ = "core__attachment_files"
269
270 id = Column(Integer, primary_key=True)
271 media_entry = Column(
272 Integer, ForeignKey(MediaEntry.id),
273 nullable=False)
274 name = Column(Unicode, nullable=False)
275 filepath = Column(PathTupleWithSlashes)
276 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
277
278 @property
279 def dict_view(self):
280 """A dict like view on this object"""
281 return DictReadAttrProxy(self)
282
283
ccca0fbf 284class Tag(Base):
2f5ce68c 285 __tablename__ = "core__tags"
ccca0fbf
CAW
286
287 id = Column(Integer, primary_key=True)
288 slug = Column(Unicode, nullable=False, unique=True)
289
de917303
E
290 def __repr__(self):
291 return "<Tag %r: %r>" % (self.id, self.slug)
292
293 @classmethod
294 def find_or_new(cls, slug):
295 t = cls.query.filter_by(slug=slug).first()
296 if t is not None:
297 return t
298 return cls(slug=slug)
299
ccca0fbf
CAW
300
301class MediaTag(Base):
2f5ce68c 302 __tablename__ = "core__media_tags"
ccca0fbf
CAW
303
304 id = Column(Integer, primary_key=True)
ccca0fbf 305 media_entry = Column(
de917303 306 Integer, ForeignKey(MediaEntry.id),
ecd538bb
E
307 nullable=False, index=True)
308 tag = Column(Integer, ForeignKey(Tag.id), nullable=False, index=True)
de917303 309 name = Column(Unicode)
ccca0fbf
CAW
310 # created = Column(DateTime, nullable=False, default=datetime.datetime.now)
311
312 __table_args__ = (
313 UniqueConstraint('tag', 'media_entry'),
314 {})
315
de917303
E
316 tag_helper = relationship(Tag)
317 slug = association_proxy('tag_helper', 'slug',
318 creator=Tag.find_or_new
319 )
320
6456cefa 321 def __init__(self, name=None, slug=None):
de917303 322 Base.__init__(self)
6456cefa
E
323 if name is not None:
324 self.name = name
325 if slug is not None:
326 self.tag_helper = Tag.find_or_new(slug)
de917303
E
327
328 @property
329 def dict_view(self):
330 """A dict like view on this object"""
331 return DictReadAttrProxy(self)
332
ccca0fbf 333
feba5c52 334class MediaComment(Base, MediaCommentMixin):
2f5ce68c 335 __tablename__ = "core__media_comments"
fbad3a9f 336
ccca0fbf
CAW
337 id = Column(Integer, primary_key=True)
338 media_entry = Column(
ecd538bb
E
339 Integer, ForeignKey(MediaEntry.id), nullable=False, index=True)
340 author = Column(Integer, ForeignKey(User.id), nullable=False)
ccca0fbf
CAW
341 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
342 content = Column(UnicodeText, nullable=False)
e365f980 343
88e90f41
E
344 get_author = relationship(User)
345
e365f980 346
be5be115
AW
347class Collection(Base, CollectionMixin):
348 __tablename__ = "core__collections"
349
350 id = Column(Integer, primary_key=True)
351 title = Column(Unicode, nullable=False)
352 slug = Column(Unicode)
353 created = Column(DateTime, nullable=False, default=datetime.datetime.now,
354 index=True)
88a9662b 355 description = Column(UnicodeText)
be5be115
AW
356 creator = Column(Integer, ForeignKey(User.id), nullable=False)
357 items = Column(Integer, default=0)
358
359 get_creator = relationship(User)
88a9662b 360
be5be115
AW
361 def get_collection_items(self, ascending=False):
362 order_col = CollectionItem.position
363 if not ascending:
364 order_col = desc(order_col)
365 return CollectionItem.query.filter_by(
366 collection=self.id).order_by(order_col)
367
be5be115
AW
368
369class CollectionItem(Base, CollectionItemMixin):
370 __tablename__ = "core__collection_items"
371
372 id = Column(Integer, primary_key=True)
373 media_entry = Column(
374 Integer, ForeignKey(MediaEntry.id), nullable=False, index=True)
375 collection = Column(Integer, ForeignKey(Collection.id), nullable=False)
376 note = Column(UnicodeText, nullable=True)
377 added = Column(DateTime, nullable=False, default=datetime.datetime.now)
378 position = Column(Integer)
379 in_collection = relationship("Collection")
380
381 get_media_entry = relationship(MediaEntry)
382
be5be115
AW
383 __table_args__ = (
384 UniqueConstraint('collection', 'media_entry'),
385 {})
386
387 @property
388 def dict_view(self):
389 """A dict like view on this object"""
390 return DictReadAttrProxy(self)
391
392
5354f954
JW
393class ProcessingMetaData(Base):
394 __tablename__ = 'core__processing_metadata'
395
396 id = Column(Integer, primary_key=True)
397 media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False,
398 index=True)
942084fb
JW
399 media_entry = relationship(MediaEntry,
400 backref=backref('processing_metadata',
401 cascade='all, delete-orphan'))
5354f954
JW
402 callback_url = Column(Unicode)
403
404 @property
405 def dict_view(self):
406 """A dict like view on this object"""
407 return DictReadAttrProxy(self)
408
409
70b44584 410MODELS = [
be5be115 411 User, MediaEntry, Tag, MediaTag, MediaComment, Collection, CollectionItem, MediaFile, FileKeynames,
5354f954 412 MediaAttachmentFile, ProcessingMetaData]
70b44584
CAW
413
414
415######################################################
416# Special, migrations-tracking table
417#
418# Not listed in MODELS because this is special and not
419# really migrated, but used for migrations (for now)
420######################################################
421
422class MigrationData(Base):
2f5ce68c 423 __tablename__ = "core__migrations"
70b44584 424
bf813828 425 name = Column(Unicode, primary_key=True)
70b44584
CAW
426 version = Column(Integer, nullable=False, default=0)
427
428######################################################
429
430
eea6d276
E
431def show_table_init(engine_uri):
432 if engine_uri is None:
433 engine_uri = 'sqlite:///:memory:'
e365f980 434 from sqlalchemy import create_engine
eea6d276 435 engine = create_engine(engine_uri, echo=True)
e365f980
E
436
437 Base.metadata.create_all(engine)
438
439
440if __name__ == '__main__':
eea6d276
E
441 from sys import argv
442 print repr(argv)
443 if len(argv) == 2:
444 uri = argv[1]
445 else:
446 uri = None
447 show_table_init(uri)