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