# GNU MediaGoblin -- federated, autonomous media hosting # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import logging import datetime from mediagoblin import messages, mg_globals from mediagoblin.db.models import (MediaEntry, MediaTag, Collection, CollectionItem, User) from mediagoblin.tools.response import render_to_response, render_404, redirect from mediagoblin.tools.translate import pass_to_ugettext as _ from mediagoblin.tools.pagination import Pagination from mediagoblin.user_pages import forms as user_forms from mediagoblin.user_pages.lib import send_comment_email from mediagoblin.decorators import (uses_pagination, get_user_media_entry, get_media_entry_by_id, require_active_login, user_may_delete_media, user_may_alter_collection, get_user_collection, get_user_collection_item, active_user_from_url) from werkzeug.contrib.atom import AtomFeed _log = logging.getLogger(__name__) _log.setLevel(logging.DEBUG) @uses_pagination def user_home(request, page): """'Homepage' of a User()""" # TODO: decide if we only want homepages for active users, we can # then use the @get_active_user decorator and also simplify the # template html. user = User.query.filter_by(username=request.matchdict['user']).first() if not user: return render_404(request) elif user.status != u'active': return render_to_response( request, 'mediagoblin/user_pages/user.html', {'user': user}) cursor = MediaEntry.query.\ filter_by(uploader = user.id, state = u'processed').order_by(MediaEntry.created.desc()) pagination = Pagination(page, cursor) media_entries = pagination() #if no data is available, return NotFound if media_entries == None: return render_404(request) user_gallery_url = request.urlgen( 'mediagoblin.user_pages.user_gallery', user=user.username) return render_to_response( request, 'mediagoblin/user_pages/user.html', {'user': user, 'user_gallery_url': user_gallery_url, 'media_entries': media_entries, 'pagination': pagination}) @active_user_from_url @uses_pagination def user_gallery(request, page, url_user=None): """'Gallery' of a User()""" tag = request.matchdict.get('tag', None) cursor = MediaEntry.query.filter_by( uploader=url_user.id, state=u'processed').order_by(MediaEntry.created.desc()) # Filter potentially by tag too: if tag: cursor = cursor.filter( MediaEntry.tags_helper.any( MediaTag.name == request.matchdict['tag'])) # Paginate gallery pagination = Pagination(page, cursor) media_entries = pagination() #if no data is available, return NotFound # TODO: Should we really also return 404 for empty galleries? if media_entries == None: return render_404(request) return render_to_response( request, 'mediagoblin/user_pages/gallery.html', {'user': url_user, 'tag': tag, 'media_entries': media_entries, 'pagination': pagination}) MEDIA_COMMENTS_PER_PAGE = 50 @get_user_media_entry @uses_pagination def media_home(request, media, page, **kwargs): """ 'Homepage' of a MediaEntry() """ comment_id = request.matchdict.get('comment', None) if comment_id: pagination = Pagination( page, media.get_comments( mg_globals.app_config['comments_ascending']), MEDIA_COMMENTS_PER_PAGE, comment_id) else: pagination = Pagination( page, media.get_comments( mg_globals.app_config['comments_ascending']), MEDIA_COMMENTS_PER_PAGE) comments = pagination() comment_form = user_forms.MediaCommentForm(request.form) media_template_name = media.media_manager['display_template'] return render_to_response( request, media_template_name, {'media': media, 'comments': comments, 'pagination': pagination, 'comment_form': comment_form, 'app_config': mg_globals.app_config}) @get_media_entry_by_id @require_active_login def media_post_comment(request, media): """ recieves POST from a MediaEntry() comment form, saves the comment. """ assert request.method == 'POST' comment = request.db.MediaComment() comment.media_entry = media.id comment.author = request.user.id comment.content = unicode(request.form['comment_content']) if not comment.content.strip(): messages.add_message( request, messages.ERROR, _("Oops, your comment was empty.")) else: comment.save() messages.add_message( request, messages.SUCCESS, _('Your comment has been posted!')) media_uploader = media.get_uploader #don't send email if you comment on your own post if (comment.author != media_uploader and media_uploader.wants_comment_notification): send_comment_email(media_uploader, comment, media, request) return redirect(request, location=media.url_for_self(request.urlgen)) @get_user_media_entry @require_active_login def media_collect(request, media): """Add media to collection submission""" form = user_forms.MediaCollectForm(request.form) # A user's own collections: form.collection.query = Collection.query.filter_by( creator = request.user.id).order_by(Collection.title) if request.method != 'POST' or not form.validate(): # No POST submission, or invalid form if not form.validate(): messages.add_message(request, messages.ERROR, _('Please check your entries and try again.')) return render_to_response( request, 'mediagoblin/user_pages/media_collect.html', {'media': media, 'form': form}) # If we are here, method=POST and the form is valid, submit things. # If the user is adding a new collection, use that: if request.form['collection_title']: # Make sure this user isn't duplicating an existing collection existing_collection = Collection.query.filter_by( creator=request.user.id, title=request.form['collection_title']).first() if existing_collection: messages.add_message(request, messages.ERROR, _('You already have a collection called "%s"!' % existing_collection.title)) return redirect(request, "mediagoblin.user_pages.media_home", user=request.user.username, media=media.id) collection = Collection() collection.title = request.form['collection_title'] collection.description = request.form.get('collection_description') collection.creator = request.user.id collection.generate_slug() collection.save() # Otherwise, use the collection selected from the drop-down else: collection = Collection.query.filter_by( id=request.form.get('collection')).first() # Make sure the user actually selected a collection if not collection: messages.add_message( request, messages.ERROR, _('You have to select or add a collection')) return redirect(request, "mediagoblin.user_pages.media_collect", user=media.get_uploader.username, media=media.id) # Check whether media already exists in collection elif CollectionItem.query.filter_by( media_entry=media.id, collection=collection.id).first(): messages.add_message(request, messages.ERROR, _('"%s" already in collection "%s"' % (media.title, collection.title))) else: # Add item to collection collection_item = request.db.CollectionItem() collection_item.collection = collection.id collection_item.media_entry = media.id collection_item.author = request.user.id collection_item.note = request.form['note'] collection_item.save() collection.items = collection.items + 1 collection.save() media.collected = media.collected + 1 media.save() messages.add_message(request, messages.SUCCESS, _('"%s" added to collection "%s"' % (media.title, collection.title))) return redirect(request, "mediagoblin.user_pages.media_home", user=media.get_uploader.username, media=media.id) #TODO: Why does @user_may_delete_media not implicate @require_active_login? @get_media_entry_by_id @require_active_login @user_may_delete_media def media_confirm_delete(request, media): form = user_forms.ConfirmDeleteForm(request.form) if request.method == 'POST' and form.validate(): if form.confirm.data is True: username = media.get_uploader.username # Delete MediaEntry and all related files, comments etc. media.delete() messages.add_message( request, messages.SUCCESS, _('You deleted the media.')) return redirect(request, "mediagoblin.user_pages.user_home", user=username) else: messages.add_message( request, messages.ERROR, _("The media was not deleted because you didn't check that you were sure.")) return redirect(request, location=media.url_for_self(request.urlgen)) if ((request.user.is_admin and request.user.id != media.uploader)): messages.add_message( request, messages.WARNING, _("You are about to delete another user's media. " "Proceed with caution.")) return render_to_response( request, 'mediagoblin/user_pages/media_confirm_delete.html', {'media': media, 'form': form}) @active_user_from_url @uses_pagination def user_collection(request, page, url_user=None): """A User-defined Collection""" collection = Collection.query.filter_by( get_creator=url_user, slug=request.matchdict['collection']).first() if not collection: return render_404(request) cursor = collection.get_collection_items() pagination = Pagination(page, cursor) collection_items = pagination() # if no data is available, return NotFound # TODO: Should an empty collection really also return 404? if collection_items == None: return render_404(request) return render_to_response( request, 'mediagoblin/user_pages/collection.html', {'user': url_user, 'collection': collection, 'collection_items': collection_items, 'pagination': pagination}) @active_user_from_url def collection_list(request, url_user=None): """A User-defined Collection""" collections = Collection.query.filter_by( get_creator=url_user) return render_to_response( request, 'mediagoblin/user_pages/collection_list.html', {'user': url_user, 'collections': collections}) @get_user_collection_item @require_active_login @user_may_alter_collection def collection_item_confirm_remove(request, collection_item): form = user_forms.ConfirmCollectionItemRemoveForm(request.form) if request.method == 'POST' and form.validate(): username = collection_item.in_collection.get_creator.username collection = collection_item.in_collection if form.confirm.data is True: entry = collection_item.get_media_entry entry.collected = entry.collected - 1 entry.save() collection_item.delete() collection.items = collection.items - 1 collection.save() messages.add_message( request, messages.SUCCESS, _('You deleted the item from the collection.')) else: messages.add_message( request, messages.ERROR, _("The item was not removed because you didn't check that you were sure.")) return redirect(request, "mediagoblin.user_pages.user_collection", user=username, collection=collection.slug) if ((request.user.is_admin and request.user.id != collection_item.in_collection.creator)): messages.add_message( request, messages.WARNING, _("You are about to delete an item from another user's collection. " "Proceed with caution.")) return render_to_response( request, 'mediagoblin/user_pages/collection_item_confirm_remove.html', {'collection_item': collection_item, 'form': form}) @get_user_collection @require_active_login @user_may_alter_collection def collection_confirm_delete(request, collection): form = user_forms.ConfirmDeleteForm(request.form) if request.method == 'POST' and form.validate(): username = collection.get_creator.username if form.confirm.data is True: collection_title = collection.title # Delete all the associated collection items for item in collection.get_collection_items(): entry = item.get_media_entry entry.collected = entry.collected - 1 entry.save() item.delete() collection.delete() messages.add_message( request, messages.SUCCESS, _('You deleted the collection "%s"' % collection_title)) return redirect(request, "mediagoblin.user_pages.user_home", user=username) else: messages.add_message( request, messages.ERROR, _("The collection was not deleted because you didn't check that you were sure.")) return redirect(request, "mediagoblin.user_pages.user_collection", user=username, collection=collection.slug) if ((request.user.is_admin and request.user.id != collection.creator)): messages.add_message( request, messages.WARNING, _("You are about to delete another user's collection. " "Proceed with caution.")) return render_to_response( request, 'mediagoblin/user_pages/collection_confirm_delete.html', {'collection': collection, 'form': form}) ATOM_DEFAULT_NR_OF_UPDATED_ITEMS = 15 def atom_feed(request): """ generates the atom feed with the newest images """ user = User.query.filter_by( username = request.matchdict['user'], status = u'active').first() if not user: return render_404(request) cursor = MediaEntry.query.filter_by( uploader = user.id, state = u'processed').\ order_by(MediaEntry.created.desc()).\ limit(ATOM_DEFAULT_NR_OF_UPDATED_ITEMS) """ ATOM feed id is a tag URI (see http://en.wikipedia.org/wiki/Tag_URI) """ atomlinks = [{ 'href': request.urlgen( 'mediagoblin.user_pages.user_home', qualified=True, user=request.matchdict['user']), 'rel': 'alternate', 'type': 'text/html' }] if mg_globals.app_config["push_urls"]: for push_url in mg_globals.app_config["push_urls"]: atomlinks.append({ 'rel': 'hub', 'href': push_url}) feed = AtomFeed( "MediaGoblin: Feed for user '%s'" % request.matchdict['user'], feed_url=request.url, id='tag:{host},{year}:gallery.user-{user}'.format( host=request.host, year=datetime.datetime.today().strftime('%Y'), user=request.matchdict['user']), links=atomlinks) for entry in cursor: feed.add(entry.get('title'), entry.description_html, id=entry.url_for_self(request.urlgen, qualified=True), content_type='html', author={ 'name': entry.get_uploader.username, 'uri': request.urlgen( 'mediagoblin.user_pages.user_home', qualified=True, user=entry.get_uploader.username)}, updated=entry.get('created'), links=[{ 'href': entry.url_for_self( request.urlgen, qualified=True), 'rel': 'alternate', 'type': 'text/html'}]) return feed.get_response() def collection_atom_feed(request): """ generates the atom feed with the newest images from a collection """ user = User.query.filter_by( username = request.matchdict['user'], status = u'active').first() if not user: return render_404(request) collection = Collection.query.filter_by( creator=user.id, slug=request.matchdict['collection']).first() if not collection: return render_404(request) cursor = CollectionItem.query.filter_by( collection=collection.id) \ .order_by(CollectionItem.added.desc()) \ .limit(ATOM_DEFAULT_NR_OF_UPDATED_ITEMS) """ ATOM feed id is a tag URI (see http://en.wikipedia.org/wiki/Tag_URI) """ atomlinks = [{ 'href': request.urlgen( 'mediagoblin.user_pages.user_collection', qualified=True, user=request.matchdict['user'], collection=collection.slug), 'rel': 'alternate', 'type': 'text/html' }] if mg_globals.app_config["push_urls"]: for push_url in mg_globals.app_config["push_urls"]: atomlinks.append({ 'rel': 'hub', 'href': push_url}) feed = AtomFeed( "MediaGoblin: Feed for %s's collection %s" % (request.matchdict['user'], collection.title), feed_url=request.url, id=u'tag:{host},{year}:gnu-mediagoblin.{user}.collection.{slug}'\ .format( host=request.host, year=collection.created.strftime('%Y'), user=request.matchdict['user'], slug=collection.slug), links=atomlinks) for item in cursor: entry = item.get_media_entry feed.add(entry.get('title'), item.note_html, id=entry.url_for_self(request.urlgen, qualified=True), content_type='html', author={ 'name': entry.get_uploader.username, 'uri': request.urlgen( 'mediagoblin.user_pages.user_home', qualified=True, user=entry.get_uploader.username)}, updated=item.get('added'), links=[{ 'href': entry.url_for_self( request.urlgen, qualified=True), 'rel': 'alternate', 'type': 'text/html'}]) return feed.get_response() @require_active_login def processing_panel(request): """ Show to the user what media is still in conversion/processing... and what failed, and why! """ user = User.query.filter_by(username=request.matchdict['user']).first() # TODO: XXX: Should this be a decorator? # # Make sure we have permission to access this user's panel. Only # admins and this user herself should be able to do so. if not (user.id == request.user.id or request.user.is_admin): # No? Simply redirect to this user's homepage. return redirect( request, 'mediagoblin.user_pages.user_home', user=user.username) # Get media entries which are in-processing processing_entries = MediaEntry.query.\ filter_by(uploader = user.id, state = u'processing').\ order_by(MediaEntry.created.desc()) # Get media entries which have failed to process failed_entries = MediaEntry.query.\ filter_by(uploader = user.id, state = u'failed').\ order_by(MediaEntry.created.desc()) processed_entries = MediaEntry.query.\ filter_by(uploader = user.id, state = u'processed').\ order_by(MediaEntry.created.desc()).\ limit(10) # Render to response return render_to_response( request, 'mediagoblin/user_pages/processing_panel.html', {'user': user, 'processing_entries': processing_entries, 'failed_entries': failed_entries, 'processed_entries': processed_entries})