At this point, I am very close to done with this code! I made one big change at
[mediagoblin: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 from mediagoblin import messages, mg_globals
22 from mediagoblin.db.models import (MediaEntry, MediaTag, Collection,
23                                    CollectionItem, User, MediaComment,
24                                    CommentReport, MediaReport)
25 from mediagoblin.tools.response import render_to_response, render_404, \
26     redirect, redirect_obj
27 from mediagoblin.tools.text import cleaned_markdown_conversion
28 from mediagoblin.tools.translate import pass_to_ugettext as _
29 from mediagoblin.tools.pagination import Pagination
30 from mediagoblin.user_pages import forms as user_forms
31 from mediagoblin.user_pages.lib import (send_comment_email,
32         add_media_to_collection, build_report_object)
33 from mediagoblin.notifications import trigger_notification, \
34     add_comment_subscription, mark_comment_notification_seen
35
36 from mediagoblin.decorators import (uses_pagination, get_user_media_entry,
37     get_media_entry_by_id, user_has_privilege, user_not_banned,
38     require_active_login, user_may_delete_media, user_may_alter_collection,
39     get_user_collection, get_user_collection_item, active_user_from_url,
40     get_optional_media_comment_by_id, allow_reporting)
41
42 from werkzeug.contrib.atom import AtomFeed
43 from werkzeug.exceptions import MethodNotAllowed
44 from werkzeug.wrappers import Response
45
46
47 _log = logging.getLogger(__name__)
48 _log.setLevel(logging.DEBUG)
49
50 @user_not_banned
51 @uses_pagination
52 def user_home(request, page):
53     """'Homepage' of a User()"""
54     # TODO: decide if we only want homepages for active users, we can
55     # then use the @get_active_user decorator and also simplify the
56     # template html.
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.html',
64             {'user': user})
65
66     cursor = MediaEntry.query.\
67         filter_by(uploader = user.id,
68                   state = u'processed').order_by(MediaEntry.created.desc())
69
70     pagination = Pagination(page, cursor)
71     media_entries = pagination()
72
73     #if no data is available, return NotFound
74     if media_entries == None:
75         return render_404(request)
76
77     user_gallery_url = request.urlgen(
78         'mediagoblin.user_pages.user_gallery',
79         user=user.username)
80
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})
88
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())
98
99     # Filter potentially by tag too:
100     if tag:
101         cursor = cursor.filter(
102             MediaEntry.tags_helper.any(
103                 MediaTag.slug == request.matchdict['tag']))
104
105     # Paginate gallery
106     pagination = Pagination(page, cursor)
107     media_entries = pagination()
108
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)
113
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})
120
121
122 MEDIA_COMMENTS_PER_PAGE = 50
123
124 @user_not_banned
125 @get_user_media_entry
126 @uses_pagination
127 def media_home(request, media, page, **kwargs):
128     """
129     'Homepage' of a MediaEntry()
130     """
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)
135
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)
146
147     comments = pagination()
148
149     comment_form = user_forms.MediaCommentForm(request.form)
150
151     media_template_name = media.media_manager.display_template
152
153     return render_to_response(
154         request,
155         media_template_name,
156         {'media': media,
157          'comments': comments,
158          'pagination': pagination,
159          'comment_form': comment_form,
160          'app_config': mg_globals.app_config})
161
162
163 @get_media_entry_by_id
164 @user_has_privilege(u'commenter')
165 def media_post_comment(request, media):
166     """
167     recieves POST from a MediaEntry() comment form, saves the comment.
168     """
169     if not request.method == 'POST':
170         raise MethodNotAllowed()
171
172     comment = request.db.MediaComment()
173     comment.media_entry = media.id
174     comment.author = request.user.id
175     comment.content = unicode(request.form['comment_content'])
176
177     # Show error message if commenting is disabled.
178     if not mg_globals.app_config['allow_comments']:
179         messages.add_message(
180             request,
181             messages.ERROR,
182             _("Sorry, comments are disabled."))
183     elif not comment.content.strip():
184         messages.add_message(
185             request,
186             messages.ERROR,
187             _("Oops, your comment was empty."))
188     else:
189         comment.save()
190
191         messages.add_message(
192             request, messages.SUCCESS,
193             _('Your comment has been posted!'))
194
195         trigger_notification(comment, media, request)
196
197         add_comment_subscription(request.user, media)
198
199     return redirect_obj(request, media)
200
201
202
203 def media_preview_comment(request):
204     """Runs a comment through markdown so it can be previewed."""
205     # If this isn't an ajax request, render_404
206     if not request.is_xhr:
207         return render_404(request)
208
209     comment = unicode(request.form['comment_content'])
210     cleancomment = { "content":cleaned_markdown_conversion(comment)}
211
212     return Response(json.dumps(cleancomment))
213
214 @user_not_banned
215 @get_media_entry_by_id
216 @require_active_login
217 def media_collect(request, media):
218     """Add media to collection submission"""
219
220     form = user_forms.MediaCollectForm(request.form)
221     # A user's own collections:
222     form.collection.query = Collection.query.filter_by(
223         creator = request.user.id).order_by(Collection.title)
224
225     if request.method != 'POST' or not form.validate():
226         # No POST submission, or invalid form
227         if not form.validate():
228             messages.add_message(request, messages.ERROR,
229                 _('Please check your entries and try again.'))
230
231         return render_to_response(
232             request,
233             'mediagoblin/user_pages/media_collect.html',
234             {'media': media,
235              'form': form})
236
237     # If we are here, method=POST and the form is valid, submit things.
238     # If the user is adding a new collection, use that:
239     if form.collection_title.data:
240         # Make sure this user isn't duplicating an existing collection
241         existing_collection = Collection.query.filter_by(
242                                 creator=request.user.id,
243                                 title=form.collection_title.data).first()
244         if existing_collection:
245             messages.add_message(request, messages.ERROR,
246                 _('You already have a collection called "%s"!')
247                 % existing_collection.title)
248             return redirect(request, "mediagoblin.user_pages.media_home",
249                             user=media.get_uploader.username,
250                             media=media.slug_or_id)
251
252         collection = Collection()
253         collection.title = form.collection_title.data
254         collection.description = form.collection_description.data
255         collection.creator = request.user.id
256         collection.generate_slug()
257         collection.save()
258
259     # Otherwise, use the collection selected from the drop-down
260     else:
261         collection = form.collection.data
262         if collection and collection.creator != request.user.id:
263             collection = None
264
265     # Make sure the user actually selected a collection
266     if not collection:
267         messages.add_message(
268             request, messages.ERROR,
269             _('You have to select or add a collection'))
270         return redirect(request, "mediagoblin.user_pages.media_collect",
271                     user=media.get_uploader.username,
272                     media_id=media.id)
273
274
275     # Check whether media already exists in collection
276     elif CollectionItem.query.filter_by(
277         media_entry=media.id,
278         collection=collection.id).first():
279         messages.add_message(request, messages.ERROR,
280                              _('"%s" already in collection "%s"')
281                              % (media.title, collection.title))
282     else: # Add item to collection
283         add_media_to_collection(collection, media, form.note.data)
284
285         messages.add_message(request, messages.SUCCESS,
286                              _('"%s" added to collection "%s"')
287                              % (media.title, collection.title))
288
289     return redirect_obj(request, media)
290
291
292 #TODO: Why does @user_may_delete_media not implicate @require_active_login?
293 @get_media_entry_by_id
294 @require_active_login
295 @user_may_delete_media
296 def media_confirm_delete(request, media):
297
298     form = user_forms.ConfirmDeleteForm(request.form)
299
300     if request.method == 'POST' and form.validate():
301         if form.confirm.data is True:
302             username = media.get_uploader.username
303             # Delete MediaEntry and all related files, comments etc.
304             media.delete()
305             messages.add_message(
306                 request, messages.SUCCESS, _('You deleted the media.'))
307
308             location = media.url_to_next(request.urlgen)
309             if not location:
310                 location=media.url_to_prev(request.urlgen)
311             if not location:
312                 location=request.urlgen("mediagoblin.user_pages.user_home",
313                                         user=username)
314             return redirect(request, location=location)
315         else:
316             messages.add_message(
317                 request, messages.ERROR,
318                 _("The media was not deleted because you didn't check that you were sure."))
319             return redirect_obj(request, media)
320
321     if ((request.user.has_privilege(u'admin') and
322          request.user.id != media.uploader)):
323         messages.add_message(
324             request, messages.WARNING,
325             _("You are about to delete another user's media. "
326               "Proceed with caution."))
327
328     return render_to_response(
329         request,
330         'mediagoblin/user_pages/media_confirm_delete.html',
331         {'media': media,
332          'form': form})
333
334 @user_not_banned
335 @active_user_from_url
336 @uses_pagination
337 def user_collection(request, page, url_user=None):
338     """A User-defined Collection"""
339     collection = Collection.query.filter_by(
340         get_creator=url_user,
341         slug=request.matchdict['collection']).first()
342
343     if not collection:
344         return render_404(request)
345
346     cursor = collection.get_collection_items()
347
348     pagination = Pagination(page, cursor)
349     collection_items = pagination()
350
351     # if no data is available, return NotFound
352     # TODO: Should an empty collection really also return 404?
353     if collection_items == None:
354         return render_404(request)
355
356     return render_to_response(
357         request,
358         'mediagoblin/user_pages/collection.html',
359         {'user': url_user,
360          'collection': collection,
361          'collection_items': collection_items,
362          'pagination': pagination})
363
364 @user_not_banned
365 @active_user_from_url
366 def collection_list(request, url_user=None):
367     """A User-defined Collection"""
368     collections = Collection.query.filter_by(
369         get_creator=url_user)
370
371     return render_to_response(
372         request,
373         'mediagoblin/user_pages/collection_list.html',
374         {'user': url_user,
375          'collections': collections})
376
377
378 @get_user_collection_item
379 @require_active_login
380 @user_may_alter_collection
381 def collection_item_confirm_remove(request, collection_item):
382
383     form = user_forms.ConfirmCollectionItemRemoveForm(request.form)
384
385     if request.method == 'POST' and form.validate():
386         username = collection_item.in_collection.get_creator.username
387         collection = collection_item.in_collection
388
389         if form.confirm.data is True:
390             entry = collection_item.get_media_entry
391             entry.collected = entry.collected - 1
392             entry.save()
393
394             collection_item.delete()
395             collection.items = collection.items - 1
396             collection.save()
397
398             messages.add_message(
399                 request, messages.SUCCESS, _('You deleted the item from the collection.'))
400         else:
401             messages.add_message(
402                 request, messages.ERROR,
403                 _("The item was not removed because you didn't check that you were sure."))
404
405         return redirect_obj(request, collection)
406
407     if ((request.user.has_privilege(u'admin') and
408          request.user.id != collection_item.in_collection.creator)):
409         messages.add_message(
410             request, messages.WARNING,
411             _("You are about to delete an item from another user's collection. "
412               "Proceed with caution."))
413
414     return render_to_response(
415         request,
416         'mediagoblin/user_pages/collection_item_confirm_remove.html',
417         {'collection_item': collection_item,
418          'form': form})
419
420
421 @get_user_collection
422 @require_active_login
423 @user_may_alter_collection
424 def collection_confirm_delete(request, collection):
425
426     form = user_forms.ConfirmDeleteForm(request.form)
427
428     if request.method == 'POST' and form.validate():
429
430         username = collection.get_creator.username
431
432         if form.confirm.data is True:
433             collection_title = collection.title
434
435             # Delete all the associated collection items
436             for item in collection.get_collection_items():
437                 entry = item.get_media_entry
438                 entry.collected = entry.collected - 1
439                 entry.save()
440                 item.delete()
441
442             collection.delete()
443             messages.add_message(request, messages.SUCCESS,
444                 _('You deleted the collection "%s"') % collection_title)
445
446             return redirect(request, "mediagoblin.user_pages.user_home",
447                 user=username)
448         else:
449             messages.add_message(
450                 request, messages.ERROR,
451                 _("The collection was not deleted because you didn't check that you were sure."))
452
453             return redirect_obj(request, collection)
454
455     if ((request.user.has_privilege(u'admin') and
456          request.user.id != collection.creator)):
457         messages.add_message(
458             request, messages.WARNING,
459             _("You are about to delete another user's collection. "
460               "Proceed with caution."))
461
462     return render_to_response(
463         request,
464         'mediagoblin/user_pages/collection_confirm_delete.html',
465         {'collection': collection,
466          'form': form})
467
468
469 ATOM_DEFAULT_NR_OF_UPDATED_ITEMS = 15
470
471
472 def atom_feed(request):
473     """
474     generates the atom feed with the newest images
475     """
476     user = User.query.filter_by(
477         username = request.matchdict['user']).first()
478     if not user or not user.has_privilege(u'active'):
479         return render_404(request)
480
481     cursor = MediaEntry.query.filter_by(
482         uploader = user.id,
483         state = u'processed').\
484         order_by(MediaEntry.created.desc()).\
485         limit(ATOM_DEFAULT_NR_OF_UPDATED_ITEMS)
486
487     """
488     ATOM feed id is a tag URI (see http://en.wikipedia.org/wiki/Tag_URI)
489     """
490     atomlinks = [{
491            'href': request.urlgen(
492                'mediagoblin.user_pages.user_home',
493                qualified=True, user=request.matchdict['user']),
494            'rel': 'alternate',
495            'type': 'text/html'
496            }]
497
498     if mg_globals.app_config["push_urls"]:
499         for push_url in mg_globals.app_config["push_urls"]:
500             atomlinks.append({
501                 'rel': 'hub',
502                 'href': push_url})
503
504     feed = AtomFeed(
505                "MediaGoblin: Feed for user '%s'" % request.matchdict['user'],
506                feed_url=request.url,
507                id='tag:{host},{year}:gallery.user-{user}'.format(
508                    host=request.host,
509                    year=datetime.datetime.today().strftime('%Y'),
510                    user=request.matchdict['user']),
511                links=atomlinks)
512
513     for entry in cursor:
514         feed.add(entry.get('title'),
515             entry.description_html,
516             id=entry.url_for_self(request.urlgen, qualified=True),
517             content_type='html',
518             author={
519                 'name': entry.get_uploader.username,
520                 'uri': request.urlgen(
521                     'mediagoblin.user_pages.user_home',
522                     qualified=True, user=entry.get_uploader.username)},
523             updated=entry.get('created'),
524             links=[{
525                 'href': entry.url_for_self(
526                     request.urlgen,
527                     qualified=True),
528                 'rel': 'alternate',
529                 'type': 'text/html'}])
530
531     return feed.get_response()
532
533
534 def collection_atom_feed(request):
535     """
536     generates the atom feed with the newest images from a collection
537     """
538     user = User.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
543     collection = Collection.query.filter_by(
544                creator=user.id,
545                slug=request.matchdict['collection']).first()
546     if not collection:
547         return render_404(request)
548
549     cursor = CollectionItem.query.filter_by(
550                  collection=collection.id) \
551                  .order_by(CollectionItem.added.desc()) \
552                  .limit(ATOM_DEFAULT_NR_OF_UPDATED_ITEMS)
553
554     """
555     ATOM feed id is a tag URI (see http://en.wikipedia.org/wiki/Tag_URI)
556     """
557     atomlinks = [{
558            'href': collection.url_for_self(request.urlgen, qualified=True),
559            'rel': 'alternate',
560            'type': 'text/html'
561            }]
562
563     if mg_globals.app_config["push_urls"]:
564         for push_url in mg_globals.app_config["push_urls"]:
565             atomlinks.append({
566                 'rel': 'hub',
567                 'href': push_url})
568
569     feed = AtomFeed(
570                 "MediaGoblin: Feed for %s's collection %s" %
571                 (request.matchdict['user'], collection.title),
572                 feed_url=request.url,
573                 id=u'tag:{host},{year}:gnu-mediagoblin.{user}.collection.{slug}'\
574                     .format(
575                     host=request.host,
576                     year=collection.created.strftime('%Y'),
577                     user=request.matchdict['user'],
578                     slug=collection.slug),
579                 links=atomlinks)
580
581     for item in cursor:
582         entry = item.get_media_entry
583         feed.add(entry.get('title'),
584             item.note_html,
585             id=entry.url_for_self(request.urlgen, qualified=True),
586             content_type='html',
587             author={
588                 'name': entry.get_uploader.username,
589                 'uri': request.urlgen(
590                     'mediagoblin.user_pages.user_home',
591                     qualified=True, user=entry.get_uploader.username)},
592             updated=item.get('added'),
593             links=[{
594                 'href': entry.url_for_self(
595                     request.urlgen,
596                     qualified=True),
597                 'rel': 'alternate',
598                 'type': 'text/html'}])
599
600     return feed.get_response()
601
602 @require_active_login
603 def processing_panel(request):
604     """
605     Show to the user what media is still in conversion/processing...
606     and what failed, and why!
607     """
608     user = User.query.filter_by(username=request.matchdict['user']).first()
609     # TODO: XXX: Should this be a decorator?
610     #
611     # Make sure we have permission to access this user's panel.  Only
612     # admins and this user herself should be able to do so.
613     if not (user.id == request.user.id or request.user.has_privilege(u'admin')):
614         # No?  Simply redirect to this user's homepage.
615         return redirect(
616             request, 'mediagoblin.user_pages.user_home',
617             user=user.username)
618
619     # Get media entries which are in-processing
620     processing_entries = MediaEntry.query.\
621         filter_by(uploader = user.id,
622                   state = u'processing').\
623         order_by(MediaEntry.created.desc())
624
625     # Get media entries which have failed to process
626     failed_entries = MediaEntry.query.\
627         filter_by(uploader = user.id,
628                   state = u'failed').\
629         order_by(MediaEntry.created.desc())
630
631     processed_entries = MediaEntry.query.\
632         filter_by(uploader = user.id,
633                   state = u'processed').\
634         order_by(MediaEntry.created.desc()).\
635         limit(10)
636
637     # Render to response
638     return render_to_response(
639         request,
640         'mediagoblin/user_pages/processing_panel.html',
641         {'user': user,
642          'processing_entries': processing_entries,
643          'failed_entries': failed_entries,
644          'processed_entries': processed_entries})
645
646 @allow_reporting
647 @get_user_media_entry
648 @user_has_privilege(u'reporter')
649 @get_optional_media_comment_by_id
650 def file_a_report(request, media, comment):
651     """
652     This view handles the filing of a MediaReport or a CommentReport.
653     """
654     if comment is not None:
655         if not comment.get_media_entry.id == media.id:
656             return render_404(request)
657
658         form = user_forms.CommentReportForm(request.form)
659         context = {'media': media,
660                    'comment':comment,
661                    'form':form}
662     else:
663         form = user_forms.MediaReportForm(request.form)
664         context = {'media': media,
665                    'form':form}
666     form.reporter_id.data = request.user.id
667
668
669     if request.method == "POST":
670         report_object = build_report_object(form,
671             media_entry=media,
672             comment=comment)
673
674         # if the object was built successfully, report_table will not be None
675         if report_object:
676             report_object.save()
677             return redirect(
678                 request,
679                 'index')
680
681
682     return render_to_response(
683         request,
684         'mediagoblin/user_pages/report.html',
685         context)