trac#687: Add unit tests for `redirect` and `redirect_obj`.
[mediagoblin.git] / mediagoblin / user_pages / views.py
index a0eb67db751f449f4c0e8a51470e94deb1016f89..ba94ec1626ee57e2e2ed2efad4c536d63610240c 100644 (file)
 
 import logging
 import datetime
+import json
+
+import six
 
 from mediagoblin import messages, mg_globals
-from mediagoblin.db.models import (MediaEntry, MediaTag, Collection,
-                                   CollectionItem, User, MediaComment,
-                                   CommentReport, MediaReport, Group)
+from mediagoblin.db.models import (MediaEntry, MediaTag, Collection, Comment,
+                                   CollectionItem, LocalUser, Activity, \
+                                   GenericModelReference)
 from mediagoblin.tools.response import render_to_response, render_404, \
     redirect, redirect_obj
+from mediagoblin.tools.text import cleaned_markdown_conversion
 from mediagoblin.tools.translate import pass_to_ugettext as _
 from mediagoblin.tools.pagination import Pagination
+from mediagoblin.tools.federation import create_activity
 from mediagoblin.user_pages import forms as user_forms
-from mediagoblin.user_pages.lib import (send_comment_email, build_report_form,
-    add_media_to_collection)
+from mediagoblin.user_pages.lib import (send_comment_email,
+       add_media_to_collection, build_report_object)
+from mediagoblin.notifications import trigger_notification, \
+    add_comment_subscription, mark_comment_notification_seen
+from mediagoblin.tools.pluginapi import hook_transform
 
 from mediagoblin.decorators import (uses_pagination, get_user_media_entry,
-    get_media_entry_by_id, user_in_group,
+    get_media_entry_by_id, user_has_privilege, user_not_banned,
     require_active_login, user_may_delete_media, user_may_alter_collection,
     get_user_collection, get_user_collection_item, active_user_from_url,
-    get_media_comment_by_id)
+    get_optional_media_comment_by_id, allow_reporting)
 
 from werkzeug.contrib.atom import AtomFeed
+from werkzeug.exceptions import MethodNotAllowed
+from werkzeug.wrappers import Response
 
 
 _log = logging.getLogger(__name__)
 _log.setLevel(logging.DEBUG)
 
-
+@user_not_banned
 @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()
+    """'Homepage' of a LocalUser()"""
+    user = LocalUser.query.filter_by(username=request.matchdict['user']).first()
     if not user:
         return render_404(request)
-    elif user.status != u'active':
+    elif not user.has_privilege(u'active'):
         return render_to_response(
             request,
-            'mediagoblin/user_pages/user.html',
+            'mediagoblin/user_pages/user_nonactive.html',
             {'user': user})
 
     cursor = MediaEntry.query.\
-        filter_by(uploader = user.id,
+        filter_by(actor = user.id,
                   state = u'processed').order_by(MediaEntry.created.desc())
 
     pagination = Pagination(page, cursor)
@@ -80,14 +87,14 @@ def user_home(request, page):
          'media_entries': media_entries,
          'pagination': pagination})
 
-
+@user_not_banned
 @active_user_from_url
 @uses_pagination
 def user_gallery(request, page, url_user=None):
-    """'Gallery' of a User()"""
+    """'Gallery' of a LocalUser()"""
     tag = request.matchdict.get('tag', None)
     cursor = MediaEntry.query.filter_by(
-        uploader=url_user.id,
+        actor=url_user.id,
         state=u'processed').order_by(MediaEntry.created.desc())
 
     # Filter potentially by tag too:
@@ -112,9 +119,10 @@ def user_gallery(request, page, url_user=None):
          'media_entries': media_entries,
          'pagination': pagination})
 
-MEDIA_COMMENTS_PER_PAGE = 50
 
+MEDIA_COMMENTS_PER_PAGE = 50
 
+@user_not_banned
 @get_user_media_entry
 @uses_pagination
 def media_home(request, media, page, **kwargs):
@@ -123,6 +131,9 @@ def media_home(request, media, page, **kwargs):
     """
     comment_id = request.matchdict.get('comment', None)
     if comment_id:
+        if request.user:
+            mark_comment_notification_seen(comment_id, request.user)
+
         pagination = Pagination(
             page, media.get_comments(
                 mg_globals.app_config['comments_ascending']),
@@ -138,30 +149,39 @@ def media_home(request, media, page, **kwargs):
 
     comment_form = user_forms.MediaCommentForm(request.form)
 
-    media_template_name = media.media_manager['display_template']
+    media_template_name = media.media_manager.display_template
+
+    context = {
+        'media': media,
+        'comments': comments,
+        'pagination': pagination,
+        'comment_form': comment_form,
+        'app_config': mg_globals.app_config}
+
+    # Since the media template name gets swapped out for each media
+    # type, normal context hooks don't work if you want to affect all
+    # media displays.  This gives a general purpose hook.
+    context = hook_transform(
+        "media_home_context", context)
 
     return render_to_response(
         request,
         media_template_name,
-        {'media': media,
-         'comments': comments,
-         'pagination': pagination,
-         'comment_form': comment_form,
-         'app_config': mg_globals.app_config})
+        context)
 
 
 @get_media_entry_by_id
-@require_active_login
+@user_has_privilege(u'commenter')
 def media_post_comment(request, media):
     """
     recieves POST from a MediaEntry() comment form, saves the comment.
     """
-    assert request.method == 'POST'
+    if not request.method == 'POST':
+        raise MethodNotAllowed()
 
-    comment = request.db.MediaComment()
-    comment.media_entry = media.id
-    comment.author = request.user.id
-    comment.content = unicode(request.form['comment_content'])
+    comment = request.db.TextComment()
+    comment.actor = request.user.id
+    comment.content = six.text_type(request.form['comment_content'])
 
     # Show error message if commenting is disabled.
     if not mg_globals.app_config['allow_comments']:
@@ -175,21 +195,36 @@ def media_post_comment(request, media):
             messages.ERROR,
             _("Oops, your comment was empty."))
     else:
+        create_activity("post", comment, comment.actor, target=media)
+        add_comment_subscription(request.user, media)
         comment.save()
 
+        link = request.db.Comment()
+        link.target = media
+        link.comment = comment
+        link.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)
+        trigger_notification(comment, media, request)
 
     return redirect_obj(request, media)
 
 
+
+def media_preview_comment(request):
+    """Runs a comment through markdown so it can be previewed."""
+    # If this isn't an ajax request, render_404
+    if not request.is_xhr:
+        return render_404(request)
+
+    comment = six.text_type(request.form['comment_content'])
+    cleancomment = { "content":cleaned_markdown_conversion(comment)}
+
+    return Response(json.dumps(cleancomment))
+
+@user_not_banned
 @get_media_entry_by_id
 @require_active_login
 def media_collect(request, media):
@@ -198,7 +233,9 @@ def media_collect(request, media):
     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)
+        actor=request.user.id,
+        type=Collection.USER_DEFINED_TYPE
+    ).order_by(Collection.title)
 
     if request.method != 'POST' or not form.validate():
         # No POST submission, or invalid form
@@ -217,49 +254,57 @@ def media_collect(request, media):
     if form.collection_title.data:
         # Make sure this user isn't duplicating an existing collection
         existing_collection = Collection.query.filter_by(
-                                creator=request.user.id,
-                                title=form.collection_title.data).first()
+            actor=request.user.id,
+            title=form.collection_title.data,
+            type=Collection.USER_DEFINED_TYPE
+        ).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=media.get_uploader.username,
+                            user=media.get_actor.username,
                             media=media.slug_or_id)
 
         collection = Collection()
         collection.title = form.collection_title.data
         collection.description = form.collection_description.data
-        collection.creator = request.user.id
+        collection.actor = request.user.id
+        collection.type = Collection.USER_DEFINED_TYPE
         collection.generate_slug()
+        collection.get_public_id(request.urlgen)
+        create_activity("create", collection, collection.actor)
         collection.save()
 
     # Otherwise, use the collection selected from the drop-down
     else:
         collection = form.collection.data
-        if collection and collection.creator != request.user.id:
+        if collection and collection.actor != request.user.id:
             collection = None
 
     # Make sure the user actually selected a collection
+    item = CollectionItem.query.filter_by(collection=collection.id)
+    item = item.join(CollectionItem.object_helper).filter_by(
+        model_type=media.__tablename__,
+        obj_pk=media.id
+    ).first()
+
     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,
+                    user=media.get_actor.username,
                     media_id=media.id)
 
-
     # Check whether media already exists in collection
-    elif CollectionItem.query.filter_by(
-        media_entry=media.id,
-        collection=collection.id).first():
+    elif item is not None:
         messages.add_message(request, messages.ERROR,
                              _('"%s" already in collection "%s"')
                              % (media.title, collection.title))
     else: # Add item to collection
         add_media_to_collection(collection, media, form.note.data)
-
+        create_activity("add", media, request.user, target=collection)
         messages.add_message(request, messages.SUCCESS,
                              _('"%s" added to collection "%s"')
                              % (media.title, collection.title))
@@ -277,22 +322,38 @@ def media_confirm_delete(request, media):
 
     if request.method == 'POST' and form.validate():
         if form.confirm.data is True:
-            username = media.get_uploader.username
+            username = media.get_actor.username
+
+            # This probably is already filled but just in case it has slipped
+            # through the net somehow, we need to try and make sure the
+            # MediaEntry has a public ID so it gets properly soft-deleted.
+            media.get_public_id(request.urlgen)
+
+            # Decrement the users uploaded quota.
+            media.get_actor.uploaded = media.get_actor.uploaded - \
+                media.file_size
+            media.get_actor.save()
+
             # 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)
+            location = media.url_to_next(request.urlgen)
+            if not location:
+                location=media.url_to_prev(request.urlgen)
+            if not location:
+                location=request.urlgen("mediagoblin.user_pages.user_home",
+                                        user=username)
+            return redirect(request, location=location)
         else:
             messages.add_message(
                 request, messages.ERROR,
                 _("The media was not deleted because you didn't check that you were sure."))
             return redirect_obj(request, media)
 
-    if ((request.user.is_admin and
-         request.user.id != media.uploader)):
+    if ((request.user.has_privilege(u'admin') and
+         request.user.id != media.actor)):
         messages.add_message(
             request, messages.WARNING,
             _("You are about to delete another user's media. "
@@ -304,13 +365,13 @@ def media_confirm_delete(request, media):
         {'media': media,
          'form': form})
 
-
+@user_not_banned
 @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,
+        get_actor=url_user,
         slug=request.matchdict['collection']).first()
 
     if not collection:
@@ -334,12 +395,12 @@ def user_collection(request, page, url_user=None):
          'collection_items': collection_items,
          'pagination': pagination})
 
-
+@user_not_banned
 @active_user_from_url
 def collection_list(request, url_user=None):
     """A User-defined Collection"""
     collections = Collection.query.filter_by(
-        get_creator=url_user)
+        get_actor=url_user)
 
     return render_to_response(
         request,
@@ -356,16 +417,15 @@ 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
+        username = collection_item.in_collection.get_actor.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()
+            obj = collection_item.get_object()
+            obj.save()
 
             collection_item.delete()
-            collection.items = collection.items - 1
+            collection.num_items = collection.num_items - 1
             collection.save()
 
             messages.add_message(
@@ -377,8 +437,8 @@ def collection_item_confirm_remove(request, collection_item):
 
         return redirect_obj(request, collection)
 
-    if ((request.user.is_admin and
-         request.user.id != collection_item.in_collection.creator)):
+    if ((request.user.has_privilege(u'admin') and
+         request.user.id != collection_item.in_collection.actor)):
         messages.add_message(
             request, messages.WARNING,
             _("You are about to delete an item from another user's collection. "
@@ -400,16 +460,19 @@ def collection_confirm_delete(request, collection):
 
     if request.method == 'POST' and form.validate():
 
-        username = collection.get_creator.username
+        username = collection.get_actor.username
 
         if form.confirm.data is True:
             collection_title = collection.title
 
+            # Firstly like with the MediaEntry delete, lets ensure the
+            # public_id is populated as this is really important!
+            collection.get_public_id(request.urlgen)
+
             # 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()
+                obj = item.get_object()
+                obj.save()
                 item.delete()
 
             collection.delete()
@@ -425,8 +488,8 @@ def collection_confirm_delete(request, collection):
 
             return redirect_obj(request, collection)
 
-    if ((request.user.is_admin and
-         request.user.id != collection.creator)):
+    if ((request.user.has_privilege(u'admin') and
+         request.user.id != collection.actor)):
         messages.add_message(
             request, messages.WARNING,
             _("You are about to delete another user's collection. "
@@ -446,14 +509,13 @@ 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:
+    user = LocalUser.query.filter_by(
+        username = request.matchdict['user']).first()
+    if not user or not user.has_privilege(u'active'):
         return render_404(request)
 
     cursor = MediaEntry.query.filter_by(
-        uploader = user.id,
+        actor = user.id,
         state = u'processed').\
         order_by(MediaEntry.created.desc()).\
         limit(ATOM_DEFAULT_NR_OF_UPDATED_ITEMS)
@@ -485,15 +547,16 @@ def atom_feed(request):
                links=atomlinks)
 
     for entry in cursor:
-        feed.add(entry.get('title'),
+        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,
+                'name': entry.get_actor.username,
                 'uri': request.urlgen(
                     'mediagoblin.user_pages.user_home',
-                    qualified=True, user=entry.get_uploader.username)},
+                    qualified=True, user=entry.get_actor.username)},
             updated=entry.get('created'),
             links=[{
                 'href': entry.url_for_self(
@@ -509,14 +572,13 @@ 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:
+    user = LocalUser.query.filter_by(
+        username = request.matchdict['user']).first()
+    if not user or not user.has_privilege(u'active'):
         return render_404(request)
 
     collection = Collection.query.filter_by(
-               creator=user.id,
+               actor=user.id,
                slug=request.matchdict['collection']).first()
     if not collection:
         return render_404(request)
@@ -554,19 +616,20 @@ def collection_atom_feed(request):
                 links=atomlinks)
 
     for item in cursor:
-        entry = item.get_media_entry
-        feed.add(entry.get('title'),
+        obj = item.get_object()
+        feed.add(
+            obj.get('title'),
             item.note_html,
-            id=entry.url_for_self(request.urlgen, qualified=True),
+            id=obj.url_for_self(request.urlgen, qualified=True),
             content_type='html',
             author={
-                'name': entry.get_uploader.username,
+                'name': obj.get_actor().username,
                 'uri': request.urlgen(
                     'mediagoblin.user_pages.user_home',
-                    qualified=True, user=entry.get_uploader.username)},
+                    qualified=True, user=obj.get_actor().username)},
             updated=item.get('added'),
             links=[{
-                'href': entry.url_for_self(
+                'href': obj.url_for_self(
                     request.urlgen,
                     qualified=True),
                 'rel': 'alternate',
@@ -574,19 +637,18 @@ def collection_atom_feed(request):
 
     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()
+    user = LocalUser.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):
+    if not (user.id == request.user.id or request.user.has_privilege(u'admin')):
         # No?  Simply redirect to this user's homepage.
         return redirect(
             request, 'mediagoblin.user_pages.user_home',
@@ -594,18 +656,18 @@ def processing_panel(request):
 
     # Get media entries which are in-processing
     processing_entries = MediaEntry.query.\
-        filter_by(uploader = user.id,
+        filter_by(actor = 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,
+        filter_by(actor = user.id,
                   state = u'failed').\
         order_by(MediaEntry.created.desc())
 
     processed_entries = MediaEntry.query.\
-        filter_by(uploader = user.id,
+        filter_by(actor = user.id,
                   state = u'processed').\
         order_by(MediaEntry.created.desc()).\
         limit(10)
@@ -619,23 +681,43 @@ def processing_panel(request):
          'failed_entries': failed_entries,
          'processed_entries': processed_entries})
 
-@require_active_login
+@allow_reporting
 @get_user_media_entry
-@user_in_group(u'reporter')
-def file_a_report(request, media, comment=None, required_group=1):
+@user_has_privilege(u'reporter')
+@get_optional_media_comment_by_id
+def file_a_report(request, media, comment):
+    """
+    This view handles the filing of a Report.
+    """
+    if comment is not None:
+        if not comment.target().id == media.id:
+            return render_404(request)
+
+        form = user_forms.CommentReportForm(request.form)
+        context = {'media': comment.target(),
+                   'comment':comment.comment(),
+                   'form':form}
+    else:
+        form = user_forms.MediaReportForm(request.form)
+        context = {'media': media,
+                   'form':form}
+    form.reporter_id.data = request.user.id
+
+
     if request.method == "POST":
-        report_form = build_report_form(request.form)
-        report_form.save()
+        report_object = build_report_object(
+            form,
+            media_entry=media,
+            comment=comment
+        )
 
-        return redirect(
-            request,
-            'index')
+        # if the object was built successfully, report_table will not be None
+        if report_object:
+            report_object.save()
+            return redirect(
+                request,
+                'index')
 
-    if comment is not None:
-        context = {'media': media,
-                   'comment':comment}
-    else:
-        context = {'media': media}
 
     return render_to_response(
         request,
@@ -643,7 +725,35 @@ def file_a_report(request, media, comment=None, required_group=1):
         context)
 
 @require_active_login
-@get_user_media_entry
-@get_media_comment_by_id
-def file_a_comment_report(request, media, comment):        
-        return file_a_report(request, comment=comment)
+def activity_view(request):
+    """ /<username>/activity/<id> - Display activity
+
+    This should display a HTML presentation of the activity
+    this is NOT an API endpoint.
+    """
+    # Get the user object.
+    username = request.matchdict["username"]
+    user = LocalUser.query.filter_by(username=username).first()
+
+    activity_id = request.matchdict["id"]
+
+    if request.user is None:
+        return render_404(request)
+
+    activity = Activity.query.filter_by(
+        id=activity_id,
+        author=user.id
+    ).first()
+
+    # There isn't many places to check that the public_id is filled so this
+    # will do, it really should be, lets try and fix that if it isn't.
+    activity.get_public_id(request.urlgen)
+
+    if activity is None:
+        return render_404(request)
+
+    return render_to_response(
+        request,
+        "mediagoblin/api/activity.html",
+        {"activity": activity}
+    )