1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011 MediaGoblin contributors. See AUTHORS.
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.
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.
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/>.
20 from mongokit
import Document
22 from mediagoblin
.auth
import lib
as auth_lib
23 from mediagoblin
import mg_globals
24 from mediagoblin
.db
import migrations
25 from mediagoblin
.db
.util
import ASCENDING
, DESCENDING
, ObjectId
26 from mediagoblin
.tools
.pagination
import Pagination
27 from mediagoblin
.tools
import url
, common
40 A user of MediaGoblin.
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
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.
65 __collection__
= 'users'
66 use_dot_notation
= True
71 'created': datetime
.datetime
,
72 'plugin_data': dict, # plugins can dump stuff here.
74 'email_verified': bool,
76 'verification_key': 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
,
85 required_fields
= ['username', 'created', 'pw_hash', 'email']
88 'created': datetime
.datetime
.utcnow
,
89 'email_verified': False,
90 'status': u
'needs_email_verification',
91 'verification_key': lambda: unicode(uuid
.uuid4()),
94 def check_login(self
, password
):
96 See if a user can login with this password
98 return auth_lib
.bcrypt_check_password(
99 password
, self
.pw_hash
)
102 class MediaEntry(Document
):
104 Record of a piece of media.
107 - uploader: A reference to a User who uploaded this.
109 - title: Title of this work
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.)
118 - created: Date and time of when this piece of work was uploaded.
120 - description: Uploader-set description of this work. This can be marked
121 up with MarkDown for slight fanciness (links, boldness, italics,
124 - description_html: Rendered version of the description, run through
125 Markdown and cleaned with our cleaning tool.
127 - media_type: What type of media is this? Currently we only support
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:
134 mediaentry['media_data']['exif'] = {
135 'manufacturer': 'CASIO',
137 'exposure_time': .659}
139 Alternately for video you might store:
141 # play length in seconds
142 mediaentry['media_data']['play_length'] = 340
144 ... so what's appropriate here really depends on the media type.
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
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
153 [{'name': 'Gully Gardens',
154 'slug': 'gully-gardens'},
155 {'name': 'Castle Adventure Time?!",
156 'slug': 'castle-adventure-time'}]
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
163 - queued_media_file: storage interface style filepath describing a file
164 queued for processing. This is stored in the mg_globals.queue_store
167 - queued_task_id: celery task id. Use this to fetch the task state.
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'}
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.)
177 - fail_error: path to the exception raised
180 __collection__
= 'media_entries'
181 use_dot_notation
= True
184 'uploader': ObjectId
,
187 'created': datetime
.datetime
,
188 'description': unicode, # May contain markdown/up
189 'description_html': unicode, # May contain plaintext, or HTML
190 'media_type': unicode,
191 'media_data': dict, # extra data relevant to this media_type
192 'plugin_data': dict, # plugins can dump stuff here.
196 # For now let's assume there can only be one main file queued
198 'queued_media_file': [unicode],
199 'queued_task_id': unicode,
201 # A dictionary of logical names to filepaths
204 # The following should be lists of lists, in appropriate file
206 'attachment_files': list,
208 # If things go badly in processing things, we'll store that
210 'fail_error': unicode,
211 'fail_metadata': dict}
214 'uploader', 'created', 'media_type', 'slug']
217 'created': datetime
.datetime
.utcnow
,
218 'state': u
'unprocessed'}
220 def get_comments(self
, ascending
=False):
226 return self
.db
.MediaComment
.find({
227 'media_entry': self
._id
}).sort('created', order
)
229 def get_display_media(self
, media_map
,
230 fetch_order
=common
.DISPLAY_IMAGE_FETCHING_ORDER
):
232 Find the best media for display.
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
240 (media_size, media_path)
242 media_sizes
= media_map
.keys()
244 for media_size
in common
.DISPLAY_IMAGE_FETCHING_ORDER
:
245 if media_size
in media_sizes
:
246 return media_map
[media_size
]
248 def main_mediafile(self
):
251 def generate_slug(self
):
252 self
['slug'] = url
.slugify(self
['title'])
254 duplicate
= mg_globals
.database
.media_entries
.find_one(
255 {'slug': self
['slug']})
258 self
['slug'] = "%s-%s" % (self
._id
, self
['slug'])
260 def url_for_self(self
, urlgen
):
262 Generate an appropriate url for ourselves
264 Use a slug if we have one, else use our '_id'.
266 uploader
= self
.get_uploader()
270 'mediagoblin.user_pages.media_home',
271 user
=uploader
.username
,
275 'mediagoblin.user_pages.media_home',
276 user
=uploader
.username
,
277 media
=unicode(self
._id
))
279 def url_to_prev(self
, urlgen
):
281 Provide a url to the previous entry from this user, if there is one
283 cursor
= self
.db
.MediaEntry
.find({'_id': {"$gt": self
._id
},
284 'uploader': self
['uploader'],
285 'state': 'processed'}).sort(
286 '_id', ASCENDING
).limit(1)
288 return urlgen('mediagoblin.user_pages.media_home',
289 user
=self
.get_uploader().username
,
290 media
=unicode(cursor
[0]['slug']))
292 def url_to_next(self
, urlgen
):
294 Provide a url to the next entry from this user, if there is one
296 cursor
= self
.db
.MediaEntry
.find({'_id': {"$lt": self
._id
},
297 'uploader': self
['uploader'],
298 'state': 'processed'}).sort(
299 '_id', DESCENDING
).limit(1)
302 return urlgen('mediagoblin.user_pages.media_home',
303 user
=self
.get_uploader().username
,
304 media
=unicode(cursor
[0]['slug']))
306 def get_uploader(self
):
307 return self
.db
.User
.find_one({'_id': self
['uploader']})
309 def get_fail_exception(self
):
311 Get the exception that's appropriate for this error
313 if self
['fail_error']:
314 return common
.import_component(self
['fail_error'])
317 class MediaComment(Document
):
319 A comment on a MediaEntry.
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.
330 __collection__
= 'media_comments'
331 use_dot_notation
= True
334 'media_entry': ObjectId
,
336 'created': datetime
.datetime
,
338 'content_html': unicode}
341 'media_entry', 'author', 'created', 'content']
344 'created': datetime
.datetime
.utcnow
}
346 def media_entry(self
):
347 return self
.db
.MediaEntry
.find_one({'_id': self
['media_entry']})
350 return self
.db
.User
.find_one({'_id': self
['author']})
359 def register_models(connection
):
361 Register all models in REGISTER_MODELS with this connection.
363 connection
.register(REGISTER_MODELS
)