Commit | Line | Data |
---|---|---|
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 |
17 | import datetime |
18 | import uuid | |
4ad5af85 | 19 | |
c2ddd85e | 20 | from mongokit import Document |
4329be14 | 21 | |
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 |
152a3bfa AW |
26 | from mediagoblin.tools.pagination import Pagination |
27 | from mediagoblin.tools import url, common | |
d232e0f6 | 28 | |
7bf3f5db CAW |
29 | ################### |
30 | # Custom validators | |
31 | ################### | |
32 | ||
33 | ######## | |
34 | # Models | |
35 | ######## | |
36 | ||
37 | ||
d232e0f6 | 38 | class 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 | 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 | 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 | 317 | class 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 |
353 | REGISTER_MODELS = [ |
354 | MediaEntry, | |
355 | User, | |
356 | MediaComment] | |
d232e0f6 | 357 | |
4329be14 | 358 | |
d232e0f6 CAW |
359 | def register_models(connection): |
360 | """ | |
361 | Register all models in REGISTER_MODELS with this connection. | |
362 | """ | |
db61f7d1 | 363 | connection.register(REGISTER_MODELS) |