Apply consistency & PEP8 to atom feeds
[mediagoblin.git] / mediagoblin / user_pages / views.py
1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
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
17 import logging
18 import datetime
19 import json
20
21 import six
22
23 from mediagoblin import messages, mg_globals
24 from mediagoblin.db.models import (MediaEntry, MediaTag, Collection, Comment,
25 CollectionItem, LocalUser, Activity, \
26 GenericModelReference)
27 from mediagoblin.plugins.api.tools import get_media_file_paths
28 from mediagoblin.tools.response import render_to_response, render_404, \
29 redirect, redirect_obj
30 from mediagoblin.tools.text import cleaned_markdown_conversion
31 from mediagoblin.tools.translate import pass_to_ugettext as _
32 from mediagoblin.tools.pagination import Pagination
33 from mediagoblin.tools.federation import create_activity
34 from mediagoblin.user_pages import forms as user_forms
35 from mediagoblin.user_pages.lib import (send_comment_email,
36 add_media_to_collection, build_report_object)
37 from mediagoblin.notifications import trigger_notification, \
38 add_comment_subscription, mark_comment_notification_seen
39 from mediagoblin.tools.pluginapi import hook_transform
40
41 from mediagoblin.decorators import (uses_pagination, get_user_media_entry,
42 get_media_entry_by_id, user_has_privilege, user_not_banned,
43 require_active_login, user_may_delete_media, user_may_alter_collection,
44 get_user_collection, get_user_collection_item, active_user_from_url,
45 get_optional_media_comment_by_id, allow_reporting)
46
47 from werkzeug.contrib.atom import AtomFeed
48 from werkzeug.exceptions import MethodNotAllowed
49 from werkzeug.wrappers import Response
50
51
52 _log = logging.getLogger(__name__)
53 _log.setLevel(logging.DEBUG)
54
55 @user_not_banned
56 @uses_pagination
57 def user_home(request, page):
58 """'Homepage' of a LocalUser()"""
59 user = LocalUser.query.filter_by(username=request.matchdict['user']).first()
60 if not user:
61 return render_404(request)
62 elif not user.has_privilege(u'active'):
63 return render_to_response(
64 request,
65 'mediagoblin/user_pages/user_nonactive.html',
66 {'user': user})
67
68 cursor = MediaEntry.query.\
69 filter_by(actor = user.id,
70 state = u'processed').order_by(MediaEntry.created.desc())
71
72 pagination = Pagination(page, cursor)
73 media_entries = pagination()
74
75 #if no data is available, return NotFound
76 if media_entries == None:
77 return render_404(request)
78
79 user_gallery_url = request.urlgen(
80 'mediagoblin.user_pages.user_gallery',
81 user=user.username)
82
83 return render_to_response(
84 request,
85 'mediagoblin/user_pages/user.html',
86 {'user': user,
87 'user_gallery_url': user_gallery_url,
88 'media_entries': media_entries,
89 'pagination': pagination})
90
91 @user_not_banned
92 @active_user_from_url
93 @uses_pagination
94 def user_gallery(request, page, url_user=None):
95 """'Gallery' of a LocalUser()"""
96 tag = request.matchdict.get('tag', None)
97 cursor = MediaEntry.query.filter_by(
98 actor=url_user.id,
99 state=u'processed').order_by(MediaEntry.created.desc())
100
101 # Filter potentially by tag too:
102 if tag:
103 cursor = cursor.filter(
104 MediaEntry.tags_helper.any(
105 MediaTag.slug == request.matchdict['tag']))
106
107 # Paginate gallery
108 pagination = Pagination(page, cursor)
109 media_entries = pagination()
110
111 #if no data is available, return NotFound
112 # TODO: Should we really also return 404 for empty galleries?
113 if media_entries == None:
114 return render_404(request)
115
116 return render_to_response(
117 request,
118 'mediagoblin/user_pages/gallery.html',
119 {'user': url_user, 'tag': tag,
120 'media_entries': media_entries,
121 'pagination': pagination})
122
123
124 MEDIA_COMMENTS_PER_PAGE = 50
125
126 @user_not_banned
127 @get_user_media_entry
128 @uses_pagination
129 def media_home(request, media, page, **kwargs):
130 """
131 'Homepage' of a MediaEntry()
132 """
133 comment_id = request.matchdict.get('comment', None)
134 if comment_id:
135 if request.user:
136 mark_comment_notification_seen(comment_id, request.user)
137
138 pagination = Pagination(
139 page, media.get_comments(
140 mg_globals.app_config['comments_ascending']),
141 MEDIA_COMMENTS_PER_PAGE,
142 comment_id)
143 else:
144 pagination = Pagination(
145 page, media.get_comments(
146 mg_globals.app_config['comments_ascending']),
147 MEDIA_COMMENTS_PER_PAGE)
148
149 comments = pagination()
150
151 comment_form = user_forms.MediaCommentForm(request.form)
152
153 media_template_name = media.media_manager.display_template
154
155 context = {
156 'media': media,
157 'comments': comments,
158 'pagination': pagination,
159 'comment_form': comment_form,
160 'app_config': mg_globals.app_config}
161
162 # Since the media template name gets swapped out for each media
163 # type, normal context hooks don't work if you want to affect all
164 # media displays. This gives a general purpose hook.
165 context = hook_transform(
166 "media_home_context", context)
167
168 return render_to_response(
169 request,
170 media_template_name,
171 context)
172
173
174 @get_media_entry_by_id
175 @user_has_privilege(u'commenter')
176 def media_post_comment(request, media):
177 """
178 recieves POST from a MediaEntry() comment form, saves the comment.
179 """
180 if not request.method == 'POST':
181 raise MethodNotAllowed()
182
183 comment = request.db.TextComment()
184 comment.actor = request.user.id
185 comment.content = six.text_type(request.form['comment_content'])
186
187 # Show error message if commenting is disabled.
188 if not mg_globals.app_config['allow_comments']:
189 messages.add_message(
190 request,
191 messages.ERROR,
192 _("Sorry, comments are disabled."))
193 elif not comment.content.strip():
194 messages.add_message(
195 request,
196 messages.ERROR,
197 _("Oops, your comment was empty."))
198 else:
199 create_activity("post", comment, comment.actor, target=media)
200 add_comment_subscription(request.user, media)
201 comment.save()
202
203 link = request.db.Comment()
204 link.target = media
205 link.comment = comment
206 link.save()
207
208 messages.add_message(
209 request,
210 messages.SUCCESS,
211 _('Your comment has been posted!'))
212 trigger_notification(link, media, request)
213
214 return redirect_obj(request, media)
215
216
217
218 def media_preview_comment(request):
219 """Runs a comment through markdown so it can be previewed."""
220 # If this isn't an ajax request, render_404
221 if not request.is_xhr:
222 return render_404(request)
223
224 comment = six.text_type(request.form['comment_content'])
225 cleancomment = { "content":cleaned_markdown_conversion(comment)}
226
227 return Response(json.dumps(cleancomment))
228
229 @user_not_banned
230 @get_media_entry_by_id
231 @require_active_login
232 def media_collect(request, media):
233 """Add media to collection submission"""
234
235 form = user_forms.MediaCollectForm(request.form)
236 # A user's own collections:
237 form.collection.query = Collection.query.filter_by(
238 actor=request.user.id,
239 type=Collection.USER_DEFINED_TYPE
240 ).order_by(Collection.title)
241
242 if request.method != 'POST' or not form.validate():
243 # No POST submission, or invalid form
244 if not form.validate():
245 messages.add_message(
246 request,
247 messages.ERROR,
248 _('Please check your entries and try again.'))
249
250 return render_to_response(
251 request,
252 'mediagoblin/user_pages/media_collect.html',
253 {'media': media,
254 'form': form})
255
256 # If we are here, method=POST and the form is valid, submit things.
257 # If the user is adding a new collection, use that:
258 if form.collection_title.data:
259 # Make sure this user isn't duplicating an existing collection
260 existing_collection = Collection.query.filter_by(
261 actor=request.user.id,
262 title=form.collection_title.data,
263 type=Collection.USER_DEFINED_TYPE
264 ).first()
265 if existing_collection:
266 messages.add_message(
267 request,
268 messages.ERROR,
269 _('You already have a collection called "%s"!') %
270 existing_collection.title)
271 return redirect(request, "mediagoblin.user_pages.media_home",
272 user=media.get_actor.username,
273 media=media.slug_or_id)
274
275 collection = Collection()
276 collection.title = form.collection_title.data
277 collection.description = form.collection_description.data
278 collection.actor = request.user.id
279 collection.type = Collection.USER_DEFINED_TYPE
280 collection.generate_slug()
281 collection.get_public_id(request.urlgen)
282 create_activity("create", collection, collection.actor)
283 collection.save()
284
285 # Otherwise, use the collection selected from the drop-down
286 else:
287 collection = form.collection.data
288 if collection and collection.actor != request.user.id:
289 collection = None
290
291 # Make sure the user actually selected a collection
292 item = CollectionItem.query.filter_by(collection=collection.id)
293 item = item.join(CollectionItem.object_helper).filter_by(
294 model_type=media.__tablename__,
295 obj_pk=media.id
296 ).first()
297
298 if not collection:
299 messages.add_message(
300 request,
301 messages.ERROR,
302 _('You have to select or add a collection'))
303 return redirect(request, "mediagoblin.user_pages.media_collect",
304 user=media.get_actor.username,
305 media_id=media.id)
306
307 # Check whether media already exists in collection
308 elif item is not None:
309 messages.add_message(
310 request,
311 messages.ERROR,
312 _('"%s" already in collection "%s"') %
313 (media.title, collection.title))
314 else: # Add item to collection
315 add_media_to_collection(collection, media, form.note.data)
316 create_activity("add", media, request.user, target=collection)
317 messages.add_message(
318 request,
319 messages.SUCCESS,
320 _('"%s" added to collection "%s"') %
321 (media.title, collection.title))
322
323 return redirect_obj(request, media)
324
325
326 #TODO: Why does @user_may_delete_media not implicate @require_active_login?
327 @get_media_entry_by_id
328 @require_active_login
329 @user_may_delete_media
330 def media_confirm_delete(request, media):
331
332 form = user_forms.ConfirmDeleteForm(request.form)
333
334 if request.method == 'POST' and form.validate():
335 if form.confirm.data is True:
336 username = media.get_actor.username
337
338 # This probably is already filled but just in case it has slipped
339 # through the net somehow, we need to try and make sure the
340 # MediaEntry has a public ID so it gets properly soft-deleted.
341 media.get_public_id(request.urlgen)
342
343 # Decrement the users uploaded quota.
344 media.get_actor.uploaded = media.get_actor.uploaded - \
345 media.file_size
346 media.get_actor.save()
347
348 # Delete MediaEntry and all related files, comments etc.
349 media.delete()
350 messages.add_message(
351 request,
352 messages.SUCCESS,
353 _('You deleted the media.'))
354
355 location = media.url_to_next(request.urlgen)
356 if not location:
357 location=media.url_to_prev(request.urlgen)
358 if not location:
359 location=request.urlgen("mediagoblin.user_pages.user_home",
360 user=username)
361 return redirect(request, location=location)
362 else:
363 messages.add_message(
364 request,
365 messages.ERROR,
366 _("The media was not deleted because you didn't check "
367 "that you were sure."))
368 return redirect_obj(request, media)
369
370 if ((request.user.has_privilege(u'admin') and
371 request.user.id != media.actor)):
372 messages.add_message(
373 request,
374 messages.WARNING,
375 _("You are about to delete another user's media. "
376 "Proceed with caution."))
377
378 return render_to_response(
379 request,
380 'mediagoblin/user_pages/media_confirm_delete.html',
381 {'media': media,
382 'form': form})
383
384 @user_not_banned
385 @active_user_from_url
386 @uses_pagination
387 def user_collection(request, page, url_user=None):
388 """A User-defined Collection"""
389 collection = Collection.query.filter_by(
390 get_actor=url_user,
391 slug=request.matchdict['collection']).first()
392
393 if not collection:
394 return render_404(request)
395
396 cursor = collection.get_collection_items()
397
398 pagination = Pagination(page, cursor)
399 collection_items = pagination()
400
401 # if no data is available, return NotFound
402 # TODO: Should an empty collection really also return 404?
403 if collection_items == None:
404 return render_404(request)
405
406 return render_to_response(
407 request,
408 'mediagoblin/user_pages/collection.html',
409 {'user': url_user,
410 'collection': collection,
411 'collection_items': collection_items,
412 'pagination': pagination})
413
414 @user_not_banned
415 @active_user_from_url
416 def collection_list(request, url_user=None):
417 """A User-defined Collection"""
418 collections = Collection.query.filter_by(
419 get_actor=url_user)
420
421 return render_to_response(
422 request,
423 'mediagoblin/user_pages/collection_list.html',
424 {'user': url_user,
425 'collections': collections})
426
427
428 @get_user_collection_item
429 @require_active_login
430 @user_may_alter_collection
431 def collection_item_confirm_remove(request, collection_item):
432
433 form = user_forms.ConfirmCollectionItemRemoveForm(request.form)
434
435 if request.method == 'POST' and form.validate():
436 username = collection_item.in_collection.get_actor.username
437 collection = collection_item.in_collection
438
439 if form.confirm.data is True:
440 obj = collection_item.get_object()
441 obj.save()
442
443 collection_item.delete()
444 collection.num_items = collection.num_items - 1
445 collection.save()
446
447 messages.add_message(
448 request,
449 messages.SUCCESS,
450 _('You deleted the item from the collection.'))
451 else:
452 messages.add_message(
453 request,
454 messages.ERROR,
455 _("The item was not removed because you didn't check "
456 "that you were sure."))
457
458 return redirect_obj(request, collection)
459
460 if ((request.user.has_privilege(u'admin') and
461 request.user.id != collection_item.in_collection.actor)):
462 messages.add_message(
463 request,
464 messages.WARNING,
465 _("You are about to delete an item from another user's collection. "
466 "Proceed with caution."))
467
468 return render_to_response(
469 request,
470 'mediagoblin/user_pages/collection_item_confirm_remove.html',
471 {'collection_item': collection_item,
472 'form': form})
473
474
475 @get_user_collection
476 @require_active_login
477 @user_may_alter_collection
478 def collection_confirm_delete(request, collection):
479
480 form = user_forms.ConfirmDeleteForm(request.form)
481
482 if request.method == 'POST' and form.validate():
483
484 username = collection.get_actor.username
485
486 if form.confirm.data is True:
487 collection_title = collection.title
488
489 # Firstly like with the MediaEntry delete, lets ensure the
490 # public_id is populated as this is really important!
491 collection.get_public_id(request.urlgen)
492
493 # Delete all the associated collection items
494 for item in collection.get_collection_items():
495 obj = item.get_object()
496 obj.save()
497 item.delete()
498
499 collection.delete()
500 messages.add_message(
501 request,
502 messages.SUCCESS,
503 _('You deleted the collection "%s"') %
504 collection_title)
505
506 return redirect(request, "mediagoblin.user_pages.user_home",
507 user=username)
508 else:
509 messages.add_message(
510 request,
511 messages.ERROR,
512 _("The collection was not deleted because you didn't "
513 "check that you were sure."))
514
515 return redirect_obj(request, collection)
516
517 if ((request.user.has_privilege(u'admin') and
518 request.user.id != collection.actor)):
519 messages.add_message(
520 request, messages.WARNING,
521 _("You are about to delete another user's collection. "
522 "Proceed with caution."))
523
524 return render_to_response(
525 request,
526 'mediagoblin/user_pages/collection_confirm_delete.html',
527 {'collection': collection,
528 'form': form})
529
530
531 ATOM_DEFAULT_NR_OF_UPDATED_ITEMS = 15
532
533
534 def atom_feed(request):
535 """
536 generates the atom feed with the newest images
537 """
538 user = LocalUser.query.filter_by(
539 username = request.matchdict['user']).first()
540 if not user or not user.has_privilege(u'active'):
541 return render_404(request)
542 feed_title = "MediaGoblin Feed for user '%s'" % request.matchdict['user']
543 link = request.urlgen('mediagoblin.user_pages.user_home',
544 qualified=True, user=request.matchdict['user'])
545 cursor = MediaEntry.query.filter_by(actor=user.id, state=u'processed')
546 cursor = cursor.order_by(MediaEntry.created.desc())
547 cursor = cursor.limit(ATOM_DEFAULT_NR_OF_UPDATED_ITEMS)
548
549
550 """
551 ATOM feed id is a tag URI (see http://en.wikipedia.org/wiki/Tag_URI)
552 """
553 atomlinks = [{
554 'href': link,
555 'rel': 'alternate',
556 'type': 'text/html'}]
557
558 if mg_globals.app_config["push_urls"]:
559 for push_url in mg_globals.app_config["push_urls"]:
560 atomlinks.append({
561 'rel': 'hub',
562 'href': push_url})
563
564 feed = AtomFeed(
565 feed_title,
566 feed_url=request.url,
567 id='tag:{host},{year}:gallery.user-{user}'.format(
568 host=request.host,
569 year=datetime.datetime.today().strftime('%Y'),
570 user=request.matchdict['user']),
571 links=atomlinks)
572
573 for entry in cursor:
574 # Include a thumbnail image in content.
575 file_urls = get_media_file_paths(entry.media_files, request.urlgen)
576 if 'thumb' in file_urls:
577 content = '<img src="{thumb}" alt='' /> {desc}'.format(
578 thumb=file_urls['thumb'], desc=entry.description_html)
579 else:
580 content = entry.description_html
581
582 feed.add(
583 entry.get('title'),
584 content,
585 id=entry.url_for_self(request.urlgen, qualified=True),
586 content_type='html',
587 author={
588 'name': entry.get_actor.username,
589 'uri': request.urlgen(
590 'mediagoblin.user_pages.user_home',
591 qualified=True,
592 user=entry.get_actor.username)},
593 updated=entry.get('created'),
594 links=[{
595 'href': entry.url_for_self(
596 request.urlgen,
597 qualified=True),
598 'rel': 'alternate',
599 'type': 'text/html'}])
600
601 return feed.get_response()
602
603
604 def collection_atom_feed(request):
605 """
606 generates the atom feed with the newest images from a collection
607 """
608 user = LocalUser.query.filter_by(
609 username = request.matchdict['user']).first()
610 if not user or not user.has_privilege(u'active'):
611 return render_404(request)
612
613 collection = Collection.query.filter_by(
614 actor=user.id,
615 slug=request.matchdict['collection']).first()
616 if not collection:
617 return render_404(request)
618
619 cursor = CollectionItem.query.filter_by(
620 collection=collection.id) \
621 .order_by(CollectionItem.added.desc()) \
622 .limit(ATOM_DEFAULT_NR_OF_UPDATED_ITEMS)
623
624 """
625 ATOM feed id is a tag URI (see http://en.wikipedia.org/wiki/Tag_URI)
626 """
627 atomlinks = [{
628 'href': collection.url_for_self(request.urlgen, qualified=True),
629 'rel': 'alternate',
630 'type': 'text/html'
631 }]
632
633 if mg_globals.app_config["push_urls"]:
634 for push_url in mg_globals.app_config["push_urls"]:
635 atomlinks.append({
636 'rel': 'hub',
637 'href': push_url})
638
639 feed = AtomFeed(
640 "MediaGoblin: Feed for %s's collection %s" %
641 (request.matchdict['user'], collection.title),
642 feed_url=request.url,
643 id=u'tag:{host},{year}:gnu-mediagoblin.{user}.collection.{slug}'\
644 .format(
645 host=request.host,
646 year=collection.created.strftime('%Y'),
647 user=request.matchdict['user'],
648 slug=collection.slug),
649 links=atomlinks)
650
651 for item in cursor:
652 obj = item.get_object()
653 feed.add(
654 obj.get('title'),
655 item.note_html,
656 id=obj.url_for_self(request.urlgen, qualified=True),
657 content_type='html',
658 author={
659 'name': obj.get_actor.username,
660 'uri': request.urlgen(
661 'mediagoblin.user_pages.user_home',
662 qualified=True, user=obj.get_actor.username)},
663 updated=item.get('added'),
664 links=[{
665 'href': obj.url_for_self(
666 request.urlgen,
667 qualified=True),
668 'rel': 'alternate',
669 'type': 'text/html'}])
670
671 return feed.get_response()
672
673 @active_user_from_url
674 @uses_pagination
675 @require_active_login
676 def processing_panel(request, page, url_user):
677 """
678 Show to the user what media is still in conversion/processing...
679 and what failed, and why!
680 """
681 user = LocalUser.query.filter_by(username=request.matchdict['user']).first()
682 # TODO: XXX: Should this be a decorator?
683 #
684 # Make sure we have permission to access this user's panel. Only
685 # admins and this user herself should be able to do so.
686 if not (user.id == request.user.id or request.user.has_privilege(u'admin')):
687 # No? Simply redirect to this user's homepage.
688 return redirect(
689 request, 'mediagoblin.user_pages.user_home',
690 user=user.username)
691 # Get media entries which are in-processing
692 entries = (MediaEntry.query.filter_by(actor=user.id)
693 .order_by(MediaEntry.created.desc()))
694
695 try:
696 state = request.matchdict['state']
697 # no exception was thrown, filter entries by state
698 entries = entries.filter_by(state=state)
699 except KeyError:
700 # show all entries
701 pass
702
703 pagination = Pagination(page, entries)
704 pagination.per_page = 30
705 entries_on_a_page = pagination()
706
707 # Render to response
708 return render_to_response(
709 request,
710 'mediagoblin/user_pages/processing_panel.html',
711 {'user': user,
712 'entries': entries_on_a_page,
713 'pagination': pagination})
714
715 @allow_reporting
716 @get_user_media_entry
717 @user_has_privilege(u'reporter')
718 @get_optional_media_comment_by_id
719 def file_a_report(request, media, comment):
720 """
721 This view handles the filing of a Report.
722 """
723 if comment is not None:
724 if not comment.target().id == media.id:
725 return render_404(request)
726
727 form = user_forms.CommentReportForm(request.form)
728 context = {'media': comment.target(),
729 'comment':comment,
730 'form':form}
731 else:
732 form = user_forms.MediaReportForm(request.form)
733 context = {'media': media,
734 'form':form}
735 form.reporter_id.data = request.user.id
736
737
738 if request.method == "POST":
739 report_object = build_report_object(
740 form,
741 media_entry=media,
742 comment=comment
743 )
744
745 # if the object was built successfully, report_table will not be None
746 if report_object:
747 report_object.save()
748 return redirect(
749 request,
750 'index')
751
752
753 return render_to_response(
754 request,
755 'mediagoblin/user_pages/report.html',
756 context)
757
758 @require_active_login
759 def activity_view(request):
760 """ /<username>/activity/<id> - Display activity
761
762 This should display a HTML presentation of the activity
763 this is NOT an API endpoint.
764 """
765 # Get the user object.
766 username = request.matchdict["username"]
767 user = LocalUser.query.filter_by(username=username).first()
768
769 activity_id = request.matchdict["id"]
770
771 if request.user is None:
772 return render_404(request)
773
774 activity = Activity.query.filter_by(
775 id=activity_id,
776 author=user.id
777 ).first()
778
779 # There isn't many places to check that the public_id is filled so this
780 # will do, it really should be, lets try and fix that if it isn't.
781 activity.get_public_id(request.urlgen)
782
783 if activity is None:
784 return render_404(request)
785
786 return render_to_response(
787 request,
788 "mediagoblin/api/activity.html",
789 {"activity": activity}
790 )