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