Video media_data: Change layout in the mongo world
[mediagoblin.git] / mediagoblin / db / sql / 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
CAW
22import datetime
23
ccca0fbf
CAW
24from sqlalchemy import (
25 Column, Integer, Unicode, UnicodeText, DateTime, Boolean, ForeignKey,
26 UniqueConstraint)
88e90f41 27from sqlalchemy.orm import relationship
02db7e0a 28from sqlalchemy.orm.collections import attribute_mapped_collection
c47a03b9 29from sqlalchemy.sql.expression import desc
02db7e0a 30from sqlalchemy.ext.associationproxy import association_proxy
ccca0fbf 31
cf27accc 32from mediagoblin.db.sql.extratypes import PathTupleWithSlashes, JSONEncoded
de917303 33from mediagoblin.db.sql.base import Base, DictReadAttrProxy
feba5c52 34from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, MediaCommentMixin
ccca0fbf 35
780fdd7b
CAW
36# It's actually kind of annoying how sqlalchemy-migrate does this, if
37# I understand it right, but whatever. Anyway, don't remove this :P
38#
39# We could do migration calls more manually instead of relying on
40# this import-based meddling...
41from migrate import changeset
42
7b194a79 43
19ed039b
E
44class SimpleFieldAlias(object):
45 """An alias for any field"""
46 def __init__(self, fieldname):
47 self.fieldname = fieldname
48
49 def __get__(self, instance, cls):
50 return getattr(instance, self.fieldname)
51
52 def __set__(self, instance, val):
53 setattr(instance, self.fieldname, val)
54
55
f42e49c3 56class User(Base, UserMixin):
eea6d276
E
57 """
58 TODO: We should consider moving some rarely used fields
59 into some sort of "shadow" table.
60 """
ccca0fbf
CAW
61 __tablename__ = "users"
62
63 id = Column(Integer, primary_key=True)
64 username = Column(Unicode, nullable=False, unique=True)
65 email = Column(Unicode, nullable=False)
66 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
67 pw_hash = Column(Unicode, nullable=False)
51fba991 68 email_verified = Column(Boolean, default=False)
e365f980 69 status = Column(Unicode, default=u"needs_email_verification", nullable=False)
ccca0fbf
CAW
70 verification_key = Column(Unicode)
71 is_admin = Column(Boolean, default=False, nullable=False)
72 url = Column(Unicode)
fbad3a9f 73 bio = Column(UnicodeText) # ??
ccca0fbf 74 fp_verification_key = Column(Unicode)
7c2c56a5 75 fp_token_expire = Column(DateTime)
ccca0fbf
CAW
76
77 ## TODO
78 # plugin data would be in a separate model
79
19ed039b
E
80 _id = SimpleFieldAlias("id")
81
ccca0fbf 82
f42e49c3 83class MediaEntry(Base, MediaEntryMixin):
eea6d276
E
84 """
85 TODO: Consider fetching the media_files using join
86 """
ccca0fbf
CAW
87 __tablename__ = "media_entries"
88
89 id = Column(Integer, primary_key=True)
90 uploader = Column(Integer, ForeignKey('users.id'), nullable=False)
7c2c56a5 91 title = Column(Unicode, nullable=False)
3e907d55 92 slug = Column(Unicode)
ccca0fbf
CAW
93 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
94 description = Column(UnicodeText) # ??
ccca0fbf 95 media_type = Column(Unicode, nullable=False)
51fba991
E
96 state = Column(Unicode, default=u'unprocessed', nullable=False)
97 # or use sqlalchemy.types.Enum?
2788e6a1 98 license = Column(Unicode)
fbad3a9f 99
ccca0fbf 100 fail_error = Column(Unicode)
cf27accc 101 fail_metadata = Column(JSONEncoded)
ccca0fbf 102
02db7e0a 103 queued_media_file = Column(PathTupleWithSlashes)
ccca0fbf
CAW
104
105 queued_task_id = Column(Unicode)
106
107 __table_args__ = (
108 UniqueConstraint('uploader', 'slug'),
109 {})
110
88e90f41
E
111 get_uploader = relationship(User)
112
02db7e0a
E
113 media_files_helper = relationship("MediaFile",
114 collection_class=attribute_mapped_collection("name"),
115 cascade="all, delete-orphan"
116 )
117 media_files = association_proxy('media_files_helper', 'file_path',
fbad3a9f 118 creator=lambda k, v: MediaFile(name=k, file_path=v)
02db7e0a
E
119 )
120
35029581
E
121 attachment_files_helper = relationship("MediaAttachmentFile",
122 cascade="all, delete-orphan",
123 order_by="MediaAttachmentFile.created"
124 )
125 attachment_files = association_proxy("attachment_files_helper", "dict_view",
126 creator=lambda v: MediaAttachmentFile(
127 name=v["name"], filepath=v["filepath"])
128 )
129
de917303
E
130 tags_helper = relationship("MediaTag",
131 cascade="all, delete-orphan"
132 )
133 tags = association_proxy("tags_helper", "dict_view",
134 creator=lambda v: MediaTag(name=v["name"], slug=v["slug"])
135 )
136
ccca0fbf 137 ## TODO
ccca0fbf 138 # media_data
ccca0fbf
CAW
139 # fail_error
140
51fba991
E
141 _id = SimpleFieldAlias("id")
142
02ede858
E
143 def get_comments(self, ascending=False):
144 order_col = MediaComment.created
145 if not ascending:
146 order_col = desc(order_col)
147 return MediaComment.query.filter_by(
148 media_entry=self.id).order_by(order_col)
149
c47a03b9
E
150 def url_to_prev(self, urlgen):
151 """get the next 'newer' entry by this user"""
152 media = MediaEntry.query.filter(
153 (MediaEntry.uploader == self.uploader)
154 & (MediaEntry.state == 'processed')
155 & (MediaEntry.id > self.id)).order_by(MediaEntry.id).first()
156
157 if media is not None:
158 return media.url_for_self(urlgen)
159
160 def url_to_next(self, urlgen):
161 """get the next 'older' entry by this user"""
162 media = MediaEntry.query.filter(
163 (MediaEntry.uploader == self.uploader)
164 & (MediaEntry.state == 'processed')
165 & (MediaEntry.id < self.id)).order_by(desc(MediaEntry.id)).first()
166
167 if media is not None:
168 return media.url_for_self(urlgen)
169
ccca0fbf 170
02db7e0a 171class MediaFile(Base):
eea6d276
E
172 """
173 TODO: Highly consider moving "name" into a new table.
174 TODO: Consider preloading said table in software
175 """
02db7e0a
E
176 __tablename__ = "mediafiles"
177
178 media_entry = Column(
179 Integer, ForeignKey(MediaEntry.id),
180 nullable=False, primary_key=True)
181 name = Column(Unicode, primary_key=True)
182 file_path = Column(PathTupleWithSlashes)
183
184 def __repr__(self):
185 return "<MediaFile %s: %r>" % (self.name, self.file_path)
186
187
35029581
E
188class MediaAttachmentFile(Base):
189 __tablename__ = "core__attachment_files"
190
191 id = Column(Integer, primary_key=True)
192 media_entry = Column(
193 Integer, ForeignKey(MediaEntry.id),
194 nullable=False)
195 name = Column(Unicode, nullable=False)
196 filepath = Column(PathTupleWithSlashes)
197 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
198
199 @property
200 def dict_view(self):
201 """A dict like view on this object"""
202 return DictReadAttrProxy(self)
203
204
ccca0fbf
CAW
205class Tag(Base):
206 __tablename__ = "tags"
207
208 id = Column(Integer, primary_key=True)
209 slug = Column(Unicode, nullable=False, unique=True)
210
de917303
E
211 def __repr__(self):
212 return "<Tag %r: %r>" % (self.id, self.slug)
213
214 @classmethod
215 def find_or_new(cls, slug):
216 t = cls.query.filter_by(slug=slug).first()
217 if t is not None:
218 return t
219 return cls(slug=slug)
220
ccca0fbf
CAW
221
222class MediaTag(Base):
223 __tablename__ = "media_tags"
224
225 id = Column(Integer, primary_key=True)
ccca0fbf 226 media_entry = Column(
de917303 227 Integer, ForeignKey(MediaEntry.id),
ccca0fbf 228 nullable=False)
de917303
E
229 tag = Column(Integer, ForeignKey('tags.id'), nullable=False)
230 name = Column(Unicode)
ccca0fbf
CAW
231 # created = Column(DateTime, nullable=False, default=datetime.datetime.now)
232
233 __table_args__ = (
234 UniqueConstraint('tag', 'media_entry'),
235 {})
236
de917303
E
237 tag_helper = relationship(Tag)
238 slug = association_proxy('tag_helper', 'slug',
239 creator=Tag.find_or_new
240 )
241
6456cefa 242 def __init__(self, name=None, slug=None):
de917303 243 Base.__init__(self)
6456cefa
E
244 if name is not None:
245 self.name = name
246 if slug is not None:
247 self.tag_helper = Tag.find_or_new(slug)
de917303
E
248
249 @property
250 def dict_view(self):
251 """A dict like view on this object"""
252 return DictReadAttrProxy(self)
253
ccca0fbf 254
feba5c52 255class MediaComment(Base, MediaCommentMixin):
ccca0fbf 256 __tablename__ = "media_comments"
fbad3a9f 257
ccca0fbf
CAW
258 id = Column(Integer, primary_key=True)
259 media_entry = Column(
260 Integer, ForeignKey('media_entries.id'), nullable=False)
261 author = Column(Integer, ForeignKey('users.id'), nullable=False)
262 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
263 content = Column(UnicodeText, nullable=False)
e365f980 264
88e90f41
E
265 get_author = relationship(User)
266
51fba991
E
267 _id = SimpleFieldAlias("id")
268
e365f980 269
70b44584
CAW
270MODELS = [
271 User, MediaEntry, Tag, MediaTag, MediaComment]
272
273
274######################################################
275# Special, migrations-tracking table
276#
277# Not listed in MODELS because this is special and not
278# really migrated, but used for migrations (for now)
279######################################################
280
281class MigrationData(Base):
282 __tablename__ = "migrations"
283
bf813828 284 name = Column(Unicode, primary_key=True)
70b44584
CAW
285 version = Column(Integer, nullable=False, default=0)
286
287######################################################
288
289
eea6d276
E
290def show_table_init(engine_uri):
291 if engine_uri is None:
292 engine_uri = 'sqlite:///:memory:'
e365f980 293 from sqlalchemy import create_engine
eea6d276 294 engine = create_engine(engine_uri, echo=True)
e365f980
E
295
296 Base.metadata.create_all(engine)
297
298
299if __name__ == '__main__':
eea6d276
E
300 from sys import argv
301 print repr(argv)
302 if len(argv) == 2:
303 uri = argv[1]
304 else:
305 uri = None
306 show_table_init(uri)