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 | |
d232e0f6 CAW |
81 | } |
82 | ||
db5912e3 | 83 | required_fields = ['username', 'created', 'pw_hash', 'email'] |
fc9bb821 CAW |
84 | |
85 | default_values = { | |
24181820 | 86 | 'created': datetime.datetime.utcnow, |
4d75522b | 87 | 'email_verified': False, |
db1a438f | 88 | 'status': u'needs_email_verification', |
18cf34d4 CAW |
89 | 'verification_key': lambda: unicode(uuid.uuid4()), |
90 | 'is_admin': False} | |
080a81ec | 91 | |
4ad5af85 CAW |
92 | def check_login(self, password): |
93 | """ | |
94 | See if a user can login with this password | |
95 | """ | |
96 | return auth_lib.bcrypt_check_password( | |
97 | password, self['pw_hash']) | |
98 | ||
d232e0f6 | 99 | |
4d75522b | 100 | class MediaEntry(Document): |
080a81ec CAW |
101 | """ |
102 | Record of a piece of media. | |
103 | ||
104 | Structure: | |
105 | - uploader: A reference to a User who uploaded this. | |
106 | ||
107 | - title: Title of this work | |
108 | ||
109 | - slug: A normalized "slug" which can be used as part of a URL to retrieve | |
110 | this work, such as 'my-works-name-in-slug-form' may be viewable by | |
111 | 'http://mg.example.org/u/username/m/my-works-name-in-slug-form/' | |
112 | Note that since URLs are constructed this way, slugs must be unique | |
113 | per-uploader. (An index is provided to enforce that but code should be | |
114 | written on the python side to ensure this as well.) | |
115 | ||
116 | - created: Date and time of when this piece of work was uploaded. | |
117 | ||
118 | - description: Uploader-set description of this work. This can be marked | |
119 | up with MarkDown for slight fanciness (links, boldness, italics, | |
120 | paragraphs...) | |
121 | ||
122 | - description_html: Rendered version of the description, run through | |
123 | Markdown and cleaned with our cleaning tool. | |
124 | ||
125 | - media_type: What type of media is this? Currently we only support | |
126 | 'image' ;) | |
127 | ||
128 | - media_data: Extra information that's media-format-dependent. | |
129 | For example, images might contain some EXIF data that's not appropriate | |
130 | to other formats. You might store it like: | |
131 | ||
132 | mediaentry['media_data']['exif'] = { | |
133 | 'manufacturer': 'CASIO', | |
134 | 'model': 'QV-4000', | |
135 | 'exposure_time': .659} | |
136 | ||
137 | Alternately for video you might store: | |
138 | ||
139 | # play length in seconds | |
140 | mediaentry['media_data']['play_length'] = 340 | |
141 | ||
142 | ... so what's appropriate here really depends on the media type. | |
143 | ||
144 | - plugin_data: a mapping of extra plugin information for this User. | |
145 | Nothing uses this yet as we don't have plugins, but someday we | |
146 | might... :) | |
147 | ||
148 | - tags: A list of tags. Each tag is stored as a dictionary that has a key | |
149 | for the actual name and the normalized name-as-slug, so ultimately this | |
150 | looks like: | |
151 | [{'name': 'Gully Gardens', | |
152 | 'slug': 'gully-gardens'}, | |
153 | {'name': 'Castle Adventure Time?!", | |
154 | 'slug': 'castle-adventure-time'}] | |
155 | ||
156 | - state: What's the state of this file? Active, inactive, disabled, etc... | |
157 | But really for now there are only two states: | |
158 | "unprocessed": uploaded but needs to go through processing for display | |
159 | "processed": processed and able to be displayed | |
160 | ||
161 | - queued_media_file: storage interface style filepath describing a file | |
162 | queued for processing. This is stored in the mg_globals.queue_store | |
163 | storage system. | |
164 | ||
6b9ee0ca CAW |
165 | - queued_task_id: celery task id. Use this to fetch the task state. |
166 | ||
080a81ec CAW |
167 | - media_files: Files relevant to this that have actually been processed |
168 | and are available for various types of display. Stored like: | |
169 | {'thumb': ['dir1', 'dir2', 'pic.png'} | |
170 | ||
171 | - attachment_files: A list of "attachment" files, ones that aren't | |
172 | critical to this piece of media but may be usefully relevant to people | |
173 | viewing the work. (currently unused.) | |
080a81ec | 174 | """ |
4d75522b CAW |
175 | __collection__ = 'media_entries' |
176 | ||
177 | structure = { | |
757f37a5 | 178 | 'uploader': ObjectId, |
4d75522b | 179 | 'title': unicode, |
1013bdaf | 180 | 'slug': unicode, |
4d75522b | 181 | 'created': datetime.datetime, |
44e2da2f JW |
182 | 'description': unicode, # May contain markdown/up |
183 | 'description_html': unicode, # May contain plaintext, or HTML | |
4d75522b CAW |
184 | 'media_type': unicode, |
185 | 'media_data': dict, # extra data relevant to this media_type | |
186 | 'plugin_data': dict, # plugins can dump stuff here. | |
0712a06d | 187 | 'tags': [dict], |
74ae6b11 CAW |
188 | 'state': unicode, |
189 | ||
fa7f9c61 CAW |
190 | # For now let's assume there can only be one main file queued |
191 | # at a time | |
192 | 'queued_media_file': [unicode], | |
6b9ee0ca | 193 | 'queued_task_id': unicode, |
fa7f9c61 CAW |
194 | |
195 | # A dictionary of logical names to filepaths | |
196 | 'media_files': dict, | |
197 | ||
74ae6b11 CAW |
198 | # The following should be lists of lists, in appropriate file |
199 | # record form | |
84abd2bb | 200 | 'attachment_files': list} |
4d75522b CAW |
201 | |
202 | required_fields = [ | |
b1ae76ae | 203 | 'uploader', 'created', 'media_type', 'slug'] |
4d75522b CAW |
204 | |
205 | default_values = { | |
74ae6b11 CAW |
206 | 'created': datetime.datetime.utcnow, |
207 | 'state': u'unprocessed'} | |
4d75522b | 208 | |
6f59a3a3 JW |
209 | def get_comments(self): |
210 | return self.db.MediaComment.find({ | |
211 | 'media_entry': self['_id']}).sort('created', DESCENDING) | |
212 | ||
2c9e635a JW |
213 | def get_display_media(self, media_map, fetch_order=DISPLAY_IMAGE_FETCHING_ORDER): |
214 | """ | |
215 | Find the best media for display. | |
216 | ||
217 | Args: | |
218 | - media_map: a dict like | |
219 | {u'image_size': [u'dir1', u'dir2', u'image.jpg']} | |
220 | - fetch_order: the order we should try fetching images in | |
221 | ||
222 | Returns: | |
223 | (media_size, media_path) | |
224 | """ | |
225 | media_sizes = media_map.keys() | |
380ac094 | 226 | |
2c9e635a JW |
227 | for media_size in DISPLAY_IMAGE_FETCHING_ORDER: |
228 | if media_size in media_sizes: | |
229 | return media_map[media_size] | |
230 | ||
4d75522b CAW |
231 | def main_mediafile(self): |
232 | pass | |
6f59a3a3 | 233 | |
0546833c AW |
234 | def generate_slug(self): |
235 | self['slug'] = util.slugify(self['title']) | |
236 | ||
6e7ce8d1 | 237 | duplicate = mg_globals.database.media_entries.find_one( |
f0545dde | 238 | {'slug': self['slug']}) |
080a81ec | 239 | |
0546833c AW |
240 | if duplicate: |
241 | self['slug'] = "%s-%s" % (self['_id'], self['slug']) | |
4d75522b | 242 | |
6926b23d CAW |
243 | def url_for_self(self, urlgen): |
244 | """ | |
245 | Generate an appropriate url for ourselves | |
246 | ||
247 | Use a slug if we have one, else use our '_id'. | |
248 | """ | |
16509be1 CAW |
249 | uploader = self.uploader() |
250 | ||
6926b23d CAW |
251 | if self.get('slug'): |
252 | return urlgen( | |
253 | 'mediagoblin.user_pages.media_home', | |
16509be1 | 254 | user=uploader['username'], |
6926b23d CAW |
255 | media=self['slug']) |
256 | else: | |
257 | return urlgen( | |
258 | 'mediagoblin.user_pages.media_home', | |
16509be1 | 259 | user=uploader['username'], |
6926b23d | 260 | media=unicode(self['_id'])) |
080a81ec | 261 | |
9c0fe63f CFD |
262 | def url_to_prev(self, urlgen): |
263 | """ | |
264 | Provide a url to the previous entry from this user, if there is one | |
265 | """ | |
080a81ec | 266 | cursor = self.db.MediaEntry.find({'_id' : {"$gt": self['_id']}, |
ce2ac488 CFD |
267 | 'uploader': self['uploader'], |
268 | 'state': 'processed'}).sort( | |
77b95801 | 269 | '_id', ASCENDING).limit(1) |
9c0fe63f CFD |
270 | if cursor.count(): |
271 | return urlgen('mediagoblin.user_pages.media_home', | |
272 | user=self.uploader()['username'], | |
b1db2c2e | 273 | media=unicode(cursor[0]['slug'])) |
080a81ec | 274 | |
9c0fe63f CFD |
275 | def url_to_next(self, urlgen): |
276 | """ | |
277 | Provide a url to the next entry from this user, if there is one | |
278 | """ | |
080a81ec | 279 | cursor = self.db.MediaEntry.find({'_id' : {"$lt": self['_id']}, |
ce2ac488 CFD |
280 | 'uploader': self['uploader'], |
281 | 'state': 'processed'}).sort( | |
77b95801 | 282 | '_id', DESCENDING).limit(1) |
9c0fe63f CFD |
283 | |
284 | if cursor.count(): | |
285 | return urlgen('mediagoblin.user_pages.media_home', | |
286 | user=self.uploader()['username'], | |
b1db2c2e | 287 | media=unicode(cursor[0]['slug'])) |
6926b23d | 288 | |
16509be1 CAW |
289 | def uploader(self): |
290 | return self.db.User.find_one({'_id': self['uploader']}) | |
291 | ||
b27ec167 | 292 | |
c11f21ab | 293 | class MediaComment(Document): |
e83dc091 CAW |
294 | """ |
295 | A comment on a MediaEntry. | |
296 | ||
297 | Structure: | |
298 | - media_entry: The media entry this comment is attached to | |
299 | - author: user who posted this comment | |
300 | - created: when the comment was created | |
301 | - content: plaintext (but markdown'able) version of the comment's content. | |
302 | - content_html: the actual html-rendered version of the comment displayed. | |
303 | Run through Markdown and the HTML cleaner. | |
304 | """ | |
305 | ||
c11f21ab | 306 | __collection__ = 'media_comments' |
6926b23d | 307 | |
c11f21ab JW |
308 | structure = { |
309 | 'media_entry': ObjectId, | |
310 | 'author': ObjectId, | |
311 | 'created': datetime.datetime, | |
312 | 'content': unicode, | |
313 | 'content_html': unicode} | |
314 | ||
315 | required_fields = [ | |
7bd8197f | 316 | 'media_entry', 'author', 'created', 'content'] |
c11f21ab JW |
317 | |
318 | default_values = { | |
319 | 'created': datetime.datetime.utcnow} | |
320 | ||
321 | def media_entry(self): | |
7bd8197f | 322 | return self.db.MediaEntry.find_one({'_id': self['media_entry']}) |
c11f21ab JW |
323 | |
324 | def author(self): | |
325 | return self.db.User.find_one({'_id': self['author']}) | |
6926b23d | 326 | |
c2ddd85e | 327 | |
c11f21ab JW |
328 | REGISTER_MODELS = [ |
329 | MediaEntry, | |
330 | User, | |
331 | MediaComment] | |
d232e0f6 | 332 | |
4329be14 | 333 | |
d232e0f6 CAW |
334 | def register_models(connection): |
335 | """ | |
336 | Register all models in REGISTER_MODELS with this connection. | |
337 | """ | |
db61f7d1 CAW |
338 | connection.register(REGISTER_MODELS) |
339 |