Updates so that dbupdate command works
[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
de917303
E
121 tags_helper = relationship("MediaTag",
122 cascade="all, delete-orphan"
123 )
124 tags = association_proxy("tags_helper", "dict_view",
125 creator=lambda v: MediaTag(name=v["name"], slug=v["slug"])
126 )
127
ccca0fbf 128 ## TODO
ccca0fbf
CAW
129 # media_data
130 # attachment_files
131 # fail_error
132
51fba991
E
133 _id = SimpleFieldAlias("id")
134
02ede858
E
135 def get_comments(self, ascending=False):
136 order_col = MediaComment.created
137 if not ascending:
138 order_col = desc(order_col)
139 return MediaComment.query.filter_by(
140 media_entry=self.id).order_by(order_col)
141
c47a03b9
E
142 def url_to_prev(self, urlgen):
143 """get the next 'newer' entry by this user"""
144 media = MediaEntry.query.filter(
145 (MediaEntry.uploader == self.uploader)
146 & (MediaEntry.state == 'processed')
147 & (MediaEntry.id > self.id)).order_by(MediaEntry.id).first()
148
149 if media is not None:
150 return media.url_for_self(urlgen)
151
152 def url_to_next(self, urlgen):
153 """get the next 'older' entry by this user"""
154 media = MediaEntry.query.filter(
155 (MediaEntry.uploader == self.uploader)
156 & (MediaEntry.state == 'processed')
157 & (MediaEntry.id < self.id)).order_by(desc(MediaEntry.id)).first()
158
159 if media is not None:
160 return media.url_for_self(urlgen)
161
ccca0fbf 162
02db7e0a 163class MediaFile(Base):
eea6d276
E
164 """
165 TODO: Highly consider moving "name" into a new table.
166 TODO: Consider preloading said table in software
167 """
02db7e0a
E
168 __tablename__ = "mediafiles"
169
170 media_entry = Column(
171 Integer, ForeignKey(MediaEntry.id),
172 nullable=False, primary_key=True)
173 name = Column(Unicode, primary_key=True)
174 file_path = Column(PathTupleWithSlashes)
175
176 def __repr__(self):
177 return "<MediaFile %s: %r>" % (self.name, self.file_path)
178
179
ccca0fbf
CAW
180class Tag(Base):
181 __tablename__ = "tags"
182
183 id = Column(Integer, primary_key=True)
184 slug = Column(Unicode, nullable=False, unique=True)
185
de917303
E
186 def __repr__(self):
187 return "<Tag %r: %r>" % (self.id, self.slug)
188
189 @classmethod
190 def find_or_new(cls, slug):
191 t = cls.query.filter_by(slug=slug).first()
192 if t is not None:
193 return t
194 return cls(slug=slug)
195
ccca0fbf
CAW
196
197class MediaTag(Base):
198 __tablename__ = "media_tags"
199
200 id = Column(Integer, primary_key=True)
ccca0fbf 201 media_entry = Column(
de917303 202 Integer, ForeignKey(MediaEntry.id),
ccca0fbf 203 nullable=False)
de917303
E
204 tag = Column(Integer, ForeignKey('tags.id'), nullable=False)
205 name = Column(Unicode)
ccca0fbf
CAW
206 # created = Column(DateTime, nullable=False, default=datetime.datetime.now)
207
208 __table_args__ = (
209 UniqueConstraint('tag', 'media_entry'),
210 {})
211
de917303
E
212 tag_helper = relationship(Tag)
213 slug = association_proxy('tag_helper', 'slug',
214 creator=Tag.find_or_new
215 )
216
6456cefa 217 def __init__(self, name=None, slug=None):
de917303 218 Base.__init__(self)
6456cefa
E
219 if name is not None:
220 self.name = name
221 if slug is not None:
222 self.tag_helper = Tag.find_or_new(slug)
de917303
E
223
224 @property
225 def dict_view(self):
226 """A dict like view on this object"""
227 return DictReadAttrProxy(self)
228
ccca0fbf 229
feba5c52 230class MediaComment(Base, MediaCommentMixin):
ccca0fbf 231 __tablename__ = "media_comments"
fbad3a9f 232
ccca0fbf
CAW
233 id = Column(Integer, primary_key=True)
234 media_entry = Column(
235 Integer, ForeignKey('media_entries.id'), nullable=False)
236 author = Column(Integer, ForeignKey('users.id'), nullable=False)
237 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
238 content = Column(UnicodeText, nullable=False)
e365f980 239
88e90f41
E
240 get_author = relationship(User)
241
51fba991
E
242 _id = SimpleFieldAlias("id")
243
e365f980 244
70b44584
CAW
245MODELS = [
246 User, MediaEntry, Tag, MediaTag, MediaComment]
247
248
249######################################################
250# Special, migrations-tracking table
251#
252# Not listed in MODELS because this is special and not
253# really migrated, but used for migrations (for now)
254######################################################
255
256class MigrationData(Base):
257 __tablename__ = "migrations"
258
bf813828 259 name = Column(Unicode, primary_key=True)
70b44584
CAW
260 version = Column(Integer, nullable=False, default=0)
261
262######################################################
263
264
eea6d276
E
265def show_table_init(engine_uri):
266 if engine_uri is None:
267 engine_uri = 'sqlite:///:memory:'
e365f980 268 from sqlalchemy import create_engine
eea6d276 269 engine = create_engine(engine_uri, echo=True)
e365f980
E
270
271 Base.metadata.create_all(engine)
272
273
274if __name__ == '__main__':
eea6d276
E
275 from sys import argv
276 print repr(argv)
277 if len(argv) == 2:
278 uri = argv[1]
279 else:
280 uri = None
281 show_table_init(uri)