Change models to a _v0 suffix.
[mediagoblin.git] / mediagoblin / db / sql / models_v0.py
CommitLineData
7f5ae1c3
E
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
17"""
18TODO: indexes on foreignkeys, where useful.
19"""
20
21
22import datetime
23import sys
24
25from sqlalchemy import (
26 Column, Integer, Unicode, UnicodeText, DateTime, Boolean, ForeignKey,
27 UniqueConstraint, PrimaryKeyConstraint, SmallInteger)
28from sqlalchemy.orm import relationship
29from sqlalchemy.orm.collections import attribute_mapped_collection
30from sqlalchemy.sql.expression import desc
31from sqlalchemy.ext.associationproxy import association_proxy
32from sqlalchemy.util import memoized_property
33
34from mediagoblin.db.sql.extratypes import PathTupleWithSlashes, JSONEncoded
c0fddc63 35from mediagoblin.db.sql.base import GMGTableBase, DictReadAttrProxy
7f5ae1c3
E
36from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, MediaCommentMixin
37from mediagoblin.db.sql.base import Session
38
c0fddc63
E
39
40Base_v0 = declarative_base(cls=GMGTableBase)
7f5ae1c3
E
41
42
43class SimpleFieldAlias(object):
44 """An alias for any field"""
45 def __init__(self, fieldname):
46 self.fieldname = fieldname
47
48 def __get__(self, instance, cls):
49 return getattr(instance, self.fieldname)
50
51 def __set__(self, instance, val):
52 setattr(instance, self.fieldname, val)
53
54
c0fddc63 55class User(Base_v0, UserMixin):
7f5ae1c3
E
56 """
57 TODO: We should consider moving some rarely used fields
58 into some sort of "shadow" table.
59 """
60 __tablename__ = "core__users"
61
62 id = Column(Integer, primary_key=True)
63 username = Column(Unicode, nullable=False, unique=True)
64 email = Column(Unicode, nullable=False)
65 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
66 pw_hash = Column(Unicode, nullable=False)
67 email_verified = Column(Boolean, default=False)
68 status = Column(Unicode, default=u"needs_email_verification", nullable=False)
69 verification_key = Column(Unicode)
70 is_admin = Column(Boolean, default=False, nullable=False)
71 url = Column(Unicode)
72 bio = Column(UnicodeText) # ??
73 fp_verification_key = Column(Unicode)
74 fp_token_expire = Column(DateTime)
75
76 ## TODO
77 # plugin data would be in a separate model
78
79 _id = SimpleFieldAlias("id")
80
81
c0fddc63 82class MediaEntry(Base_v0, MediaEntryMixin):
7f5ae1c3
E
83 """
84 TODO: Consider fetching the media_files using join
85 """
86 __tablename__ = "core__media_entries"
87
88 id = Column(Integer, primary_key=True)
89 uploader = Column(Integer, ForeignKey(User.id), nullable=False, index=True)
90 title = Column(Unicode, nullable=False)
91 slug = Column(Unicode)
92 created = Column(DateTime, nullable=False, default=datetime.datetime.now,
93 index=True)
94 description = Column(UnicodeText) # ??
95 media_type = Column(Unicode, nullable=False)
96 state = Column(Unicode, default=u'unprocessed', nullable=False)
97 # or use sqlalchemy.types.Enum?
98 license = Column(Unicode)
99
100 fail_error = Column(Unicode)
101 fail_metadata = Column(JSONEncoded)
102
103 queued_media_file = Column(PathTupleWithSlashes)
104
105 queued_task_id = Column(Unicode)
106
107 __table_args__ = (
108 UniqueConstraint('uploader', 'slug'),
109 {})
110
111 get_uploader = relationship(User)
112
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',
118 creator=lambda k, v: MediaFile(name=k, file_path=v)
119 )
120
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
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
137 ## TODO
138 # media_data
139 # fail_error
140
141 _id = SimpleFieldAlias("id")
142
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
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
170 #@memoized_property
171 @property
172 def media_data(self):
173 session = Session()
174
175 return session.query(self.media_data_table).filter_by(
176 media_entry=self.id).first()
177
178 def media_data_init(self, **kwargs):
179 """
180 Initialize or update the contents of a media entry's media_data row
181 """
182 session = Session()
183
184 media_data = session.query(self.media_data_table).filter_by(
185 media_entry=self.id).first()
186
187 # No media data, so actually add a new one
188 if media_data is None:
189 media_data = self.media_data_table(
190 media_entry=self.id,
191 **kwargs)
192 session.add(media_data)
193 # Update old media data
194 else:
195 for field, value in kwargs.iteritems():
196 setattr(media_data, field, value)
197
198 @memoized_property
199 def media_data_table(self):
200 # TODO: memoize this
201 models_module = self.media_type + '.models'
202 __import__(models_module)
203 return sys.modules[models_module].DATA_MODEL
204
205
c0fddc63 206class FileKeynames(Base_v0):
7f5ae1c3
E
207 """
208 keywords for various places.
209 currently the MediaFile keys
210 """
211 __tablename__ = "core__file_keynames"
212 id = Column(Integer, primary_key=True)
213 name = Column(Unicode, unique=True)
214
215 def __repr__(self):
216 return "<FileKeyname %r: %r>" % (self.id, self.name)
217
218 @classmethod
219 def find_or_new(cls, name):
220 t = cls.query.filter_by(name=name).first()
221 if t is not None:
222 return t
223 return cls(name=name)
224
225
c0fddc63 226class MediaFile(Base_v0):
7f5ae1c3
E
227 """
228 TODO: Highly consider moving "name" into a new table.
229 TODO: Consider preloading said table in software
230 """
231 __tablename__ = "core__mediafiles"
232
233 media_entry = Column(
234 Integer, ForeignKey(MediaEntry.id),
235 nullable=False)
236 name_id = Column(SmallInteger, ForeignKey(FileKeynames.id), nullable=False)
237 file_path = Column(PathTupleWithSlashes)
238
239 __table_args__ = (
240 PrimaryKeyConstraint('media_entry', 'name_id'),
241 {})
242
243 def __repr__(self):
244 return "<MediaFile %s: %r>" % (self.name, self.file_path)
245
246 name_helper = relationship(FileKeynames, lazy="joined", innerjoin=True)
247 name = association_proxy('name_helper', 'name',
248 creator=FileKeynames.find_or_new
249 )
250
251
c0fddc63 252class MediaAttachmentFile(Base_v0):
7f5ae1c3
E
253 __tablename__ = "core__attachment_files"
254
255 id = Column(Integer, primary_key=True)
256 media_entry = Column(
257 Integer, ForeignKey(MediaEntry.id),
258 nullable=False)
259 name = Column(Unicode, nullable=False)
260 filepath = Column(PathTupleWithSlashes)
261 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
262
263 @property
264 def dict_view(self):
265 """A dict like view on this object"""
266 return DictReadAttrProxy(self)
267
268
c0fddc63 269class Tag(Base_v0):
7f5ae1c3
E
270 __tablename__ = "core__tags"
271
272 id = Column(Integer, primary_key=True)
273 slug = Column(Unicode, nullable=False, unique=True)
274
275 def __repr__(self):
276 return "<Tag %r: %r>" % (self.id, self.slug)
277
278 @classmethod
279 def find_or_new(cls, slug):
280 t = cls.query.filter_by(slug=slug).first()
281 if t is not None:
282 return t
283 return cls(slug=slug)
284
285
c0fddc63 286class MediaTag(Base_v0):
7f5ae1c3
E
287 __tablename__ = "core__media_tags"
288
289 id = Column(Integer, primary_key=True)
290 media_entry = Column(
291 Integer, ForeignKey(MediaEntry.id),
292 nullable=False, index=True)
293 tag = Column(Integer, ForeignKey(Tag.id), nullable=False, index=True)
294 name = Column(Unicode)
295 # created = Column(DateTime, nullable=False, default=datetime.datetime.now)
296
297 __table_args__ = (
298 UniqueConstraint('tag', 'media_entry'),
299 {})
300
301 tag_helper = relationship(Tag)
302 slug = association_proxy('tag_helper', 'slug',
303 creator=Tag.find_or_new
304 )
305
306 def __init__(self, name=None, slug=None):
c0fddc63 307 Base_v0.__init__(self)
7f5ae1c3
E
308 if name is not None:
309 self.name = name
310 if slug is not None:
311 self.tag_helper = Tag.find_or_new(slug)
312
313 @property
314 def dict_view(self):
315 """A dict like view on this object"""
316 return DictReadAttrProxy(self)
317
318
c0fddc63 319class MediaComment(Base_v0, MediaCommentMixin):
7f5ae1c3
E
320 __tablename__ = "core__media_comments"
321
322 id = Column(Integer, primary_key=True)
323 media_entry = Column(
324 Integer, ForeignKey(MediaEntry.id), nullable=False, index=True)
325 author = Column(Integer, ForeignKey(User.id), nullable=False)
326 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
327 content = Column(UnicodeText, nullable=False)
328
329 get_author = relationship(User)
330
331 _id = SimpleFieldAlias("id")
332
333
c0fddc63
E
334class ImageData(Base_v0):
335 __tablename__ = "image__mediadata"
336
337 # The primary key *and* reference to the main media_entry
338 media_entry = Column(Integer, ForeignKey('core__media_entries.id'),
339 primary_key=True)
340 get_media_entry = relationship("MediaEntry",
341 backref=backref("image__media_data", cascade="all, delete-orphan"))
342
343 width = Column(Integer)
344 height = Column(Integer)
345 exif_all = Column(JSONEncoded)
346 gps_longitude = Column(Float)
347 gps_latitude = Column(Float)
348 gps_altitude = Column(Float)
349 gps_direction = Column(Float)
350
351
352class VideoData(Base_v0):
353 __tablename__ = "video__mediadata"
354
355 # The primary key *and* reference to the main media_entry
356 media_entry = Column(Integer, ForeignKey('core__media_entries.id'),
357 primary_key=True)
358 get_media_entry = relationship("MediaEntry",
359 backref=backref("video__media_data", cascade="all, delete-orphan"))
360
361 width = Column(SmallInteger)
362 height = Column(SmallInteger)
7f5ae1c3
E
363
364
365######################################################
366# Special, migrations-tracking table
367#
368# Not listed in MODELS because this is special and not
369# really migrated, but used for migrations (for now)
370######################################################
371
c0fddc63 372class MigrationData(Base_v0):
7f5ae1c3
E
373 __tablename__ = "core__migrations"
374
375 name = Column(Unicode, primary_key=True)
376 version = Column(Integer, nullable=False, default=0)
377
378######################################################