1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 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/>.
21 from mediagoblin
import messages
, mg_globals
22 from mediagoblin
.db
.models
import (MediaEntry
, MediaTag
, Collection
,
24 from mediagoblin
.tools
.response
import render_to_response
, render_404
, \
25 redirect
, redirect_obj
26 from mediagoblin
.tools
.text
import cleaned_markdown_conversion
27 from mediagoblin
.tools
.translate
import pass_to_ugettext
as _
28 from mediagoblin
.tools
.pagination
import Pagination
29 from mediagoblin
.user_pages
import forms
as user_forms
30 from mediagoblin
.user_pages
.lib
import add_media_to_collection
31 from mediagoblin
.notifications
import trigger_notification
, \
32 add_comment_subscription
, mark_comment_notification_seen
33 from mediagoblin
.decorators
import (uses_pagination
, get_user_media_entry
,
34 get_media_entry_by_id
,
35 require_active_login
, user_may_delete_media
, user_may_alter_collection
,
36 get_user_collection
, get_user_collection_item
, active_user_from_url
)
38 from werkzeug
.contrib
.atom
import AtomFeed
39 from werkzeug
.exceptions
import MethodNotAllowed
40 from werkzeug
.wrappers
import Response
43 _log
= logging
.getLogger(__name__
)
44 _log
.setLevel(logging
.DEBUG
)
48 def user_home(request
, page
):
49 """'Homepage' of a User()"""
50 # TODO: decide if we only want homepages for active users, we can
51 # then use the @get_active_user decorator and also simplify the
53 user
= User
.query
.filter_by(username
=request
.matchdict
['user']).first()
55 return render_404(request
)
56 elif user
.status
!= u
'active':
57 return render_to_response(
59 'mediagoblin/user_pages/user.html',
62 cursor
= MediaEntry
.query
.\
63 filter_by(uploader
= user
.id,
64 state
= u
'processed').order_by(MediaEntry
.created
.desc())
66 pagination
= Pagination(page
, cursor
)
67 media_entries
= pagination()
69 #if no data is available, return NotFound
70 if media_entries
== None:
71 return render_404(request
)
73 user_gallery_url
= request
.urlgen(
74 'mediagoblin.user_pages.user_gallery',
77 return render_to_response(
79 'mediagoblin/user_pages/user.html',
81 'user_gallery_url': user_gallery_url
,
82 'media_entries': media_entries
,
83 'pagination': pagination
})
88 def user_gallery(request
, page
, url_user
=None):
89 """'Gallery' of a User()"""
90 tag
= request
.matchdict
.get('tag', None)
91 cursor
= MediaEntry
.query
.filter_by(
93 state
=u
'processed').order_by(MediaEntry
.created
.desc())
95 # Filter potentially by tag too:
97 cursor
= cursor
.filter(
98 MediaEntry
.tags_helper
.any(
99 MediaTag
.slug
== request
.matchdict
['tag']))
102 pagination
= Pagination(page
, cursor
)
103 media_entries
= pagination()
105 #if no data is available, return NotFound
106 # TODO: Should we really also return 404 for empty galleries?
107 if media_entries
== None:
108 return render_404(request
)
110 return render_to_response(
112 'mediagoblin/user_pages/gallery.html',
113 {'user': url_user
, 'tag': tag
,
114 'media_entries': media_entries
,
115 'pagination': pagination
})
118 MEDIA_COMMENTS_PER_PAGE
= 50
121 @get_user_media_entry
123 def media_home(request
, media
, page
, **kwargs
):
125 'Homepage' of a MediaEntry()
127 comment_id
= request
.matchdict
.get('comment', None)
130 mark_comment_notification_seen(comment_id
, request
.user
)
132 pagination
= Pagination(
133 page
, media
.get_comments(
134 mg_globals
.app_config
['comments_ascending']),
135 MEDIA_COMMENTS_PER_PAGE
,
138 pagination
= Pagination(
139 page
, media
.get_comments(
140 mg_globals
.app_config
['comments_ascending']),
141 MEDIA_COMMENTS_PER_PAGE
)
143 comments
= pagination()
145 comment_form
= user_forms
.MediaCommentForm(request
.form
)
147 media_template_name
= media
.media_manager
.display_template
149 return render_to_response(
153 'comments': comments
,
154 'pagination': pagination
,
155 'comment_form': comment_form
,
156 'app_config': mg_globals
.app_config
})
159 @get_media_entry_by_id
160 @require_active_login
161 def media_post_comment(request
, media
):
163 recieves POST from a MediaEntry() comment form, saves the comment.
165 if not request
.method
== 'POST':
166 raise MethodNotAllowed()
168 comment
= request
.db
.MediaComment()
169 comment
.media_entry
= media
.id
170 comment
.author
= request
.user
.id
171 print request
.form
['comment_content']
172 comment
.content
= unicode(request
.form
['comment_content'])
174 # Show error message if commenting is disabled.
175 if not mg_globals
.app_config
['allow_comments']:
176 messages
.add_message(
179 _("Sorry, comments are disabled."))
180 elif not comment
.content
.strip():
181 messages
.add_message(
184 _("Oops, your comment was empty."))
188 messages
.add_message(
189 request
, messages
.SUCCESS
,
190 _('Your comment has been posted!'))
192 trigger_notification(comment
, media
, request
)
194 add_comment_subscription(request
.user
, media
)
196 return redirect_obj(request
, media
)
200 def media_preview_comment(request
):
201 """Runs a comment through markdown so it can be previewed."""
202 # If this isn't an ajax request, render_404
203 if not request
.is_xhr
:
204 return render_404(request
)
206 comment
= unicode(request
.form
['comment_content'])
207 cleancomment
= { "content":cleaned_markdown_conversion(comment
)}
209 return Response(json
.dumps(cleancomment
))
211 @get_media_entry_by_id
212 @require_active_login
213 def media_collect(request
, media
):
214 """Add media to collection submission"""
216 form
= user_forms
.MediaCollectForm(request
.form
)
217 # A user's own collections:
218 form
.collection
.query
= Collection
.query
.filter_by(
219 creator
= request
.user
.id).order_by(Collection
.title
)
221 if request
.method
!= 'POST' or not form
.validate():
222 # No POST submission, or invalid form
223 if not form
.validate():
224 messages
.add_message(request
, messages
.ERROR
,
225 _('Please check your entries and try again.'))
227 return render_to_response(
229 'mediagoblin/user_pages/media_collect.html',
233 # If we are here, method=POST and the form is valid, submit things.
234 # If the user is adding a new collection, use that:
235 if form
.collection_title
.data
:
236 # Make sure this user isn't duplicating an existing collection
237 existing_collection
= Collection
.query
.filter_by(
238 creator
=request
.user
.id,
239 title
=form
.collection_title
.data
).first()
240 if existing_collection
:
241 messages
.add_message(request
, messages
.ERROR
,
242 _('You already have a collection called "%s"!')
243 % existing_collection
.title
)
244 return redirect(request
, "mediagoblin.user_pages.media_home",
245 user
=media
.get_uploader
.username
,
246 media
=media
.slug_or_id
)
248 collection
= Collection()
249 collection
.title
= form
.collection_title
.data
250 collection
.description
= form
.collection_description
.data
251 collection
.creator
= request
.user
.id
252 collection
.generate_slug()
255 # Otherwise, use the collection selected from the drop-down
257 collection
= form
.collection
.data
258 if collection
and collection
.creator
!= request
.user
.id:
261 # Make sure the user actually selected a collection
263 messages
.add_message(
264 request
, messages
.ERROR
,
265 _('You have to select or add a collection'))
266 return redirect(request
, "mediagoblin.user_pages.media_collect",
267 user
=media
.get_uploader
.username
,
271 # Check whether media already exists in collection
272 elif CollectionItem
.query
.filter_by(
273 media_entry
=media
.id,
274 collection
=collection
.id).first():
275 messages
.add_message(request
, messages
.ERROR
,
276 _('"%s" already in collection "%s"')
277 % (media
.title
, collection
.title
))
278 else: # Add item to collection
279 add_media_to_collection(collection
, media
, form
.note
.data
)
281 messages
.add_message(request
, messages
.SUCCESS
,
282 _('"%s" added to collection "%s"')
283 % (media
.title
, collection
.title
))
285 return redirect_obj(request
, media
)
288 #TODO: Why does @user_may_delete_media not implicate @require_active_login?
289 @get_media_entry_by_id
290 @require_active_login
291 @user_may_delete_media
292 def media_confirm_delete(request
, media
):
294 form
= user_forms
.ConfirmDeleteForm(request
.form
)
296 if request
.method
== 'POST' and form
.validate():
297 if form
.confirm
.data
is True:
298 username
= media
.get_uploader
.username
299 # Delete MediaEntry and all related files, comments etc.
301 messages
.add_message(
302 request
, messages
.SUCCESS
, _('You deleted the media.'))
304 return redirect(request
, "mediagoblin.user_pages.user_home",
307 messages
.add_message(
308 request
, messages
.ERROR
,
309 _("The media was not deleted because you didn't check that you were sure."))
310 return redirect_obj(request
, media
)
312 if ((request
.user
.is_admin
and
313 request
.user
.id != media
.uploader
)):
314 messages
.add_message(
315 request
, messages
.WARNING
,
316 _("You are about to delete another user's media. "
317 "Proceed with caution."))
319 return render_to_response(
321 'mediagoblin/user_pages/media_confirm_delete.html',
326 @active_user_from_url
328 def user_collection(request
, page
, url_user
=None):
329 """A User-defined Collection"""
330 collection
= Collection
.query
.filter_by(
331 get_creator
=url_user
,
332 slug
=request
.matchdict
['collection']).first()
335 return render_404(request
)
337 cursor
= collection
.get_collection_items()
339 pagination
= Pagination(page
, cursor
)
340 collection_items
= pagination()
342 # if no data is available, return NotFound
343 # TODO: Should an empty collection really also return 404?
344 if collection_items
== None:
345 return render_404(request
)
347 return render_to_response(
349 'mediagoblin/user_pages/collection.html',
351 'collection': collection
,
352 'collection_items': collection_items
,
353 'pagination': pagination
})
356 @active_user_from_url
357 def collection_list(request
, url_user
=None):
358 """A User-defined Collection"""
359 collections
= Collection
.query
.filter_by(
360 get_creator
=url_user
)
362 return render_to_response(
364 'mediagoblin/user_pages/collection_list.html',
366 'collections': collections
})
369 @get_user_collection_item
370 @require_active_login
371 @user_may_alter_collection
372 def collection_item_confirm_remove(request
, collection_item
):
374 form
= user_forms
.ConfirmCollectionItemRemoveForm(request
.form
)
376 if request
.method
== 'POST' and form
.validate():
377 username
= collection_item
.in_collection
.get_creator
.username
378 collection
= collection_item
.in_collection
380 if form
.confirm
.data
is True:
381 entry
= collection_item
.get_media_entry
382 entry
.collected
= entry
.collected
- 1
385 collection_item
.delete()
386 collection
.items
= collection
.items
- 1
389 messages
.add_message(
390 request
, messages
.SUCCESS
, _('You deleted the item from the collection.'))
392 messages
.add_message(
393 request
, messages
.ERROR
,
394 _("The item was not removed because you didn't check that you were sure."))
396 return redirect_obj(request
, collection
)
398 if ((request
.user
.is_admin
and
399 request
.user
.id != collection_item
.in_collection
.creator
)):
400 messages
.add_message(
401 request
, messages
.WARNING
,
402 _("You are about to delete an item from another user's collection. "
403 "Proceed with caution."))
405 return render_to_response(
407 'mediagoblin/user_pages/collection_item_confirm_remove.html',
408 {'collection_item': collection_item
,
413 @require_active_login
414 @user_may_alter_collection
415 def collection_confirm_delete(request
, collection
):
417 form
= user_forms
.ConfirmDeleteForm(request
.form
)
419 if request
.method
== 'POST' and form
.validate():
421 username
= collection
.get_creator
.username
423 if form
.confirm
.data
is True:
424 collection_title
= collection
.title
426 # Delete all the associated collection items
427 for item
in collection
.get_collection_items():
428 entry
= item
.get_media_entry
429 entry
.collected
= entry
.collected
- 1
434 messages
.add_message(request
, messages
.SUCCESS
,
435 _('You deleted the collection "%s"') % collection_title
)
437 return redirect(request
, "mediagoblin.user_pages.user_home",
440 messages
.add_message(
441 request
, messages
.ERROR
,
442 _("The collection was not deleted because you didn't check that you were sure."))
444 return redirect_obj(request
, collection
)
446 if ((request
.user
.is_admin
and
447 request
.user
.id != collection
.creator
)):
448 messages
.add_message(
449 request
, messages
.WARNING
,
450 _("You are about to delete another user's collection. "
451 "Proceed with caution."))
453 return render_to_response(
455 'mediagoblin/user_pages/collection_confirm_delete.html',
456 {'collection': collection
,
460 ATOM_DEFAULT_NR_OF_UPDATED_ITEMS
= 15
463 def atom_feed(request
):
465 generates the atom feed with the newest images
467 user
= User
.query
.filter_by(
468 username
= request
.matchdict
['user'],
469 status
= u
'active').first()
471 return render_404(request
)
473 cursor
= MediaEntry
.query
.filter_by(
475 state
= u
'processed').\
476 order_by(MediaEntry
.created
.desc()).\
477 limit(ATOM_DEFAULT_NR_OF_UPDATED_ITEMS
)
480 ATOM feed id is a tag URI (see http://en.wikipedia.org/wiki/Tag_URI)
483 'href': request
.urlgen(
484 'mediagoblin.user_pages.user_home',
485 qualified
=True, user
=request
.matchdict
['user']),
490 if mg_globals
.app_config
["push_urls"]:
491 for push_url
in mg_globals
.app_config
["push_urls"]:
497 "MediaGoblin: Feed for user '%s'" % request
.matchdict
['user'],
498 feed_url
=request
.url
,
499 id='tag:{host},{year}:gallery.user-{user}'.format(
501 year
=datetime
.datetime
.today().strftime('%Y'),
502 user
=request
.matchdict
['user']),
506 feed
.add(entry
.get('title'),
507 entry
.description_html
,
508 id=entry
.url_for_self(request
.urlgen
, qualified
=True),
511 'name': entry
.get_uploader
.username
,
512 'uri': request
.urlgen(
513 'mediagoblin.user_pages.user_home',
514 qualified
=True, user
=entry
.get_uploader
.username
)},
515 updated
=entry
.get('created'),
517 'href': entry
.url_for_self(
521 'type': 'text/html'}])
523 return feed
.get_response()
526 def collection_atom_feed(request
):
528 generates the atom feed with the newest images from a collection
530 user
= User
.query
.filter_by(
531 username
= request
.matchdict
['user'],
532 status
= u
'active').first()
534 return render_404(request
)
536 collection
= Collection
.query
.filter_by(
538 slug
=request
.matchdict
['collection']).first()
540 return render_404(request
)
542 cursor
= CollectionItem
.query
.filter_by(
543 collection
=collection
.id) \
544 .order_by(CollectionItem
.added
.desc()) \
545 .limit(ATOM_DEFAULT_NR_OF_UPDATED_ITEMS
)
548 ATOM feed id is a tag URI (see http://en.wikipedia.org/wiki/Tag_URI)
551 'href': collection
.url_for_self(request
.urlgen
, qualified
=True),
556 if mg_globals
.app_config
["push_urls"]:
557 for push_url
in mg_globals
.app_config
["push_urls"]:
563 "MediaGoblin: Feed for %s's collection %s" %
564 (request
.matchdict
['user'], collection
.title
),
565 feed_url
=request
.url
,
566 id=u
'tag:{host},{year}:gnu-mediagoblin.{user}.collection.{slug}'\
569 year
=collection
.created
.strftime('%Y'),
570 user
=request
.matchdict
['user'],
571 slug
=collection
.slug
),
575 entry
= item
.get_media_entry
576 feed
.add(entry
.get('title'),
578 id=entry
.url_for_self(request
.urlgen
, qualified
=True),
581 'name': entry
.get_uploader
.username
,
582 'uri': request
.urlgen(
583 'mediagoblin.user_pages.user_home',
584 qualified
=True, user
=entry
.get_uploader
.username
)},
585 updated
=item
.get('added'),
587 'href': entry
.url_for_self(
591 'type': 'text/html'}])
593 return feed
.get_response()
596 @require_active_login
597 def processing_panel(request
):
599 Show to the user what media is still in conversion/processing...
600 and what failed, and why!
602 user
= User
.query
.filter_by(username
=request
.matchdict
['user']).first()
603 # TODO: XXX: Should this be a decorator?
605 # Make sure we have permission to access this user's panel. Only
606 # admins and this user herself should be able to do so.
607 if not (user
.id == request
.user
.id or request
.user
.is_admin
):
608 # No? Simply redirect to this user's homepage.
610 request
, 'mediagoblin.user_pages.user_home',
613 # Get media entries which are in-processing
614 processing_entries
= MediaEntry
.query
.\
615 filter_by(uploader
= user
.id,
616 state
= u
'processing').\
617 order_by(MediaEntry
.created
.desc())
619 # Get media entries which have failed to process
620 failed_entries
= MediaEntry
.query
.\
621 filter_by(uploader
= user
.id,
623 order_by(MediaEntry
.created
.desc())
625 processed_entries
= MediaEntry
.query
.\
626 filter_by(uploader
= user
.id,
627 state
= u
'processed').\
628 order_by(MediaEntry
.created
.desc()).\
632 return render_to_response(
634 'mediagoblin/user_pages/processing_panel.html',
636 'processing_entries': processing_entries
,
637 'failed_entries': failed_entries
,
638 'processed_entries': processed_entries
})