Rename MediaEntry.uploader() to .get_uploader()
[mediagoblin.git] / mediagoblin / db / models.py
CommitLineData
8e1e744d 1# GNU MediaGoblin -- federated, autonomous media hosting
12a100e4 2# Copyright (C) 2011 MediaGoblin contributors. See AUTHORS.
e5572c60
ML
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
243c3843
NY
17import datetime
18import uuid
4ad5af85 19
c2ddd85e 20from mongokit import Document
4329be14 21
4ad5af85 22from mediagoblin.auth import lib as auth_lib
6e7ce8d1 23from mediagoblin import mg_globals
757f37a5 24from mediagoblin.db import migrations
9c0fe63f 25from mediagoblin.db.util import ASCENDING, DESCENDING, ObjectId
152a3bfa
AW
26from mediagoblin.tools.pagination import Pagination
27from mediagoblin.tools import url, common
d232e0f6 28
7bf3f5db
CAW
29###################
30# Custom validators
31###################
32
33########
34# Models
35########
36
37
d232e0f6 38class User(Document):
16bcd1e7
CAW
39 """
40 A user of MediaGoblin.
41
42 Structure:
43 - username: The username of this user, should be unique to this instance.
44 - email: Email address of this user
45 - created: When the user was created
46 - plugin_data: a mapping of extra plugin information for this User.
47 Nothing uses this yet as we don't have plugins, but someday we
48 might... :)
49 - pw_hash: Hashed version of user's password.
50 - email_verified: Whether or not the user has verified their email or not.
51 Most parts of the site are disabled for users who haven't yet.
52 - status: whether or not the user is active, etc. Currently only has two
53 values, 'needs_email_verification' or 'active'. (In the future, maybe
54 we'll change this to a boolean with a key of 'active' and have a
55 separate field for a reason the user's been disabled if that's
56 appropriate... email_verified is already separate, after all.)
57 - verification_key: If the user is awaiting email verification, the user
58 will have to provide this key (which will be encoded in the presented
59 URL) in order to confirm their email as active.
60 - is_admin: Whether or not this user is an administrator or not.
61 - url: this user's personal webpage/website, if appropriate.
62 - bio: biography of this user (plaintext, in markdown)
63 - bio_html: biography of the user converted to proper HTML.
64 """
73a6e206 65 __collection__ = 'users'
7cbddc96 66 use_dot_notation = True
73a6e206 67
d232e0f6
CAW
68 structure = {
69 'username': unicode,
24181820 70 'email': unicode,
d232e0f6 71 'created': datetime.datetime,
243c3843 72 'plugin_data': dict, # plugins can dump stuff here.
d232e0f6 73 'pw_hash': unicode,
24181820 74 'email_verified': bool,
4d75522b 75 'status': unicode,
18cf34d4
CAW
76 'verification_key': unicode,
77 'is_admin': bool,
243c3843
NY
78 'url': unicode,
79 'bio': unicode, # May contain markdown
80 'bio_html': unicode, # May contain plaintext, or HTML
81 'fp_verification_key': unicode, # forgotten password verification key
82 'fp_token_expire': datetime.datetime,
d232e0f6
CAW
83 }
84
db5912e3 85 required_fields = ['username', 'created', 'pw_hash', 'email']
fc9bb821
CAW
86
87 default_values = {
24181820 88 'created': datetime.datetime.utcnow,
4d75522b 89 'email_verified': False,
db1a438f 90 'status': u'needs_email_verification',
18cf34d4
CAW
91 'verification_key': lambda: unicode(uuid.uuid4()),
92 'is_admin': False}
080a81ec 93
4ad5af85
CAW
94 def check_login(self, password):
95 """
96 See if a user can login with this password
97 """
98 return auth_lib.bcrypt_check_password(
99 password, self['pw_hash'])
100
d232e0f6 101
4d75522b 102class MediaEntry(Document):
080a81ec
CAW
103 """
104 Record of a piece of media.
105
106 Structure:
107 - uploader: A reference to a User who uploaded this.
108
109 - title: Title of this work
110
111 - slug: A normalized "slug" which can be used as part of a URL to retrieve
112 this work, such as 'my-works-name-in-slug-form' may be viewable by
113 'http://mg.example.org/u/username/m/my-works-name-in-slug-form/'
114 Note that since URLs are constructed this way, slugs must be unique
115 per-uploader. (An index is provided to enforce that but code should be
116 written on the python side to ensure this as well.)
117
118 - created: Date and time of when this piece of work was uploaded.
119
120 - description: Uploader-set description of this work. This can be marked
121 up with MarkDown for slight fanciness (links, boldness, italics,
122 paragraphs...)
123
124 - description_html: Rendered version of the description, run through
125 Markdown and cleaned with our cleaning tool.
126
127 - media_type: What type of media is this? Currently we only support
128 'image' ;)
129
130 - media_data: Extra information that's media-format-dependent.
131 For example, images might contain some EXIF data that's not appropriate
132 to other formats. You might store it like:
133
134 mediaentry['media_data']['exif'] = {
135 'manufacturer': 'CASIO',
136 'model': 'QV-4000',
137 'exposure_time': .659}
138
139 Alternately for video you might store:
140
141 # play length in seconds
142 mediaentry['media_data']['play_length'] = 340
143
144 ... so what's appropriate here really depends on the media type.
145
146 - plugin_data: a mapping of extra plugin information for this User.
147 Nothing uses this yet as we don't have plugins, but someday we
148 might... :)
149
150 - tags: A list of tags. Each tag is stored as a dictionary that has a key
151 for the actual name and the normalized name-as-slug, so ultimately this
152 looks like:
153 [{'name': 'Gully Gardens',
154 'slug': 'gully-gardens'},
155 {'name': 'Castle Adventure Time?!",
156 'slug': 'castle-adventure-time'}]
157
158 - state: What's the state of this file? Active, inactive, disabled, etc...
159 But really for now there are only two states:
160 "unprocessed": uploaded but needs to go through processing for display
161 "processed": processed and able to be displayed
162
163 - queued_media_file: storage interface style filepath describing a file
164 queued for processing. This is stored in the mg_globals.queue_store
165 storage system.
166
6b9ee0ca
CAW
167 - queued_task_id: celery task id. Use this to fetch the task state.
168
080a81ec
CAW
169 - media_files: Files relevant to this that have actually been processed
170 and are available for various types of display. Stored like:
171 {'thumb': ['dir1', 'dir2', 'pic.png'}
172
173 - attachment_files: A list of "attachment" files, ones that aren't
174 critical to this piece of media but may be usefully relevant to people
175 viewing the work. (currently unused.)
6c50c210 176
243c3843
NY
177 - fail_error: path to the exception raised
178 - fail_metadata:
080a81ec 179 """
4d75522b 180 __collection__ = 'media_entries'
7cbddc96 181 use_dot_notation = True
4d75522b
CAW
182
183 structure = {
757f37a5 184 'uploader': ObjectId,
4d75522b 185 'title': unicode,
1013bdaf 186 'slug': unicode,
4d75522b 187 'created': datetime.datetime,
243c3843
NY
188 'description': unicode, # May contain markdown/up
189 'description_html': unicode, # May contain plaintext, or HTML
4d75522b 190 'media_type': unicode,
243c3843
NY
191 'media_data': dict, # extra data relevant to this media_type
192 'plugin_data': dict, # plugins can dump stuff here.
0712a06d 193 'tags': [dict],
74ae6b11
CAW
194 'state': unicode,
195
fa7f9c61
CAW
196 # For now let's assume there can only be one main file queued
197 # at a time
198 'queued_media_file': [unicode],
6b9ee0ca 199 'queued_task_id': unicode,
fa7f9c61
CAW
200
201 # A dictionary of logical names to filepaths
202 'media_files': dict,
203
74ae6b11
CAW
204 # The following should be lists of lists, in appropriate file
205 # record form
6c50c210
CAW
206 'attachment_files': list,
207
208 # If things go badly in processing things, we'll store that
209 # data here
210 'fail_error': unicode,
211 'fail_metadata': dict}
4d75522b
CAW
212
213 required_fields = [
b1ae76ae 214 'uploader', 'created', 'media_type', 'slug']
4d75522b
CAW
215
216 default_values = {
74ae6b11
CAW
217 'created': datetime.datetime.utcnow,
218 'state': u'unprocessed'}
4d75522b 219
e62fc611
PUS
220 def get_comments(self, ascending=False):
221 if ascending:
222 order = ASCENDING
223 else:
224 order = DESCENDING
225
6f59a3a3 226 return self.db.MediaComment.find({
e62fc611 227 'media_entry': self._id}).sort('created', order)
6f59a3a3 228
243c3843 229 def get_display_media(self, media_map,
ee91c2b8 230 fetch_order=common.DISPLAY_IMAGE_FETCHING_ORDER):
2c9e635a
JW
231 """
232 Find the best media for display.
233
234 Args:
235 - media_map: a dict like
236 {u'image_size': [u'dir1', u'dir2', u'image.jpg']}
237 - fetch_order: the order we should try fetching images in
238
239 Returns:
240 (media_size, media_path)
241 """
242 media_sizes = media_map.keys()
380ac094 243
152a3bfa 244 for media_size in common.DISPLAY_IMAGE_FETCHING_ORDER:
2c9e635a
JW
245 if media_size in media_sizes:
246 return media_map[media_size]
247
4d75522b
CAW
248 def main_mediafile(self):
249 pass
6f59a3a3 250
0546833c 251 def generate_slug(self):
ae3bc7fa 252 self['slug'] = url.slugify(self['title'])
0546833c 253
6e7ce8d1 254 duplicate = mg_globals.database.media_entries.find_one(
f0545dde 255 {'slug': self['slug']})
080a81ec 256
0546833c 257 if duplicate:
eabe6b67 258 self['slug'] = "%s-%s" % (self._id, self['slug'])
4d75522b 259
6926b23d
CAW
260 def url_for_self(self, urlgen):
261 """
262 Generate an appropriate url for ourselves
263
264 Use a slug if we have one, else use our '_id'.
265 """
30188321 266 uploader = self.get_uploader()
16509be1 267
6926b23d
CAW
268 if self.get('slug'):
269 return urlgen(
270 'mediagoblin.user_pages.media_home',
16509be1 271 user=uploader['username'],
6926b23d
CAW
272 media=self['slug'])
273 else:
274 return urlgen(
275 'mediagoblin.user_pages.media_home',
16509be1 276 user=uploader['username'],
eabe6b67 277 media=unicode(self._id))
080a81ec 278
9c0fe63f
CFD
279 def url_to_prev(self, urlgen):
280 """
281 Provide a url to the previous entry from this user, if there is one
282 """
eabe6b67 283 cursor = self.db.MediaEntry.find({'_id': {"$gt": self._id},
ce2ac488
CFD
284 'uploader': self['uploader'],
285 'state': 'processed'}).sort(
77b95801 286 '_id', ASCENDING).limit(1)
9c0fe63f
CFD
287 if cursor.count():
288 return urlgen('mediagoblin.user_pages.media_home',
30188321 289 user=self.get_uploader()['username'],
b1db2c2e 290 media=unicode(cursor[0]['slug']))
080a81ec 291
9c0fe63f
CFD
292 def url_to_next(self, urlgen):
293 """
294 Provide a url to the next entry from this user, if there is one
295 """
eabe6b67 296 cursor = self.db.MediaEntry.find({'_id': {"$lt": self._id},
ce2ac488
CFD
297 'uploader': self['uploader'],
298 'state': 'processed'}).sort(
77b95801 299 '_id', DESCENDING).limit(1)
9c0fe63f
CFD
300
301 if cursor.count():
302 return urlgen('mediagoblin.user_pages.media_home',
30188321 303 user=self.get_uploader()['username'],
b1db2c2e 304 media=unicode(cursor[0]['slug']))
6926b23d 305
30188321 306 def get_uploader(self):
16509be1
CAW
307 return self.db.User.find_one({'_id': self['uploader']})
308
6ee9c719
CAW
309 def get_fail_exception(self):
310 """
311 Get the exception that's appropriate for this error
312 """
313 if self['fail_error']:
152a3bfa 314 return common.import_component(self['fail_error'])
6ee9c719 315
b27ec167 316
c11f21ab 317class MediaComment(Document):
e83dc091
CAW
318 """
319 A comment on a MediaEntry.
320
321 Structure:
322 - media_entry: The media entry this comment is attached to
323 - author: user who posted this comment
324 - created: when the comment was created
325 - content: plaintext (but markdown'able) version of the comment's content.
326 - content_html: the actual html-rendered version of the comment displayed.
327 Run through Markdown and the HTML cleaner.
328 """
329
c11f21ab 330 __collection__ = 'media_comments'
7cbddc96 331 use_dot_notation = True
6926b23d 332
c11f21ab
JW
333 structure = {
334 'media_entry': ObjectId,
335 'author': ObjectId,
336 'created': datetime.datetime,
337 'content': unicode,
338 'content_html': unicode}
339
340 required_fields = [
7bd8197f 341 'media_entry', 'author', 'created', 'content']
c11f21ab
JW
342
343 default_values = {
344 'created': datetime.datetime.utcnow}
345
346 def media_entry(self):
7bd8197f 347 return self.db.MediaEntry.find_one({'_id': self['media_entry']})
c11f21ab
JW
348
349 def author(self):
350 return self.db.User.find_one({'_id': self['author']})
6926b23d 351
c2ddd85e 352
c11f21ab
JW
353REGISTER_MODELS = [
354 MediaEntry,
355 User,
356 MediaComment]
d232e0f6 357
4329be14 358
d232e0f6
CAW
359def register_models(connection):
360 """
361 Register all models in REGISTER_MODELS with this connection.
362 """
db61f7d1 363 connection.register(REGISTER_MODELS)