Merge commit '9408938' from 565_workbench_cleanup (spaetz)
[mediagoblin.git] / mediagoblin / db / models_v0.py
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 """
18 TODO: indexes on foreignkeys, where useful.
19 """
20
21
22 import datetime
23 import sys
24
25 from sqlalchemy import (
26 Column, Integer, Unicode, UnicodeText, DateTime, Boolean, ForeignKey,
27 UniqueConstraint, PrimaryKeyConstraint, SmallInteger, Float)
28 from sqlalchemy.ext.declarative import declarative_base
29 from sqlalchemy.orm import relationship, backref
30 from sqlalchemy.orm.collections import attribute_mapped_collection
31 from sqlalchemy.ext.associationproxy import association_proxy
32 from sqlalchemy.util import memoized_property
33
34 from mediagoblin.db.extratypes import PathTupleWithSlashes, JSONEncoded
35 from mediagoblin.db.base import GMGTableBase, Session
36
37
38 Base_v0 = declarative_base(cls=GMGTableBase)
39
40
41 class User(Base_v0):
42 """
43 TODO: We should consider moving some rarely used fields
44 into some sort of "shadow" table.
45 """
46 __tablename__ = "core__users"
47
48 id = Column(Integer, primary_key=True)
49 username = Column(Unicode, nullable=False, unique=True)
50 email = Column(Unicode, nullable=False)
51 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
52 pw_hash = Column(Unicode, nullable=False)
53 email_verified = Column(Boolean, default=False)
54 status = Column(Unicode, default=u"needs_email_verification", nullable=False)
55 verification_key = Column(Unicode)
56 is_admin = Column(Boolean, default=False, nullable=False)
57 url = Column(Unicode)
58 bio = Column(UnicodeText) # ??
59 fp_verification_key = Column(Unicode)
60 fp_token_expire = Column(DateTime)
61
62 ## TODO
63 # plugin data would be in a separate model
64
65
66 class MediaEntry(Base_v0):
67 """
68 TODO: Consider fetching the media_files using join
69 """
70 __tablename__ = "core__media_entries"
71
72 id = Column(Integer, primary_key=True)
73 uploader = Column(Integer, ForeignKey(User.id), nullable=False, index=True)
74 title = Column(Unicode, nullable=False)
75 slug = Column(Unicode)
76 created = Column(DateTime, nullable=False, default=datetime.datetime.now,
77 index=True)
78 description = Column(UnicodeText) # ??
79 media_type = Column(Unicode, nullable=False)
80 state = Column(Unicode, default=u'unprocessed', nullable=False)
81 # or use sqlalchemy.types.Enum?
82 license = Column(Unicode)
83
84 fail_error = Column(Unicode)
85 fail_metadata = Column(JSONEncoded)
86
87 queued_media_file = Column(PathTupleWithSlashes)
88
89 queued_task_id = Column(Unicode)
90
91 __table_args__ = (
92 UniqueConstraint('uploader', 'slug'),
93 {})
94
95 get_uploader = relationship(User)
96
97 media_files_helper = relationship("MediaFile",
98 collection_class=attribute_mapped_collection("name"),
99 cascade="all, delete-orphan"
100 )
101
102 attachment_files_helper = relationship("MediaAttachmentFile",
103 cascade="all, delete-orphan",
104 order_by="MediaAttachmentFile.created"
105 )
106
107 tags_helper = relationship("MediaTag",
108 cascade="all, delete-orphan"
109 )
110
111 def media_data_init(self, **kwargs):
112 """
113 Initialize or update the contents of a media entry's media_data row
114 """
115 session = Session()
116
117 media_data = session.query(self.media_data_table).filter_by(
118 media_entry=self.id).first()
119
120 # No media data, so actually add a new one
121 if media_data is None:
122 media_data = self.media_data_table(
123 media_entry=self.id,
124 **kwargs)
125 session.add(media_data)
126 # Update old media data
127 else:
128 for field, value in kwargs.iteritems():
129 setattr(media_data, field, value)
130
131 @memoized_property
132 def media_data_table(self):
133 # TODO: memoize this
134 models_module = self.media_type + '.models'
135 __import__(models_module)
136 return sys.modules[models_module].DATA_MODEL
137
138
139 class FileKeynames(Base_v0):
140 """
141 keywords for various places.
142 currently the MediaFile keys
143 """
144 __tablename__ = "core__file_keynames"
145 id = Column(Integer, primary_key=True)
146 name = Column(Unicode, unique=True)
147
148 def __repr__(self):
149 return "<FileKeyname %r: %r>" % (self.id, self.name)
150
151 @classmethod
152 def find_or_new(cls, name):
153 t = cls.query.filter_by(name=name).first()
154 if t is not None:
155 return t
156 return cls(name=name)
157
158
159 class MediaFile(Base_v0):
160 """
161 TODO: Highly consider moving "name" into a new table.
162 TODO: Consider preloading said table in software
163 """
164 __tablename__ = "core__mediafiles"
165
166 media_entry = Column(
167 Integer, ForeignKey(MediaEntry.id),
168 nullable=False)
169 name_id = Column(SmallInteger, ForeignKey(FileKeynames.id), nullable=False)
170 file_path = Column(PathTupleWithSlashes)
171
172 __table_args__ = (
173 PrimaryKeyConstraint('media_entry', 'name_id'),
174 {})
175
176 def __repr__(self):
177 return "<MediaFile %s: %r>" % (self.name, self.file_path)
178
179 name_helper = relationship(FileKeynames, lazy="joined", innerjoin=True)
180 name = association_proxy('name_helper', 'name',
181 creator=FileKeynames.find_or_new
182 )
183
184
185 class MediaAttachmentFile(Base_v0):
186 __tablename__ = "core__attachment_files"
187
188 id = Column(Integer, primary_key=True)
189 media_entry = Column(
190 Integer, ForeignKey(MediaEntry.id),
191 nullable=False)
192 name = Column(Unicode, nullable=False)
193 filepath = Column(PathTupleWithSlashes)
194 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
195
196
197 class Tag(Base_v0):
198 __tablename__ = "core__tags"
199
200 id = Column(Integer, primary_key=True)
201 slug = Column(Unicode, nullable=False, unique=True)
202
203 def __repr__(self):
204 return "<Tag %r: %r>" % (self.id, self.slug)
205
206 @classmethod
207 def find_or_new(cls, slug):
208 t = cls.query.filter_by(slug=slug).first()
209 if t is not None:
210 return t
211 return cls(slug=slug)
212
213
214 class MediaTag(Base_v0):
215 __tablename__ = "core__media_tags"
216
217 id = Column(Integer, primary_key=True)
218 media_entry = Column(
219 Integer, ForeignKey(MediaEntry.id),
220 nullable=False, index=True)
221 tag = Column(Integer, ForeignKey(Tag.id), nullable=False, index=True)
222 name = Column(Unicode)
223 # created = Column(DateTime, nullable=False, default=datetime.datetime.now)
224
225 __table_args__ = (
226 UniqueConstraint('tag', 'media_entry'),
227 {})
228
229 tag_helper = relationship(Tag)
230 slug = association_proxy('tag_helper', 'slug',
231 creator=Tag.find_or_new
232 )
233
234 def __init__(self, name=None, slug=None):
235 Base_v0.__init__(self)
236 if name is not None:
237 self.name = name
238 if slug is not None:
239 self.tag_helper = Tag.find_or_new(slug)
240
241
242 class MediaComment(Base_v0):
243 __tablename__ = "core__media_comments"
244
245 id = Column(Integer, primary_key=True)
246 media_entry = Column(
247 Integer, ForeignKey(MediaEntry.id), nullable=False, index=True)
248 author = Column(Integer, ForeignKey(User.id), nullable=False)
249 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
250 content = Column(UnicodeText, nullable=False)
251
252 get_author = relationship(User)
253
254
255 class ImageData(Base_v0):
256 __tablename__ = "image__mediadata"
257
258 # The primary key *and* reference to the main media_entry
259 media_entry = Column(Integer, ForeignKey('core__media_entries.id'),
260 primary_key=True)
261 get_media_entry = relationship("MediaEntry",
262 backref=backref("image__media_data", cascade="all, delete-orphan"))
263
264 width = Column(Integer)
265 height = Column(Integer)
266 exif_all = Column(JSONEncoded)
267 gps_longitude = Column(Float)
268 gps_latitude = Column(Float)
269 gps_altitude = Column(Float)
270 gps_direction = Column(Float)
271
272
273 class VideoData(Base_v0):
274 __tablename__ = "video__mediadata"
275
276 # The primary key *and* reference to the main media_entry
277 media_entry = Column(Integer, ForeignKey('core__media_entries.id'),
278 primary_key=True)
279 get_media_entry = relationship("MediaEntry",
280 backref=backref("video__media_data", cascade="all, delete-orphan"))
281
282 width = Column(SmallInteger)
283 height = Column(SmallInteger)
284
285
286 class AsciiData(Base_v0):
287 __tablename__ = "ascii__mediadata"
288
289 # The primary key *and* reference to the main media_entry
290 media_entry = Column(Integer, ForeignKey('core__media_entries.id'),
291 primary_key=True)
292 get_media_entry = relationship("MediaEntry",
293 backref=backref("ascii__media_data", cascade="all, delete-orphan"))
294
295
296 class AudioData(Base_v0):
297 __tablename__ = "audio__mediadata"
298
299 # The primary key *and* reference to the main media_entry
300 media_entry = Column(Integer, ForeignKey('core__media_entries.id'),
301 primary_key=True)
302 get_media_entry = relationship("MediaEntry",
303 backref=backref("audio__media_data", cascade="all, delete-orphan"))
304
305
306 ######################################################
307 # Special, migrations-tracking table
308 #
309 # Not listed in MODELS because this is special and not
310 # really migrated, but used for migrations (for now)
311 ######################################################
312
313 class MigrationData(Base_v0):
314 __tablename__ = "core__migrations"
315
316 name = Column(Unicode, primary_key=True)
317 version = Column(Integer, nullable=False, default=0)
318
319 ######################################################