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 comment
= unicode(request
.form
['comment_content'])
203 cleancomment
= { "content":cleaned_markdown_conversion(comment
)}
205 return Response(json
.dumps(cleancomment
))
207 @get_media_entry_by_id
208 @require_active_login
209 def media_collect(request
, media
):
210 """Add media to collection submission"""
212 form
= user_forms
.MediaCollectForm(request
.form
)
213 # A user's own collections:
214 form
.collection
.query
= Collection
.query
.filter_by(
215 creator
= request
.user
.id).order_by(Collection
.title
)
217 if request
.method
!= 'POST' or not form
.validate():
218 # No POST submission, or invalid form
219 if not form
.validate():
220 messages
.add_message(request
, messages
.ERROR
,
221 _('Please check your entries and try again.'))
223 return render_to_response(
225 'mediagoblin/user_pages/media_collect.html',
229 # If we are here, method=POST and the form is valid, submit things.
230 # If the user is adding a new collection, use that:
231 if form
.collection_title
.data
:
232 # Make sure this user isn't duplicating an existing collection
233 existing_collection
= Collection
.query
.filter_by(
234 creator
=request
.user
.id,
235 title
=form
.collection_title
.data
).first()
236 if existing_collection
:
237 messages
.add_message(request
, messages
.ERROR
,
238 _('You already have a collection called "%s"!')
239 % existing_collection
.title
)
240 return redirect(request
, "mediagoblin.user_pages.media_home",
241 user
=media
.get_uploader
.username
,
242 media
=media
.slug_or_id
)
244 collection
= Collection()
245 collection
.title
= form
.collection_title
.data
246 collection
.description
= form
.collection_description
.data
247 collection
.creator
= request
.user
.id
248 collection
.generate_slug()
251 # Otherwise, use the collection selected from the drop-down
253 collection
= form
.collection
.data
254 if collection
and collection
.creator
!= request
.user
.id:
257 # Make sure the user actually selected a collection
259 messages
.add_message(
260 request
, messages
.ERROR
,
261 _('You have to select or add a collection'))
262 return redirect(request
, "mediagoblin.user_pages.media_collect",
263 user
=media
.get_uploader
.username
,
267 # Check whether media already exists in collection
268 elif CollectionItem
.query
.filter_by(
269 media_entry
=media
.id,
270 collection
=collection
.id).first():
271 messages
.add_message(request
, messages
.ERROR
,
272 _('"%s" already in collection "%s"')
273 % (media
.title
, collection
.title
))
274 else: # Add item to collection
275 add_media_to_collection(collection
, media
, form
.note
.data
)
277 messages
.add_message(request
, messages
.SUCCESS
,
278 _('"%s" added to collection "%s"')
279 % (media
.title
, collection
.title
))
281 return redirect_obj(request
, media
)
284 #TODO: Why does @user_may_delete_media not implicate @require_active_login?
285 @get_media_entry_by_id
286 @require_active_login
287 @user_may_delete_media
288 def media_confirm_delete(request
, media
):
290 form
= user_forms
.ConfirmDeleteForm(request
.form
)
292 if request
.method
== 'POST' and form
.validate():
293 if form
.confirm
.data
is True:
294 username
= media
.get_uploader
.username
295 # Delete MediaEntry and all related files, comments etc.
297 messages
.add_message(
298 request
, messages
.SUCCESS
, _('You deleted the media.'))
300 return redirect(request
, "mediagoblin.user_pages.user_home",
303 messages
.add_message(
304 request
, messages
.ERROR
,
305 _("The media was not deleted because you didn't check that you were sure."))
306 return redirect_obj(request
, media
)
308 if ((request
.user
.is_admin
and
309 request
.user
.id != media
.uploader
)):
310 messages
.add_message(
311 request
, messages
.WARNING
,
312 _("You are about to delete another user's media. "
313 "Proceed with caution."))
315 return render_to_response(
317 'mediagoblin/user_pages/media_confirm_delete.html',
322 @active_user_from_url
324 def user_collection(request
, page
, url_user
=None):
325 """A User-defined Collection"""
326 collection
= Collection
.query
.filter_by(
327 get_creator
=url_user
,
328 slug
=request
.matchdict
['collection']).first()
331 return render_404(request
)
333 cursor
= collection
.get_collection_items()
335 pagination
= Pagination(page
, cursor
)
336 collection_items
= pagination()
338 # if no data is available, return NotFound
339 # TODO: Should an empty collection really also return 404?
340 if collection_items
== None:
341 return render_404(request
)
343 return render_to_response(
345 'mediagoblin/user_pages/collection.html',
347 'collection': collection
,
348 'collection_items': collection_items
,
349 'pagination': pagination
})
352 @active_user_from_url
353 def collection_list(request
, url_user
=None):
354 """A User-defined Collection"""
355 collections
= Collection
.query
.filter_by(
356 get_creator
=url_user
)
358 return render_to_response(
360 'mediagoblin/user_pages/collection_list.html',
362 'collections': collections
})
365 @get_user_collection_item
366 @require_active_login
367 @user_may_alter_collection
368 def collection_item_confirm_remove(request
, collection_item
):
370 form
= user_forms
.ConfirmCollectionItemRemoveForm(request
.form
)
372 if request
.method
== 'POST' and form
.validate():
373 username
= collection_item
.in_collection
.get_creator
.username
374 collection
= collection_item
.in_collection
376 if form
.confirm
.data
is True:
377 entry
= collection_item
.get_media_entry
378 entry
.collected
= entry
.collected
- 1
381 collection_item
.delete()
382 collection
.items
= collection
.items
- 1
385 messages
.add_message(
386 request
, messages
.SUCCESS
, _('You deleted the item from the collection.'))
388 messages
.add_message(
389 request
, messages
.ERROR
,
390 _("The item was not removed because you didn't check that you were sure."))
392 return redirect_obj(request
, collection
)
394 if ((request
.user
.is_admin
and
395 request
.user
.id != collection_item
.in_collection
.creator
)):
396 messages
.add_message(
397 request
, messages
.WARNING
,
398 _("You are about to delete an item from another user's collection. "
399 "Proceed with caution."))
401 return render_to_response(
403 'mediagoblin/user_pages/collection_item_confirm_remove.html',
404 {'collection_item': collection_item
,
409 @require_active_login
410 @user_may_alter_collection
411 def collection_confirm_delete(request
, collection
):
413 form
= user_forms
.ConfirmDeleteForm(request
.form
)
415 if request
.method
== 'POST' and form
.validate():
417 username
= collection
.get_creator
.username
419 if form
.confirm
.data
is True:
420 collection_title
= collection
.title
422 # Delete all the associated collection items
423 for item
in collection
.get_collection_items():
424 entry
= item
.get_media_entry
425 entry
.collected
= entry
.collected
- 1
430 messages
.add_message(request
, messages
.SUCCESS
,
431 _('You deleted the collection "%s"') % collection_title
)
433 return redirect(request
, "mediagoblin.user_pages.user_home",
436 messages
.add_message(
437 request
, messages
.ERROR
,
438 _("The collection was not deleted because you didn't check that you were sure."))
440 return redirect_obj(request
, collection
)
442 if ((request
.user
.is_admin
and
443 request
.user
.id != collection
.creator
)):
444 messages
.add_message(
445 request
, messages
.WARNING
,
446 _("You are about to delete another user's collection. "
447 "Proceed with caution."))
449 return render_to_response(
451 'mediagoblin/user_pages/collection_confirm_delete.html',
452 {'collection': collection
,
456 ATOM_DEFAULT_NR_OF_UPDATED_ITEMS
= 15
459 def atom_feed(request
):
461 generates the atom feed with the newest images
463 user
= User
.query
.filter_by(
464 username
= request
.matchdict
['user'],
465 status
= u
'active').first()
467 return render_404(request
)
469 cursor
= MediaEntry
.query
.filter_by(
471 state
= u
'processed').\
472 order_by(MediaEntry
.created
.desc()).\
473 limit(ATOM_DEFAULT_NR_OF_UPDATED_ITEMS
)
476 ATOM feed id is a tag URI (see http://en.wikipedia.org/wiki/Tag_URI)
479 'href': request
.urlgen(
480 'mediagoblin.user_pages.user_home',
481 qualified
=True, user
=request
.matchdict
['user']),
486 if mg_globals
.app_config
["push_urls"]:
487 for push_url
in mg_globals
.app_config
["push_urls"]:
493 "MediaGoblin: Feed for user '%s'" % request
.matchdict
['user'],
494 feed_url
=request
.url
,
495 id='tag:{host},{year}:gallery.user-{user}'.format(
497 year
=datetime
.datetime
.today().strftime('%Y'),
498 user
=request
.matchdict
['user']),
502 feed
.add(entry
.get('title'),
503 entry
.description_html
,
504 id=entry
.url_for_self(request
.urlgen
, qualified
=True),
507 'name': entry
.get_uploader
.username
,
508 'uri': request
.urlgen(
509 'mediagoblin.user_pages.user_home',
510 qualified
=True, user
=entry
.get_uploader
.username
)},
511 updated
=entry
.get('created'),
513 'href': entry
.url_for_self(
517 'type': 'text/html'}])
519 return feed
.get_response()
522 def collection_atom_feed(request
):
524 generates the atom feed with the newest images from a collection
526 user
= User
.query
.filter_by(
527 username
= request
.matchdict
['user'],
528 status
= u
'active').first()
530 return render_404(request
)
532 collection
= Collection
.query
.filter_by(
534 slug
=request
.matchdict
['collection']).first()
536 return render_404(request
)
538 cursor
= CollectionItem
.query
.filter_by(
539 collection
=collection
.id) \
540 .order_by(CollectionItem
.added
.desc()) \
541 .limit(ATOM_DEFAULT_NR_OF_UPDATED_ITEMS
)
544 ATOM feed id is a tag URI (see http://en.wikipedia.org/wiki/Tag_URI)
547 'href': collection
.url_for_self(request
.urlgen
, qualified
=True),
552 if mg_globals
.app_config
["push_urls"]:
553 for push_url
in mg_globals
.app_config
["push_urls"]:
559 "MediaGoblin: Feed for %s's collection %s" %
560 (request
.matchdict
['user'], collection
.title
),
561 feed_url
=request
.url
,
562 id=u
'tag:{host},{year}:gnu-mediagoblin.{user}.collection.{slug}'\
565 year
=collection
.created
.strftime('%Y'),
566 user
=request
.matchdict
['user'],
567 slug
=collection
.slug
),
571 entry
= item
.get_media_entry
572 feed
.add(entry
.get('title'),
574 id=entry
.url_for_self(request
.urlgen
, qualified
=True),
577 'name': entry
.get_uploader
.username
,
578 'uri': request
.urlgen(
579 'mediagoblin.user_pages.user_home',
580 qualified
=True, user
=entry
.get_uploader
.username
)},
581 updated
=item
.get('added'),
583 'href': entry
.url_for_self(
587 'type': 'text/html'}])
589 return feed
.get_response()
592 @require_active_login
593 def processing_panel(request
):
595 Show to the user what media is still in conversion/processing...
596 and what failed, and why!
598 user
= User
.query
.filter_by(username
=request
.matchdict
['user']).first()
599 # TODO: XXX: Should this be a decorator?
601 # Make sure we have permission to access this user's panel. Only
602 # admins and this user herself should be able to do so.
603 if not (user
.id == request
.user
.id or request
.user
.is_admin
):
604 # No? Simply redirect to this user's homepage.
606 request
, 'mediagoblin.user_pages.user_home',
609 # Get media entries which are in-processing
610 processing_entries
= MediaEntry
.query
.\
611 filter_by(uploader
= user
.id,
612 state
= u
'processing').\
613 order_by(MediaEntry
.created
.desc())
615 # Get media entries which have failed to process
616 failed_entries
= MediaEntry
.query
.\
617 filter_by(uploader
= user
.id,
619 order_by(MediaEntry
.created
.desc())
621 processed_entries
= MediaEntry
.query
.\
622 filter_by(uploader
= user
.id,
623 state
= u
'processed').\
624 order_by(MediaEntry
.created
.desc()).\
628 return render_to_response(
630 'mediagoblin/user_pages/processing_panel.html',
632 'processing_entries': processing_entries
,
633 'failed_entries': failed_entries
,
634 'processed_entries': processed_entries
})