1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
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.
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.
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/>.
21 from mediagoblin
import messages
, mg_globals
22 from mediagoblin
.db
.models
import (MediaEntry
, MediaTag
, Collection
,
24 from mediagoblin
.tools
.response
import render_to_response
, render_404
, \
25 redirect
, redirect_obj
26 from mediagoblin
.tools
.text
import cleaned_markdown_conversion
27 from mediagoblin
.tools
.translate
import pass_to_ugettext
as _
28 from mediagoblin
.tools
.pagination
import Pagination
29 from mediagoblin
.user_pages
import forms
as user_forms
30 from mediagoblin
.user_pages
.lib
import add_media_to_collection
31 from mediagoblin
.notifications
import trigger_notification
, \
32 add_comment_subscription
, mark_comment_notification_seen
33 from mediagoblin
.decorators
import (uses_pagination
, get_user_media_entry
,
34 get_media_entry_by_id
,
35 require_active_login
, user_may_delete_media
, user_may_alter_collection
,
36 get_user_collection
, get_user_collection_item
, active_user_from_url
)
38 from werkzeug
.contrib
.atom
import AtomFeed
39 from werkzeug
.exceptions
import MethodNotAllowed
40 from werkzeug
.wrappers
import Response
43 _log
= logging
.getLogger(__name__
)
44 _log
.setLevel(logging
.DEBUG
)
48 def user_home(request
, page
):
49 """'Homepage' of a User()"""
50 # TODO: decide if we only want homepages for active users, we can
51 # then use the @get_active_user decorator and also simplify the
53 user
= User
.query
.filter_by(username
=request
.matchdict
['user']).first()
55 return render_404(request
)
56 elif user
.status
!= u
'active':
57 return render_to_response(
59 'mediagoblin/user_pages/user.html',
62 cursor
= MediaEntry
.query
.\
63 filter_by(uploader
= user
.id,
64 state
= u
'processed').order_by(MediaEntry
.created
.desc())
66 pagination
= Pagination(page
, cursor
)
67 media_entries
= pagination()
69 #if no data is available, return NotFound
70 if media_entries
== None:
71 return render_404(request
)
73 user_gallery_url
= request
.urlgen(
74 'mediagoblin.user_pages.user_gallery',
77 return render_to_response(
79 'mediagoblin/user_pages/user.html',
81 'user_gallery_url': user_gallery_url
,
82 'media_entries': media_entries
,
83 'pagination': pagination
})
88 def user_gallery(request
, page
, url_user
=None):
89 """'Gallery' of a User()"""
90 tag
= request
.matchdict
.get('tag', None)
91 cursor
= MediaEntry
.query
.filter_by(
93 state
=u
'processed').order_by(MediaEntry
.created
.desc())
95 # Filter potentially by tag too:
97 cursor
= cursor
.filter(
98 MediaEntry
.tags_helper
.any(
99 MediaTag
.slug
== request
.matchdict
['tag']))
102 pagination
= Pagination(page
, cursor
)
103 media_entries
= pagination()
105 #if no data is available, return NotFound
106 # TODO: Should we really also return 404 for empty galleries?
107 if media_entries
== None:
108 return render_404(request
)
110 return render_to_response(
112 'mediagoblin/user_pages/gallery.html',
113 {'user': url_user
, 'tag': tag
,
114 'media_entries': media_entries
,
115 'pagination': pagination
})
118 MEDIA_COMMENTS_PER_PAGE
= 50
121 @get_user_media_entry
123 def media_home(request
, media
, page
, **kwargs
):
125 'Homepage' of a MediaEntry()
127 comment_id
= request
.matchdict
.get('comment', None)
130 mark_comment_notification_seen(comment_id
, request
.user
)
132 pagination
= Pagination(
133 page
, media
.get_comments(
134 mg_globals
.app_config
['comments_ascending']),
135 MEDIA_COMMENTS_PER_PAGE
,
138 pagination
= Pagination(
139 page
, media
.get_comments(
140 mg_globals
.app_config
['comments_ascending']),
141 MEDIA_COMMENTS_PER_PAGE
)
143 comments
= pagination()
145 comment_form
= user_forms
.MediaCommentForm(request
.form
)
147 media_template_name
= media
.media_manager
.display_template
149 return render_to_response(
153 'comments': comments
,
154 'pagination': pagination
,
155 'comment_form': comment_form
,
156 'app_config': mg_globals
.app_config
})
159 @get_media_entry_by_id
160 @require_active_login
161 def media_post_comment(request
, media
):
163 recieves POST from a MediaEntry() comment form, saves the comment.
165 if not request
.method
== 'POST':
166 raise MethodNotAllowed()
168 comment
= request
.db
.MediaComment()
169 comment
.media_entry
= media
.id
170 comment
.author
= request
.user
.id
171 print request
.form
['comment_content']
172 comment
.content
= unicode(request
.form
['comment_content'])
174 # Show error message if commenting is disabled.
175 if not mg_globals
.app_config
['allow_comments']:
176 messages
.add_message(
179 _("Sorry, comments are disabled."))
180 elif not comment
.content
.strip():
181 messages
.add_message(
184 _("Oops, your comment was empty."))
188 messages
.add_message(
189 request
, messages
.SUCCESS
,
190 _('Your comment has been posted!'))
192 trigger_notification(comment
, media
, request
)
194 add_comment_subscription(request
.user
, media
)
196 return redirect_obj(request
, media
)
200 def media_preview_comment(request
):
201 """Runs a comment through markdown so it can be previewed."""
202 # If this isn't an ajax request, render_404
203 if not request
.is_xhr
:
204 return render_404(request
)
206 comment
= unicode(request
.form
['comment_content'])
207 cleancomment
= { "content":cleaned_markdown_conversion(comment
)}
209 return Response(json
.dumps(cleancomment
))
211 @get_media_entry_by_id
212 @require_active_login
213 def media_collect(request
, media
):
214 """Add media to collection submission"""
216 form
= user_forms
.MediaCollectForm(request
.form
)
217 # A user's own collections:
218 form
.collection
.query
= Collection
.query
.filter_by(
219 creator
= request
.user
.id).order_by(Collection
.title
)
221 if request
.method
!= 'POST' or not form
.validate():
222 # No POST submission, or invalid form
223 if not form
.validate():
224 messages
.add_message(request
, messages
.ERROR
,
225 _('Please check your entries and try again.'))
227 return render_to_response(
229 'mediagoblin/user_pages/media_collect.html',
233 # If we are here, method=POST and the form is valid, submit things.
234 # If the user is adding a new collection, use that:
235 if form
.collection_title
.data
:
236 # Make sure this user isn't duplicating an existing collection
237 existing_collection
= Collection
.query
.filter_by(
238 creator
=request
.user
.id,
239 title
=form
.collection_title
.data
).first()
240 if existing_collection
:
241 messages
.add_message(request
, messages
.ERROR
,
242 _('You already have a collection called "%s"!')
243 % existing_collection
.title
)
244 return redirect(request
, "mediagoblin.user_pages.media_home",
245 user
=media
.get_uploader
.username
,
246 media
=media
.slug_or_id
)
248 collection
= Collection()
249 collection
.title
= form
.collection_title
.data
250 collection
.description
= form
.collection_description
.data
251 collection
.creator
= request
.user
.id
252 collection
.generate_slug()
255 # Otherwise, use the collection selected from the drop-down
257 collection
= form
.collection
.data
258 if collection
and collection
.creator
!= request
.user
.id:
261 # Make sure the user actually selected a collection
263 messages
.add_message(
264 request
, messages
.ERROR
,
265 _('You have to select or add a collection'))
266 return redirect(request
, "mediagoblin.user_pages.media_collect",
267 user
=media
.get_uploader
.username
,
271 # Check whether media already exists in collection
272 elif CollectionItem
.query
.filter_by(
273 media_entry
=media
.id,
274 collection
=collection
.id).first():
275 messages
.add_message(request
, messages
.ERROR
,
276 _('"%s" already in collection "%s"')
277 % (media
.title
, collection
.title
))
278 else: # Add item to collection
279 add_media_to_collection(collection
, media
, form
.note
.data
)
281 messages
.add_message(request
, messages
.SUCCESS
,
282 _('"%s" added to collection "%s"')
283 % (media
.title
, collection
.title
))
285 return redirect_obj(request
, media
)
288 #TODO: Why does @user_may_delete_media not implicate @require_active_login?
290 @require_active_login
291 def media_confirm_delete(request
):
293 allowed_state
= [u
'failed', u
'processed']
295 for media_state
in allowed_state
:
296 media
= request
.db
.MediaEntry
.query
.filter_by(id=request
.matchdict
['media_id'], state
=media_state
).first()
301 return render_404(request
)
303 given_username
= request
.matchdict
.get('user')
304 if given_username
and (given_username
!= media
.get_uploader
.username
):
305 return render_404(request
)
307 uploader_id
= media
.uploader
308 if not (request
.user
.is_admin
or
309 request
.user
.id == uploader_id
):
312 form
= user_forms
.ConfirmDeleteForm(request
.form
)
314 if request
.method
== 'POST' and form
.validate():
315 if form
.confirm
.data
is True:
316 username
= media
.get_uploader
.username
318 media
.get_uploader
.uploaded
= media
.get_uploader
.uploaded
- \
320 media
.get_uploader
.save()
322 # Delete MediaEntry and all related files, comments etc.
324 messages
.add_message(
325 request
, messages
.SUCCESS
, _('You deleted the media.'))
327 location
= media
.url_to_next(request
.urlgen
)
329 location
=media
.url_to_prev(request
.urlgen
)
331 location
=request
.urlgen("mediagoblin.user_pages.user_home",
333 return redirect(request
, location
=location
)
335 messages
.add_message(
336 request
, messages
.ERROR
,
337 _("The media was not deleted because you didn't check that you were sure."))
338 return redirect_obj(request
, media
)
340 if ((request
.user
.is_admin
and
341 request
.user
.id != media
.uploader
)):
342 messages
.add_message(
343 request
, messages
.WARNING
,
344 _("You are about to delete another user's media. "
345 "Proceed with caution."))
347 return render_to_response(
349 'mediagoblin/user_pages/media_confirm_delete.html',
354 @active_user_from_url
356 def user_collection(request
, page
, url_user
=None):
357 """A User-defined Collection"""
358 collection
= Collection
.query
.filter_by(
359 get_creator
=url_user
,
360 slug
=request
.matchdict
['collection']).first()
363 return render_404(request
)
365 cursor
= collection
.get_collection_items()
367 pagination
= Pagination(page
, cursor
)
368 collection_items
= pagination()
370 # if no data is available, return NotFound
371 # TODO: Should an empty collection really also return 404?
372 if collection_items
== None:
373 return render_404(request
)
375 return render_to_response(
377 'mediagoblin/user_pages/collection.html',
379 'collection': collection
,
380 'collection_items': collection_items
,
381 'pagination': pagination
})
384 @active_user_from_url
385 def collection_list(request
, url_user
=None):
386 """A User-defined Collection"""
387 collections
= Collection
.query
.filter_by(
388 get_creator
=url_user
)
390 return render_to_response(
392 'mediagoblin/user_pages/collection_list.html',
394 'collections': collections
})
397 @get_user_collection_item
398 @require_active_login
399 @user_may_alter_collection
400 def collection_item_confirm_remove(request
, collection_item
):
402 form
= user_forms
.ConfirmCollectionItemRemoveForm(request
.form
)
404 if request
.method
== 'POST' and form
.validate():
405 username
= collection_item
.in_collection
.get_creator
.username
406 collection
= collection_item
.in_collection
408 if form
.confirm
.data
is True:
409 entry
= collection_item
.get_media_entry
410 entry
.collected
= entry
.collected
- 1
413 collection_item
.delete()
414 collection
.items
= collection
.items
- 1
417 messages
.add_message(
418 request
, messages
.SUCCESS
, _('You deleted the item from the collection.'))
420 messages
.add_message(
421 request
, messages
.ERROR
,
422 _("The item was not removed because you didn't check that you were sure."))
424 return redirect_obj(request
, collection
)
426 if ((request
.user
.is_admin
and
427 request
.user
.id != collection_item
.in_collection
.creator
)):
428 messages
.add_message(
429 request
, messages
.WARNING
,
430 _("You are about to delete an item from another user's collection. "
431 "Proceed with caution."))
433 return render_to_response(
435 'mediagoblin/user_pages/collection_item_confirm_remove.html',
436 {'collection_item': collection_item
,
441 @require_active_login
442 @user_may_alter_collection
443 def collection_confirm_delete(request
, collection
):
445 form
= user_forms
.ConfirmDeleteForm(request
.form
)
447 if request
.method
== 'POST' and form
.validate():
449 username
= collection
.get_creator
.username
451 if form
.confirm
.data
is True:
452 collection_title
= collection
.title
454 # Delete all the associated collection items
455 for item
in collection
.get_collection_items():
456 entry
= item
.get_media_entry
457 entry
.collected
= entry
.collected
- 1
462 messages
.add_message(request
, messages
.SUCCESS
,
463 _('You deleted the collection "%s"') % collection_title
)
465 return redirect(request
, "mediagoblin.user_pages.user_home",
468 messages
.add_message(
469 request
, messages
.ERROR
,
470 _("The collection was not deleted because you didn't check that you were sure."))
472 return redirect_obj(request
, collection
)
474 if ((request
.user
.is_admin
and
475 request
.user
.id != collection
.creator
)):
476 messages
.add_message(
477 request
, messages
.WARNING
,
478 _("You are about to delete another user's collection. "
479 "Proceed with caution."))
481 return render_to_response(
483 'mediagoblin/user_pages/collection_confirm_delete.html',
484 {'collection': collection
,
488 ATOM_DEFAULT_NR_OF_UPDATED_ITEMS
= 15
491 def atom_feed(request
):
493 generates the atom feed with the newest images
495 user
= User
.query
.filter_by(
496 username
= request
.matchdict
['user'],
497 status
= u
'active').first()
499 return render_404(request
)
501 cursor
= MediaEntry
.query
.filter_by(
503 state
= u
'processed').\
504 order_by(MediaEntry
.created
.desc()).\
505 limit(ATOM_DEFAULT_NR_OF_UPDATED_ITEMS
)
508 ATOM feed id is a tag URI (see http://en.wikipedia.org/wiki/Tag_URI)
511 'href': request
.urlgen(
512 'mediagoblin.user_pages.user_home',
513 qualified
=True, user
=request
.matchdict
['user']),
518 if mg_globals
.app_config
["push_urls"]:
519 for push_url
in mg_globals
.app_config
["push_urls"]:
525 "MediaGoblin: Feed for user '%s'" % request
.matchdict
['user'],
526 feed_url
=request
.url
,
527 id='tag:{host},{year}:gallery.user-{user}'.format(
529 year
=datetime
.datetime
.today().strftime('%Y'),
530 user
=request
.matchdict
['user']),
534 feed
.add(entry
.get('title'),
535 entry
.description_html
,
536 id=entry
.url_for_self(request
.urlgen
, qualified
=True),
539 'name': entry
.get_uploader
.username
,
540 'uri': request
.urlgen(
541 'mediagoblin.user_pages.user_home',
542 qualified
=True, user
=entry
.get_uploader
.username
)},
543 updated
=entry
.get('created'),
545 'href': entry
.url_for_self(
549 'type': 'text/html'}])
551 return feed
.get_response()
554 def collection_atom_feed(request
):
556 generates the atom feed with the newest images from a collection
558 user
= User
.query
.filter_by(
559 username
= request
.matchdict
['user'],
560 status
= u
'active').first()
562 return render_404(request
)
564 collection
= Collection
.query
.filter_by(
566 slug
=request
.matchdict
['collection']).first()
568 return render_404(request
)
570 cursor
= CollectionItem
.query
.filter_by(
571 collection
=collection
.id) \
572 .order_by(CollectionItem
.added
.desc()) \
573 .limit(ATOM_DEFAULT_NR_OF_UPDATED_ITEMS
)
576 ATOM feed id is a tag URI (see http://en.wikipedia.org/wiki/Tag_URI)
579 'href': collection
.url_for_self(request
.urlgen
, qualified
=True),
584 if mg_globals
.app_config
["push_urls"]:
585 for push_url
in mg_globals
.app_config
["push_urls"]:
591 "MediaGoblin: Feed for %s's collection %s" %
592 (request
.matchdict
['user'], collection
.title
),
593 feed_url
=request
.url
,
594 id=u
'tag:{host},{year}:gnu-mediagoblin.{user}.collection.{slug}'\
597 year
=collection
.created
.strftime('%Y'),
598 user
=request
.matchdict
['user'],
599 slug
=collection
.slug
),
603 entry
= item
.get_media_entry
604 feed
.add(entry
.get('title'),
606 id=entry
.url_for_self(request
.urlgen
, qualified
=True),
609 'name': entry
.get_uploader
.username
,
610 'uri': request
.urlgen(
611 'mediagoblin.user_pages.user_home',
612 qualified
=True, user
=entry
.get_uploader
.username
)},
613 updated
=item
.get('added'),
615 'href': entry
.url_for_self(
619 'type': 'text/html'}])
621 return feed
.get_response()
624 @require_active_login
625 def processing_panel(request
):
627 Show to the user what media is still in conversion/processing...
628 and what failed, and why!
630 user
= User
.query
.filter_by(username
=request
.matchdict
['user']).first()
631 # TODO: XXX: Should this be a decorator?
633 # Make sure we have permission to access this user's panel. Only
634 # admins and this user herself should be able to do so.
635 if not (user
.id == request
.user
.id or request
.user
.is_admin
):
636 # No? Simply redirect to this user's homepage.
638 request
, 'mediagoblin.user_pages.user_home',
641 # Get media entries which are in-processing
642 processing_entries
= MediaEntry
.query
.\
643 filter_by(uploader
= user
.id,
644 state
= u
'processing').\
645 order_by(MediaEntry
.created
.desc())
647 # Get media entries which have failed to process
648 failed_entries
= MediaEntry
.query
.\
649 filter_by(uploader
= user
.id,
651 order_by(MediaEntry
.created
.desc())
653 processed_entries
= MediaEntry
.query
.\
654 filter_by(uploader
= user
.id,
655 state
= u
'processed').\
656 order_by(MediaEntry
.created
.desc()).\
660 return render_to_response(
662 'mediagoblin/user_pages/processing_panel.html',
664 'processing_entries': processing_entries
,
665 'failed_entries': failed_entries
,
666 'processed_entries': processed_entries
})