Whew. This is a big update. I did some significant keeping work. I moved all of

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
This commit is contained in:
tilly-Q 2013-07-17 16:16:07 -04:00
parent 650a0aa90d
commit 6bba33d7e6
23 changed files with 509 additions and 189 deletions

View File

@ -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})

View File

@ -300,6 +300,7 @@ class ReportBase_v0(declarative_base()):
reported_user_id = Column(Integer, ForeignKey(User.id), nullable=False) reported_user_id = Column(Integer, ForeignKey(User.id), nullable=False)
created = Column(DateTime, nullable=False, default=datetime.datetime.now) created = Column(DateTime, nullable=False, default=datetime.datetime.now)
resolved = Column(DateTime) resolved = Column(DateTime)
result = Column(UnicodeText)
discriminator = Column('type', Unicode(50)) discriminator = Column('type', Unicode(50))
__mapper_args__ = {'polymorphic_on': discriminator} __mapper_args__ = {'polymorphic_on': discriminator}

View File

@ -239,8 +239,8 @@ class MediaEntry(Base, MediaEntryMixin):
This will *not* automatically delete unused collections, which This will *not* automatically delete unused collections, which
can remain empty... can remain empty...
:param del_orphan_tags: True/false if we delete unused Tags too :keyword del_orphan_tags: True/false if we delete unused Tags too
:param commit: True/False if this should end the db transaction""" :keyword commit: True/False if this should end the db transaction"""
# User's CollectionItems are automatically deleted via "cascade". # User's CollectionItems are automatically deleted via "cascade".
# Comments on this Media are deleted by cascade, hopefully. # Comments on this Media are deleted by cascade, hopefully.
@ -487,6 +487,14 @@ class ProcessingMetaData(Base):
class ReportBase(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' __tablename__ = 'core__reports'
@ -508,6 +516,7 @@ class ReportBase(Base):
primaryjoin="User.id==ReportBase.reported_user_id") primaryjoin="User.id==ReportBase.reported_user_id")
created = Column(DateTime, nullable=False, default=datetime.datetime.now()) created = Column(DateTime, nullable=False, default=datetime.datetime.now())
resolved = Column(DateTime) resolved = Column(DateTime)
result = Column(UnicodeText)
discriminator = Column('type', Unicode(50)) discriminator = Column('type', Unicode(50))
__mapper_args__ = {'polymorphic_on': discriminator} __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 the reason why they are banned and when (if ever) the ban will be
lifted 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 attached to. This is a one-to-one
relationship. 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 If this is null, the ban is permanent
unless a moderator manually lifts it. 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' __tablename__ = 'core__user_bans'
@ -568,6 +577,17 @@ class UserBan(Base):
class Privilege(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' __tablename__ = 'core__privileges'
id = Column(Integer, nullable=False, primary_key=True) id = Column(Integer, nullable=False, primary_key=True)
@ -578,12 +598,30 @@ class Privilege(Base):
secondary="core__privileges_users") secondary="core__privileges_users")
def __init__(self, privilege_name): 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 self.privilege_name = privilege_name
def __repr__(self): def __repr__(self):
return "<Privilege %s>" % (self.privilege_name) 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): class PrivilegeUserAssociation(Base):
'''
This table holds the many-to-many relationship between User and Privilege
'''
__tablename__ = 'core__privileges_users' __tablename__ = 'core__privileges_users'
privilege_id = Column( privilege_id = Column(

View File

@ -21,8 +21,9 @@ from werkzeug.exceptions import Forbidden, NotFound
from werkzeug.urls import url_quote from werkzeug.urls import url_quote
from mediagoblin import mg_globals as mgg from mediagoblin import mg_globals as mgg
from mediagoblin.db.models import MediaEntry, User, MediaComment, Privilege from mediagoblin.db.models import MediaEntry, User, MediaComment, Privilege, \
from mediagoblin.tools.response import redirect, render_404 UserBan
from mediagoblin.tools.response import redirect, render_404, render_user_banned
def require_active_login(controller): def require_active_login(controller):
@ -64,6 +65,7 @@ def active_user_from_url(controller):
return wrapper return wrapper
def user_has_privilege(privilege_name): def user_has_privilege(privilege_name):
def user_has_privilege_decorator(controller): def user_has_privilege_decorator(controller):
@wraps(controller) @wraps(controller)
def wrapper(request, *args, **kwargs): def wrapper(request, *args, **kwargs):
@ -71,7 +73,9 @@ def user_has_privilege(privilege_name):
privileges_of_user = Privilege.query.filter( privileges_of_user = Privilege.query.filter(
Privilege.all_users.any( Privilege.all_users.any(
User.id==user_id)) 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(): Privilege.privilege_name==privilege_name).count():
raise Forbidden() raise Forbidden()
@ -271,14 +275,18 @@ def get_workbench(func):
return new_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) @wraps(controller)
def new_controller_func(request, *args, **kwargs): 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 \ 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() raise Forbidden()
elif not request.user: elif not request.user:
next_url = urljoin( next_url = urljoin(
@ -293,3 +301,18 @@ def require_admin_login(controller):
return new_controller_func 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

View File

@ -55,6 +55,13 @@ def adduser(args):
entry.pw_hash = auth_lib.bcrypt_gen_password_hash(args.password) entry.pw_hash = auth_lib.bcrypt_gen_password_hash(args.password)
entry.status = u'active' entry.status = u'active'
entry.email_verified = True 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() entry.save()
print "User created (and email marked as verified)" print "User created (and email marked as verified)"

View File

@ -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()

View File

@ -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')]

View File

@ -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)

View File

@ -18,7 +18,7 @@ import logging
from mediagoblin.tools.routing import add_route, mount, url_map from mediagoblin.tools.routing import add_route, mount, url_map
from mediagoblin.tools.pluginapi import PluginManager 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 from mediagoblin.auth.routing import auth_routes
@ -28,7 +28,7 @@ _log = logging.getLogger(__name__)
def get_url_map(): def get_url_map():
add_route('index', '/', 'mediagoblin.views:root_view') add_route('index', '/', 'mediagoblin.views:root_view')
mount('/auth', auth_routes) mount('/auth', auth_routes)
mount('/a', admin_routes) mount('/mod', moderation_routes)
import mediagoblin.submit.routing import mediagoblin.submit.routing
import mediagoblin.user_pages.routing import mediagoblin.user_pages.routing

View File

@ -402,6 +402,8 @@ a.report_authorlink, a.report_whenlink {
color: #D486B1; color: #D486B1;
} }
ul#action_to_resolve {list-style:none; margin-left:10px;}
/* media galleries */ /* media galleries */
.media_thumbnail { .media_thumbnail {

View File

@ -1,3 +1,4 @@
{#
# GNU MediaGoblin -- federated, autonomous media hosting # GNU MediaGoblin -- federated, autonomous media hosting
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
# #
@ -13,20 +14,15 @@
# #
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
{% extends "mediagoblin/base.html" %}
admin_routes = [ {% block title %}You are Banned.{% endblock %}
('mediagoblin.admin.media_panel',
'/media', {% block mediagoblin_content %}
'mediagoblin.admin.views:admin_media_processing_panel'), <img class="right_align" src="{{ request.staticdirect('/images/404.png') }}"
('mediagoblin.admin.users', alt="{% trans %}Image of goblin stressing out{% endtrans %}" />
'/users', <h1>You have been banned until {{ expiration_date }}</h1>
'mediagoblin.admin.views:admin_users_panel'), <p>{{ reason|safe }}</p>
('mediagoblin.admin.reports', <div class="clear"></div>
'/reports', {% endblock %}
'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')]

View File

@ -104,9 +104,12 @@
{% if request.user.is_admin %} {% if request.user.is_admin %}
<p> <p>
<span class="dropdown_title">Admin powers:</span> <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 -%} {%- trans %}Media processing panel{% endtrans -%}
</a> </a>
<a href="{{ request.urlgen('mediagoblin.moderation.users') }}">
{%- trans %}User management panel{% endtrans -%}
</a>
</p> </p>
{% endif %} {% endif %}
</div> </div>

View File

@ -16,6 +16,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
#} #}
{%- extends "mediagoblin/base.html" %} {%- extends "mediagoblin/base.html" %}
{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
{%- block mediagoblin_content %} {%- block mediagoblin_content %}
{% if not report %} {% if not report %}
@ -29,7 +30,7 @@
class="comment_wrapper"> class="comment_wrapper">
<div class="comment_author"> <div class="comment_author">
<img src="{{ request.staticdirect('/images/icon_comment.png') }}" /> <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) }}" user=comment.get_author.username) }}"
class="comment_authorlink"> class="comment_authorlink">
{{- reported_user.username -}} {{- reported_user.username -}}
@ -68,12 +69,12 @@
<div class="report_author"> <div class="report_author">
<img src="{{ request.staticdirect('/images/icon_clipboard.png') }}" <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" /> 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) }}" user=report.reporter.username) }}"
class="report_authorlink"> class="report_authorlink">
{{- report.reporter.username -}} {{- report.reporter.username -}}
</a> </a>
<a href="{{ request.urlgen('mediagoblin.admin.reports_detail', <a href="{{ request.urlgen('mediagoblin.moderation.reports_detail',
report_id=report.id) }}" report_id=report.id) }}"
class="report_whenlink"> class="report_whenlink">
<span title='{{- report.created.strftime("%I:%M%p %Y-%m-%d") -}}'> <span title='{{- report.created.strftime("%I:%M%p %Y-%m-%d") -}}'>
@ -87,5 +88,50 @@
{{ report.report_content }} {{ report.report_content }}
</div> </div>
</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 %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -29,7 +29,7 @@
{% trans %}Here you can look up users in order to take punitive actions on them.{% endtrans %} {% trans %}Here you can look up users in order to take punitive actions on them.{% endtrans %}
</p> </p>
<h2>{% trans %}Reports Filed on Comments{% endtrans %}</h2> <h2>{% trans %}Reports Filed{% endtrans %}</h2>
{% if report_list.count() %} {% if report_list.count() %}
<table class="admin_panel processing"> <table class="admin_panel processing">
@ -44,8 +44,10 @@
</tr> </tr>
{% for report in report_list %} {% for report in report_list %}
<tr> <tr>
<td><a href="{{ request.urlgen('mediagoblin.moderation.reports_detail',
report_id=report.id) }}">{{ report.id }}</a></td>
{% if report.discriminator == "comment_report" %} {% if report.discriminator == "comment_report" %}
<td>{{ report.id }}</td>
<td>Comment Report</td> <td>Comment Report</td>
<td>{{ report.comment.get_author.username }}</td> <td>{{ report.comment.get_author.username }}</td>
<td>{{ report.created.strftime("%F %R") }}</td> <td>{{ report.created.strftime("%F %R") }}</td>
@ -53,7 +55,6 @@
<td>{{ report.report_content }}</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> <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" %} {% elif report.discriminator == "media_report" %}
<td>{{ report.id }}</td>
<td>Media Report</td> <td>Media Report</td>
<td>{{ report.media_entry.get_uploader.username }}</td> <td>{{ report.media_entry.get_uploader.username }}</td>
<td>{{ report.created.strftime("%F %R") }}</td> <td>{{ report.created.strftime("%F %R") }}</td>
@ -67,26 +68,36 @@
{% else %} {% else %}
<p><em>{% trans %}No open reports found.{% endtrans %}</em></p> <p><em>{% trans %}No open reports found.{% endtrans %}</em></p>
{% endif %} {% endif %}
<h2>{% trans %}Closed Reports on Comments{% endtrans %}</h2> <h2>{% trans %}Closed Reports{% endtrans %}</h2>
{% if closed_report_list.count() %} {% if closed_report_list.count() %}
<table class="media_panel processing"> <table class="media_panel processing">
<tr> <tr>
<th>ID</th> <th>ID</th>
<th>Resolved</th>
<th>Offender</th> <th>Offender</th>
<th>When Reported</th> <th>Action Taken</th>
<th>Reported By</th> <th>Reported By</th>
<th>Reason</th> <th>Reason</th>
<th>Comment Posted On</th> <th>Reported Comment or Media Entry</th>
</tr> </tr>
{% for report in closed_report_list %} {% for report in closed_report_list %}
<tr> <td><a href="{{ request.urlgen('mediagoblin.moderation.reports_detail',
<td>{{ report.id }}</td> report_id=report.id) }}">{{ report.id }}</a></td>
<td>{{ report.comment.get_author.username }}</td> {% if report.discriminator == "comment_report" %}
<td>{{ report.created.strftime("%F %R") }}</td> <td>{{ report.resolved.strftime("%F %R") }}</td>
<td>{{ report.reporter.username }}</td> <td>{{ report.comment.get_author.username }}</td>
<td>{{ report.report_content }}</td> <td>{{ report.created.strftime("%F %R") }}</td>
<td><a href="{{ report.comment.get_media_entry.url_for_self(request.urlgen) }}">{{ report.comment.get_media_entry.title }}</a></td> <td>{{ report.reporter.username }}</td>
</tr> <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 %} {% endfor %}
</table> </table>
{% else %} {% else %}

View File

@ -85,7 +85,7 @@
</div> </div>
{% endif %} {% endif %}
{% if user %} {% if user %}
<h2>{%- trans %}Active Reports on{% endtrans -%} {{ user.username }}</h2> <h2>{%- trans %}Active Reports on {% endtrans -%}{{ user.username }}</h2>
{% if reports.count() %} {% if reports.count() %}
<table class="admin_side_panel"> <table class="admin_side_panel">
<tr> <tr>
@ -97,7 +97,7 @@
<tr> <tr>
<td> <td>
<img src="{{ request.staticdirect('/images/icon_clipboard.png') }}" /> <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) }}"> report_id=report.id) }}">
{%- trans %}Report #{% endtrans -%}{{ report.id }} {%- trans %}Report #{% endtrans -%}{{ report.id }}
</a> </a>
@ -116,7 +116,7 @@
<tr><td></td><td></td> <tr><td></td><td></td>
</table> </table>
{% else %} {% else %}
{%- trans %}No active reports filed on{% endtrans -%} {{ user.username }} {%- trans %}No active reports filed on {% endtrans -%} {{ user.username }}
{% endif %} {% endif %}
<a class="right_align">{{ user.username }}'s report history</a> <a class="right_align">{{ user.username }}'s report history</a>
<span class=clear></span> <span class=clear></span>
@ -125,13 +125,31 @@
<tr> <tr>
<th>{% trans %}Privilege{% endtrans %}</th> <th>{% trans %}Privilege{% endtrans %}</th>
<th>{% trans %}User Has 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> </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> </table>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -42,7 +42,7 @@
{% for user in user_list %} {% for user in user_list %}
<tr> <tr>
<td>{{ user.id }}</td> <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> user= user.username) }}">{{ user.username }}</a></td>
<td>{{ user.created.strftime("%F %R") }}</td> <td>{{ user.created.strftime("%F %R") }}</td>
<td>{{ user.posted_comments.count() }}</td> <td>{{ user.posted_comments.count() }}</td>

View File

@ -19,6 +19,7 @@ from werkzeug.wrappers import Response as wz_Response
from mediagoblin.tools.template import render_template from mediagoblin.tools.template import render_template
from mediagoblin.tools.translate import (lazy_pass_to_ugettext as _, from mediagoblin.tools.translate import (lazy_pass_to_ugettext as _,
pass_to_ugettext) pass_to_ugettext)
from mediagoblin.db.models import UserBan
class Response(wz_Response): class Response(wz_Response):
"""Set default response mimetype to HTML, otherwise we get text/plain""" """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.") "you're looking for has been moved or deleted.")
return render_error(request, 404, err_msg=err_msg) 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): def render_http_exception(request, exc, description):
"""Return Response() given a werkzeug.HTTPException """Return Response() given a werkzeug.HTTPException

View File

@ -51,11 +51,15 @@ class MediaCollectForm(wtforms.Form):
Markdown</a> for formatting.""")) Markdown</a> for formatting."""))
class CommentReportForm(wtforms.Form): 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() comment_id = wtforms.IntegerField()
reporter_id = wtforms.IntegerField() reporter_id = wtforms.IntegerField()
class MediaReportForm(wtforms.Form): 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() media_entry_id = wtforms.IntegerField()
reporter_id = wtforms.IntegerField() reporter_id = wtforms.IntegerField()

View File

@ -78,7 +78,7 @@ def add_media_to_collection(collection, media, note=None, commit=True):
if commit: if commit:
Session.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 This function is used to convert a form dictionary (from a User filing a
report) into either a MediaReport or CommentReport object. report) into either a MediaReport or CommentReport object.

View 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.translate import pass_to_ugettext as _
from mediagoblin.tools.pagination import Pagination from mediagoblin.tools.pagination import Pagination
from mediagoblin.user_pages import forms as user_forms 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) add_media_to_collection)
from mediagoblin.decorators import (uses_pagination, get_user_media_entry, from mediagoblin.decorators import (uses_pagination, get_user_media_entry,
get_media_entry_by_id, user_has_privilege, get_media_entry_by_id, user_has_privilege,
require_active_login, user_may_delete_media, user_may_alter_collection, require_active_login, user_may_delete_media, user_may_alter_collection,
get_user_collection, get_user_collection_item, active_user_from_url, 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 from werkzeug.contrib.atom import AtomFeed
@ -41,7 +41,7 @@ from werkzeug.contrib.atom import AtomFeed
_log = logging.getLogger(__name__) _log = logging.getLogger(__name__)
_log.setLevel(logging.DEBUG) _log.setLevel(logging.DEBUG)
@user_not_banned
@uses_pagination @uses_pagination
def user_home(request, page): def user_home(request, page):
"""'Homepage' of a User()""" """'Homepage' of a User()"""
@ -80,7 +80,7 @@ def user_home(request, page):
'media_entries': media_entries, 'media_entries': media_entries,
'pagination': pagination}) 'pagination': pagination})
@user_not_banned
@active_user_from_url @active_user_from_url
@uses_pagination @uses_pagination
def user_gallery(request, page, url_user=None): 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 MEDIA_COMMENTS_PER_PAGE = 50
@user_not_banned
@get_user_media_entry @get_user_media_entry
@uses_pagination @uses_pagination
def media_home(request, media, page, **kwargs): def media_home(request, media, page, **kwargs):
@ -190,7 +190,7 @@ def media_post_comment(request, media):
return redirect_obj(request, media) return redirect_obj(request, media)
@user_not_banned
@get_media_entry_by_id @get_media_entry_by_id
@require_active_login @require_active_login
def media_collect(request, media): 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? #TODO: Why does @user_may_delete_media not implicate @require_active_login?
@user_not_banned
@get_media_entry_by_id @get_media_entry_by_id
@require_active_login @require_active_login
@user_may_delete_media @user_may_delete_media
@ -305,7 +306,7 @@ def media_confirm_delete(request, media):
{'media': media, {'media': media,
'form': form}) 'form': form})
@user_not_banned
@active_user_from_url @active_user_from_url
@uses_pagination @uses_pagination
def user_collection(request, page, url_user=None): def user_collection(request, page, url_user=None):
@ -335,7 +336,7 @@ def user_collection(request, page, url_user=None):
'collection_items': collection_items, 'collection_items': collection_items,
'pagination': pagination}) 'pagination': pagination})
@user_not_banned
@active_user_from_url @active_user_from_url
def collection_list(request, url_user=None): def collection_list(request, url_user=None):
"""A User-defined Collection""" """A User-defined Collection"""
@ -391,7 +392,7 @@ def collection_item_confirm_remove(request, collection_item):
{'collection_item': collection_item, {'collection_item': collection_item,
'form': form}) 'form': form})
@user_not_banned
@get_user_collection @get_user_collection
@require_active_login @require_active_login
@user_may_alter_collection @user_may_alter_collection
@ -575,7 +576,7 @@ def collection_atom_feed(request):
return feed.get_response() return feed.get_response()
@user_not_banned
@require_active_login @require_active_login
def processing_panel(request): def processing_panel(request):
""" """
@ -625,8 +626,8 @@ def processing_panel(request):
@user_has_privilege(u'reporter') @user_has_privilege(u'reporter')
def file_a_report(request, media, comment=None): def file_a_report(request, media, comment=None):
if request.method == "POST": if request.method == "POST":
report_form = build_report_form(request.form) report_table = build_report_table(request.form)
report_form.save() report_table.save()
return redirect( return redirect(
request, request,

View File

@ -18,10 +18,10 @@ from mediagoblin import mg_globals
from mediagoblin.db.models import MediaEntry from mediagoblin.db.models import MediaEntry
from mediagoblin.tools.pagination import Pagination from mediagoblin.tools.pagination import Pagination
from mediagoblin.tools.response import render_to_response 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 @uses_pagination
def root_view(request, page): def root_view(request, page):
cursor = MediaEntry.query.filter_by(state=u'processed').\ cursor = MediaEntry.query.filter_by(state=u'processed').\