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