diff options
Diffstat (limited to 'mediagoblin/moderation')
-rw-r--r-- | mediagoblin/moderation/__init__.py | 16 | ||||
-rw-r--r-- | mediagoblin/moderation/forms.py | 149 | ||||
-rw-r--r-- | mediagoblin/moderation/routing.py | 38 | ||||
-rw-r--r-- | mediagoblin/moderation/tools.py | 217 | ||||
-rw-r--r-- | mediagoblin/moderation/views.py | 219 |
5 files changed, 639 insertions, 0 deletions
diff --git a/mediagoblin/moderation/__init__.py b/mediagoblin/moderation/__init__.py new file mode 100644 index 00000000..719b56e7 --- /dev/null +++ b/mediagoblin/moderation/__init__.py @@ -0,0 +1,16 @@ +# 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/>. + diff --git a/mediagoblin/moderation/forms.py b/mediagoblin/moderation/forms.py new file mode 100644 index 00000000..72305b29 --- /dev/null +++ b/mediagoblin/moderation/forms.py @@ -0,0 +1,149 @@ +# 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', _(u'Take away privilege')), + (u'userban', _(u'Ban the user')), + (u'sendmessage', _(u'Send the user a message')), + (u'delete', _(u'Delete the content'))] + +class MultiCheckboxField(wtforms.SelectMultipleField): + """ + A multiple-select, except displays a list of checkboxes. + + Iterating the field will produce subfields, allowing custom rendering of + the enclosed checkbox fields. + + code from http://wtforms.simplecodes.com/docs/1.0.4/specific_problems.html + """ + widget = wtforms.widgets.ListWidget(prefix_label=False) + option_widget = wtforms.widgets.CheckboxInput() + + +# ============ Forms for mediagoblin.moderation.user page ================== # + +class PrivilegeAddRemoveForm(wtforms.Form): + """ + This form is used by an admin to give/take away a privilege directly from + their user page. + """ + privilege_name = wtforms.HiddenField('',[wtforms.validators.required()]) + +class BanForm(wtforms.Form): + """ + This form is used by an admin to ban a user directly from their user page. + """ + user_banned_until = wtforms.DateField( + _(u'User will be banned until:'), + format='%Y-%m-%d', + validators=[wtforms.validators.optional()]) + why_user_was_banned = wtforms.TextAreaField( + _(u'Why are you banning this User?'), + validators=[wtforms.validators.optional()]) + +# =========== Forms for mediagoblin.moderation.report page ================= # + +class ReportResolutionForm(wtforms.Form): + """ + This form carries all the information necessary to take punitive actions + against a user who created content that has been reported. + + :param action_to_resolve A list of Unicode objects representing + a choice from the ACTION_CHOICES const- + -ant. Every choice passed affects what + punitive actions will be taken against + the user. + + :param targeted_user A HiddenField object that holds the id + of the user that was reported. + + :param take_away_privileges A list of Unicode objects which repres- + -ent the privileges that are being tak- + -en away. This field is optional and + only relevant if u'takeaway' is in the + `action_to_resolve` list. + + :param user_banned_until A DateField object that holds the date + that the user will be unbanned. This + field is optional and only relevant if + u'userban' is in the action_to_resolve + list. If the user is being banned and + this field is blank, the user is banned + indefinitely. + + :param why_user_was_banned A TextArea object that holds the + reason that a user was banned, to disp- + -lay to them when they try to log in. + This field is optional and only relevant + if u'userban' is in the + `action_to_resolve` list. + + :param message_to_user A TextArea object that holds a message + which will be emailed to the user. This + is only relevant if the u'sendmessage' + option is in the `action_to_resolve` + list. + + :param resolution_content A TextArea object that is required for + every report filed. It represents the + reasons that the moderator/admin resol- + -ved the report in such a way. + """ + action_to_resolve = MultiCheckboxField( + _(u'What action will you take to resolve the report?'), + validators=[wtforms.validators.optional()], + choices=ACTION_CHOICES) + targeted_user = wtforms.HiddenField('', + validators=[wtforms.validators.required()]) + take_away_privileges = wtforms.SelectMultipleField( + _(u'What privileges will you take away?'), + validators=[wtforms.validators.optional()]) + user_banned_until = wtforms.DateField( + _(u'User will be banned until:'), + format='%Y-%m-%d', + validators=[wtforms.validators.optional()]) + why_user_was_banned = wtforms.TextAreaField( + validators=[wtforms.validators.optional()]) + message_to_user = wtforms.TextAreaField( + validators=[wtforms.validators.optional()]) + resolution_content = wtforms.TextAreaField() + +# ======== Forms for mediagoblin.moderation.report_panel page ============== # + +class ReportPanelSortingForm(wtforms.Form): + """ + This form is used for sorting and filtering through different reports in + the mediagoblin.moderation.reports_panel view. + + """ + active_p = wtforms.IntegerField( + validators=[wtforms.validators.optional()]) + closed_p = wtforms.IntegerField( + validators=[wtforms.validators.optional()]) + reported_user = wtforms.IntegerField( + validators=[wtforms.validators.optional()]) + reporter = wtforms.IntegerField( + validators=[wtforms.validators.optional()]) + +class UserPanelSortingForm(wtforms.Form): + """ + This form is used for sorting different reports. + """ + p = wtforms.IntegerField( + validators=[wtforms.validators.optional()]) diff --git a/mediagoblin/moderation/routing.py b/mediagoblin/moderation/routing.py new file mode 100644 index 00000000..ba10bc6d --- /dev/null +++ b/mediagoblin/moderation/routing.py @@ -0,0 +1,38 @@ +# 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.ban_or_unban', + '/users/<string:user>/ban/', + 'mediagoblin.moderation.views:ban_or_unban'), + ('mediagoblin.moderation.reports_detail', + '/reports/<int:report_id>/', + 'mediagoblin.moderation.views:moderation_reports_detail')] diff --git a/mediagoblin/moderation/tools.py b/mediagoblin/moderation/tools.py new file mode 100644 index 00000000..e0337536 --- /dev/null +++ b/mediagoblin/moderation/tools.py @@ -0,0 +1,217 @@ +# 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 mediagoblin import mg_globals +from mediagoblin.db.models import User, Privilege, UserBan +from mediagoblin.db.base import Session +from mediagoblin.tools.mail import send_email +from mediagoblin.tools.response import redirect +from datetime import datetime +from mediagoblin.tools.translate import lazy_pass_to_ugettext as _ + +def take_punitive_actions(request, form, report, user): + message_body ='' + + # The bulk of this action is running through all of the different + # punitive actions that a moderator could take. + if u'takeaway' in form.action_to_resolve.data: + for privilege_name in form.take_away_privileges.data: + take_away_privileges(user.username, privilege_name) + form.resolution_content.data += \ + u"\n{mod} took away {user}\'s {privilege} privileges.".format( + mod=request.user.username, + user=user.username, + privilege=privilege_name) + + # If the moderator elects to ban the user, a new instance of user_ban + # will be created. + if u'userban' in form.action_to_resolve.data: + user_ban = ban_user(form.targeted_user.data, + expiration_date=form.user_banned_until.data, + reason=form.why_user_was_banned.data) + Session.add(user_ban) + form.resolution_content.data += \ + u"\n{mod} banned user {user} {expiration_date}.".format( + mod=request.user.username, + user=user.username, + expiration_date = ( + "until {date}".format(date=form.user_banned_until.data) + if form.user_banned_until.data + else "indefinitely" + ) + ) + + # If the moderator elects to send a warning message. An email will be + # sent to the email address given at sign up + if u'sendmessage' in form.action_to_resolve.data: + message_body = form.message_to_user.data + form.resolution_content.data += \ + u"\n{mod} sent a warning email to the {user}.".format( + mod=request.user.username, + user=user.username) + + if u'delete' in form.action_to_resolve.data and \ + report.is_comment_report(): + deleted_comment = report.comment + Session.delete(deleted_comment) + form.resolution_content.data += \ + u"\n{mod} deleted the comment.".format( + mod=request.user.username) + elif u'delete' in form.action_to_resolve.data and \ + report.is_media_entry_report(): + deleted_media = report.media_entry + deleted_media.delete() + form.resolution_content.data += \ + u"\n{mod} deleted the media entry.".format( + mod=request.user.username) + report.archive( + resolver_id=request.user.id, + resolved=datetime.now(), + result=form.resolution_content.data) + + Session.add(report) + Session.commit() + if message_body: + send_email( + mg_globals.app_config['email_sender_address'], + [user.email], + _('Warning from')+ '- {moderator} '.format( + moderator=request.user.username), + message_body) + + return redirect( + request, + 'mediagoblin.moderation.users_detail', + user=user.username) + + +def take_away_privileges(user,*privileges): + """ + Take away all of the privileges passed as arguments. + + :param user A Unicode object representing the target user's + User.username value. + + :param privileges A variable number of Unicode objects describing + the privileges being taken away. + + + :returns True If ALL of the privileges were taken away + successfully. + + :returns False If ANY of the privileges were not taken away + successfully. This means the user did not have + (one of) the privilege(s) to begin with. + """ + if len(privileges) == 1: + privilege = Privilege.query.filter( + Privilege.privilege_name==privileges[0]).first() + user = User.query.filter( + User.username==user).first() + if privilege in user.all_privileges: + user.all_privileges.remove(privilege) + return True + return False + + elif len(privileges) > 1: + return (take_away_privileges(user, privileges[0]) and \ + take_away_privileges(user, *privileges[1:])) + +def give_privileges(user,*privileges): + """ + Take away all of the privileges passed as arguments. + + :param user A Unicode object representing the target user's + User.username value. + + :param privileges A variable number of Unicode objects describing + the privileges being granted. + + + :returns True If ALL of the privileges were granted successf- + -ully. + + :returns False If ANY of the privileges were not granted succ- + essfully. This means the user already had (one + of) the privilege(s) to begin with. + """ + if len(privileges) == 1: + privilege = Privilege.query.filter( + Privilege.privilege_name==privileges[0]).first() + user = User.query.filter( + User.username==user).first() + if privilege not in user.all_privileges: + user.all_privileges.append(privilege) + return True + return False + + elif len(privileges) > 1: + return (give_privileges(user, privileges[0]) and \ + give_privileges(user, *privileges[1:])) + +def ban_user(user_id, expiration_date=None, reason=None): + """ + This function is used to ban a user. If the user is already banned, the + function returns False. If the user is not already banned, this function + bans the user using the arguments to build a new UserBan object. + + :returns False if the user is already banned and the ban is not updated + :returns UserBan object if there is a new ban that was created. + """ + user_ban =UserBan.query.filter( + UserBan.user_id==user_id) + if user_ban.count(): + return False + new_user_ban = UserBan( + user_id=user_id, + expiration_date=expiration_date, + reason=reason) + return new_user_ban + +def unban_user(user_id): + """ + This function is used to unban a user. If the user is not currently banned, + nothing happens. + + :returns True if the operation was completed successfully and the user + has been unbanned + :returns False if the user was never banned. + """ + user_ban = UserBan.query.filter( + UserBan.user_id==user_id) + if user_ban.count() == 0: + return False + user_ban.first().delete() + return True + +def parse_report_panel_settings(form): + """ + This function parses the url arguments to which are used to filter reports + in the reports panel view. More filters can be added to make a usuable + search function. + + :returns A dictionary of sqlalchemy-usable filters. + """ + filters = {} + + if form.validate(): + filters['reported_user_id'] = form.reported_user.data + filters['reporter_id'] = form.reporter.data + + filters = dict((k, v) + for k, v in filters.iteritems() if v) + + return filters diff --git a/mediagoblin/moderation/views.py b/mediagoblin/moderation/views.py new file mode 100644 index 00000000..f4de11ad --- /dev/null +++ b/mediagoblin/moderation/views.py @@ -0,0 +1,219 @@ +# 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 mediagoblin.db.models import (MediaEntry, User,ReportBase, Privilege, + UserBan) +from mediagoblin.decorators import (require_admin_or_moderator_login, + active_user_from_url, user_has_privilege, + allow_reporting) +from mediagoblin.tools.response import render_to_response, redirect +from mediagoblin.moderation import forms as moderation_forms +from mediagoblin.moderation.tools import (take_punitive_actions, \ + take_away_privileges, give_privileges, ban_user, unban_user, \ + parse_report_panel_settings) +from math import ceil + +@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 + ''' + current_page = 1 + if len(request.args) > 0: + form = moderation_forms.UserPanelSortingForm(request.args) + if form.validate(): + current_page = form.p.data or 1 + + all_user_list = User.query + user_list = all_user_list.order_by( + User.created.desc()).offset( + (current_page-1)*10).limit(10) + last_page = int(ceil(all_user_list.count()/10.)) + + return render_to_response( + request, + 'mediagoblin/moderation/user_panel.html', + {'user_list': user_list, + 'current_page':current_page, + 'last_page':last_page}) + +@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 + user_banned = UserBan.query.get(user.id) + ban_form = moderation_forms.BanForm() + + return render_to_response( + request, + 'mediagoblin/moderation/user.html', + {'user':user, + 'privileges': privileges, + 'reports':active_reports, + 'user_banned':user_banned, + 'ban_form':ban_form}) + +@require_admin_or_moderator_login +@allow_reporting +def moderation_reports_panel(request): + ''' + Show the global panel for monitoring reports filed against comments or + media entries for this instance. + ''' + filters = [] + active_settings, closed_settings = {'current_page':1}, {'current_page':1} + + if len(request.args) > 0: + form = moderation_forms.ReportPanelSortingForm(request.args) + if form.validate(): + filters = parse_report_panel_settings(form) + active_settings['current_page'] = form.active_p.data or 1 + closed_settings['current_page'] = form.closed_p.data or 1 + filters = [ + getattr(ReportBase,key)==val + for key,val in filters.viewitems()] + + all_active = ReportBase.query.filter( + ReportBase.resolved==None).filter( + *filters) + all_closed = ReportBase.query.filter( + ReportBase.resolved!=None).filter( + *filters) + + # report_list and closed_report_list are the two lists of up to 10 + # items which are actually passed to the user in this request + report_list = all_active.order_by( + ReportBase.created.desc()).offset( + (active_settings['current_page']-1)*10).limit(10) + closed_report_list = all_closed.order_by( + ReportBase.created.desc()).offset( + (closed_settings['current_page']-1)*10).limit(10) + + active_settings['last_page'] = int(ceil(all_active.count()/10.)) + closed_settings['last_page'] = int(ceil(all_closed.count()/10.)) + # Render to response + return render_to_response( + request, + 'mediagoblin/moderation/report_panel.html', + {'report_list':report_list, + 'closed_report_list':closed_report_list, + 'active_settings':active_settings, + 'closed_settings':closed_settings}) + +@require_admin_or_moderator_login +@allow_reporting +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']) + + form.take_away_privileges.choices = [ + (s.privilege_name,s.privilege_name.title()) \ + for s in report.reported_user.all_privileges + ] + + if request.method == "POST" and form.validate() and not ( + not request.user.has_privilege(u'admin') and + report.reported_user.has_privilege(u'admin')): + + user = User.query.get(form.targeted_user.data) + return take_punitive_actions(request, form, report, user) + + + form.targeted_user.data = report.reported_user_id + + return render_to_response( + request, + 'mediagoblin/moderation/report.html', + {'report':report, + 'form':form}) + +@user_has_privilege(u'admin') +@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. + Can only be used by an admin. + ''' + form = moderation_forms.PrivilegeAddRemoveForm(request.form) + if request.method == "POST" and form.validate(): + privilege = Privilege.query.filter( + Privilege.privilege_name==form.privilege_name.data).one() + if not take_away_privileges( + url_user.username, form.privilege_name.data): + + give_privileges(url_user.username, form.privilege_name.data) + url_user.save() + + return redirect( + request, + 'mediagoblin.moderation.users_detail', + user=url_user.username) + +@user_has_privilege(u'admin') +@active_user_from_url +def ban_or_unban(request, url_user): + """ + A page to ban or unban a user. Only can be used by an admin. + """ + form = moderation_forms.BanForm(request.form) + if request.method == "POST" and form.validate(): + already_banned = unban_user(url_user.id) + same_as_requesting_user = (request.user.id == url_user.id) + if not already_banned and not same_as_requesting_user: + user_ban = ban_user(url_user.id, + expiration_date = form.user_banned_until.data, + reason = form.why_user_was_banned.data) + user_ban.save() + return redirect( + request, + 'mediagoblin.moderation.users_detail', + user=url_user.username) |