Commit | Line | Data |
---|---|---|
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 | """ | |
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) | |
28 | from sqlalchemy.orm import relationship | |
29 | from sqlalchemy.orm.collections import attribute_mapped_collection | |
30 | from sqlalchemy.sql.expression import desc | |
31 | from sqlalchemy.ext.associationproxy import association_proxy | |
32 | from sqlalchemy.util import memoized_property | |
33 | ||
34 | from mediagoblin.db.sql.extratypes import PathTupleWithSlashes, JSONEncoded | |
c0fddc63 | 35 | from mediagoblin.db.sql.base import GMGTableBase, DictReadAttrProxy |
7f5ae1c3 E |
36 | from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, MediaCommentMixin |
37 | from mediagoblin.db.sql.base import Session | |
38 | ||
c0fddc63 E |
39 | |
40 | Base_v0 = declarative_base(cls=GMGTableBase) | |
7f5ae1c3 E |
41 | |
42 | ||
43 | class 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 | 55 | class 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 | 82 | class 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 | 206 | class 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 | 226 | class 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 | 252 | class 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 | 269 | class 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 | 286 | class 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 | 319 | class 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 |
334 | class 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 | ||
352 | class 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 | 372 | class 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 | ###################################################### |