Commit | Line | Data |
---|---|---|
8e1e744d | 1 | # GNU MediaGoblin -- federated, autonomous media hosting |
e5572c60 ML |
2 | # Copyright (C) 2011 Free Software Foundation, Inc |
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 | ||
db1a438f | 17 | import datetime, uuid |
4ad5af85 | 18 | |
c2ddd85e | 19 | from mongokit import Document |
4329be14 | 20 | |
0546833c | 21 | from mediagoblin import util |
4ad5af85 | 22 | from mediagoblin.auth import lib as auth_lib |
6e7ce8d1 | 23 | from mediagoblin import mg_globals |
757f37a5 | 24 | from mediagoblin.db import migrations |
9c0fe63f | 25 | from mediagoblin.db.util import ASCENDING, DESCENDING, ObjectId |
7bd8197f | 26 | from mediagoblin.util import Pagination |
2c9e635a | 27 | from mediagoblin.util import DISPLAY_IMAGE_FETCHING_ORDER |
d232e0f6 | 28 | |
d232e0f6 | 29 | |
7bf3f5db CAW |
30 | ################### |
31 | # Custom validators | |
32 | ################### | |
33 | ||
34 | ######## | |
35 | # Models | |
36 | ######## | |
37 | ||
38 | ||
d232e0f6 | 39 | class User(Document): |
16bcd1e7 CAW |
40 | """ |
41 | A user of MediaGoblin. | |
42 | ||
43 | Structure: | |
44 | - username: The username of this user, should be unique to this instance. | |
45 | - email: Email address of this user | |
46 | - created: When the user was created | |
47 | - plugin_data: a mapping of extra plugin information for this User. | |
48 | Nothing uses this yet as we don't have plugins, but someday we | |
49 | might... :) | |
50 | - pw_hash: Hashed version of user's password. | |
51 | - email_verified: Whether or not the user has verified their email or not. | |
52 | Most parts of the site are disabled for users who haven't yet. | |
53 | - status: whether or not the user is active, etc. Currently only has two | |
54 | values, 'needs_email_verification' or 'active'. (In the future, maybe | |
55 | we'll change this to a boolean with a key of 'active' and have a | |
56 | separate field for a reason the user's been disabled if that's | |
57 | appropriate... email_verified is already separate, after all.) | |
58 | - verification_key: If the user is awaiting email verification, the user | |
59 | will have to provide this key (which will be encoded in the presented | |
60 | URL) in order to confirm their email as active. | |
61 | - is_admin: Whether or not this user is an administrator or not. | |
62 | - url: this user's personal webpage/website, if appropriate. | |
63 | - bio: biography of this user (plaintext, in markdown) | |
64 | - bio_html: biography of the user converted to proper HTML. | |
65 | """ | |
73a6e206 CAW |
66 | __collection__ = 'users' |
67 | ||
d232e0f6 CAW |
68 | structure = { |
69 | 'username': unicode, | |
24181820 | 70 | 'email': unicode, |
d232e0f6 CAW |
71 | 'created': datetime.datetime, |
72 | 'plugin_data': dict, # plugins can dump stuff here. | |
73 | 'pw_hash': unicode, | |
24181820 | 74 | 'email_verified': bool, |
4d75522b | 75 | 'status': unicode, |
18cf34d4 CAW |
76 | 'verification_key': unicode, |
77 | 'is_admin': bool, | |
630b57a3 | 78 | 'url' : unicode, |
4c465852 AW |
79 | 'bio' : unicode, # May contain markdown |
80 | 'bio_html': unicode, # May contain plaintext, or HTML | |
65030735 | 81 | 'fp_verification_key': unicode, # forgotten password verification key |
25ba955e | 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 | 102 | class 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 CAW |
176 | |
177 | - fail_error: path to the exception raised | |
178 | - fail_metadata: | |
080a81ec | 179 | """ |
4d75522b CAW |
180 | __collection__ = 'media_entries' |
181 | ||
182 | structure = { | |
757f37a5 | 183 | 'uploader': ObjectId, |
4d75522b | 184 | 'title': unicode, |
1013bdaf | 185 | 'slug': unicode, |
4d75522b | 186 | 'created': datetime.datetime, |
44e2da2f JW |
187 | 'description': unicode, # May contain markdown/up |
188 | 'description_html': unicode, # May contain plaintext, or HTML | |
4d75522b CAW |
189 | 'media_type': unicode, |
190 | 'media_data': dict, # extra data relevant to this media_type | |
191 | 'plugin_data': dict, # plugins can dump stuff here. | |
0712a06d | 192 | 'tags': [dict], |
74ae6b11 CAW |
193 | 'state': unicode, |
194 | ||
fa7f9c61 CAW |
195 | # For now let's assume there can only be one main file queued |
196 | # at a time | |
197 | 'queued_media_file': [unicode], | |
6b9ee0ca | 198 | 'queued_task_id': unicode, |
fa7f9c61 CAW |
199 | |
200 | # A dictionary of logical names to filepaths | |
201 | 'media_files': dict, | |
202 | ||
74ae6b11 CAW |
203 | # The following should be lists of lists, in appropriate file |
204 | # record form | |
6c50c210 CAW |
205 | 'attachment_files': list, |
206 | ||
207 | # If things go badly in processing things, we'll store that | |
208 | # data here | |
209 | 'fail_error': unicode, | |
210 | 'fail_metadata': dict} | |
4d75522b CAW |
211 | |
212 | required_fields = [ | |
b1ae76ae | 213 | 'uploader', 'created', 'media_type', 'slug'] |
4d75522b CAW |
214 | |
215 | default_values = { | |
74ae6b11 CAW |
216 | 'created': datetime.datetime.utcnow, |
217 | 'state': u'unprocessed'} | |
4d75522b | 218 | |
6f59a3a3 JW |
219 | def get_comments(self): |
220 | return self.db.MediaComment.find({ | |
221 | 'media_entry': self['_id']}).sort('created', DESCENDING) | |
222 | ||
2c9e635a JW |
223 | def get_display_media(self, media_map, fetch_order=DISPLAY_IMAGE_FETCHING_ORDER): |
224 | """ | |
225 | Find the best media for display. | |
226 | ||
227 | Args: | |
228 | - media_map: a dict like | |
229 | {u'image_size': [u'dir1', u'dir2', u'image.jpg']} | |
230 | - fetch_order: the order we should try fetching images in | |
231 | ||
232 | Returns: | |
233 | (media_size, media_path) | |
234 | """ | |
235 | media_sizes = media_map.keys() | |
380ac094 | 236 | |
2c9e635a JW |
237 | for media_size in DISPLAY_IMAGE_FETCHING_ORDER: |
238 | if media_size in media_sizes: | |
239 | return media_map[media_size] | |
240 | ||
4d75522b CAW |
241 | def main_mediafile(self): |
242 | pass | |
6f59a3a3 | 243 | |
0546833c AW |
244 | def generate_slug(self): |
245 | self['slug'] = util.slugify(self['title']) | |
246 | ||
6e7ce8d1 | 247 | duplicate = mg_globals.database.media_entries.find_one( |
f0545dde | 248 | {'slug': self['slug']}) |
080a81ec | 249 | |
0546833c AW |
250 | if duplicate: |
251 | self['slug'] = "%s-%s" % (self['_id'], self['slug']) | |
4d75522b | 252 | |
6926b23d CAW |
253 | def url_for_self(self, urlgen): |
254 | """ | |
255 | Generate an appropriate url for ourselves | |
256 | ||
257 | Use a slug if we have one, else use our '_id'. | |
258 | """ | |
16509be1 CAW |
259 | uploader = self.uploader() |
260 | ||
6926b23d CAW |
261 | if self.get('slug'): |
262 | return urlgen( | |
263 | 'mediagoblin.user_pages.media_home', | |
16509be1 | 264 | user=uploader['username'], |
6926b23d CAW |
265 | media=self['slug']) |
266 | else: | |
267 | return urlgen( | |
268 | 'mediagoblin.user_pages.media_home', | |
16509be1 | 269 | user=uploader['username'], |
6926b23d | 270 | media=unicode(self['_id'])) |
080a81ec | 271 | |
9c0fe63f CFD |
272 | def url_to_prev(self, urlgen): |
273 | """ | |
274 | Provide a url to the previous entry from this user, if there is one | |
275 | """ | |
080a81ec | 276 | cursor = self.db.MediaEntry.find({'_id' : {"$gt": self['_id']}, |
ce2ac488 CFD |
277 | 'uploader': self['uploader'], |
278 | 'state': 'processed'}).sort( | |
77b95801 | 279 | '_id', ASCENDING).limit(1) |
9c0fe63f CFD |
280 | if cursor.count(): |
281 | return urlgen('mediagoblin.user_pages.media_home', | |
282 | user=self.uploader()['username'], | |
b1db2c2e | 283 | media=unicode(cursor[0]['slug'])) |
080a81ec | 284 | |
9c0fe63f CFD |
285 | def url_to_next(self, urlgen): |
286 | """ | |
287 | Provide a url to the next entry from this user, if there is one | |
288 | """ | |
080a81ec | 289 | cursor = self.db.MediaEntry.find({'_id' : {"$lt": self['_id']}, |
ce2ac488 CFD |
290 | 'uploader': self['uploader'], |
291 | 'state': 'processed'}).sort( | |
77b95801 | 292 | '_id', DESCENDING).limit(1) |
9c0fe63f CFD |
293 | |
294 | if cursor.count(): | |
295 | return urlgen('mediagoblin.user_pages.media_home', | |
296 | user=self.uploader()['username'], | |
b1db2c2e | 297 | media=unicode(cursor[0]['slug'])) |
6926b23d | 298 | |
16509be1 CAW |
299 | def uploader(self): |
300 | return self.db.User.find_one({'_id': self['uploader']}) | |
301 | ||
6ee9c719 CAW |
302 | def get_fail_exception(self): |
303 | """ | |
304 | Get the exception that's appropriate for this error | |
305 | """ | |
306 | if self['fail_error']: | |
307 | return util.import_component(self['fail_error']) | |
308 | ||
b27ec167 | 309 | |
c11f21ab | 310 | class MediaComment(Document): |
e83dc091 CAW |
311 | """ |
312 | A comment on a MediaEntry. | |
313 | ||
314 | Structure: | |
315 | - media_entry: The media entry this comment is attached to | |
316 | - author: user who posted this comment | |
317 | - created: when the comment was created | |
318 | - content: plaintext (but markdown'able) version of the comment's content. | |
319 | - content_html: the actual html-rendered version of the comment displayed. | |
320 | Run through Markdown and the HTML cleaner. | |
321 | """ | |
322 | ||
c11f21ab | 323 | __collection__ = 'media_comments' |
6926b23d | 324 | |
c11f21ab JW |
325 | structure = { |
326 | 'media_entry': ObjectId, | |
327 | 'author': ObjectId, | |
328 | 'created': datetime.datetime, | |
329 | 'content': unicode, | |
330 | 'content_html': unicode} | |
331 | ||
332 | required_fields = [ | |
7bd8197f | 333 | 'media_entry', 'author', 'created', 'content'] |
c11f21ab JW |
334 | |
335 | default_values = { | |
336 | 'created': datetime.datetime.utcnow} | |
337 | ||
338 | def media_entry(self): | |
7bd8197f | 339 | return self.db.MediaEntry.find_one({'_id': self['media_entry']}) |
c11f21ab JW |
340 | |
341 | def author(self): | |
342 | return self.db.User.find_one({'_id': self['author']}) | |
6926b23d | 343 | |
c2ddd85e | 344 | |
c11f21ab JW |
345 | REGISTER_MODELS = [ |
346 | MediaEntry, | |
347 | User, | |
348 | MediaComment] | |
d232e0f6 | 349 | |
4329be14 | 350 | |
d232e0f6 CAW |
351 | def register_models(connection): |
352 | """ | |
353 | Register all models in REGISTER_MODELS with this connection. | |
354 | """ | |
db61f7d1 CAW |
355 | connection.register(REGISTER_MODELS) |
356 |