Merge branch 'newlayout' into newlayout-stage
[mediagoblin.git] / mediagoblin / db / mongo / models.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 import datetime
18
19 from mongokit import Document
20
21 from mediagoblin import mg_globals
22 from mediagoblin.db.mongo import migrations
23 from mediagoblin.db.mongo.util import ASCENDING, DESCENDING, ObjectId
24 from mediagoblin.tools.pagination import Pagination
25 from mediagoblin.tools import url
26 from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, MediaCommentMixin
27
28
29 class MongoPK(object):
30 """An alias for the _id primary key"""
31 def __get__(self, instance, cls):
32 return instance['_id']
33 def __set__(self, instance, val):
34 instance['_id'] = val
35 def __delete__(self, instance):
36 del instance['_id']
37
38
39 ###################
40 # Custom validators
41 ###################
42
43 ########
44 # Models
45 ########
46
47
48 class User(Document, UserMixin):
49 """
50 A user of MediaGoblin.
51
52 Structure:
53 - username: The username of this user, should be unique to this instance.
54 - email: Email address of this user
55 - created: When the user was created
56 - plugin_data: a mapping of extra plugin information for this User.
57 Nothing uses this yet as we don't have plugins, but someday we
58 might... :)
59 - pw_hash: Hashed version of user's password.
60 - email_verified: Whether or not the user has verified their email or not.
61 Most parts of the site are disabled for users who haven't yet.
62 - status: whether or not the user is active, etc. Currently only has two
63 values, 'needs_email_verification' or 'active'. (In the future, maybe
64 we'll change this to a boolean with a key of 'active' and have a
65 separate field for a reason the user's been disabled if that's
66 appropriate... email_verified is already separate, after all.)
67 - verification_key: If the user is awaiting email verification, the user
68 will have to provide this key (which will be encoded in the presented
69 URL) in order to confirm their email as active.
70 - is_admin: Whether or not this user is an administrator or not.
71 - url: this user's personal webpage/website, if appropriate.
72 - bio: biography of this user (plaintext, in markdown)
73 """
74 __collection__ = 'users'
75 use_dot_notation = True
76
77 structure = {
78 'username': unicode,
79 'email': unicode,
80 'created': datetime.datetime,
81 'plugin_data': dict, # plugins can dump stuff here.
82 'pw_hash': unicode,
83 'email_verified': bool,
84 'status': unicode,
85 'verification_key': unicode,
86 'is_admin': bool,
87 'url': unicode,
88 'bio': unicode, # May contain markdown
89 'fp_verification_key': unicode, # forgotten password verification key
90 'fp_token_expire': datetime.datetime,
91 }
92
93 required_fields = ['username', 'created', 'pw_hash', 'email']
94
95 default_values = {
96 'created': datetime.datetime.utcnow,
97 'email_verified': False,
98 'status': u'needs_email_verification',
99 'is_admin': False}
100
101 id = MongoPK()
102
103
104 class MediaEntry(Document, MediaEntryMixin):
105 """
106 Record of a piece of media.
107
108 Structure:
109 - uploader: A reference to a User who uploaded this.
110
111 - title: Title of this work
112
113 - slug: A normalized "slug" which can be used as part of a URL to retrieve
114 this work, such as 'my-works-name-in-slug-form' may be viewable by
115 'http://mg.example.org/u/username/m/my-works-name-in-slug-form/'
116 Note that since URLs are constructed this way, slugs must be unique
117 per-uploader. (An index is provided to enforce that but code should be
118 written on the python side to ensure this as well.)
119
120 - created: Date and time of when this piece of work was uploaded.
121
122 - description: Uploader-set description of this work. This can be marked
123 up with MarkDown for slight fanciness (links, boldness, italics,
124 paragraphs...)
125
126 - media_type: What type of media is this? Currently we only support
127 'image' ;)
128
129 - media_data: Extra information that's media-format-dependent.
130 For example, images might contain some EXIF data that's not appropriate
131 to other formats. You might store it like:
132
133 mediaentry.media_data['exif'] = {
134 'manufacturer': 'CASIO',
135 'model': 'QV-4000',
136 'exposure_time': .659}
137
138 Alternately for video you might store:
139
140 # play length in seconds
141 mediaentry.media_data['play_length'] = 340
142
143 ... so what's appropriate here really depends on the media type.
144
145 - plugin_data: a mapping of extra plugin information for this User.
146 Nothing uses this yet as we don't have plugins, but someday we
147 might... :)
148
149 - tags: A list of tags. Each tag is stored as a dictionary that has a key
150 for the actual name and the normalized name-as-slug, so ultimately this
151 looks like:
152 [{'name': 'Gully Gardens',
153 'slug': 'gully-gardens'},
154 {'name': 'Castle Adventure Time?!",
155 'slug': 'castle-adventure-time'}]
156
157 - state: What's the state of this file? Active, inactive, disabled, etc...
158 But really for now there are only two states:
159 "unprocessed": uploaded but needs to go through processing for display
160 "processed": processed and able to be displayed
161
162 - license: URI for media's license.
163
164 - queued_media_file: storage interface style filepath describing a file
165 queued for processing. This is stored in the mg_globals.queue_store
166 storage system.
167
168 - queued_task_id: celery task id. Use this to fetch the task state.
169
170 - media_files: Files relevant to this that have actually been processed
171 and are available for various types of display. Stored like:
172 {'thumb': ['dir1', 'dir2', 'pic.png'}
173
174 - attachment_files: A list of "attachment" files, ones that aren't
175 critical to this piece of media but may be usefully relevant to people
176 viewing the work. (currently unused.)
177
178 - fail_error: path to the exception raised
179 - fail_metadata:
180 """
181 __collection__ = 'media_entries'
182 use_dot_notation = True
183
184 structure = {
185 'uploader': ObjectId,
186 'title': unicode,
187 'slug': unicode,
188 'created': datetime.datetime,
189 'description': unicode, # May contain markdown/up
190 'media_type': unicode,
191 'media_data': dict, # extra data relevant to this media_type
192 'plugin_data': dict, # plugins can dump stuff here.
193 'tags': [dict],
194 'state': unicode,
195 'license': unicode,
196
197 # For now let's assume there can only be one main file queued
198 # at a time
199 'queued_media_file': [unicode],
200 'queued_task_id': unicode,
201
202 # A dictionary of logical names to filepaths
203 'media_files': dict,
204
205 # The following should be lists of lists, in appropriate file
206 # record form
207 'attachment_files': list,
208
209 # If things go badly in processing things, we'll store that
210 # data here
211 'fail_error': unicode,
212 'fail_metadata': dict}
213
214 required_fields = [
215 'uploader', 'created', 'media_type', 'slug']
216
217 default_values = {
218 'created': datetime.datetime.utcnow,
219 'state': u'unprocessed'}
220
221 id = MongoPK()
222
223 def media_data_init(self, **kwargs):
224 self.media_data.update(kwargs)
225
226 def get_comments(self, ascending=False):
227 if ascending:
228 order = ASCENDING
229 else:
230 order = DESCENDING
231
232 return self.db.MediaComment.find({
233 'media_entry': self._id}).sort('created', order)
234
235 def generate_slug(self):
236 self.slug = url.slugify(self.title)
237
238 duplicate = mg_globals.database.media_entries.find_one(
239 {'slug': self.slug})
240
241 if duplicate:
242 self.slug = "%s-%s" % (self._id, self.slug)
243
244 def url_to_prev(self, urlgen):
245 """
246 Provide a url to the previous entry from this user, if there is one
247 """
248 cursor = self.db.MediaEntry.find({'_id': {"$gt": self._id},
249 'uploader': self.uploader,
250 'state': 'processed'}).sort(
251 '_id', ASCENDING).limit(1)
252 for media in cursor:
253 return media.url_for_self(urlgen)
254
255 def url_to_next(self, urlgen):
256 """
257 Provide a url to the next entry from this user, if there is one
258 """
259 cursor = self.db.MediaEntry.find({'_id': {"$lt": self._id},
260 'uploader': self.uploader,
261 'state': 'processed'}).sort(
262 '_id', DESCENDING).limit(1)
263
264 for media in cursor:
265 return media.url_for_self(urlgen)
266
267 @property
268 def get_uploader(self):
269 return self.db.User.find_one({'_id': self.uploader})
270
271
272 class MediaComment(Document, MediaCommentMixin):
273 """
274 A comment on a MediaEntry.
275
276 Structure:
277 - media_entry: The media entry this comment is attached to
278 - author: user who posted this comment
279 - created: when the comment was created
280 - content: plaintext (but markdown'able) version of the comment's content.
281 """
282
283 __collection__ = 'media_comments'
284 use_dot_notation = True
285
286 structure = {
287 'media_entry': ObjectId,
288 'author': ObjectId,
289 'created': datetime.datetime,
290 'content': unicode,
291 }
292
293 required_fields = [
294 'media_entry', 'author', 'created', 'content']
295
296 default_values = {
297 'created': datetime.datetime.utcnow}
298
299 def media_entry(self):
300 return self.db.MediaEntry.find_one({'_id': self['media_entry']})
301
302 @property
303 def get_author(self):
304 return self.db.User.find_one({'_id': self['author']})
305
306
307 REGISTER_MODELS = [
308 MediaEntry,
309 User,
310 MediaComment]
311
312
313 def register_models(connection):
314 """
315 Register all models in REGISTER_MODELS with this connection.
316 """
317 connection.register(REGISTER_MODELS)