Merge branch '905-activities'
[larjonas-mediagoblin.git] / mediagoblin / user_pages / views.py
blobb6cbcabd4d8d07d02870bee697eab5ada577659f
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/>.
17 import logging
18 import datetime
19 import json
21 import six
23 from mediagoblin import messages, mg_globals
24 from mediagoblin.db.models import (MediaEntry, MediaTag, Collection,
25 CollectionItem, User)
26 from mediagoblin.tools.response import render_to_response, render_404, \
27 redirect, redirect_obj
28 from mediagoblin.tools.text import cleaned_markdown_conversion
29 from mediagoblin.tools.translate import pass_to_ugettext as _
30 from mediagoblin.tools.pagination import Pagination
31 from mediagoblin.tools.federation import create_activity
32 from mediagoblin.user_pages import forms as user_forms
33 from mediagoblin.user_pages.lib import (send_comment_email,
34 add_media_to_collection, build_report_object)
35 from mediagoblin.notifications import trigger_notification, \
36 add_comment_subscription, mark_comment_notification_seen
37 from mediagoblin.tools.pluginapi import hook_transform
39 from mediagoblin.decorators import (uses_pagination, get_user_media_entry,
40 get_media_entry_by_id, user_has_privilege, user_not_banned,
41 require_active_login, user_may_delete_media, user_may_alter_collection,
42 get_user_collection, get_user_collection_item, active_user_from_url,
43 get_optional_media_comment_by_id, allow_reporting)
45 from werkzeug.contrib.atom import AtomFeed
46 from werkzeug.exceptions import MethodNotAllowed
47 from werkzeug.wrappers import Response
50 _log = logging.getLogger(__name__)
51 _log.setLevel(logging.DEBUG)
53 @user_not_banned
54 @uses_pagination
55 def user_home(request, page):
56 """'Homepage' of a User()"""
57 user = User.query.filter_by(username=request.matchdict['user']).first()
58 if not user:
59 return render_404(request)
60 elif not user.has_privilege(u'active'):
61 return render_to_response(
62 request,
63 'mediagoblin/user_pages/user_nonactive.html',
64 {'user': user})
66 cursor = MediaEntry.query.\
67 filter_by(uploader = user.id,
68 state = u'processed').order_by(MediaEntry.created.desc())
70 pagination = Pagination(page, cursor)
71 media_entries = pagination()
73 #if no data is available, return NotFound
74 if media_entries == None:
75 return render_404(request)
77 user_gallery_url = request.urlgen(
78 'mediagoblin.user_pages.user_gallery',
79 user=user.username)
81 return render_to_response(
82 request,
83 'mediagoblin/user_pages/user.html',
84 {'user': user,
85 'user_gallery_url': user_gallery_url,
86 'media_entries': media_entries,
87 'pagination': pagination})
89 @user_not_banned
90 @active_user_from_url
91 @uses_pagination
92 def user_gallery(request, page, url_user=None):
93 """'Gallery' of a User()"""
94 tag = request.matchdict.get('tag', None)
95 cursor = MediaEntry.query.filter_by(
96 uploader=url_user.id,
97 state=u'processed').order_by(MediaEntry.created.desc())
99 # Filter potentially by tag too:
100 if tag:
101 cursor = cursor.filter(
102 MediaEntry.tags_helper.any(
103 MediaTag.slug == request.matchdict['tag']))
105 # Paginate gallery
106 pagination = Pagination(page, cursor)
107 media_entries = pagination()
109 #if no data is available, return NotFound
110 # TODO: Should we really also return 404 for empty galleries?
111 if media_entries == None:
112 return render_404(request)
114 return render_to_response(
115 request,
116 'mediagoblin/user_pages/gallery.html',
117 {'user': url_user, 'tag': tag,
118 'media_entries': media_entries,
119 'pagination': pagination})
122 MEDIA_COMMENTS_PER_PAGE = 50
124 @user_not_banned
125 @get_user_media_entry
126 @uses_pagination
127 def media_home(request, media, page, **kwargs):
129 'Homepage' of a MediaEntry()
131 comment_id = request.matchdict.get('comment', None)
132 if comment_id:
133 if request.user:
134 mark_comment_notification_seen(comment_id, request.user)
136 pagination = Pagination(
137 page, media.get_comments(
138 mg_globals.app_config['comments_ascending']),
139 MEDIA_COMMENTS_PER_PAGE,
140 comment_id)
141 else:
142 pagination = Pagination(
143 page, media.get_comments(
144 mg_globals.app_config['comments_ascending']),
145 MEDIA_COMMENTS_PER_PAGE)
147 comments = pagination()
149 comment_form = user_forms.MediaCommentForm(request.form)
151 media_template_name = media.media_manager.display_template
153 context = {
154 'media': media,
155 'comments': comments,
156 'pagination': pagination,
157 'comment_form': comment_form,
158 'app_config': mg_globals.app_config}
160 # Since the media template name gets swapped out for each media
161 # type, normal context hooks don't work if you want to affect all
162 # media displays. This gives a general purpose hook.
163 context = hook_transform(
164 "media_home_context", context)
166 return render_to_response(
167 request,
168 media_template_name,
169 context)
172 @get_media_entry_by_id
173 @user_has_privilege(u'commenter')
174 def media_post_comment(request, media):
176 recieves POST from a MediaEntry() comment form, saves the comment.
178 if not request.method == 'POST':
179 raise MethodNotAllowed()
181 comment = request.db.MediaComment()
182 comment.media_entry = media.id
183 comment.author = request.user.id
184 comment.content = six.text_type(request.form['comment_content'])
186 # Show error message if commenting is disabled.
187 if not mg_globals.app_config['allow_comments']:
188 messages.add_message(
189 request,
190 messages.ERROR,
191 _("Sorry, comments are disabled."))
192 elif not comment.content.strip():
193 messages.add_message(
194 request,
195 messages.ERROR,
196 _("Oops, your comment was empty."))
197 else:
198 comment.save()
200 messages.add_message(
201 request, messages.SUCCESS,
202 _('Your comment has been posted!'))
204 trigger_notification(comment, media, request)
205 create_activity("post", comment, comment.author, target=media)
206 add_comment_subscription(request.user, media)
208 return redirect_obj(request, media)
212 def media_preview_comment(request):
213 """Runs a comment through markdown so it can be previewed."""
214 # If this isn't an ajax request, render_404
215 if not request.is_xhr:
216 return render_404(request)
218 comment = six.text_type(request.form['comment_content'])
219 cleancomment = { "content":cleaned_markdown_conversion(comment)}
221 return Response(json.dumps(cleancomment))
223 @user_not_banned
224 @get_media_entry_by_id
225 @require_active_login
226 def media_collect(request, media):
227 """Add media to collection submission"""
229 form = user_forms.MediaCollectForm(request.form)
230 # A user's own collections:
231 form.collection.query = Collection.query.filter_by(
232 creator = request.user.id).order_by(Collection.title)
234 if request.method != 'POST' or not form.validate():
235 # No POST submission, or invalid form
236 if not form.validate():
237 messages.add_message(request, messages.ERROR,
238 _('Please check your entries and try again.'))
240 return render_to_response(
241 request,
242 'mediagoblin/user_pages/media_collect.html',
243 {'media': media,
244 'form': form})
246 # If we are here, method=POST and the form is valid, submit things.
247 # If the user is adding a new collection, use that:
248 if form.collection_title.data:
249 # Make sure this user isn't duplicating an existing collection
250 existing_collection = Collection.query.filter_by(
251 creator=request.user.id,
252 title=form.collection_title.data).first()
253 if existing_collection:
254 messages.add_message(request, messages.ERROR,
255 _('You already have a collection called "%s"!')
256 % existing_collection.title)
257 return redirect(request, "mediagoblin.user_pages.media_home",
258 user=media.get_uploader.username,
259 media=media.slug_or_id)
261 collection = Collection()
262 collection.title = form.collection_title.data
263 collection.description = form.collection_description.data
264 collection.creator = request.user.id
265 collection.generate_slug()
266 collection.save()
267 create_activity("create", collection, collection.creator)
269 # Otherwise, use the collection selected from the drop-down
270 else:
271 collection = form.collection.data
272 if collection and collection.creator != request.user.id:
273 collection = None
275 # Make sure the user actually selected a collection
276 if not collection:
277 messages.add_message(
278 request, messages.ERROR,
279 _('You have to select or add a collection'))
280 return redirect(request, "mediagoblin.user_pages.media_collect",
281 user=media.get_uploader.username,
282 media_id=media.id)
285 # Check whether media already exists in collection
286 elif CollectionItem.query.filter_by(
287 media_entry=media.id,
288 collection=collection.id).first():
289 messages.add_message(request, messages.ERROR,
290 _('"%s" already in collection "%s"')
291 % (media.title, collection.title))
292 else: # Add item to collection
293 add_media_to_collection(collection, media, form.note.data)
294 create_activity("add", media, request.user, target=collection)
295 messages.add_message(request, messages.SUCCESS,
296 _('"%s" added to collection "%s"')
297 % (media.title, collection.title))
299 return redirect_obj(request, media)
302 #TODO: Why does @user_may_delete_media not implicate @require_active_login?
303 @get_media_entry_by_id
304 @require_active_login
305 @user_may_delete_media
306 def media_confirm_delete(request, media):
308 form = user_forms.ConfirmDeleteForm(request.form)
310 if request.method == 'POST' and form.validate():
311 if form.confirm.data is True:
312 username = media.get_uploader.username
314 media.get_uploader.uploaded = media.get_uploader.uploaded - \
315 media.file_size
316 media.get_uploader.save()
318 # Delete MediaEntry and all related files, comments etc.
319 media.delete()
320 messages.add_message(
321 request, messages.SUCCESS, _('You deleted the media.'))
323 location = media.url_to_next(request.urlgen)
324 if not location:
325 location=media.url_to_prev(request.urlgen)
326 if not location:
327 location=request.urlgen("mediagoblin.user_pages.user_home",
328 user=username)
329 return redirect(request, location=location)
330 else:
331 messages.add_message(
332 request, messages.ERROR,
333 _("The media was not deleted because you didn't check that you were sure."))
334 return redirect_obj(request, media)
336 if ((request.user.has_privilege(u'admin') and
337 request.user.id != media.uploader)):
338 messages.add_message(
339 request, messages.WARNING,
340 _("You are about to delete another user's media. "
341 "Proceed with caution."))
343 return render_to_response(
344 request,
345 'mediagoblin/user_pages/media_confirm_delete.html',
346 {'media': media,
347 'form': form})
349 @user_not_banned
350 @active_user_from_url
351 @uses_pagination
352 def user_collection(request, page, url_user=None):
353 """A User-defined Collection"""
354 collection = Collection.query.filter_by(
355 get_creator=url_user,
356 slug=request.matchdict['collection']).first()
358 if not collection:
359 return render_404(request)
361 cursor = collection.get_collection_items()
363 pagination = Pagination(page, cursor)
364 collection_items = pagination()
366 # if no data is available, return NotFound
367 # TODO: Should an empty collection really also return 404?
368 if collection_items == None:
369 return render_404(request)
371 return render_to_response(
372 request,
373 'mediagoblin/user_pages/collection.html',
374 {'user': url_user,
375 'collection': collection,
376 'collection_items': collection_items,
377 'pagination': pagination})
379 @user_not_banned
380 @active_user_from_url
381 def collection_list(request, url_user=None):
382 """A User-defined Collection"""
383 collections = Collection.query.filter_by(
384 get_creator=url_user)
386 return render_to_response(
387 request,
388 'mediagoblin/user_pages/collection_list.html',
389 {'user': url_user,
390 'collections': collections})
393 @get_user_collection_item
394 @require_active_login
395 @user_may_alter_collection
396 def collection_item_confirm_remove(request, collection_item):
398 form = user_forms.ConfirmCollectionItemRemoveForm(request.form)
400 if request.method == 'POST' and form.validate():
401 username = collection_item.in_collection.get_creator.username
402 collection = collection_item.in_collection
404 if form.confirm.data is True:
405 entry = collection_item.get_media_entry
406 entry.save()
408 collection_item.delete()
409 collection.items = collection.items - 1
410 collection.save()
412 messages.add_message(
413 request, messages.SUCCESS, _('You deleted the item from the collection.'))
414 else:
415 messages.add_message(
416 request, messages.ERROR,
417 _("The item was not removed because you didn't check that you were sure."))
419 return redirect_obj(request, collection)
421 if ((request.user.has_privilege(u'admin') and
422 request.user.id != collection_item.in_collection.creator)):
423 messages.add_message(
424 request, messages.WARNING,
425 _("You are about to delete an item from another user's collection. "
426 "Proceed with caution."))
428 return render_to_response(
429 request,
430 'mediagoblin/user_pages/collection_item_confirm_remove.html',
431 {'collection_item': collection_item,
432 'form': form})
435 @get_user_collection
436 @require_active_login
437 @user_may_alter_collection
438 def collection_confirm_delete(request, collection):
440 form = user_forms.ConfirmDeleteForm(request.form)
442 if request.method == 'POST' and form.validate():
444 username = collection.get_creator.username
446 if form.confirm.data is True:
447 collection_title = collection.title
449 # Delete all the associated collection items
450 for item in collection.get_collection_items():
451 entry = item.get_media_entry
452 entry.save()
453 item.delete()
455 collection.delete()
456 messages.add_message(request, messages.SUCCESS,
457 _('You deleted the collection "%s"') % collection_title)
459 return redirect(request, "mediagoblin.user_pages.user_home",
460 user=username)
461 else:
462 messages.add_message(
463 request, messages.ERROR,
464 _("The collection was not deleted because you didn't check that you were sure."))
466 return redirect_obj(request, collection)
468 if ((request.user.has_privilege(u'admin') and
469 request.user.id != collection.creator)):
470 messages.add_message(
471 request, messages.WARNING,
472 _("You are about to delete another user's collection. "
473 "Proceed with caution."))
475 return render_to_response(
476 request,
477 'mediagoblin/user_pages/collection_confirm_delete.html',
478 {'collection': collection,
479 'form': form})
482 ATOM_DEFAULT_NR_OF_UPDATED_ITEMS = 15
485 def atom_feed(request):
487 generates the atom feed with the newest images
489 user = User.query.filter_by(
490 username = request.matchdict['user']).first()
491 if not user or not user.has_privilege(u'active'):
492 return render_404(request)
494 cursor = MediaEntry.query.filter_by(
495 uploader = user.id,
496 state = u'processed').\
497 order_by(MediaEntry.created.desc()).\
498 limit(ATOM_DEFAULT_NR_OF_UPDATED_ITEMS)
501 ATOM feed id is a tag URI (see http://en.wikipedia.org/wiki/Tag_URI)
503 atomlinks = [{
504 'href': request.urlgen(
505 'mediagoblin.user_pages.user_home',
506 qualified=True, user=request.matchdict['user']),
507 'rel': 'alternate',
508 'type': 'text/html'
511 if mg_globals.app_config["push_urls"]:
512 for push_url in mg_globals.app_config["push_urls"]:
513 atomlinks.append({
514 'rel': 'hub',
515 'href': push_url})
517 feed = AtomFeed(
518 "MediaGoblin: Feed for user '%s'" % request.matchdict['user'],
519 feed_url=request.url,
520 id='tag:{host},{year}:gallery.user-{user}'.format(
521 host=request.host,
522 year=datetime.datetime.today().strftime('%Y'),
523 user=request.matchdict['user']),
524 links=atomlinks)
526 for entry in cursor:
527 feed.add(entry.get('title'),
528 entry.description_html,
529 id=entry.url_for_self(request.urlgen, qualified=True),
530 content_type='html',
531 author={
532 'name': entry.get_uploader.username,
533 'uri': request.urlgen(
534 'mediagoblin.user_pages.user_home',
535 qualified=True, user=entry.get_uploader.username)},
536 updated=entry.get('created'),
537 links=[{
538 'href': entry.url_for_self(
539 request.urlgen,
540 qualified=True),
541 'rel': 'alternate',
542 'type': 'text/html'}])
544 return feed.get_response()
547 def collection_atom_feed(request):
549 generates the atom feed with the newest images from a collection
551 user = User.query.filter_by(
552 username = request.matchdict['user']).first()
553 if not user or not user.has_privilege(u'active'):
554 return render_404(request)
556 collection = Collection.query.filter_by(
557 creator=user.id,
558 slug=request.matchdict['collection']).first()
559 if not collection:
560 return render_404(request)
562 cursor = CollectionItem.query.filter_by(
563 collection=collection.id) \
564 .order_by(CollectionItem.added.desc()) \
565 .limit(ATOM_DEFAULT_NR_OF_UPDATED_ITEMS)
568 ATOM feed id is a tag URI (see http://en.wikipedia.org/wiki/Tag_URI)
570 atomlinks = [{
571 'href': collection.url_for_self(request.urlgen, qualified=True),
572 'rel': 'alternate',
573 'type': 'text/html'
576 if mg_globals.app_config["push_urls"]:
577 for push_url in mg_globals.app_config["push_urls"]:
578 atomlinks.append({
579 'rel': 'hub',
580 'href': push_url})
582 feed = AtomFeed(
583 "MediaGoblin: Feed for %s's collection %s" %
584 (request.matchdict['user'], collection.title),
585 feed_url=request.url,
586 id=u'tag:{host},{year}:gnu-mediagoblin.{user}.collection.{slug}'\
587 .format(
588 host=request.host,
589 year=collection.created.strftime('%Y'),
590 user=request.matchdict['user'],
591 slug=collection.slug),
592 links=atomlinks)
594 for item in cursor:
595 entry = item.get_media_entry
596 feed.add(entry.get('title'),
597 item.note_html,
598 id=entry.url_for_self(request.urlgen, qualified=True),
599 content_type='html',
600 author={
601 'name': entry.get_uploader.username,
602 'uri': request.urlgen(
603 'mediagoblin.user_pages.user_home',
604 qualified=True, user=entry.get_uploader.username)},
605 updated=item.get('added'),
606 links=[{
607 'href': entry.url_for_self(
608 request.urlgen,
609 qualified=True),
610 'rel': 'alternate',
611 'type': 'text/html'}])
613 return feed.get_response()
615 @require_active_login
616 def processing_panel(request):
618 Show to the user what media is still in conversion/processing...
619 and what failed, and why!
621 user = User.query.filter_by(username=request.matchdict['user']).first()
622 # TODO: XXX: Should this be a decorator?
624 # Make sure we have permission to access this user's panel. Only
625 # admins and this user herself should be able to do so.
626 if not (user.id == request.user.id or request.user.has_privilege(u'admin')):
627 # No? Simply redirect to this user's homepage.
628 return redirect(
629 request, 'mediagoblin.user_pages.user_home',
630 user=user.username)
632 # Get media entries which are in-processing
633 processing_entries = MediaEntry.query.\
634 filter_by(uploader = user.id,
635 state = u'processing').\
636 order_by(MediaEntry.created.desc())
638 # Get media entries which have failed to process
639 failed_entries = MediaEntry.query.\
640 filter_by(uploader = user.id,
641 state = u'failed').\
642 order_by(MediaEntry.created.desc())
644 processed_entries = MediaEntry.query.\
645 filter_by(uploader = user.id,
646 state = u'processed').\
647 order_by(MediaEntry.created.desc()).\
648 limit(10)
650 # Render to response
651 return render_to_response(
652 request,
653 'mediagoblin/user_pages/processing_panel.html',
654 {'user': user,
655 'processing_entries': processing_entries,
656 'failed_entries': failed_entries,
657 'processed_entries': processed_entries})
659 @allow_reporting
660 @get_user_media_entry
661 @user_has_privilege(u'reporter')
662 @get_optional_media_comment_by_id
663 def file_a_report(request, media, comment):
665 This view handles the filing of a MediaReport or a CommentReport.
667 if comment is not None:
668 if not comment.get_media_entry.id == media.id:
669 return render_404(request)
671 form = user_forms.CommentReportForm(request.form)
672 context = {'media': media,
673 'comment':comment,
674 'form':form}
675 else:
676 form = user_forms.MediaReportForm(request.form)
677 context = {'media': media,
678 'form':form}
679 form.reporter_id.data = request.user.id
682 if request.method == "POST":
683 report_object = build_report_object(form,
684 media_entry=media,
685 comment=comment)
687 # if the object was built successfully, report_table will not be None
688 if report_object:
689 report_object.save()
690 return redirect(
691 request,
692 'index')
695 return render_to_response(
696 request,
697 'mediagoblin/user_pages/report.html',
698 context)