Whew. This is a big update. I did some significant keeping work. I moved all of
authortilly-Q <nattilypigeonfowl@gmail.com>
Wed, 17 Jul 2013 20:16:07 +0000 (16:16 -0400)
committertilly-Q <nattilypigeonfowl@gmail.com>
Wed, 17 Jul 2013 20:16:07 +0000 (16:16 -0400)
the folders and enpoints labeled 'admin' to the more accurate term of 'moderat-
ion.' I also created the ability for admins and moderators to add or remove pr-
ivileges or to ban a user in response to a report. This also meant implementing
the UserBan class in various places. I also had to add a column called result
to the ReportBase table. This allows the moderator/admin to leave comments when
they respond to a report, allowing for archiving of what responses they do/n't
take.

--\ mediagoblin/db/migrations.py
--| Added result column to ReportBase

--\ mediagoblin/db/models.py
--| Added result column to ReportBase
--| Added documentation to tables I had made previously

--\ mediagoblin/decorators.py
--| Editted the user_has_privilege decorator to check whether a user has been
  | banned or not
--| Created a seperate user_not_banned decorator to prevent banned users from
  | accessing any pages
--| Changed require_admin_login into require_admin_or_moderator login

--\ mediagoblin/gmg_commands/users.py
--| Made the gmg command `adduser` create a user w/ the appropriate privileges

--\ mediagoblin/moderation/routing.py  << formerly mediagoblin/admin/routing.py
--| Renamed all of the routes from admin -> moderation

--\ mediagoblin/routing.py
--| Renamed all of the routes from admin -> moderation

--\ mediagoblin/moderation/views.py << formerly mediagoblin/admin/views.py
--| Renamed all of the routes & functions from admin -> moderation
--| Expanded greatly on the moderation_reports_detail view and functionality
--| Added in the give_or_take_away_privilege form, however this might be a use-
  | -less function which I could remove (because privilege changes should happe-
  | n in response to a report so they can be archived and visible)

--\ mediagoblin/static/css/base.css
--| Added in a style for the reports_detail page

--\ mediagoblin/templates/mediagoblin/base.html
--| Renamed all of the routes from admin -> moderation

--\ mediagoblin/templates/mediagoblin/moderation/report.html
--| Added form to allow moderators and admins to respond to reports.

--\ mediagoblin/templates/mediagoblin/moderation/reports_panel.html
--| Fixed the table for closed reports

--\ mediagoblin/templates/mediagoblin/moderation/user.html
--| Added in a table w/ all of the user's privileges and the option to add or
  | remove them. Again, this is probably vestigial
--| Renamed all of the routes from admin -> moderation

--\ mediagoblin/templates/mediagoblin/moderation/user_panel.html
--| Renamed all of the routes from admin -> moderation

--\ mediagoblin/tools/response.py
--| Added function render_user_banned, this is the view function for the redir-
  | -ect that happens when a user tries to access the site whilst banned

--\ mediagoblin/user_pages/forms.py
--| Added important translate function where I had text

--\ mediagoblin/user_pages/lib.py
--| Renamed functiion for clarity

--\ mediagoblin/user_pages/views.py
--| Added the user_not_banned decorator to every view

--\ mediagoblin/views.py
--| Added the user_not_banned decorator

--\ mediagoblin/moderation/forms.py
--| Created this new file

--\ mediagoblin/templates/mediagoblin/banned.html
--| Created this new file
--| This is the page which people are redirected to when they access the site
  | while banned

23 files changed:
mediagoblin/admin/views.py [deleted file]
mediagoblin/db/migrations.py
mediagoblin/db/models.py
mediagoblin/decorators.py
mediagoblin/gmg_commands/users.py
mediagoblin/moderation/__init__.py [moved from mediagoblin/admin/__init__.py with 100% similarity]
mediagoblin/moderation/forms.py [new file with mode: 0644]
mediagoblin/moderation/routing.py [new file with mode: 0644]
mediagoblin/moderation/views.py [new file with mode: 0644]
mediagoblin/routing.py
mediagoblin/static/css/base.css
mediagoblin/templates/mediagoblin/banned.html [moved from mediagoblin/admin/routing.py with 56% similarity]
mediagoblin/templates/mediagoblin/base.html
mediagoblin/templates/mediagoblin/moderation/media_panel.html [moved from mediagoblin/templates/mediagoblin/admin/media_panel.html with 100% similarity]
mediagoblin/templates/mediagoblin/moderation/report.html [moved from mediagoblin/templates/mediagoblin/admin/report.html with 67% similarity]
mediagoblin/templates/mediagoblin/moderation/report_panel.html [moved from mediagoblin/templates/mediagoblin/admin/report_panel.html with 65% similarity]
mediagoblin/templates/mediagoblin/moderation/user.html [moved from mediagoblin/templates/mediagoblin/admin/user.html with 74% similarity]
mediagoblin/templates/mediagoblin/moderation/user_panel.html [moved from mediagoblin/templates/mediagoblin/admin/user_panel.html with 95% similarity]
mediagoblin/tools/response.py
mediagoblin/user_pages/forms.py
mediagoblin/user_pages/lib.py
mediagoblin/user_pages/views.py
mediagoblin/views.py

diff --git a/mediagoblin/admin/views.py b/mediagoblin/admin/views.py
deleted file mode 100644 (file)
index 9797057..0000000
+++ /dev/null
@@ -1,115 +0,0 @@
-# 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 <http://www.gnu.org/licenses/>.
-
-from werkzeug.exceptions import Forbidden
-
-from mediagoblin.db.models import (MediaEntry, User, MediaComment, \
-                                   CommentReport, ReportBase, Privilege)
-from mediagoblin.decorators import require_admin_login
-from mediagoblin.tools.response import render_to_response
-
-@require_admin_login
-def admin_media_processing_panel(request):
-    '''
-    Show the global media processing panel for this instance
-    '''
-    processing_entries = MediaEntry.query.filter_by(state = u'processing').\
-        order_by(MediaEntry.created.desc())
-
-    # Get media entries which have failed to process
-    failed_entries = MediaEntry.query.filter_by(state = u'failed').\
-        order_by(MediaEntry.created.desc())
-
-    processed_entries = MediaEntry.query.filter_by(state = u'processed').\
-        order_by(MediaEntry.created.desc()).limit(10)
-
-    # Render to response
-    return render_to_response(
-        request,
-        'mediagoblin/admin/media_panel.html',
-        {'processing_entries': processing_entries,
-         'failed_entries': failed_entries,
-         'processed_entries': processed_entries})
-
-@require_admin_login
-def admin_users_panel(request):
-    '''
-    Show the global panel for monitoring users in this instance
-    '''
-    user_list = User.query
-
-    return render_to_response(
-        request,
-        'mediagoblin/admin/user_panel.html',
-        {'user_list': user_list})
-
-@require_admin_login
-def admin_users_detail(request):
-    '''
-    Shows details about a particular user.
-    '''
-    user = User.query.filter_by(username=request.matchdict['user']).first()
-    privileges = Privilege.query
-    active_reports = user.reports_filed_on.filter(
-        ReportBase.resolved==None).limit(5)
-    closed_reports = user.reports_filed_on.filter(
-        ReportBase.resolved!=None).all()
-
-    return render_to_response(
-        request,
-        'mediagoblin/admin/user.html',
-        {'user':user,
-         'privileges':privileges,
-         'reports':active_reports})
-
-@require_admin_login
-def admin_reports_panel(request):
-    '''
-    Show the global panel for monitoring reports filed against comments or 
-        media entries for this instance.
-    '''
-    report_list = ReportBase.query.filter(
-        ReportBase.resolved==None).order_by(
-        ReportBase.created.desc()).limit(10)
-    closed_report_list = ReportBase.query.filter(
-        ReportBase.resolved!=None).order_by(
-        ReportBase.created.desc()).limit(10)
-
-    # Render to response
-    return render_to_response(
-        request,
-        'mediagoblin/admin/report_panel.html',
-        {'report_list':report_list,
-         'closed_report_list':closed_report_list})
-
-@require_admin_login
-def admin_reports_detail(request):
-    report = ReportBase.query.get(request.matchdict['report_id'])
-    if report.discriminator == 'comment_report':
-        comment = MediaComment.query.get(report.comment_id)
-        media_entry = None
-    elif report.discriminator == 'media_report':
-        media_entry = MediaEntry.query.get(report.media_entry_id)
-        comment = None
-
-    return render_to_response(
-        request,
-        'mediagoblin/admin/report.html',
-        {'report':report,
-         'media_entry':media_entry,
-         'comment':comment})
-    
-
index a32f5932fdf0ef7ac11ab53b0c13806f3c474eb1..3e6791c4cb20d6ee7deb221797dab1135daef0dd 100644 (file)
@@ -300,6 +300,7 @@ class ReportBase_v0(declarative_base()):
     reported_user_id = Column(Integer, ForeignKey(User.id), nullable=False)
     created = Column(DateTime, nullable=False, default=datetime.datetime.now) 
     resolved = Column(DateTime)
+    result = Column(UnicodeText)
     discriminator = Column('type', Unicode(50))
     __mapper_args__ = {'polymorphic_on': discriminator}
 
index e4c97a2cf4722ec957b5d4192110e537c6214599..01078db802a4ac9075778ed97d87a337f091c31c 100644 (file)
@@ -239,8 +239,8 @@ class MediaEntry(Base, MediaEntryMixin):
         This will *not* automatically delete unused collections, which
         can remain empty...
 
-        :param del_orphan_tags: True/false if we delete unused Tags too
-        :param commit: True/False if this should end the db transaction"""
+        :keyword del_orphan_tags: True/false if we delete unused Tags too
+        :keyword commit: True/False if this should end the db transaction"""
         # User's CollectionItems are automatically deleted via "cascade".
         # Comments on this Media are deleted by cascade, hopefully.
 
@@ -487,6 +487,14 @@ class ProcessingMetaData(Base):
 
 class ReportBase(Base):
     """
+    This is the basic report table which the other reports are based off of.
+        :keyword reporter_id
+        :keyword report_content
+        :keyword reported_user_id
+        :keyword created
+        :keyword resolved
+        :keyword result
+        :keyword discriminator
 
     """
     __tablename__ = 'core__reports'
@@ -508,6 +516,7 @@ class ReportBase(Base):
         primaryjoin="User.id==ReportBase.reported_user_id")
     created = Column(DateTime, nullable=False, default=datetime.datetime.now())
     resolved = Column(DateTime)
+    result = Column(UnicodeText)
     discriminator = Column('type', Unicode(50))
     __mapper_args__ = {'polymorphic_on': discriminator}
 
@@ -551,13 +560,13 @@ class UserBan(Base):
         the reason why they are banned and when (if ever) the ban will be 
         lifted
 
-        :param user_id          Holds the id of the user this object is 
+        :keyword user_id          Holds the id of the user this object is 
                                     attached to. This is a one-to-one 
                                     relationship.
-        :param expiration_date  Holds the date that the ban will be lifted. 
+        :keyword expiration_date  Holds the date that the ban will be lifted. 
                                     If this is null, the ban is permanent 
                                     unless a moderator manually lifts it.
-        :param reason           Holds the reason why the user was banned.
+        :keyword reason           Holds the reason why the user was banned.
     """
     __tablename__ = 'core__user_bans'
 
@@ -568,6 +577,17 @@ class UserBan(Base):
 
 
 class Privilege(Base):
+    """
+    The Privilege table holds all of the different privileges a user can hold.
+    If a user 'has' a privilege, the User object is in a relationship with the
+    privilege object. 
+
+        :keyword privilege_name   Holds a unicode object that is the recognizable
+                                    name of this privilege. This is the column 
+                                    used for identifying whether or not a user
+                                    has a necessary privilege or not.
+                                
+    """
     __tablename__ = 'core__privileges'
 
     id = Column(Integer, nullable=False, primary_key=True)
@@ -578,12 +598,30 @@ class Privilege(Base):
         secondary="core__privileges_users")
 
     def __init__(self, privilege_name):
+        '''
+        Currently consructors are required for tables that are initialized thru
+        the FOUNDATIONS system. This is because they need to be able to be con-
+        -structed by a list object holding their arg*s
+        '''
         self.privilege_name = privilege_name
 
     def __repr__(self):
         return "<Privilege %s>" % (self.privilege_name)
 
+    def is_admin_or_moderator(self):
+        '''
+        This method is necessary to check if a user is able to take moderation
+        actions.
+        '''
+        
+        return (self.privilege_name==u'admin' or 
+                self.privilege_name==u'moderator')
+
 class PrivilegeUserAssociation(Base):
+    '''
+    This table holds the many-to-many relationship between User and Privilege
+    '''
+    
     __tablename__ = 'core__privileges_users'
 
     privilege_id = Column(
index fefbccef1ff097d15214ff260f58f48beab1a159..b39b36f57ac0684e93de7564a7d805e0bc254412 100644 (file)
@@ -21,8 +21,9 @@ from werkzeug.exceptions import Forbidden, NotFound
 from werkzeug.urls import url_quote
 
 from mediagoblin import mg_globals as mgg
-from mediagoblin.db.models import MediaEntry, User, MediaComment, Privilege
-from mediagoblin.tools.response import redirect, render_404
+from mediagoblin.db.models import MediaEntry, User, MediaComment, Privilege, \
+                            UserBan
+from mediagoblin.tools.response import redirect, render_404, render_user_banned
 
 
 def require_active_login(controller):
@@ -64,6 +65,7 @@ def active_user_from_url(controller):
     return wrapper
 
 def user_has_privilege(privilege_name):
+
     def user_has_privilege_decorator(controller):
         @wraps(controller)
         def wrapper(request, *args, **kwargs):
@@ -71,7 +73,9 @@ def user_has_privilege(privilege_name):
             privileges_of_user = Privilege.query.filter(
                 Privilege.all_users.any(
                 User.id==user_id))
-            if not privileges_of_user.filter(
+            if UserBan.query.filter(UserBan.user_id==user_id).count():
+                return render_user_banned(request)
+            elif not privileges_of_user.filter(
                 Privilege.privilege_name==privilege_name).count():
                 raise Forbidden()
 
@@ -271,14 +275,18 @@ def get_workbench(func):
 
     return new_func
 
-def require_admin_login(controller):
+def require_admin_or_moderator_login(controller):
     """
-    Require an login from an administrator.
+    Require an login from an administrator or a moderator.
     """
     @wraps(controller)
     def new_controller_func(request, *args, **kwargs):
+        admin_privilege = Privilege.one({'privilege_name':u'admin'})
+        moderator_privilege = Privilege.one({'privilege_name':u'moderator'})
         if request.user and \
-                not request.user.is_admin:
+            not admin_privilege in request.user.all_privileges and \
+                 not moderator_privilege in request.user.all_privileges:
+
             raise Forbidden()
         elif not request.user:
             next_url = urljoin(
@@ -293,3 +301,18 @@ def require_admin_login(controller):
 
     return new_controller_func
 
+def user_not_banned(controller):
+    """
+    Requires that the user has not been banned. Otherwise redirects to the page
+    explaining why they have been banned
+    """
+    @wraps(controller)
+    def wrapper(request, *args, **kwargs):
+        if request.user:
+            user_banned = UserBan.query.get(request.user.id)
+            if user_banned:
+                return render_user_banned(request)
+        return controller(request, *args, **kwargs)
+
+    return wrapper
+
index ccc4da73bc83a8faea6cf8519fefd435dfede87e..5c19d3a9aaaf0555519ef74af0c8a778932c0344 100644 (file)
@@ -55,6 +55,13 @@ def adduser(args):
         entry.pw_hash = auth_lib.bcrypt_gen_password_hash(args.password)
         entry.status = u'active'
         entry.email_verified = True
+        default_privileges = [ 
+            db.Privilege.one({'privilege_name':u'commenter'}),
+            db.Privilege.one({'privilege_name':u'uploader'}),
+            db.Privilege.one({'privilege_name':u'reporter'}),
+            db.Privilege.one({'privilege_name':u'active'})
+]
+        entry.all_privileges = default_privileges
         entry.save()
 
         print "User created (and email marked as verified)"
diff --git a/mediagoblin/moderation/forms.py b/mediagoblin/moderation/forms.py
new file mode 100644 (file)
index 0000000..0a91b9b
--- /dev/null
@@ -0,0 +1,40 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+import wtforms
+from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
+
+ACTION_CHOICES = [(_(u'takeaway'),_('Take away privilege')),
+    (_(u'userban'),_('Ban the user')),
+    (_(u'closereport'),_('Close the report without taking an action'))]
+
+class PrivilegeAddRemoveForm(wtforms.Form):
+    giving_privilege = wtforms.HiddenField('',[wtforms.validators.required()])
+    privilege_name = wtforms.HiddenField('',[wtforms.validators.required()])
+
+class ReportResolutionForm(wtforms.Form):
+    action_to_resolve = wtforms.RadioField(
+        _('What action will you take to resolve this report'), 
+        validators=[wtforms.validators.required()],
+        choices=ACTION_CHOICES)
+    targeted_user   = wtforms.HiddenField('',
+        validators=[wtforms.validators.required()])
+    user_banned_until = wtforms.DateField(
+        _('User will be banned until:'),
+        format='%Y-%m-%d',
+        validators=[wtforms.validators.optional()])
+    resolution_content = wtforms.TextAreaField()
+
diff --git a/mediagoblin/moderation/routing.py b/mediagoblin/moderation/routing.py
new file mode 100644 (file)
index 0000000..f177c32
--- /dev/null
@@ -0,0 +1,35 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+moderation_routes = [
+    ('mediagoblin.moderation.media_panel',
+        '/media/',
+        'mediagoblin.moderation.views:moderation_media_processing_panel'),
+    ('mediagoblin.moderation.users',
+        '/users/',
+        'mediagoblin.moderation.views:moderation_users_panel'),
+    ('mediagoblin.moderation.reports',
+        '/reports/',
+        'mediagoblin.moderation.views:moderation_reports_panel'),
+    ('mediagoblin.moderation.users_detail',
+        '/users/<string:user>/',
+        'mediagoblin.moderation.views:moderation_users_detail'),
+    ('mediagoblin.moderation.give_or_take_away_privilege',
+        '/users/<string:user>/privilege/',
+        'mediagoblin.moderation.views:give_or_take_away_privilege'),
+    ('mediagoblin.moderation.reports_detail',
+        '/reports/<int:report_id>/',
+        'mediagoblin.moderation.views:moderation_reports_detail')]
diff --git a/mediagoblin/moderation/views.py b/mediagoblin/moderation/views.py
new file mode 100644 (file)
index 0000000..6f6318b
--- /dev/null
@@ -0,0 +1,200 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+from werkzeug.exceptions import Forbidden
+
+from mediagoblin.db.models import (MediaEntry, User, MediaComment, \
+                                   CommentReport, ReportBase, Privilege, \
+                                   UserBan)
+from mediagoblin.decorators import (require_admin_or_moderator_login, \
+                                    active_user_from_url)
+from mediagoblin.tools.response import render_to_response, redirect
+from mediagoblin.moderation import forms as moderation_forms
+from datetime import datetime
+
+@require_admin_or_moderator_login
+def moderation_media_processing_panel(request):
+    '''
+    Show the global media processing panel for this instance
+    '''
+    processing_entries = MediaEntry.query.filter_by(state = u'processing').\
+        order_by(MediaEntry.created.desc())
+
+    # Get media entries which have failed to process
+    failed_entries = MediaEntry.query.filter_by(state = u'failed').\
+        order_by(MediaEntry.created.desc())
+
+    processed_entries = MediaEntry.query.filter_by(state = u'processed').\
+        order_by(MediaEntry.created.desc()).limit(10)
+
+    # Render to response
+    return render_to_response(
+        request,
+        'mediagoblin/moderation/media_panel.html',
+        {'processing_entries': processing_entries,
+         'failed_entries': failed_entries,
+         'processed_entries': processed_entries})
+
+@require_admin_or_moderator_login
+def moderation_users_panel(request):
+    '''
+    Show the global panel for monitoring users in this instance
+    '''
+    user_list = User.query
+
+    return render_to_response(
+        request,
+        'mediagoblin/moderation/user_panel.html',
+        {'user_list': user_list})
+
+@require_admin_or_moderator_login
+def moderation_users_detail(request):
+    '''
+    Shows details about a particular user.
+    '''
+    user = User.query.filter_by(username=request.matchdict['user']).first()
+    active_reports = user.reports_filed_on.filter(
+        ReportBase.resolved==None).limit(5)
+    closed_reports = user.reports_filed_on.filter(
+        ReportBase.resolved!=None).all()
+    privileges = Privilege.query
+
+    return render_to_response(
+        request,
+        'mediagoblin/moderation/user.html',
+        {'user':user,
+         'privileges':privileges,
+         'reports':active_reports})
+
+@require_admin_or_moderator_login
+def moderation_reports_panel(request):
+    '''
+    Show the global panel for monitoring reports filed against comments or 
+        media entries for this instance.
+    '''
+    report_list = ReportBase.query.filter(
+        ReportBase.resolved==None).order_by(
+        ReportBase.created.desc()).limit(10)
+    closed_report_list = ReportBase.query.filter(
+        ReportBase.resolved!=None).order_by(
+        ReportBase.created.desc()).limit(10)
+
+    # Render to response
+    return render_to_response(
+        request,
+        'mediagoblin/moderation/report_panel.html',
+        {'report_list':report_list,
+         'closed_report_list':closed_report_list})
+
+@require_admin_or_moderator_login
+def moderation_reports_detail(request):
+    """
+    This is the page an admin or moderator goes to see the details of a report.
+    The report can be resolved or unresolved. This is also the page that a mod-
+    erator would go to to take an action to resolve a report.
+    """
+    form = moderation_forms.ReportResolutionForm(request.form)
+    report = ReportBase.query.get(request.matchdict['report_id'])
+
+    if request.method == "POST" and form.validate():
+        user = User.query.get(form.targeted_user.data)
+        if form.action_to_resolve.data == u'takeaway':
+            if report.discriminator == u'comment_report':
+                privilege = Privilege.one({'privilege_name':u'commenter'})
+                form.resolution_content.data += \
+                    u"<br>%s took away %s\'s commenting privileges" % (
+                        request.user.username,
+                        user.username)
+            else:
+                privilege = Privilege.one({'privilege_name':u'uploader'})
+                form.resolution_content.data += \
+                    u"<br>%s took away %s\'s media uploading privileges" % (
+                        request.user.username,
+                        user.username)
+            user.all_privileges.remove(privilege)
+            user.save()
+            report.result = form.resolution_content.data
+            report.resolved = datetime.now()
+            report.save()
+            
+        elif form.action_to_resolve.data == u'userban':
+            reason = form.resolution_content.data + \
+                "<br>"+request.user.username
+            user_ban = UserBan(
+                user_id=form.targeted_user.data,
+                expiration_date=form.user_banned_until.data,
+                reason= form.resolution_content.data)
+            user_ban.save()
+            if not form.user_banned_until == "":
+                form.resolution_content.data += \
+                    u"<br>%s banned user %s until %s." % (
+                    request.user.username,
+                    user.username,
+                    form.user_banned_until.data)
+            else:
+                form.resolution_content.data += \
+                    u"<br>%s banned user %s indefinitely." % (
+                    request.user.username,
+                    user.username,
+                    form.user_banned_until.data)
+
+            report.result = form.resolution_content.data
+            report.resolved = datetime.now()
+            report.save()
+
+        else:
+            pass
+
+        return redirect(
+            request,
+            'mediagoblin.moderation.users_detail',
+            user=user.username)
+
+    if report.discriminator == 'comment_report':
+        comment = MediaComment.query.get(report.comment_id)
+        media_entry = None
+    elif report.discriminator == 'media_report':
+        media_entry = MediaEntry.query.get(report.media_entry_id)
+        comment = None
+
+    form.targeted_user.data = report.reported_user_id
+
+    return render_to_response(
+        request,
+        'mediagoblin/moderation/report.html',
+        {'report':report,
+         'media_entry':media_entry,
+         'comment':comment,
+         'form':form})
+
+@require_admin_or_moderator_login
+@active_user_from_url
+def give_or_take_away_privilege(request, url_user):
+    '''
+    A form action to give or take away a particular privilege from a user
+    '''
+    form = moderation_forms.PrivilegeAddRemoveForm(request.form)
+    if request.method == "POST" and form.validate():
+        privilege = Privilege.one({'privilege_name':form.privilege_name.data})
+        if privilege in url_user.all_privileges is True:
+            url_user.all_privileges.remove(privilege)
+        else:      
+            url_user.all_privileges.append(privilege)
+        url_user.save()
+        return redirect(
+            request,
+            'mediagoblin.moderation.users_detail',
+            user=url_user.username)
index a650f22fbd7d10b6daf5b3bcf4849758bd12f532..0642ad8e33aea0d175dea7d658eec7897c3d22e7 100644 (file)
@@ -18,7 +18,7 @@ import logging
 
 from mediagoblin.tools.routing import add_route, mount, url_map
 from mediagoblin.tools.pluginapi import PluginManager
-from mediagoblin.admin.routing import admin_routes
+from mediagoblin.moderation.routing import moderation_routes
 from mediagoblin.auth.routing import auth_routes
 
 
@@ -28,7 +28,7 @@ _log = logging.getLogger(__name__)
 def get_url_map():
     add_route('index', '/', 'mediagoblin.views:root_view')
     mount('/auth', auth_routes)
-    mount('/a', admin_routes)
+    mount('/mod', moderation_routes)
 
     import mediagoblin.submit.routing
     import mediagoblin.user_pages.routing
index 1cded530a6d632f03f8ac51c40c5367d2bd10aa8..343648d8d7f5586d6214a33a7b473281f9de241e 100644 (file)
@@ -402,6 +402,8 @@ a.report_authorlink, a.report_whenlink {
   color: #D486B1;
 }
 
+ul#action_to_resolve {list-style:none; margin-left:10px;}
+
 /* media galleries */
 
 .media_thumbnail {
similarity index 56%
rename from mediagoblin/admin/routing.py
rename to mediagoblin/templates/mediagoblin/banned.html
index c7ca5b92f098b292ec547498b0cd7bbc9b404e6f..4eda0540fc8fef75d2629556f2397a6ebe561542 100644 (file)
@@ -1,3 +1,4 @@
+{#
 # GNU MediaGoblin -- federated, autonomous media hosting
 # Copyright (C) 2011, 2012 MediaGoblin contributors.  See AUTHORS.
 #
 #
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#}
+{% extends "mediagoblin/base.html" %}
 
-admin_routes = [
-    ('mediagoblin.admin.media_panel',
-        '/media',
-        'mediagoblin.admin.views:admin_media_processing_panel'),
-    ('mediagoblin.admin.users',
-        '/users',
-        'mediagoblin.admin.views:admin_users_panel'),
-    ('mediagoblin.admin.reports',
-        '/reports',
-        'mediagoblin.admin.views:admin_reports_panel'),
-    ('mediagoblin.admin.users_detail',
-        '/users/<string:user>',
-        'mediagoblin.admin.views:admin_users_detail'),
-    ('mediagoblin.admin.reports_detail',
-        '/reports/<int:report_id>',
-        'mediagoblin.admin.views:admin_reports_detail')]
+{% block title %}You are Banned.{% endblock %}
+
+{% block mediagoblin_content %}
+  <img class="right_align" src="{{ request.staticdirect('/images/404.png') }}"
+       alt="{% trans %}Image of goblin stressing out{% endtrans %}" />
+  <h1>You have been banned until {{ expiration_date }}</h1>
+  <p>{{ reason|safe }}</p>
+  <div class="clear"></div>
+{% endblock %}
index e9a18f22c8aa765b83e88a566e6980428d602f6a..b52b65e7e0415966421a70e99bb605508f0a60c4 100644 (file)
               {% if request.user.is_admin %}
                 <p>
                   <span class="dropdown_title">Admin powers:</span>
-                  <a href="{{ request.urlgen('mediagoblin.admin.media_panel') }}">
+                  <a href="{{ request.urlgen('mediagoblin.moderation.media_panel') }}">
                     {%- trans %}Media processing panel{% endtrans -%}
                   </a>
+                  <a href="{{ request.urlgen('mediagoblin.moderation.users') }}">
+                    {%- trans %}User management panel{% endtrans -%}
+                  </a>
                 </p>
               {% endif %}
             </div>
similarity index 67%
rename from mediagoblin/templates/mediagoblin/admin/report.html
rename to mediagoblin/templates/mediagoblin/moderation/report.html
index ef90df40876173701d19b8f98f322ab285237bb6..6938569d7c9e04158293498d18eb5da705ea0928 100644 (file)
@@ -16,6 +16,7 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #}
 {%- extends "mediagoblin/base.html" %}
+{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
 
 {%- block mediagoblin_content %}
 {% if not report %}
@@ -29,7 +30,7 @@
         class="comment_wrapper">
       <div class="comment_author">
         <img src="{{ request.staticdirect('/images/icon_comment.png') }}" />
-            <a href="{{ request.urlgen('mediagoblin.admin.users_detail',
+            <a href="{{ request.urlgen('mediagoblin.moderation.users_detail',
                             user=comment.get_author.username) }}"
                class="comment_authorlink">
           {{- reported_user.username -}}
     <div class="report_author">
         <img src="{{ request.staticdirect('/images/icon_clipboard.png') }}" 
              alt="Under a GNU LGPL v.3 or Creative Commons BY-SA 3.0 license. Distributed by the GNOME project http://www.gnome.org" />
-        <a href="{{ request.urlgen('mediagoblin.admin.users_detail',
+        <a href="{{ request.urlgen('mediagoblin.moderation.users_detail',
                         user=report.reporter.username) }}"
            class="report_authorlink">
         {{- report.reporter.username -}}
         </a>
-        <a href="{{ request.urlgen('mediagoblin.admin.reports_detail',
+        <a href="{{ request.urlgen('mediagoblin.moderation.reports_detail',
                         report_id=report.id) }}"
            class="report_whenlink">
           <span title='{{- report.created.strftime("%I:%M%p %Y-%m-%d") -}}'>
       {{ report.report_content }}
     </div>
   </div>
+  {% if not report.resolved %}
+    <input type=button value=Resolve id=open_resolution_form />
+    <form action="" method="POST" id=resolution_form>
+      {{ wtforms_util.render_divs(form) }}
+      {{ csrf_token }}
+      <input type=submit id="submit_this_report" value="Resolve This Report"/>
+    </form>
+
+
+  <script>
+  $(document).ready(function() {
+      $('form#resolution_form').hide()
+      $('#user_banned_until').val("YYYY-MM-DD")
+      $('#open_resolution_form').click(function() {
+          $('form#resolution_form').toggle();
+          $('#user_banned_until').hide();
+          $('label[for=user_banned_until]').hide();
+      });
+      $('#action_to_resolve').change(function() {
+          if ($('ul#action_to_resolve li input:checked').val() == "userban") {
+              $('#user_banned_until').show();
+              $('label[for=user_banned_until]').show();
+          } else {
+              $('#user_banned_until').hide();
+              $('label[for=user_banned_until]').hide();
+          }
+      });
+      $("#user_banned_until").focus(function() {
+          $(this).val("");
+          $(this).unbind('focus');
+      });
+      $("#submit_this_report").click(function(){
+          if ($("#user_banned_until").val() == 'YYYY-MM-DD'){
+              $("#user_banned_until").val("");
+          }
+      });
+  });
+  </script>
+  {% else %}
+    <h2>Status:</h2>
+    RESOLVED on {{ report.resolved.strftime("%I:%M%p %Y-%m-%d") }}
+    {% autoescape False %}
+    <p>{{ report.result }}</p>
+    {% endautoescape %}
+  {% endif %}
 {% endif %}
 {% endblock %}
similarity index 65%
rename from mediagoblin/templates/mediagoblin/admin/report_panel.html
rename to mediagoblin/templates/mediagoblin/moderation/report_panel.html
index 301945774b535b797f522cea5f17839c048df240..126b247c41ec4e54d0b06218cf7c37665e734aac 100644 (file)
@@ -29,7 +29,7 @@
   {% trans %}Here you can look up users in order to take punitive actions on them.{% endtrans %}
 </p>
     
-<h2>{% trans %}Reports Filed on Comments{% endtrans %}</h2>
+<h2>{% trans %}Reports Filed{% endtrans %}</h2>
 
 {% if report_list.count() %}
   <table class="admin_panel processing">
     </tr>
     {% for report in report_list %}
       <tr>
+
+            <td><a href="{{ request.urlgen('mediagoblin.moderation.reports_detail',
+                                    report_id=report.id) }}">{{ report.id }}</a></td>
         {% if report.discriminator == "comment_report" %}
-            <td>{{ report.id }}</td>
             <td>Comment Report</td>
             <td>{{ report.comment.get_author.username }}</td>
             <td>{{ report.created.strftime("%F %R") }}</td>
@@ -53,7 +55,6 @@
             <td>{{ report.report_content }}</td>
             <td><a href="{{ report.comment.get_media_entry.url_for_self(request.urlgen) }}">{{ report.comment.get_media_entry.title }}</a></td>
         {% elif report.discriminator == "media_report" %}
-            <td>{{ report.id }}</td>
             <td>Media Report</td>
             <td>{{ report.media_entry.get_uploader.username }}</td>
             <td>{{ report.created.strftime("%F %R") }}</td>
 {% else %}
   <p><em>{% trans %}No open reports found.{% endtrans %}</em></p>
 {% endif %}
-<h2>{% trans %}Closed Reports on Comments{% endtrans %}</h2>
+<h2>{% trans %}Closed Reports{% endtrans %}</h2>
 {% if closed_report_list.count() %}
   <table class="media_panel processing">
     <tr>
       <th>ID</th>
+      <th>Resolved</th>
       <th>Offender</th>
-      <th>When Reported</th>
+      <th>Action Taken</th>
       <th>Reported By</th>
       <th>Reason</th>
-      <th>Comment Posted On</th>
+      <th>Reported Comment or Media Entry</th>
     </tr>
     {% for report in closed_report_list %}
-      <tr>
-        <td>{{ report.id }}</td>
-        <td>{{ report.comment.get_author.username }}</td>
-        <td>{{ report.created.strftime("%F %R") }}</td>
-        <td>{{ report.reporter.username }}</td>
-        <td>{{ report.report_content }}</td>
-        <td><a href="{{ report.comment.get_media_entry.url_for_self(request.urlgen) }}">{{ report.comment.get_media_entry.title }}</a></td>
-      </tr>
+            <td><a href="{{ request.urlgen('mediagoblin.moderation.reports_detail',
+                                    report_id=report.id) }}">{{ report.id }}</a></td>
+        {% if report.discriminator == "comment_report" %}
+            <td>{{ report.resolved.strftime("%F %R") }}</td>
+            <td>{{ report.comment.get_author.username }}</td>
+            <td>{{ report.created.strftime("%F %R") }}</td>
+            <td>{{ report.reporter.username }}</td>
+            <td>{{ report.report_content }}</td>
+            <td><a href="{{ report.comment.get_media_entry.url_for_self(request.urlgen) }}">{{ report.comment.get_media_entry.title }}</a></td>
+        {% elif report.discriminator == "media_report" %}
+            <td>{{ report.resolved.strftime("%F %R") }}</td>
+            <td>{{ report.media_entry.get_uploader.username }}</td>
+            <td>{{ report.created.strftime("%F %R") }}</td>
+            <td>{{ report.reporter.username }}</td>
+            <td>{{ report.report_content[0:20] }}...</td>
+            <td><a href="{{ report.media_entry.url_for_self(request.urlgen) }}">{{ report.media_entry.title }}</a></td>
+        {% endif %}
     {% endfor %}
   </table>
 {% else %}
similarity index 74%
rename from mediagoblin/templates/mediagoblin/admin/user.html
rename to mediagoblin/templates/mediagoblin/moderation/user.html
index 90b3f583ad69eae4aca55fac5876a5b9379c9bfa..f868aa8a1ca338df26a0910710dff651dd573559 100644 (file)
@@ -85,7 +85,7 @@
     </div>
   {% endif %}
   {% if user %}
-    <h2>{%- trans %}Active Reports on{% endtrans -%} {{ user.username }}</h2>
+    <h2>{%- trans %}Active Reports on {% endtrans -%}{{ user.username }}</h2>
     {% if reports.count() %}
     <table class="admin_side_panel">
       <tr>
@@ -97,7 +97,7 @@
         <tr>
           <td>
             <img src="{{ request.staticdirect('/images/icon_clipboard.png') }}" />
-            <a href="{{ request.urlgen('mediagoblin.admin.reports_detail',
+            <a href="{{ request.urlgen('mediagoblin.moderation.reports_detail',
                                                report_id=report.id) }}">
               {%- trans %}Report #{% endtrans -%}{{ report.id }}
             </a>
         <tr><td></td><td></td>
     </table>
     {% else %}
-      {%- trans %}No active reports filed on{% endtrans -%} {{ user.username }}
+      {%- trans %}No active reports filed on {% endtrans -%} {{ user.username }}
     {% endif %}
     <a class="right_align">{{ user.username }}'s report history</a>
     <span class=clear></span>
         <tr>
           <th>{% trans %}Privilege{% endtrans %}</th>
           <th>{% trans %}User Has Privilege{% endtrans %}</th>
-      {% for privilege in privileges %}
-        <tr>
-          <td>{{ privilege.privilege_name }}</td>
-          <td>{% if privilege in user.all_privileges %}Yes{% else %}No{% endif %}</td>
-          <td>{% if privilege in user.all_privileges and privilege.id < request.user.get_highest_privilege().id %}<a>{% trans %}Take Away{% endtrans %}</a>{% else %}<a>{% trans %}Give Privilege{% endtrans %}</a>{% endif %}</td>
         </tr>
-      {% endfor %}
+        {% for privilege in privileges %}
+          <tr>
+            <form action="{{ request.urlgen('mediagoblin.moderation.give_or_take_away_privilege',
+                             user=user.username) }}" 
+                  method=post >
+            <td>{{ privilege.privilege_name }}</td>
+            <td>
+              {% if privilege in user.all_privileges %}Yes</td>
+                {% if (not privilege.is_admin_or_moderator() or request.user.is_admin) and not (user.is_admin and not request.user.is_admin) %}                
+                  <td><input type=submit value="{% trans %}Take Away{% endtrans %}" />
+                  <input type=hidden name=giving_privilege />
+                {% endif %}
+              {% else %}No</td>
+                {% if (not privilege.is_admin_or_moderator() or request.user.is_admin) and not (user.is_admin and not request.user.is_admin) %}                
+                  <td><input type=submit value="{% trans %}Give Privilege{% endtrans %}" >
+                  <input type=hidden name=giving_privilege value=True />
+                {% endif %}
+              {% endif %}
+                <input type=hidden name=privilege_name value="{{ privilege.privilege_name }}" />
+            </td>           
+       {{ csrf_token }}
+            </form> 
+          </tr>    
+        {% endfor %}
     </table>
   {% endif %}
 {% endblock %}
similarity index 95%
rename from mediagoblin/templates/mediagoblin/admin/user_panel.html
rename to mediagoblin/templates/mediagoblin/moderation/user_panel.html
index cc965b739aa14b1e25f3be67e2326c5465ad6032..498770742fc8b488e08f25511290136ab372eb1a 100644 (file)
@@ -42,7 +42,7 @@
     {% for user in user_list %}
       <tr>
         <td>{{ user.id }}</td>
-        <td><a href="{{ request.urlgen('mediagoblin.admin.users_detail',
+        <td><a href="{{ request.urlgen('mediagoblin.moderation.users_detail',
                                          user= user.username) }}">{{ user.username }}</a></td>
         <td>{{ user.created.strftime("%F %R") }}</td>
         <td>{{ user.posted_comments.count() }}</td>
index aaf31d0b3a665c3509338403ccb9757ee177e54c..ecea307e8c3e9dd9de776a09fd84fe5bd610de40 100644 (file)
@@ -19,6 +19,7 @@ from werkzeug.wrappers import Response as wz_Response
 from mediagoblin.tools.template import render_template
 from mediagoblin.tools.translate import (lazy_pass_to_ugettext as _,
                                          pass_to_ugettext)
+from mediagoblin.db.models import UserBan
 
 class Response(wz_Response):
     """Set default response mimetype to HTML, otherwise we get text/plain"""
@@ -62,6 +63,15 @@ def render_404(request):
                 "you're looking for has been moved or deleted.")
     return render_error(request, 404, err_msg=err_msg)
 
+def render_user_banned(request):
+    """Renders the page which tells a user they have been banned, for how long
+    and the reason why they have been banned"
+    """
+    user_ban = UserBan.query.get(request.user.id)
+    return render_to_response(request,
+        'mediagoblin/banned.html',
+        {'reason':user_ban.reason,
+         'expiration_date':user_ban.expiration_date})
 
 def render_http_exception(request, exc, description):
     """Return Response() given a werkzeug.HTTPException
index 284dd7b8c65a97c7909205f8ea58bb585132e686..260fe02b49e09b9915cd54653104571ed46f431b 100644 (file)
@@ -51,11 +51,15 @@ class MediaCollectForm(wtforms.Form):
                       Markdown</a> for formatting."""))
 
 class CommentReportForm(wtforms.Form):
-    report_reason = wtforms.TextAreaField('Reason for Reporting')
+    report_reason = wtforms.TextAreaField(
+        _('Reason for Reporting'),
+        [wtforms.validators.Required()])
     comment_id = wtforms.IntegerField()
     reporter_id = wtforms.IntegerField()
 
 class MediaReportForm(wtforms.Form):
-    report_reason = wtforms.TextAreaField('Reason for Reporting')
+    report_reason = wtforms.TextAreaField(
+        _('Reason for Reporting'),
+        [wtforms.validators.Required()])
     media_entry_id = wtforms.IntegerField()
     reporter_id = wtforms.IntegerField()
index 2558b06692b5c1d5ec9e4b2f2c21d161cc18c6bb..cf7b604d7d980383ed15d2b56d28b9089c650c2e 100644 (file)
@@ -78,7 +78,7 @@ def add_media_to_collection(collection, media, note=None, commit=True):
     if commit:
         Session.commit()
 
-def build_report_form(form_dict):
+def build_report_table(form_dict):
     """
     This function is used to convert a form dictionary (from a User filing a 
         report) into either a MediaReport or CommentReport object.
index abf5e5c1da200c7dce3574ae0a56baadec871a85..c163827677a682687efe8b72b9587e8986fef91e 100644 (file)
@@ -26,14 +26,14 @@ from mediagoblin.tools.response import render_to_response, render_404, \
 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, build_report_form,
+from mediagoblin.user_pages.lib import (send_comment_email, build_report_table,
     add_media_to_collection)
 
 from mediagoblin.decorators import (uses_pagination, get_user_media_entry,
     get_media_entry_by_id, user_has_privilege,
     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_media_comment_by_id, user_not_banned)
 
 from werkzeug.contrib.atom import AtomFeed
 
@@ -41,7 +41,7 @@ from werkzeug.contrib.atom import AtomFeed
 _log = logging.getLogger(__name__)
 _log.setLevel(logging.DEBUG)
 
-
+@user_not_banned
 @uses_pagination
 def user_home(request, page):
     """'Homepage' of a User()"""
@@ -80,7 +80,7 @@ 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):
@@ -114,7 +114,7 @@ def user_gallery(request, page, url_user=None):
 
 MEDIA_COMMENTS_PER_PAGE = 50
 
-
+@user_not_banned
 @get_user_media_entry
 @uses_pagination
 def media_home(request, media, page, **kwargs):
@@ -190,7 +190,7 @@ def media_post_comment(request, media):
 
     return redirect_obj(request, media)
 
-
+@user_not_banned
 @get_media_entry_by_id
 @require_active_login
 def media_collect(request, media):
@@ -269,6 +269,7 @@ def media_collect(request, media):
 
 
 #TODO: Why does @user_may_delete_media not implicate @require_active_login?
+@user_not_banned
 @get_media_entry_by_id
 @require_active_login
 @user_may_delete_media
@@ -305,7 +306,7 @@ 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):
@@ -335,7 +336,7 @@ 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"""
@@ -391,7 +392,7 @@ def collection_item_confirm_remove(request, collection_item):
         {'collection_item': collection_item,
          'form': form})
 
-
+@user_not_banned
 @get_user_collection
 @require_active_login
 @user_may_alter_collection
@@ -575,7 +576,7 @@ def collection_atom_feed(request):
 
     return feed.get_response()
 
-
+@user_not_banned
 @require_active_login
 def processing_panel(request):
     """
@@ -625,8 +626,8 @@ def processing_panel(request):
 @user_has_privilege(u'reporter')
 def file_a_report(request, media, comment=None):
     if request.method == "POST":
-        report_form = build_report_form(request.form)
-        report_form.save()
+        report_table = build_report_table(request.form)
+        report_table.save()
 
         return redirect(
             request,
index 6acd7e96f8aa8f56187a34eb7aa109f1a1d10c22..595f2725aaefaf8712346da69c789afe87e6f90c 100644 (file)
@@ -18,10 +18,10 @@ from mediagoblin import mg_globals
 from mediagoblin.db.models import MediaEntry
 from mediagoblin.tools.pagination import Pagination
 from mediagoblin.tools.response import render_to_response
-from mediagoblin.decorators import uses_pagination
-
+from mediagoblin.decorators import uses_pagination, user_not_banned
 
 
+@user_not_banned
 @uses_pagination
 def root_view(request, page):
     cursor = MediaEntry.query.filter_by(state=u'processed').\