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