diff options
59 files changed, 3409 insertions, 206 deletions
diff --git a/mediagoblin.ini b/mediagoblin.ini index 30dacadf..874ebcbd 100644 --- a/mediagoblin.ini +++ b/mediagoblin.ini @@ -20,6 +20,9 @@ email_debug_mode = true # Set to false to disable registrations allow_registration = true +# Set to false to disable the ability for users to report offensive content +allow_reporting = true + ## Uncomment this to put some user-overriding templates here # local_templates = %(here)s/user_dev/templates/ diff --git a/mediagoblin/admin/views.py b/mediagoblin/admin/views.py deleted file mode 100644 index 22ca74a3..00000000 --- a/mediagoblin/admin/views.py +++ /dev/null @@ -1,48 +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 -from mediagoblin.decorators import require_active_login -from mediagoblin.tools.response import render_to_response - -@require_active_login -def admin_processing_panel(request): - ''' - Show the global processing panel for this instance - ''' - # TODO: Why not a "require_admin_login" decorator throwing a 403 exception? - if not request.user.is_admin: - raise Forbidden() - - 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/panel.html', - {'processing_entries': processing_entries, - 'failed_entries': failed_entries, - 'processed_entries': processed_entries}) diff --git a/mediagoblin/auth/tools.py b/mediagoblin/auth/tools.py index 20c1f5c2..88716e1c 100644 --- a/mediagoblin/auth/tools.py +++ b/mediagoblin/auth/tools.py @@ -14,12 +14,14 @@ # 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 logging import wtforms +from sqlalchemy import or_ from mediagoblin import mg_globals from mediagoblin.tools.crypto import get_timed_signer_url -from mediagoblin.db.models import User +from mediagoblin.db.models import User, Privilege from mediagoblin.tools.mail import (normalize_email, send_email, email_debug_message) from mediagoblin.tools.template import render_template @@ -129,6 +131,14 @@ def register_user(request, register_form): # Create the user user = auth.create_user(register_form) + # give the user the default privileges + default_privileges = [ + Privilege.query.filter(Privilege.privilege_name==u'commenter').first(), + Privilege.query.filter(Privilege.privilege_name==u'uploader').first(), + Privilege.query.filter(Privilege.privilege_name==u'reporter').first()] + user.all_privileges += default_privileges + user.save() + # log the user in request.session['user_id'] = unicode(user.id) request.session.save() diff --git a/mediagoblin/auth/views.py b/mediagoblin/auth/views.py index 8563195f..dc03515b 100644 --- a/mediagoblin/auth/views.py +++ b/mediagoblin/auth/views.py @@ -17,7 +17,7 @@ from itsdangerous import BadSignature from mediagoblin import messages, mg_globals -from mediagoblin.db.models import User +from mediagoblin.db.models import User, Privilege from mediagoblin.tools.crypto import get_timed_signer_url from mediagoblin.decorators import auth_enabled, allow_registration from mediagoblin.tools.response import render_to_response, redirect, render_404 @@ -147,9 +147,12 @@ def verify_email(request): user = User.query.filter_by(id=int(token)).first() - if user and user.email_verified is False: - user.status = u'active' - user.email_verified = True + if user and user.has_privilege(u'active') is False: + user.verification_key = None + user.all_privileges.append( + Privilege.query.filter( + Privilege.privilege_name==u'active').first()) + user.save() messages.add_message( @@ -183,7 +186,7 @@ def resend_activation(request): return redirect(request, 'mediagoblin.auth.login') - if request.user.email_verified: + if request.user.has_privilege(u'active'): messages.add_message( request, messages.ERROR, @@ -248,7 +251,7 @@ def forgot_password(request): success_message=_("An email has been sent with instructions " "on how to change your password.") - if user and not(user.email_verified and user.status == 'active'): + if user and not(user.has_privilege(u'active')): # Don't send reminder because user is inactive or has no verified email messages.add_message(request, messages.WARNING, @@ -304,8 +307,8 @@ def verify_forgot_password(request): return redirect( request, 'index') - # check if user active and has email verified - if user.email_verified and user.status == 'active': + # check if user active + if user.has_privilege(u'active'): cp_form = auth_forms.ChangePassForm(formdata_vars) @@ -325,13 +328,13 @@ def verify_forgot_password(request): 'mediagoblin/auth/change_fp.html', {'cp_form': cp_form,}) - if not user.email_verified: + if not user.has_privilege(u'active'): messages.add_message( request, messages.ERROR, _('You need to verify your email before you can reset your' ' password.')) - if not user.status == 'active': + if not user.has_privilege(u'active'): messages.add_message( request, messages.ERROR, _('You are no longer an active user. Please contact the system' diff --git a/mediagoblin/config_spec.ini b/mediagoblin/config_spec.ini index 6f318d64..d738074d 100644 --- a/mediagoblin/config_spec.ini +++ b/mediagoblin/config_spec.ini @@ -42,6 +42,9 @@ allow_comments = boolean(default=True) # Whether comments are ascending or descending comments_ascending = boolean(default=True) +# Enable/disable reporting +allow_reporting = boolean(default=True) + # By default not set, but you might want something like: # "%(here)s/user_dev/templates/" local_templates = string() diff --git a/mediagoblin/db/migrations.py b/mediagoblin/db/migrations.py index 423508f6..5c2a23aa 100644 --- a/mediagoblin/db/migrations.py +++ b/mediagoblin/db/migrations.py @@ -19,7 +19,7 @@ import uuid from sqlalchemy import (MetaData, Table, Column, Boolean, SmallInteger, Integer, Unicode, UnicodeText, DateTime, - ForeignKey) + ForeignKey, Date) from sqlalchemy.exc import ProgrammingError from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.sql import and_ @@ -28,7 +28,8 @@ from migrate.changeset.constraint import UniqueConstraint from mediagoblin.db.extratypes import JSONEncoded, MutationDict from mediagoblin.db.migration_tools import RegisterMigration, inspect_table -from mediagoblin.db.models import MediaEntry, Collection, User, MediaComment +from mediagoblin.db.models import (MediaEntry, Collection, MediaComment, User, + Privilege) MIGRATIONS = {} @@ -469,9 +470,204 @@ def wants_notifications(db): """Add a wants_notifications field to User model""" metadata = MetaData(bind=db.bind) user_table = inspect_table(metadata, "core__users") - col = Column('wants_notifications', Boolean, default=True) col.create(user_table) + db.commit() + +class ReportBase_v0(declarative_base()): + __tablename__ = 'core__reports' + id = Column(Integer, primary_key=True) + reporter_id = Column(Integer, ForeignKey(User.id), nullable=False) + report_content = Column(UnicodeText) + reported_user_id = Column(Integer, ForeignKey(User.id), nullable=False) + created = Column(DateTime, nullable=False, default=datetime.datetime.now) + discriminator = Column('type', Unicode(50)) + resolver_id = Column(Integer, ForeignKey(User.id)) + resolved = Column(DateTime) + result = Column(UnicodeText) + __mapper_args__ = {'polymorphic_on': discriminator} + +class CommentReport_v0(ReportBase_v0): + __tablename__ = 'core__reports_on_comments' + __mapper_args__ = {'polymorphic_identity': 'comment_report'} + + id = Column('id',Integer, ForeignKey('core__reports.id'), + primary_key=True) + comment_id = Column(Integer, ForeignKey(MediaComment.id), nullable=True) + + + +class MediaReport_v0(ReportBase_v0): + __tablename__ = 'core__reports_on_media' + __mapper_args__ = {'polymorphic_identity': 'media_report'} + + id = Column('id',Integer, ForeignKey('core__reports.id'), primary_key=True) + media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=True) + +class UserBan_v0(declarative_base()): + __tablename__ = 'core__user_bans' + user_id = Column(Integer, ForeignKey(User.id), nullable=False, + primary_key=True) + expiration_date = Column(Date) + reason = Column(UnicodeText, nullable=False) + +class Privilege_v0(declarative_base()): + __tablename__ = 'core__privileges' + id = Column(Integer, nullable=False, primary_key=True, unique=True) + privilege_name = Column(Unicode, nullable=False, unique=True) + +class PrivilegeUserAssociation_v0(declarative_base()): + __tablename__ = 'core__privileges_users' + privilege_id = Column( + 'core__privilege_id', + Integer, + ForeignKey(User.id), + primary_key=True) + user_id = Column( + 'core__user_id', + Integer, + ForeignKey(Privilege.id), + primary_key=True) + +PRIVILEGE_FOUNDATIONS_v0 = [{'privilege_name':u'admin'}, + {'privilege_name':u'moderator'}, + {'privilege_name':u'uploader'}, + {'privilege_name':u'reporter'}, + {'privilege_name':u'commenter'}, + {'privilege_name':u'active'}] + + +class User_vR1(declarative_base()): + __tablename__ = 'rename__users' + id = Column(Integer, primary_key=True) + username = Column(Unicode, nullable=False, unique=True) + email = Column(Unicode, nullable=False) + pw_hash = Column(Unicode) + created = Column(DateTime, nullable=False, default=datetime.datetime.now) + wants_comment_notification = Column(Boolean, default=True) + wants_notifications = Column(Boolean, default=True) + license_preference = Column(Unicode) + url = Column(Unicode) + bio = Column(UnicodeText) # ?? + +@RegisterMigration(18, MIGRATIONS) +def create_moderation_tables(db): + + # First, we will create the new tables in the database. + #-------------------------------------------------------------------------- + ReportBase_v0.__table__.create(db.bind) + CommentReport_v0.__table__.create(db.bind) + MediaReport_v0.__table__.create(db.bind) + UserBan_v0.__table__.create(db.bind) + Privilege_v0.__table__.create(db.bind) + PrivilegeUserAssociation_v0.__table__.create(db.bind) + + db.commit() + + # Then initialize the tables that we will later use + #-------------------------------------------------------------------------- + metadata = MetaData(bind=db.bind) + privileges_table= inspect_table(metadata, "core__privileges") + user_table = inspect_table(metadata, 'core__users') + user_privilege_assoc = inspect_table( + metadata, 'core__privileges_users') + + # This section initializes the default Privilege foundations, that + # would be created through the FOUNDATIONS system in a new instance + #-------------------------------------------------------------------------- + for parameters in PRIVILEGE_FOUNDATIONS_v0: + db.execute(privileges_table.insert().values(**parameters)) + + db.commit() + + # This next section takes the information from the old is_admin and status + # columns and converts those to the new privilege system + #-------------------------------------------------------------------------- + admin_users_ids, active_users_ids, inactive_users_ids = ( + db.execute( + user_table.select().where( + user_table.c.is_admin==1)).fetchall(), + db.execute( + user_table.select().where( + user_table.c.is_admin==0).where( + user_table.c.status==u"active")).fetchall(), + db.execute( + user_table.select().where( + user_table.c.is_admin==0).where( + user_table.c.status!=u"active")).fetchall()) + + # Get the ids for each of the privileges so we can reference them ~~~~~~~~~ + (admin_privilege_id, uploader_privilege_id, + reporter_privilege_id, commenter_privilege_id, + active_privilege_id) = [ + db.execute(privileges_table.select().where( + privileges_table.c.privilege_name==privilege_name)).first()['id'] + for privilege_name in + [u"admin",u"uploader",u"reporter",u"commenter",u"active"] + ] + + # Give each user the appopriate privileges depending whether they are an + # admin, an active user or an inactivated user ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + for admin_user in admin_users_ids: + admin_user_id = admin_user['id'] + for privilege_id in [admin_privilege_id, uploader_privilege_id, reporter_privilege_id, commenter_privilege_id, active_privilege_id]: + db.execute(user_privilege_assoc.insert().values( + core__privilege_id=admin_user_id, + core__user_id=privilege_id)) + + for active_user in active_users_ids: + active_user_id = active_user['id'] + for privilege_id in [uploader_privilege_id, reporter_privilege_id, commenter_privilege_id, active_privilege_id]: + db.execute(user_privilege_assoc.insert().values( + core__privilege_id=active_user_id, + core__user_id=privilege_id)) + + for inactive_user in inactive_users_ids: + inactive_user_id = inactive_user['id'] + for privilege_id in [uploader_privilege_id, reporter_privilege_id, commenter_privilege_id]: + db.execute(user_privilege_assoc.insert().values( + core__privilege_id=inactive_user_id, + core__user_id=privilege_id)) + + db.commit() + + # And then, once the information is taken from the is_admin & status columns + # we drop all of the vestigial columns from the User table. + #-------------------------------------------------------------------------- + if db.bind.url.drivername == 'sqlite': + # SQLite has some issues that make it *impossible* to drop boolean + # columns. So, the following code is a very hacky workaround which + # makes it possible. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + User_vR1.__table__.create(db.bind) + db.commit() + new_user_table = inspect_table(metadata, 'rename__users') + for row in db.execute(user_table.select()): + db.execute(new_user_table.insert().values( + username=row.username, + email=row.email, + pw_hash=row.pw_hash, + created=row.created, + wants_comment_notification=row.wants_comment_notification, + wants_notifications=row.wants_notifications, + license_preference=row.license_preference, + url=row.url, + bio=row.bio)) + + db.commit() + user_table.drop() + + db.commit() + new_user_table.rename("core__users") + else: + # If the db is not SQLite, this process is much simpler ~~~~~~~~~~~~~~~ + + status = user_table.columns['status'] + email_verified = user_table.columns['email_verified'] + is_admin = user_table.columns['is_admin'] + status.drop() + email_verified.drop() + is_admin.drop() db.commit() diff --git a/mediagoblin/db/mixin.py b/mediagoblin/db/mixin.py index 57b27d83..25ce6642 100644 --- a/mediagoblin/db/mixin.py +++ b/mediagoblin/db/mixin.py @@ -46,7 +46,6 @@ class UserMixin(object): def bio_html(self): return cleaned_markdown_conversion(self.bio) - class GenerateSlugMixin(object): """ Mixin to add a generate_slug method to objects. diff --git a/mediagoblin/db/models.py b/mediagoblin/db/models.py index 68a2faa0..1514a3aa 100644 --- a/mediagoblin/db/models.py +++ b/mediagoblin/db/models.py @@ -23,7 +23,7 @@ import datetime from sqlalchemy import Column, Integer, Unicode, UnicodeText, DateTime, \ Boolean, ForeignKey, UniqueConstraint, PrimaryKeyConstraint, \ - SmallInteger + SmallInteger, Date from sqlalchemy.orm import relationship, backref, with_polymorphic from sqlalchemy.orm.collections import attribute_mapped_collection from sqlalchemy.sql.expression import desc @@ -64,15 +64,12 @@ class User(Base, UserMixin): # point. email = Column(Unicode, nullable=False) pw_hash = Column(Unicode) - email_verified = Column(Boolean, default=False) created = Column(DateTime, nullable=False, default=datetime.datetime.now) - status = Column(Unicode, default=u"needs_email_verification", nullable=False) # Intented to be nullable=False, but migrations would not work for it # set to nullable=True implicitly. wants_comment_notification = Column(Boolean, default=True) wants_notifications = Column(Boolean, default=True) license_preference = Column(Unicode) - is_admin = Column(Boolean, default=False, nullable=False) url = Column(Unicode) bio = Column(UnicodeText) # ?? uploaded = Column(Integer, default=0) @@ -85,8 +82,8 @@ class User(Base, UserMixin): return '<{0} #{1} {2} {3} "{4}">'.format( self.__class__.__name__, self.id, - 'verified' if self.email_verified else 'non-verified', - 'admin' if self.is_admin else 'user', + 'verified' if self.has_privilege(u'active') else 'non-verified', + 'admin' if self.has_privilege(u'admin') else 'user', self.username) def delete(self, **kwargs): @@ -108,6 +105,36 @@ class User(Base, UserMixin): super(User, self).delete(**kwargs) _log.info('Deleted user "{0}" account'.format(self.username)) + def has_privilege(self,*priv_names): + """ + This method checks to make sure a user has all the correct privileges + to access a piece of content. + + :param priv_names A variable number of unicode objects which rep- + -resent the different privileges which may give + the user access to this content. If you pass + multiple arguments, the user will be granted + access if they have ANY of the privileges + passed. + """ + if len(priv_names) == 1: + priv = Privilege.query.filter( + Privilege.privilege_name==priv_names[0]).one() + return (priv in self.all_privileges) + elif len(priv_names) > 1: + return self.has_privilege(priv_names[0]) or \ + self.has_privilege(*priv_names[1:]) + return False + + def is_banned(self): + """ + Checks if this user is banned. + + :returns True if self is banned + :returns False if self is not + """ + return UserBan.query.get(self.id) is not None + class Client(Base): """ @@ -675,16 +702,198 @@ class ProcessingNotification(Notification): 'polymorphic_identity': 'processing_notification' } - with_polymorphic( Notification, [ProcessingNotification, CommentNotification]) +class ReportBase(Base): + """ + This is the basic report object which the other reports are based off of. + + :keyword reporter_id Holds the id of the user who created + the report, as an Integer column. + :keyword report_content Hold the explanation left by the repor- + -ter to indicate why they filed the + report in the first place, as a + Unicode column. + :keyword reported_user_id Holds the id of the user who created + the content which was reported, as + an Integer column. + :keyword created Holds a datetime column of when the re- + -port was filed. + :keyword discriminator This column distinguishes between the + different types of reports. + :keyword resolver_id Holds the id of the moderator/admin who + resolved the report. + :keyword resolved Holds the DateTime object which descri- + -bes when this report was resolved + :keyword result Holds the UnicodeText column of the + resolver's reasons for resolving + the report this way. Some of this + is auto-generated + """ + __tablename__ = 'core__reports' + id = Column(Integer, primary_key=True) + reporter_id = Column(Integer, ForeignKey(User.id), nullable=False) + reporter = relationship( + User, + backref=backref("reports_filed_by", + lazy="dynamic", + cascade="all, delete-orphan"), + primaryjoin="User.id==ReportBase.reporter_id") + report_content = Column(UnicodeText) + reported_user_id = Column(Integer, ForeignKey(User.id), nullable=False) + reported_user = relationship( + User, + backref=backref("reports_filed_on", + lazy="dynamic", + cascade="all, delete-orphan"), + primaryjoin="User.id==ReportBase.reported_user_id") + created = Column(DateTime, nullable=False, default=datetime.datetime.now()) + discriminator = Column('type', Unicode(50)) + resolver_id = Column(Integer, ForeignKey(User.id)) + resolver = relationship( + User, + backref=backref("reports_resolved_by", + lazy="dynamic", + cascade="all, delete-orphan"), + primaryjoin="User.id==ReportBase.resolver_id") + + resolved = Column(DateTime) + result = Column(UnicodeText) + __mapper_args__ = {'polymorphic_on': discriminator} + + def is_comment_report(self): + return self.discriminator=='comment_report' + + def is_media_entry_report(self): + return self.discriminator=='media_report' + + def is_archived_report(self): + return self.resolved is not None + + def archive(self,resolver_id, resolved, result): + self.resolver_id = resolver_id + self.resolved = resolved + self.result = result + + +class CommentReport(ReportBase): + """ + Reports that have been filed on comments. + :keyword comment_id Holds the integer value of the reported + comment's ID + """ + __tablename__ = 'core__reports_on_comments' + __mapper_args__ = {'polymorphic_identity': 'comment_report'} + + id = Column('id',Integer, ForeignKey('core__reports.id'), + primary_key=True) + comment_id = Column(Integer, ForeignKey(MediaComment.id), nullable=True) + comment = relationship( + MediaComment, backref=backref("reports_filed_on", + lazy="dynamic")) + + +class MediaReport(ReportBase): + """ + Reports that have been filed on media entries + :keyword media_entry_id Holds the integer value of the reported + media entry's ID + """ + __tablename__ = 'core__reports_on_media' + __mapper_args__ = {'polymorphic_identity': 'media_report'} + + id = Column('id',Integer, ForeignKey('core__reports.id'), + primary_key=True) + media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=True) + media_entry = relationship( + MediaEntry, + backref=backref("reports_filed_on", + lazy="dynamic")) + +class UserBan(Base): + """ + Holds the information on a specific user's ban-state. As long as one of + these is attached to a user, they are banned from accessing mediagoblin. + When they try to log in, they are greeted with a page that tells them + the reason why they are banned and when (if ever) the ban will be + lifted + + :keyword user_id Holds the id of the user this object is + attached to. This is a one-to-one + relationship. + :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. + :keyword reason Holds the reason why the user was banned. + """ + __tablename__ = 'core__user_bans' + + user_id = Column(Integer, ForeignKey(User.id), nullable=False, + primary_key=True) + expiration_date = Column(Date) + reason = Column(UnicodeText, nullable=False) + + +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) + privilege_name = Column(Unicode, nullable=False, unique=True) + all_users = relationship( + User, + backref='all_privileges', + 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) + + +class PrivilegeUserAssociation(Base): + ''' + This table holds the many-to-many relationship between User and Privilege + ''' + + __tablename__ = 'core__privileges_users' + + privilege_id = Column( + 'core__privilege_id', + Integer, + ForeignKey(User.id), + primary_key=True) + user_id = Column( + 'core__user_id', + Integer, + ForeignKey(Privilege.id), + primary_key=True) + MODELS = [ - User, Client, RequestToken, AccessToken, NonceTimestamp, MediaEntry, Tag, - MediaTag, MediaComment, Collection, CollectionItem, MediaFile, FileKeynames, - MediaAttachmentFile, ProcessingMetaData, Notification, CommentNotification, - ProcessingNotification, CommentSubscription] + User, MediaEntry, Tag, MediaTag, MediaComment, Collection, CollectionItem, + MediaFile, FileKeynames, MediaAttachmentFile, ProcessingMetaData, + Notification, CommentNotification, ProcessingNotification, Client, + CommentSubscription, ReportBase, CommentReport, MediaReport, UserBan, + Privilege, PrivilegeUserAssociation, + RequestToken, AccessToken, NonceTimestamp] """ Foundations are the default rows that are created immediately after the tables @@ -700,7 +909,13 @@ MODELS = [ FOUNDATIONS = {User:user_foundations} """ -FOUNDATIONS = {} +privilege_foundations = [{'privilege_name':u'admin'}, + {'privilege_name':u'moderator'}, + {'privilege_name':u'uploader'}, + {'privilege_name':u'reporter'}, + {'privilege_name':u'commenter'}, + {'privilege_name':u'active'}] +FOUNDATIONS = {Privilege:privilege_foundations} ###################################################### # Special, migrations-tracking table diff --git a/mediagoblin/db/util.py b/mediagoblin/db/util.py index 8431361a..7a0a3a73 100644 --- a/mediagoblin/db/util.py +++ b/mediagoblin/db/util.py @@ -67,7 +67,6 @@ def check_collection_slug_used(creator_id, slug, ignore_c_id): does_exist = Session.query(Collection.id).filter(filt).first() is not None return does_exist - if __name__ == '__main__': from mediagoblin.db.open import setup_connection_and_db_from_config diff --git a/mediagoblin/decorators.py b/mediagoblin/decorators.py index 685d0d98..872dc1f4 100644 --- a/mediagoblin/decorators.py +++ b/mediagoblin/decorators.py @@ -22,25 +22,44 @@ from oauthlib.oauth1 import ResourceEndpoint from mediagoblin import mg_globals as mgg from mediagoblin import messages -from mediagoblin.db.models import MediaEntry, User -from mediagoblin.tools.response import json_response, redirect, render_404 +from mediagoblin.db.models import MediaEntry, User, MediaComment +from mediagoblin.tools.response import (redirect, render_404, + render_user_banned, json_response) from mediagoblin.tools.translate import pass_to_ugettext as _ from mediagoblin.oauth.tools.request import decode_authorization_header from mediagoblin.oauth.oauth import GMGRequestValidator + +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: + if request.user.is_banned(): + return render_user_banned(request) + return controller(request, *args, **kwargs) + + return wrapper + + def require_active_login(controller): """ - Require an active login from the user. + Require an active login from the user. If the user is banned, redirects to + the "You are Banned" page. """ @wraps(controller) + @user_not_banned def new_controller_func(request, *args, **kwargs): if request.user and \ - request.user.status == u'needs_email_verification': + not request.user.has_privilege(u'active'): return redirect( request, 'mediagoblin.user_pages.user_home', user=request.user.username) - elif not request.user or request.user.status != u'active': + elif not request.user or not request.user.has_privilege(u'active'): next_url = urljoin( request.urlgen('mediagoblin.auth.login', qualified=True), @@ -53,6 +72,34 @@ def require_active_login(controller): return new_controller_func + +def user_has_privilege(privilege_name): + """ + Requires that a user have a particular privilege in order to access a page. + In order to require that a user have multiple privileges, use this + decorator twice on the same view. This decorator also makes sure that the + user is not banned, or else it redirects them to the "You are Banned" page. + + :param privilege_name A unicode object that is that represents + the privilege object. This object is + the name of the privilege, as assigned + in the Privilege.privilege_name column + """ + + def user_has_privilege_decorator(controller): + @wraps(controller) + @require_active_login + def wrapper(request, *args, **kwargs): + user_id = request.user.id + if not request.user.has_privilege(privilege_name): + raise Forbidden() + + return controller(request, *args, **kwargs) + + return wrapper + return user_has_privilege_decorator + + def active_user_from_url(controller): """Retrieve User() from <user> URL pattern and pass in as url_user=... @@ -75,7 +122,7 @@ def user_may_delete_media(controller): @wraps(controller) def wrapper(request, *args, **kwargs): uploader_id = kwargs['media'].uploader - if not (request.user.is_admin or + if not (request.user.has_privilege(u'admin') or request.user.id == uploader_id): raise Forbidden() @@ -92,7 +139,7 @@ def user_may_alter_collection(controller): def wrapper(request, *args, **kwargs): creator_id = request.db.User.query.filter_by( username=request.matchdict['user']).first().id - if not (request.user.is_admin or + if not (request.user.has_privilege(u'admin') or request.user.id == creator_id): raise Forbidden() @@ -256,6 +303,48 @@ def allow_registration(controller): return wrapper +def allow_reporting(controller): + """ Decorator for if reporting is enabled""" + @wraps(controller) + def wrapper(request, *args, **kwargs): + if not mgg.app_config["allow_reporting"]: + messages.add_message( + request, + messages.WARNING, + _('Sorry, reporting is disabled on this instance.')) + return redirect(request, 'index') + + return controller(request, *args, **kwargs) + + return wrapper + +def get_optional_media_comment_by_id(controller): + """ + Pass in a MediaComment based off of a url component. Because of this decor- + -ator's use in filing Media or Comment Reports, it has two valid outcomes. + + :returns The view function being wrapped with kwarg `comment` set to + the MediaComment who's id is in the URL. If there is a + comment id in the URL and if it is valid. + :returns The view function being wrapped with kwarg `comment` set to + None. If there is no comment id in the URL. + :returns A 404 Error page, if there is a comment if in the URL and it + is invalid. + """ + @wraps(controller) + def wrapper(request, *args, **kwargs): + if 'comment' in request.matchdict: + comment = MediaComment.query.filter_by( + id=request.matchdict['comment']).first() + + if comment is None: + return render_404(request) + + return controller(request, comment=comment, *args, **kwargs) + else: + return controller(request, comment=None, *args, **kwargs) + return wrapper + def auth_enabled(controller): """Decorator for if an auth plugin is enabled""" @@ -272,6 +361,31 @@ def auth_enabled(controller): return wrapper +def require_admin_or_moderator_login(controller): + """ + Require a login from an administrator or a moderator. + """ + @wraps(controller) + def new_controller_func(request, *args, **kwargs): + if request.user and \ + not request.user.has_privilege(u'admin',u'moderator'): + + raise Forbidden() + elif not request.user: + next_url = urljoin( + request.urlgen('mediagoblin.auth.login', + qualified=True), + request.url) + + return redirect(request, 'mediagoblin.auth.login', + next=next_url) + + return controller(request, *args, **kwargs) + + return new_controller_func + + + def oauth_required(controller): """ Used to wrap API endpoints where oauth is required """ @wraps(controller) @@ -283,7 +397,7 @@ def oauth_required(controller): error = "Missing required parameter." return json_response({"error": error}, status=400) - + request_validator = GMGRequestValidator() resource_endpoint = ResourceEndpoint(request_validator) valid, request = resource_endpoint.validate_protected_resource_request( diff --git a/mediagoblin/edit/lib.py b/mediagoblin/edit/lib.py index aab537a0..6acebc96 100644 --- a/mediagoblin/edit/lib.py +++ b/mediagoblin/edit/lib.py @@ -19,6 +19,6 @@ def may_edit_media(request, media): """Check, if the request's user may edit the media details""" if media.uploader == request.user.id: return True - if request.user.is_admin: + if request.user.has_privilege(u'admin'): return True return False diff --git a/mediagoblin/edit/views.py b/mediagoblin/edit/views.py index 140f9eec..80590875 100644 --- a/mediagoblin/edit/views.py +++ b/mediagoblin/edit/views.py @@ -83,7 +83,7 @@ def edit_media(request, media): return redirect_obj(request, media) - if request.user.is_admin \ + if request.user.has_privilege(u'admin') \ and media.uploader != request.user.id \ and request.method != 'POST': messages.add_message( @@ -184,7 +184,7 @@ def legacy_edit_profile(request): def edit_profile(request, url_user=None): # admins may edit any user profile if request.user.username != url_user.username: - if not request.user.is_admin: + if not request.user.has_privilege(u'admin'): raise Forbidden(_("You can only edit your own profile.")) # No need to warn again if admin just submitted an edited profile @@ -324,7 +324,7 @@ def edit_collection(request, collection): return redirect_obj(request, collection) - if request.user.is_admin \ + if request.user.has_privilege(u'admin') \ and collection.creator != request.user.id \ and request.method != 'POST': messages.add_message( diff --git a/mediagoblin/gmg_commands/users.py b/mediagoblin/gmg_commands/users.py index e44b0aa9..4a730d9e 100644 --- a/mediagoblin/gmg_commands/users.py +++ b/mediagoblin/gmg_commands/users.py @@ -53,8 +53,17 @@ def adduser(args): entry.username = unicode(args.username.lower()) entry.email = unicode(args.email) entry.pw_hash = auth.gen_password_hash(args.password) - entry.status = u'active' - entry.email_verified = True + default_privileges = [ + db.Privilege.query.filter( + db.Privilege.privilege_name==u'commenter').one(), + db.Privilege.query.filter( + db.Privilege.privilege_name==u'uploader').one(), + db.Privilege.query.filter( + db.Privilege.privilege_name==u'reporter').one(), + db.Privilege.query.filter( + db.Privilege.privilege_name==u'active').one() + ] + entry.all_privileges = default_privileges entry.save() print "User created (and email marked as verified)" @@ -74,7 +83,10 @@ def makeadmin(args): user = db.User.query.filter_by( username=unicode(args.username.lower())).one() if user: - user.is_admin = True + user.all_privileges.append( + db.Privilege.query.filter( + db.Privilege.privilege_name==u'admin').one() + ) user.save() print 'The user is now Admin' else: diff --git a/mediagoblin/admin/__init__.py b/mediagoblin/moderation/__init__.py index 719b56e7..719b56e7 100644 --- a/mediagoblin/admin/__init__.py +++ b/mediagoblin/moderation/__init__.py diff --git a/mediagoblin/moderation/forms.py b/mediagoblin/moderation/forms.py new file mode 100644 index 00000000..5582abdd --- /dev/null +++ b/mediagoblin/moderation/forms.py @@ -0,0 +1,148 @@ +# 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) diff --git a/mediagoblin/routing.py b/mediagoblin/routing.py index 5961f33b..c2c6d284 100644 --- a/mediagoblin/routing.py +++ b/mediagoblin/routing.py @@ -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 @@ -27,8 +27,10 @@ _log = logging.getLogger(__name__) def get_url_map(): add_route('index', '/', 'mediagoblin.views:root_view') + add_route('terms_of_service','/terms_of_service', + 'mediagoblin.views:terms_of_service') mount('/auth', auth_routes) - mount('/a', admin_routes) + mount('/mod', moderation_routes) import mediagoblin.submit.routing import mediagoblin.user_pages.routing diff --git a/mediagoblin/static/css/base.css b/mediagoblin/static/css/base.css index d96b9200..b9b806f9 100644 --- a/mediagoblin/static/css/base.css +++ b/mediagoblin/static/css/base.css @@ -156,6 +156,10 @@ a.logo { margin: 6px 8px 6px 0; } +.fine_print { + font-size: 0.8em; +} + .mediagoblin_content { width: 100%; padding-bottom: 74px; @@ -220,6 +224,7 @@ footer { color: #283F35; } + .button_form { min-width: 99px; margin: 10px 0px 10px 15px; @@ -351,40 +356,40 @@ textarea#description, textarea#bio { /* comments */ -.comment_wrapper { +.comment_wrapper, .report_wrapper { margin-top: 20px; margin-bottom: 20px; } -.comment_wrapper p { +.comment_wrapper p, .report_wrapper p { margin-bottom: 2px; } -.comment_author { +.comment_author, .report_author { padding-top: 4px; font-size: 0.9em; } -a.comment_authorlink { +a.comment_authorlink, a.report_authorlink { text-decoration: none; padding-right: 5px; font-weight: bold; padding-left: 2px; } -a.comment_authorlink:hover { +a.comment_authorlink:hover, a.report_authorlink:hover { text-decoration: underline; } -a.comment_whenlink { +a.comment_whenlink, a.report_whenlink { text-decoration: none; } -a.comment_whenlink:hover { +a.comment_whenlink:hover, a.report_whenlink:hover { text-decoration: underline; } -.comment_content { +.comment_content, .report_content { margin-left: 8px; margin-top: 8px; } @@ -408,6 +413,13 @@ textarea#comment_content { padding-right: 6px; } + +a.report_authorlink, a.report_whenlink { + color: #D486B1; +} + +ul#action_to_resolve {list-style:none; margin-left:10px;} + /* media galleries */ .media_thumbnail { @@ -608,6 +620,38 @@ table.media_panel th { text-align: left; } +/* moderator panels */ + +table.admin_panel { + width: 100% +} + +table.admin_side_panel { + width: 60% +} + +table.admin_panel th, table.admin_side_panel th { + font-weight: bold; + padding-bottom: 4px; + text-align: left; + color: #fff; +} + +table td.user_with_privilege { + font-weight: bold; + color: #86D4B1; +} + +table td.user_without_privilege { + font-weight: bold; + color: #D486B1; +} + +.return_to_panel { + text-align:right; + float: right; + font-size:1.2em +} /* Delete panel */ @@ -616,6 +660,27 @@ table.media_panel th { margin-left: 10px; } +/* code of conduct */ + +#code_of_conduct_list { + margin-left:25px; + margin-bottom: 10px; +} +#code_of_conduct_list li { + margin:5px 0 15px 25px; +} +#code_of_conduct_list strong{ + color:#fff; +} + +.nested_sublist { + margin: 5px 0 10px 25px; + font-size:80%; +} +.nested_sublist li { + margin-bottom: 10px; +} + /* ASCII art and code */ @font-face { diff --git a/mediagoblin/static/images/icon_clipboard.png b/mediagoblin/static/images/icon_clipboard.png Binary files differnew file mode 100644 index 00000000..6f94498b --- /dev/null +++ b/mediagoblin/static/images/icon_clipboard.png diff --git a/mediagoblin/static/images/icon_clipboard_alert.png b/mediagoblin/static/images/icon_clipboard_alert.png Binary files differnew file mode 100644 index 00000000..952c588d --- /dev/null +++ b/mediagoblin/static/images/icon_clipboard_alert.png diff --git a/mediagoblin/static/js/setup_report_forms.js b/mediagoblin/static/js/setup_report_forms.js new file mode 100644 index 00000000..a75a92dd --- /dev/null +++ b/mediagoblin/static/js/setup_report_forms.js @@ -0,0 +1,67 @@ +/** + * 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/>. + */ + +function init_report_resolution_form() { + hidden_input_names = { + 'takeaway':['take_away_privileges'], + 'userban':['user_banned_until','why_user_was_banned'], + 'sendmessage':['message_to_user'] +} + init_user_banned_form(); + $('form#resolution_form').hide() + $('#open_resolution_form').click(function() { + $('form#resolution_form').toggle(); + $.each(hidden_input_names, function(key, list){ + $.each(list, function(index, name){ + $('label[for='+name+']').hide(); + $('#'+name).hide(); + }); + }); + }); + $('#action_to_resolve').change(function() { + $('ul#action_to_resolve li input:checked').each(function() { + $.each(hidden_input_names[$(this).val()], function(index, name){ + $('label[for='+name+']').show(); + $('#'+name).show(); + }); + }); + $('ul#action_to_resolve li input:not(:checked)').each(function() { + $.each(hidden_input_names[$(this).val()], function(index, name){ + $('label[for='+name+']').hide(); + $('#'+name).hide(); + }); + }); + }); + $("#submit_this_report").click(function(){ + submit_user_banned_form() + }); +} + +function submit_user_banned_form() { + if ($("#user_banned_until").val() == 'YYYY-MM-DD'){ + $("#user_banned_until").val(""); + } +} + +function init_user_banned_form() { + $('#user_banned_until').val("YYYY-MM-DD") + $("#user_banned_until").focus(function() { + $(this).val(""); + $(this).unbind('focus'); + }); +} diff --git a/mediagoblin/submit/views.py b/mediagoblin/submit/views.py index 7f7dee33..e0e2f1a5 100644 --- a/mediagoblin/submit/views.py +++ b/mediagoblin/submit/views.py @@ -27,7 +27,7 @@ _log = logging.getLogger(__name__) from mediagoblin.tools.text import convert_to_tag_list_of_dicts from mediagoblin.tools.translate import pass_to_ugettext as _ from mediagoblin.tools.response import render_to_response, redirect -from mediagoblin.decorators import require_active_login +from mediagoblin.decorators import require_active_login, user_has_privilege from mediagoblin.submit import forms as submit_forms from mediagoblin.messages import add_message, SUCCESS from mediagoblin.media_types import sniff_media, \ @@ -39,6 +39,7 @@ from mediagoblin.notifications import add_comment_subscription @require_active_login +@user_has_privilege(u'uploader') def submit_start(request): """ First view for submitting a file. diff --git a/mediagoblin/templates/mediagoblin/banned.html b/mediagoblin/templates/mediagoblin/banned.html new file mode 100644 index 00000000..cd54158a --- /dev/null +++ b/mediagoblin/templates/mediagoblin/banned.html @@ -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/>. +#} +{% extends "mediagoblin/base.html" %} + +{% block title %}{% trans %}You are Banned.{% endtrans %}{% endblock %} + +{% block mediagoblin_content %} + <img class="right_align" src="{{ request.staticdirect('/images/404.png') }}" + alt="{% trans %}Image of goblin stressing out{% endtrans %}" /> + + <h1>{% trans %}You have been banned{% endtrans %} + {% if expiration_date %} + {% trans %}until{% endtrans %} {{ expiration_date }} + {% else %} + {% trans %}indefinitely{% endtrans %} + {% endif %} + </h2> + <p>{{ reason|safe }}</p> + <div class="clear"></div> +{% endblock %} diff --git a/mediagoblin/templates/mediagoblin/base.html b/mediagoblin/templates/mediagoblin/base.html index f9deb2ad..94ca7aa5 100644 --- a/mediagoblin/templates/mediagoblin/base.html +++ b/mediagoblin/templates/mediagoblin/base.html @@ -62,7 +62,7 @@ {% block mediagoblin_header_title %}{% endblock %} <div class="header_right"> {%- if request.user %} - {% if request.user and request.user.status == 'active' %} + {% if request.user and request.user.has_privilege('active') and not request.user.is_banned() %} {% set notification_count = get_notification_count(request.user.id) %} {% if notification_count %} @@ -71,7 +71,7 @@ {% endif %} <a href="javascript:;" class="button_action header_dropdown_down">▼</a> <a href="javascript:;" class="button_action header_dropdown_up">▲</a> - {% elif request.user and request.user.status == "needs_email_verification" %} + {% elif request.user and not request.user.has_privilege('active') %} {# the following link should only appear when verification is needed #} <a href="{{ request.urlgen('mediagoblin.user_pages.user_home', user=request.user.username) }}" @@ -84,6 +84,19 @@ "javascript:;" {% endif %} >{% trans %}log out{% endtrans %}</a> + {% elif request.user and request.user.is_banned() %} + <a id="logout" href= + {% if persona is not defined %} + "{{ request.urlgen('mediagoblin.auth.logout') }}" + {% else %} + "javascript:;" + {% endif %} + >{% trans %}log out{% endtrans %}</a> + <p class="fine_print"> + <a href="{{ request.urlgen('terms_of_service') }}"> + {%- trans %}Terms of Service{%- endtrans %} + </a> + </p> {% endif %} {%- elif auth %} <a href= @@ -98,7 +111,7 @@ {%- endif %} </div> <div class="clear"></div> - {% if request.user and request.user.status == 'active' %} + {% if request.user and request.user.has_privilege('active') %} <div class="header_dropdown"> <p> <span class="dropdown_title"> @@ -130,14 +143,25 @@ <a class="button_action" href="{{ request.urlgen('mediagoblin.submit.collection') }}"> {%- trans %}Create new collection{% endtrans -%} </a> - {% if request.user.is_admin %} + {% if request.user.has_privilege('admin','moderator') %} <p> - <span class="dropdown_title">Admin powers:</span> - <a href="{{ request.urlgen('mediagoblin.admin.panel') }}"> + <span class="dropdown_title">Moderation powers:</span> + <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> + · + <a href="{{ request.urlgen('mediagoblin.moderation.reports') }}"> + {%- trans %}Report management panel{% endtrans -%} + </a> </p> {% endif %} + <p class="fine_print"> + <a href="{{ request.urlgen('terms_of_service') }}">Terms of Service</a> + </p> {% include 'mediagoblin/fragments/header_notifications.html' %} </div> {% endif %} diff --git a/mediagoblin/templates/mediagoblin/admin/panel.html b/mediagoblin/templates/mediagoblin/moderation/media_panel.html index 1c3c866e..3c929d4f 100644 --- a/mediagoblin/templates/mediagoblin/admin/panel.html +++ b/mediagoblin/templates/mediagoblin/moderation/media_panel.html @@ -21,6 +21,7 @@ {% trans %}Media processing panel{% endtrans %} — {{ super() }} {%- endblock %} + {% block mediagoblin_content %} <h1>{% trans %}Media processing panel{% endtrans %}</h1> @@ -28,7 +29,7 @@ <p> {% trans %}Here you can track the state of media being processed on this instance.{% endtrans %} </p> - + <h2>{% trans %}Media in-processing{% endtrans %}</h2> {% if processing_entries.count() %} @@ -56,7 +57,7 @@ </table> {% else %} <p><em>{% trans %}No media in-processing{% endtrans %}</em></p> -{% endif %} +{% endif %} <h2>{% trans %}These uploads failed to process:{% endtrans %}</h2> {% if failed_entries.count() %} diff --git a/mediagoblin/templates/mediagoblin/moderation/report.html b/mediagoblin/templates/mediagoblin/moderation/report.html new file mode 100644 index 00000000..cb717cde --- /dev/null +++ b/mediagoblin/templates/mediagoblin/moderation/report.html @@ -0,0 +1,154 @@ +{# +# 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/>. +#} +{%- extends "mediagoblin/base.html" %} +{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %} +{%- block mediagoblin_head %} + <script src="{{ request.staticdirect('/js/setup_report_forms.js') }}"></script> +{% endblock %} + +{%- block mediagoblin_content %} +{% if not report %} + Sorry, no such report found. +{% else %} + <a href="{{ request.urlgen('mediagoblin.moderation.reports') }}" + class="return_to_panel button_action" + title="Return to Reports Panel"> + {% trans %}Return to Reports Panel{% endtrans %}</a> + <h2>{% trans %}Report{% endtrans %} #{{ report.id }}</h2> + {% if report.is_comment_report() and report.comment %} + + {% trans %}Reported comment{% endtrans %}: + {% set comment = report.comment %} + {% set reported_user = comment.get_author %} + <div id="comment-{{ comment.id }}" + class="comment_wrapper"> + <div class="comment_author"> + <img src="{{ request.staticdirect('/images/icon_comment.png') }}" /> + <a href="{{ request.urlgen('mediagoblin.moderation.users_detail', + user=comment.get_author.username) }}" + class="comment_authorlink"> + {{- reported_user.username -}} + </a> + <a href="{{ request.urlgen( + 'mediagoblin.user_pages.media_home.view_comment', + comment=comment.id, + user=comment.get_media_entry.get_uploader.username, + media=comment.get_media_entry.slug_or_id) }}#comment" + class="comment_whenlink"> + <span title='{{- comment.created.strftime("%I:%M%p %Y-%m-%d") -}}'> + {%- trans formatted_time=timesince(comment.created) -%} + {{ formatted_time }} ago + {%- endtrans -%} + </span></a>: + </div> + <div class=comment_content> + {% autoescape False %} + {{ comment.content_html }} + {% endautoescape %} + </div> + </div> + {% elif report.is_media_entry_report() and report.media_entry %} + + {% set media_entry = report.media_entry %} + <div class="media_thumbnail"> + <a href="{{ request.urlgen('mediagoblin.user_pages.media_home', + user=media_entry.get_uploader.username, + media=media_entry.slug_or_id) }}"> + <img src="{{ media_entry.thumb_url}}"/></a> + <a href="{{ request.urlgen('mediagoblin.user_pages.media_home', + user=media_entry.get_uploader.username, + media=media_entry.slug_or_id) }}" class=thumb_entry_title> + {{ media_entry.title }}</a> + </div> + <div class=clear></div> + <p> + {% trans user_name=report.reported_user.username, + user_url=request.urlgen( + 'mediagoblin.moderation.users_detail', + user=report.reported_user.username) %} + ❖ Reported media by <a href="{{ user_url }}">{{ user_name }}</a> + {% endtrans %} + </p> + <div class=clear></div> + {% else %} + <h2>{% trans user_url=request.urlgen( + 'mediagoblin.moderation.users_detail', + user=report.reporter.username), + user_name=report.reported_user.username %} + CONTENT BY + <a href="{{ user_url }}"> {{ user_name }}</a> + HAS BEEN DELETED + {% endtrans %} + </h2> + {% endif %} + Reason for report: + <div id="report-{{ report.id }}" + class="report_wrapper"> + <div class="report_author"> + <img src="{{ request.staticdirect( + '/images/icon_clipboard_alert.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.moderation.users_detail', + user=report.reporter.username) }}" + class="report_authorlink"> + {{- report.reporter.username -}} + </a> + <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") -}}'> + {%- trans formatted_time=timesince(report.created) -%} + {{ formatted_time }} ago + {%- endtrans -%} + </span> + </a> + </div> + <div class="report_content"> + {{ report.report_content }} + </div> + </div> + {% if not report.is_archived_report() and not (report.reported_user.has_privilege('admin') and not request.user.has_privilege('admin')) %} + <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() { + init_report_resolution_form(); + }); + </script> + {% elif report.is_archived_report() %} + <h2><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" /> + {% trans %}Status{% endtrans %}: + </h2> + <b>{% trans %}RESOLVED{% endtrans %}</b> + {{ report.resolved.strftime("%I:%M%p %Y-%m-%d") }} + <pre> + <p>{{ report.result }}</p> + </pre> + {% else %} + <input type=button disabled=disabled value="Resolve This Report"/> + <p>You cannot take action against an administrator</p> + {% endif %} +{% endif %} +{% endblock %} diff --git a/mediagoblin/templates/mediagoblin/moderation/report_panel.html b/mediagoblin/templates/mediagoblin/moderation/report_panel.html new file mode 100644 index 00000000..4eb16b2b --- /dev/null +++ b/mediagoblin/templates/mediagoblin/moderation/report_panel.html @@ -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/>. +#} +{% extends "mediagoblin/base.html" %} +{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %} + +{% block title -%} + {% trans %}Report panel{% endtrans %} — {{ super() }} +{%- endblock %} + +{% block mediagoblin_content %} + +<h1>{% trans %}Report panel{% endtrans %}</h1> + +<p> + {% trans %} + Here you can look up open reports that have been filed by users. + {% endtrans %} +</p> + +<h2>{% trans %}Active Reports Filed{% endtrans %}</h2> +{% if report_list.count() %} + {% if not active_settings.last_page == 1 %} + {% if 'active_p='~active_settings.current_page in request.query_string %} + {% set query_string = request.query_string %}{% else %} + {% set query_string = +'active_p='~active_settings.current_page~"&"+request.query_string %} + {% endif %} + <div class="right_align"> + {% set first_vis = active_settings.current_page-3 %} + {% set last_vis = active_settings.current_page+3 %} + {% set curr_page = active_settings.current_page %} + {% if 1 == curr_page %}<b>1</b>{% else %} + <a href ="?{{ query_string.replace( + 'active_p='~active_settings.current_page, + 'active_p='~1) }}"> + 1</a>{% endif %} + {% if first_vis > 1 %}...{% endif %} + {% for p in range(first_vis,last_vis+1) %} + {% if p > 1 and p < active_settings.last_page and +curr_page !=p %} + <a href="?{{ query_string.replace( + 'active_p='~active_settings.current_page, + 'active_p='~p) }}"> + {{ p }}</a> + {% elif p > 1 and p < active_settings.last_page %} + <b>{{ p }}</b> + {% endif %} + {% endfor %} + {% if last_vis < active_settings.last_page %}...{% endif %} + {% if active_settings.last_page != curr_page %} + <a href ="?{{ query_string.replace( + 'active_p='~active_settings.current_page, + 'active_p='~active_settings.last_page) }}"> + {{ active_settings.last_page }}</a> + {% else %}<b>{{ active_settings.last_page }}</b> + {% endif %} + </div> + {% endif %} + <table class="admin_panel processing"> + <tr> + <th></th> + <th>{% trans %}Offender{% endtrans %}</th> + <th>{% trans %}When Reported{% endtrans %}</th> + <th>{% trans %}Reported By{% endtrans %}</th> + <th>{% trans %}Reason{% endtrans %}</th> + </tr> + {% for report in report_list %} + <tr> + {% if report.discriminator == "comment_report" %} + <td> + <img + src="{{ request.staticdirect('/images/icon_clipboard_alert.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.moderation.reports_detail', + report_id=report.id) }}"> + {% trans report_id=report.id %} + Comment Report #{{ report_id }} + {% endtrans %} + </a> + </td> + {% elif report.discriminator == "media_report" %} + <td> + <img + src="{{ request.staticdirect('/images/icon_clipboard_alert.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.moderation.reports_detail', + report_id=report.id) }}"> + {% trans report_id=report.id %} + Media Report #{{ report_id }} + {% endtrans %} + </a> + </td> + {% endif %} + <td>{{ report.reported_user.username }}</td> + <td>{{ report.created.strftime("%F %R") }}</td> + <td>{{ report.reporter.username }}</td> + <td>{{ report.report_content[0:20] }}...</td> + </tr> + {% endfor %} + </table> +{% else %} + <p><em>{% trans %}No open reports found.{% endtrans %}</em></p> +{% endif %} +<h2>{% trans %}Closed Reports{% endtrans %}</h2> +{% if closed_report_list.count() %} + {% if not closed_settings.last_page == 1 %} + {% if 'closed_p='~closed_settings.current_page in request.query_string %} + {% set query_string = request.query_string %}{% else %} + {% set query_string = +'closed_p='~closed_settings.current_page~"&"+request.query_string %} + {% endif %} + <div class="right_align"> + {% set first_vis = closed_settings.current_page-3 %} + {% set last_vis = closed_settings.current_page+3 %} + {% set curr_page = closed_settings.current_page %} + {% if not curr_page==1 %} + <a href ="?{{ query_string.replace( + 'closed_p='~closed_settings.current_page, + 'closed_p='~1) }}">1</a> + {% else %} + <b>1 </b> + {% endif %} + {% if first_vis > 1 %}...{% endif %} + {% for p in range(first_vis,last_vis+1) %} + {% if p > 1 and p < closed_settings.last_page and +curr_page !=p %} + <a href="?{{ query_string.replace( + 'closed_p='~closed_settings.current_page, + 'closed_p='~p) }}"> + {{ p }}</a> + + {% elif p > 1 and p < closed_settings.last_page %} + <b>{{ p }}</b> + {% endif %} + {% endfor %} + {% if last_vis < closed_settings.last_page %}...{% endif %} + {% if curr_page != closed_settings.last_page %} + <a href ="?{{ query_string.replace( + 'closed_p='~closed_settings.current_page, + 'closed_p='~closed_settings.last_page) }}"> + {{ closed_settings.last_page }}</a> + {% else %}<b>{{ closed_settings.last_page }}</b> + {% endif %} + </div> + {% endif %} + <table class="media_panel processing"> + <tr> + <th></th> + <th>{% trans %}Resolved{% endtrans %}</th> + <th>{% trans %}Offender{% endtrans %}</th> + <th>{% trans %}Action Taken{% endtrans %}</th> + <th>{% trans %}Reported By{% endtrans %}</th> + <th>{% trans %}Reason{% endtrans %}</th> + </tr> + {% for report in closed_report_list %} + <tr> + <td> + <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.moderation.reports_detail', + report_id=report.id) }}"> + {% trans report_id=report.id %} + Closed Report #{{ report_id }} + {% endtrans %} + </a> + </td> + <td>{{ report.resolved.strftime("%F %R") }}</td> + <td>{{ report.reported_user.username }}</td> + <td>{{ report.created.strftime("%F %R") }}</td> + <td>{{ report.reporter.username }}</td> + <td>{{ report.report_content[:15] }}...</td> + </tr> + {% endfor %} + </table> +{% else %} + <p><em>{% trans %}No closed reports found.{% endtrans %}</em></p> +{% endif %} + +{% endblock %} diff --git a/mediagoblin/templates/mediagoblin/moderation/user.html b/mediagoblin/templates/mediagoblin/moderation/user.html new file mode 100644 index 00000000..36ba42ac --- /dev/null +++ b/mediagoblin/templates/mediagoblin/moderation/user.html @@ -0,0 +1,204 @@ +{# +# 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/>. +#} +{% extends "mediagoblin/base.html" %} +{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %} + +{% block title %} + {%- if user -%} + {%- trans username=user.username -%} + User: {{ username }} + {%- endtrans %} — {{ super() }} + {%- else -%} + {{ super() }} + {%- endif -%} +{% endblock %} + +{%- block mediagoblin_head %} + <script src="{{ request.staticdirect('/js/setup_report_forms.js') }}"></script> +{% endblock %} + +{% block mediagoblin_content -%} + {# If no user... #} + {% if not user %} + <p>{% trans %}Sorry, no such user found.{% endtrans %}</p> + {# User exists, but needs verification #} + {% elif not user.has_privilege('active') %} + <div class="profile_sidebar empty_space"> + <h1>{% trans %}Email verification needed{% endtrans %}</h1> + <p> + {% trans -%} + Someone has registered an account with this username, but it still has + to be activated. + {%- endtrans %} + </p> + + </div> + + {# Active(?) (or at least verified at some point) user, horray! #} + {% else %} + <a href="{{ request.urlgen('mediagoblin.moderation.users') }}" + class="return_to_panel button_action" + title="Return to Users Panel"> + {% trans %}Return to Users Panel{% endtrans %}</a> + <h1> + {%- trans username=user.username %}{{ username }}'s profile{% endtrans -%} + {% if user_banned and user_banned.expiration_date %} + — BANNED until {{ user_banned.expiration_date }} + {% elif user_banned %} + — Banned Indefinitely + {% endif %} + </h1> + {% if not user.url and not user.bio %} + <div class="profile_sidebar empty_space"> + <p> + {% trans -%} + This user hasn't filled in their profile (yet). + {%- endtrans %} + </p> + {% else %} + <div class="profile_sidebar"> + {% include "mediagoblin/utils/profile.html" %} + {% if request.user and + (request.user.id == user.id or request.user.has_privilege('admin')) %} + <a href="{{ request.urlgen('mediagoblin.edit.profile', + user=user.username) }}"> + {%- trans %}Edit profile{% endtrans -%} + </a> + {% endif %} + {% endif %} + <p> + <a href="{{ request.urlgen('mediagoblin.user_pages.collection_list', + user=user.username) }}"> + {%- trans %}Browse collections{% endtrans -%} + </a> + </p> + </div> + {% endif %} + {% if user %} + <h2>{%- trans %}Active Reports on {% endtrans -%}{{ user.username }}</h2> + {% if reports.count() %} + <table class="admin_side_panel"> + <tr> + <th>{%- trans %}Report ID{% endtrans -%}</th> + <th>{%- trans %}Reported Content{% endtrans -%}</th> + <th>{%- trans %}Description of Report{% endtrans -%}</th> + </tr> + {% for report in reports %} + <tr> + <td> + <img src="{{ request.staticdirect('/images/icon_clipboard.png') }}" /> + <a href="{{ request.urlgen('mediagoblin.moderation.reports_detail', + report_id=report.id) }}"> + {%- trans %}Report #{% endtrans -%}{{ report.id }} + </a> + </td> + <td> + {% if report.discriminator == "comment_report" %} + <a>{%- trans %}Reported Comment{% endtrans -%}</a> + {% elif report.discriminator == "media_report" %} + <a>{%- trans %}Reported Media Entry{% endtrans -%}</a> + {% endif %} + </td> + <td>{{ report.report_content[:21] }} + {% if report.report_content|count >20 %}...{% endif %}</td> + <td>{%- trans %}Resolve{% endtrans -%}</td> + </tr> + {% endfor %} + <tr><td></td><td></td> + </table> + {% else %} + {%- trans %}No active reports filed on {% endtrans -%} {{ user.username }} + {% endif %} + <span class="right_align"> + <a href="{{ request.urlgen( + 'mediagoblin.moderation.reports') }}?reported_user={{user.id}}"> + {%- trans + username=user.username %}All reports on {{ username }}{% endtrans %}</a> + · + <a href="{{ request.urlgen( + 'mediagoblin.moderation.reports') }}?reporter={{user.id}}"> + {%- trans + username=user.username %}All reports that {{ username }} has filed{% endtrans %}</a> + </span> + <span class=clear></span> + <h2>{{ user.username }}'s Privileges</h2> + <form method=POST action="{{ request.urlgen( + 'mediagoblin.moderation.ban_or_unban', + user=user.username) }}" class="right_align"> + {{ csrf_token }} + {% if request.user.has_privilege('admin') and not user_banned and + not user.id == request.user.id %} + {{ wtforms_util.render_divs(ban_form) }} + <input type=submit class="button_action" + value="{% trans %}Ban User{% endtrans %}" + id="ban_user_submit" /> + {% elif request.user.has_privilege('admin') and + not user.id == request.user.id %} + <input type=submit class="button_action right_align" + value="{% trans %}UnBan User{% endtrans %}" /> + {% endif %} + </form> + <form action="{{ request.urlgen('mediagoblin.moderation.give_or_take_away_privilege', + user=user.username) }}" + method=post > + <table class="admin_side_panel"> + <tr> + <th>{% trans %}Privilege{% endtrans %}</th> + <th>{% trans %}User Has Privilege{% endtrans %}</th> + </tr> + {% for privilege in privileges %} + <tr> + <td>{{ privilege.privilege_name }}</td> + {% if privilege in user.all_privileges %} + <td class="user_with_privilege"> + Yes{% else %} + <td class="user_without_privilege"> + No{% endif %} + </td> + {% if request.user.has_privilege('admin') %} + <td> + {% if privilege in user.all_privileges %} + <input type=submit id="{{ privilege.privilege_name }}" + class="submit_button button_action" + value =" -" /> + {% else %} + <input type=submit id="{{ privilege.privilege_name }}" + class="submit_button button_action" + value ="+" /> + {% endif %} + </td> + {% endif %} + </tr> + {% endfor %} + </table> + {{ csrf_token }} + <input type=hidden name=privilege_name id=hidden_privilege_name /> + </form> + {% endif %} + <script> +$(document).ready(function(){ + $('.submit_button').click(function(){ + $('#hidden_privilege_name').val($(this).attr('id')); + }); + init_user_banned_form(); + $('#ban_user_submit').click(function(){ + submit_user_banned_form() + }); +}); + </script> +{% endblock %} diff --git a/mediagoblin/templates/mediagoblin/moderation/user_panel.html b/mediagoblin/templates/mediagoblin/moderation/user_panel.html new file mode 100644 index 00000000..4949960e --- /dev/null +++ b/mediagoblin/templates/mediagoblin/moderation/user_panel.html @@ -0,0 +1,97 @@ +{# +# 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/>. +#} +{% extends "mediagoblin/base.html" %} + +{% block title -%} + {% trans %}User panel{% endtrans %} — {{ super() }} +{%- endblock %} + +{% block mediagoblin_content %} + +<h1>{% trans %}User panel{% endtrans %}</h1> + +<p> + {% trans %} + Here you can look up users in order to take punitive actions on them. + {% endtrans %} +</p> + +<h2>{% trans %}Active Users{% endtrans %}</h2> + +{% if user_list.count() %} + {% if not last_page == 1 %} + {% if 'p='~current_page in request.query_string %} + {% set query_string = request.query_string %}{% else %} + {% set query_string = +'p='~current_page~"&"+request.query_string %} + {% endif %} + <div class="right_align"> + {% set first_vis = current_page-3 %} + {% set last_vis = current_page+3 %} + {% if 1 == current_page %}<b>1</b>{% else %} + <a href ="?{{ query_string.replace( + 'p='~current_page, + 'p='~1) }}"> + 1</a>{% endif %} + {% if first_vis > 1 %}...{% endif %} + {% for p in range(first_vis,last_vis+1) %} + {% if p > 1 and p < last_page and +current_page !=p %} + <a href="?{{ query_string.replace( + 'p='~current_page, + 'p='~p) }}"> + {{ p }}</a> + {% elif p > 1 and p < last_page %} + <b>{{ p }}</b> + {% endif %} + {% endfor %} + {% if last_vis < last_page %}...{% endif %} + {% if last_page != current_page %} + <a href ="?{{ query_string.replace( + 'p='~current_page, + 'p='~last_page) }}"> + {{ last_page }}</a> + {% else %}<b>{{ last_page }}</b> + {% endif %} + </div> + {% endif %} + <table class="admin_panel processing"> + <tr> + <th>{% trans %}ID{% endtrans %}</th> + <th>{% trans %}Username{% endtrans %}</th> + <th>{% trans %}When Joined{% endtrans %}</th> + <th>{% trans %}# of Comments Posted{% endtrans %}</th> + </tr> + {% for user in user_list %} + <tr> + <td>{{ user.id }}</td> + <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> + </tr> + {% endfor %} + </table> +{% else %} + <p><em>{% trans %}No users found.{% endtrans %}</em></p> +{% endif %} +{% endblock %} diff --git a/mediagoblin/templates/mediagoblin/terms_of_service.html b/mediagoblin/templates/mediagoblin/terms_of_service.html new file mode 100644 index 00000000..a9e8ebf2 --- /dev/null +++ b/mediagoblin/templates/mediagoblin/terms_of_service.html @@ -0,0 +1,291 @@ +{# +# 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/>. +#} +{% extends "mediagoblin/base.html" %} + +{% block title %} + Terms of Service +{% endblock %} + +{% block mediagoblin_content -%} +{# <h2>The gist</h2> +# This is where you might insert your own particular rules, unique to your +# own website. Or your own worded summary. +#} + +<h2>Terms of Service</h2> + +The following terms and conditions govern all use of the +{{ app_config['html_title'] }} website and all content, services and products +available at or through the website (taken together, the Website). The Website +is owned and operated by Status.net (“Operator”). The Website is offered +subject to your acceptance without modification of all of the terms and +conditions contained herein and all other operating rules, policies +(including, without limitation, Operator’s Privacy Policy) and procedures +that may be published from time to time on this Site by Operator (collectively, + the “Agreement”). + +Please read this Agreement carefully before accessing or using the Website. +By accessing or using any part of the web site, you agree to become bound by +the terms and conditions of this agreement. If you do not agree to all the +terms and conditions of this agreement, then you may not access the Website +or use any services. If these terms and conditions are considered an offer by +Operator, acceptance is expressly limited to these terms. The Website is +available only to individuals who are at least 13 years old. + +<ol id="code_of_conduct_list"> + <li><strong>Your {{ app_config['html_title'] }} Account and Site.</strong> + If you create a notice stream on the Website, you are responsible for + maintaining the security of your account and notice stream, and you are + fully responsible for all activities that occur under the account and any + other actions taken in connection with the notice stream. You must not + describe or assign keywords to your notice stream in a misleading or + unlawful manner, + including in a manner intended to trade on the name or reputation of + others, and Operator may change or remove any description or keyword that + it considers inappropriate or unlawful, or otherwise likely to cause + Operator liability. You must immediately notify Operator of any + unauthorized uses of your notice stream, your account or any other breaches + of security. Operator will not be liable for any acts or omissions by You, + including any damages of any kind incurred as a result of such acts or + omissions. + </li> + <li><strong>Responsibility of Contributors.</strong> If you operate a notice + stream, comment on a notice stream, post material to the Website, post + links on the Website, or otherwise make + (or allow any third party to make) material + available by means of the Website (any such material, “Content”), You are + entirely responsible for the content of, and any harm resulting from, that + Content. That is the case regardless of whether the Content in question + constitutes text, graphics, an audio file, or computer software. By making + Content available, you represent and warrant that: + <ul class="nested_sublist"> + <li>the downloading, copying and use of the Content will not infringe + the proprietary rights, including but not limited to the copyright, + patent, trademark or trade secret rights, of any third party; + </li> + <li>if your employer has rights to intellectual property you create, you + have either (i) received permission from your employer to post or make + available the Content, including but not limited to any software, or + (ii) secured from your employer a waiver as to all rights in or to the + Content; + </li> + <li>you have fully complied with any third-party licenses relating to the + Content, and have done all things necessary to successfully pass + through to end users any required terms; + </li> + <li>the Content does not contain or install any viruses, worms, malware, + Trojan horses or other harmful or destructive content; + </li> + <li>the Content is not spam, and does not contain unethical or unwanted + commercial content designed to drive traffic to third party sites or + boost the search engine rankings of third party sites, or to further + unlawful acts (such as phishing) or mislead recipients as to the + source of the material (such as spoofing); + </li> + <li>if the Content is machine- or randomly-generated, it is for purposes + of direct entertainment, information and/or utility for you or other + users, and not for spam, + </li> + <li>the Content is not libelous or defamatory (more info on what that + means), does not contain threats or incite violence towards individuals + or entities, and does not violate the privacy or publicity rights of + any third party; + </li> + <li>your notice stream is not getting advertised via unwanted electronic + messages such as spam links on newsgroups, email lists, other notice + streams and web sites, and similar unsolicited promotional methods; + </li> + <li>your notice stream is not named in a manner that misleads your + readers into thinking that you are another person or company. For + example, your notice stream’s URL or name is not the name of a person + other than yourself or company other than your own; and + </li> + <li>you have, in the case of Content that includes computer code, + accurately categorized and/or described the type, nature, uses and + effects of the materials, whether requested to do so by Operator or + otherwise.</li> + </ul> + By submitting Content to Operator for inclusion on your Website, you grant + Operator a world-wide, royalty-free, and non-exclusive license to + reproduce, modify, adapt and publish the Content solely for the purpose of + displaying, distributing and promoting your notice stream. + By submitting Content to Operator for inclusion on your Website, you grant + all readers the right to use, re-use, modify and/or re-distribute the + Content under the terms of the Creative Commons Attribution 3.0. + If you delete Content, Operator will use reasonable efforts to remove it + from the Website, but you acknowledge that caching or references to the + Content may not be made immediately unavailable. + Without limiting any of those representations or warranties, Operator has + the right (though not the obligation) to, in Operator’s sole discretion + (i) refuse or remove any content that, in Operator’s reasonable opinion, + violates any Operator policy or is in any way harmful or objectionable, or + (ii) terminate or deny access to and use of the Website to any individual + or entity for any reason, in Operator’s sole discretion. + </li> + <li><strong>Responsibility of Website Visitors.</strong> Operator has not + reviewed, and cannot + review, all of the material, including computer software, posted to the + Website, and cannot therefore be responsible for that material’s content, + use or effects. By operating the Website, Operator does not represent or + imply that it endorses the material there posted, or that it believes such + material to be accurate, useful or non-harmful. You are responsible for + taking precautions as necessary to protect yourself and your computer + systems from viruses, worms, Trojan horses, and other harmful or + destructive content. The Website may contain content that is offensive, + indecent, or otherwise objectionable, as well as content containing + technical inaccuracies, typographical mistakes, and other errors. The + Website may also contain material that violates the privacy or publicity + rights, or infringes the intellectual property and other proprietary + rights, of third parties, or the downloading, copying or use of which is + subject to additional terms and conditions, stated or unstated. Operator + disclaims any responsibility for any harm resulting from the use by + visitors of the Website, or from any downloading by those visitors of + content there posted. + </li> + <li><strong>Content Posted on Other Websites.</strong> We have not reviewed, + and cannot + review, all of the material, including computer software, made available + through the websites and webpages to which {{ app_config['html_title'] }} + links, and that link to {{ app_config['html_title'] }}. Operator does not + have any control over those external websites and webpages, and is not + responsible for their contents or their use. By linking to a external + website or webpage, Operator does not represent or imply that it endorses + such website or webpage. You are responsible for taking precautions as + necessary to protect yourself and your computer systems from viruses, + worms, Trojan horses, and other harmful or destructive content. Operator + disclaims any responsibility for any harm resulting from your use of + external websites and webpages. + </li> + <li><strong>Copyright Infringement and DMCA Policy.</strong> As Operator asks + others to + respect its intellectual property rights, it respects the intellectual + property rights of others. If you believe that material located on or + linked to by {{ app_config['html_title'] }} violates your copyright, you + are encouraged to notify Operator in accordance with Operator’s Digital + Millennium Copyright Act (”DMCA”) Policy. Operator will respond to all + such notices, including as required or appropriate by removing the + infringing material or disabling all links to the infringing material. In + the case of a visitor who may infringe or repeatedly infringes the + copyrights or other intellectual property rights of Operator or others, + Operator may, in its discretion, terminate or deny access to and use of + the Website. In the case of such termination, Operator will have no + obligation to provide a refund of any amounts previously paid to Operator. + </li> + <li><strong>Intellectual Property.</strong> This Agreement does not transfer + from Operator to + you any Operator or third party intellectual property, and all right, + title and interest in and to such property will remain (as between the + parties) solely with Operator. {{ app_config['html_title'] }}, the + {{ app_config['html_title'] }} logo, and all other trademarks, service + marks, graphics and logos used in connection with + {{ app_config['html_title'] }}, or the Website are trademarks or + registered trademarks of Operator or Operator’s licensors. Other + trademarks, service marks, graphics and logos used in connection with the + Website may be the trademarks of other third parties. Your use of the + Website grants you no right or license to reproduce or otherwise use any + Operator or third-party trademarks. + </li> + <li><strong>Changes.</strong> Operator reserves the right, at its sole + discretion, to modify + or replace any part of this Agreement. It is your responsibility to check + this Agreement periodically for changes. Your continued use of or access + to the Website following the posting of any changes to this Agreement + constitutes acceptance of those changes. Operator may also, in the future, + offer new services and/or features through the Website (including, the + release of new tools and resources). Such new features and/or services + shall be subject to the terms and conditions of this Agreement. + </li> + <li><strong>Termination.</strong> Operator may terminate your access to all + or any part of + the Website at any time, with or without cause, with or without notice, + effective immediately. If you wish to terminate this Agreement or your + {{ app_config['html_title'] }} account (if you have one), you may simply + discontinue using the Website. All provisions of this Agreement which by + their nature should survive termination shall survive termination, + including, without limitation, ownership provisions, warranty disclaimers, + indemnity and limitations of liability. + </li> + <li><strong>Disclaimer of Warranties.</strong> The Website is provided + “as is”. Operator and + its suppliers and licensors hereby disclaim all warranties of any kind, + express or implied, including, without limitation, the warranties of + merchantability, fitness for a particular purpose and non-infringement. + Neither Operator nor its suppliers and licensors, makes any warranty that + the Website will be error free or that access thereto will be continuous + or uninterrupted. If you’re actually reading this, here’s a treat. You + understand that you download from, or otherwise obtain content or services + through, the Website at your own discretion and risk. + </li> + <li><strong>Limitation of Liability.</strong> In no event will Operator, or + its suppliers or + licensors, be liable with respect to any subject matter of this agreement + under any contract, negligence, strict liability or other legal or + equitable theory for: (i) any special, incidental or consequential damages; + (ii) the cost of procurement or substitute products or services; (iii) for + interruption of use or loss or corruption of data; or (iv) for any amounts + that exceed the fees paid by you to Operator under this agreement during + the twelve (12) month period prior to the cause of action. Operator shall + have no liability for any failure or delay due to matters beyond their + reasonable control. The foregoing shall not apply to the extent prohibited + by applicable law. + </li> + <li><strong>General Representation and Warranty.</strong> You represent and + warrant that (i) + your use of the Website will be in strict accordance with the Operator + Privacy Policy, with this Agreement and with all applicable laws and + regulations (including without limitation any local laws or regulations in + your country, state, city, or other governmental area, regarding online + conduct and acceptable content, and including all applicable laws regarding + the transmission of technical data exported from the United States or the + country in which you reside) and (ii) your use of the Website will not + infringe or misappropriate the intellectual property rights of any third + party. + </li> + <li><strong>Indemnification.</strong> You agree to indemnify and hold + harmless Operator, its + contractors, and its licensors, and their respective directors, officers, + employees and agents from and against any and all claims and expenses, + including attorneys’ fees, arising out of your use of the Website, + including but not limited to out of your violation this Agreement. + </li> + <li><strong>Miscellaneous.</strong> This Agreement constitutes the entire + agreement between + Operator and you concerning the subject matter hereof, and they may only + be modified by a written amendment signed by an authorized executive of + Operator, or by the posting by Operator of a revised version. If any part + of this Agreement is held invalid or unenforceable, that part will be + construed to reflect the parties’ original intent, and the remaining + portions will remain in full force and effect. A waiver by either party of + any term or condition of this Agreement or any breach thereof, in any one + instance, will not waive such term or condition or any subsequent breach + thereof. You may assign your rights under this Agreement to any party that + consents to, and agrees to be bound by, its terms and conditions; Operator + may assign its rights under this Agreement without condition. This + Agreement will be binding upon and will inure to the benefit of the + parties, their successors and permitted assigns. + </li> +</ol> + +Originally published by Automattic, Inc. as the WordPress.com Terms of Service +and made available by them under the Creative Commons Attribution- +ShareAlike 3.0 License. Modifications to remove reference to "VIP services", +rename "blog" to "notice stream", remove the choice-of-venue clause, and add +variables specific to instances of this software made by Control Yourself, Inc. +and made available under the terms of the same license. + +{% endblock -%} diff --git a/mediagoblin/templates/mediagoblin/user_pages/collection.html b/mediagoblin/templates/mediagoblin/user_pages/collection.html index 5a7baadd..87635dcb 100644 --- a/mediagoblin/templates/mediagoblin/user_pages/collection.html +++ b/mediagoblin/templates/mediagoblin/user_pages/collection.html @@ -45,7 +45,7 @@ {%- endtrans %} </h1> {% if request.user and (collection.creator == request.user.id or - request.user.is_admin) %} + request.user.has_privilege(u'admin')) %} {% set edit_url = request.urlgen('mediagoblin.edit.edit_collection', user=collection.get_creator.username, collection=collection.slug) %} diff --git a/mediagoblin/templates/mediagoblin/user_pages/collection_list.html b/mediagoblin/templates/mediagoblin/user_pages/collection_list.html index 8ac0b988..4b449c76 100644 --- a/mediagoblin/templates/mediagoblin/user_pages/collection_list.html +++ b/mediagoblin/templates/mediagoblin/user_pages/collection_list.html @@ -34,7 +34,7 @@ </h1> {% if request.user %} - {% if request.user.status == 'active' %} + {% if request.user.has_privilege('active') %} <p> <a href="{{ request.urlgen('mediagoblin.submit.collection', user=user.username) }}"> diff --git a/mediagoblin/templates/mediagoblin/user_pages/media.html b/mediagoblin/templates/mediagoblin/user_pages/media.html index 39935b40..81e5013e 100644 --- a/mediagoblin/templates/mediagoblin/user_pages/media.html +++ b/mediagoblin/templates/mediagoblin/user_pages/media.html @@ -72,7 +72,7 @@ </h2> {% if request.user and (media.uploader == request.user.id or - request.user.is_admin) %} + request.user.has_privilege('admin')) %} {% set edit_url = request.urlgen('mediagoblin.edit.edit_media', user= media.get_uploader.username, media_id=media.id) %} @@ -86,7 +86,7 @@ {% autoescape False %} <p>{{ media.description_html }}</p> {% endautoescape %} - {% if comments %} + {% if comments and request.user and request.user.has_privilege('commenter') %} {% if app_config['allow_comments'] %} <a {% if not request.user %} @@ -146,6 +146,15 @@ {{ comment.content_html }} {%- endautoescape %} </div> + <div> + {% if app_config.allow_reporting %} + <a href="{{ request.urlgen('mediagoblin.user_pages.media_home.report_comment', + user=media.get_uploader.username, + media=media.slug_or_id, + comment=comment.id) }}"> + {% trans %}Report{% endtrans %}</a> + {% endif %} + </div> </li> {% endfor %} </ul> @@ -156,7 +165,7 @@ <div class="media_sidebar"> <h3>{% trans %}Added{% endtrans %}</h3> <p><span title="{{ media.created.strftime("%I:%M%p %Y-%m-%d") }}"> - {%- trans formatted_time=timesince(media.created) -%} + {%- trans formatted_time=timesince(media.created) -%} {{ formatted_time }} ago {%- endtrans -%} </span></p> @@ -170,6 +179,10 @@ {% include "mediagoblin/utils/collections.html" %} + {% if app_config.allow_reporting %} + {% include "mediagoblin/utils/report.html" %} + {% endif %} + {% include "mediagoblin/utils/license.html" %} {% include "mediagoblin/utils/exif.html" %} @@ -189,7 +202,7 @@ {%- if app_config['allow_attachments'] and request.user and (media.uploader == request.user.id - or request.user.is_admin) %} + or request.user.has_privilege('admin')) %} {%- if not media.attachment_files|count %} <h3>{% trans %}Attachments{% endtrans %}</h3> {%- endif %} diff --git a/mediagoblin/templates/mediagoblin/user_pages/report.html b/mediagoblin/templates/mediagoblin/user_pages/report.html new file mode 100644 index 00000000..ce0fb1bc --- /dev/null +++ b/mediagoblin/templates/mediagoblin/user_pages/report.html @@ -0,0 +1,83 @@ +{# +# 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/>. +#} +{%- extends "mediagoblin/base.html" %} +{%- import "/mediagoblin/utils/wtforms.html" as wtforms_util %} +{%- block mediagoblin_content -%} +{% trans %}<h2>File a Report</h2>{% endtrans %} +<form action="" method=POST > + {% if comment is defined %} + <h3>{% trans %}Reporting this Comment{% endtrans %}</h3> + {%- set comment_author = comment.get_author %} + {%- set comment_author_url = request.urlgen( + 'mediagoblin.user_pages.user_home', + user=comment_author.username) %} + {%- set comment_url = request.urlgen( + 'mediagoblin.user_pages.media_home.view_comment', + comment=comment.id, + user=media.get_uploader.username, + media=media.slug_or_id) %} + <div id="comment-{{ comment.id }}" + class="comment_wrapper"> + <div class="comment_author"> + <img + src="{{ request.staticdirect('/images/icon_comment.png') }}" /> + <a href="{{ comment_author_url }}" + class="comment_authorlink"> + {{- comment_author.username -}} + </a> + <a href="{{ comment_url }}" + class="comment_whenlink"> + <span + title='{{- comment.created.strftime("%I:%M%p %Y-%m-%d") -}}'> + + {%- trans formatted_time=timesince(comment.created) -%} + {{ formatted_time }} ago + {%- endtrans -%} + </span></a>: + </div> + <div class="comment_content"> + {% autoescape False -%} + {{ comment.content_html }} + {%- endautoescape %} + </div> + </div> + {% elif media is defined %} + <h3>{% trans %}Reporting this Media Entry{% endtrans %}</h3> + <div class="media_thumbnail"> + <a href="{{ request.urlgen('mediagoblin.user_pages.media_home', + user=media.get_uploader.username, + media=media.slug_or_id) }}"> + <img src="{{ media.thumb_url }}"/></a> + <a href="{{ request.urlgen('mediagoblin.user_pages.media_home', + user=media.get_uploader.username, + media=media.slug_or_id) }}" + class=thumb_entry_title>{{ media.title }}</a> + </div> + <div class=clear></div> + {%- trans user_url = request.urlgen('mediagoblin.user_pages.user_home', user=media.get_uploader.username), + username = media.get_uploader.username %} + ❖ Published by <a href="{{ user_url }}" + class="comment_authorlink">{{ username }}</a> + {% endtrans %} + {%- endif %} + + {{- wtforms_util.render_divs(form) }} + {{ csrf_token }} + <input type=submit value="{% trans %}File Report {% endtrans %}" /> +</form> +{% endblock %} diff --git a/mediagoblin/templates/mediagoblin/user_pages/user.html b/mediagoblin/templates/mediagoblin/user_pages/user.html index 50ad766a..37983400 100644 --- a/mediagoblin/templates/mediagoblin/user_pages/user.html +++ b/mediagoblin/templates/mediagoblin/user_pages/user.html @@ -67,7 +67,7 @@ <div class="profile_sidebar"> {% include "mediagoblin/utils/profile.html" %} {% if request.user and - (request.user.id == user.id or request.user.is_admin) %} + (request.user.id == user.id or request.user.has_privilege('admin')) %} <a href="{{ request.urlgen('mediagoblin.edit.profile', user=user.username) }}"> {%- trans %}Edit profile{% endtrans -%} diff --git a/mediagoblin/templates/mediagoblin/user_pages/user_nonactive.html b/mediagoblin/templates/mediagoblin/user_pages/user_nonactive.html index b3066665..d924198b 100644 --- a/mediagoblin/templates/mediagoblin/user_pages/user_nonactive.html +++ b/mediagoblin/templates/mediagoblin/user_pages/user_nonactive.html @@ -33,7 +33,7 @@ {% block mediagoblin_content -%} {# User exists, but needs verification #} - {% if user.status == "needs_email_verification" %} + {% if not user.has_privilege('active') %} {% if user == request.user %} {# this should only be visible when you are this user #} <div class="form_box"> diff --git a/mediagoblin/templates/mediagoblin/utils/collection_gallery.html b/mediagoblin/templates/mediagoblin/utils/collection_gallery.html index dcc59244..24bf6832 100644 --- a/mediagoblin/templates/mediagoblin/utils/collection_gallery.html +++ b/mediagoblin/templates/mediagoblin/utils/collection_gallery.html @@ -39,7 +39,7 @@ {% endif %} {% if request.user and (item.in_collection.creator == request.user.id or - request.user.is_admin) %} + request.user.has_privilege(u'admin')) %} {%- set remove_url=request.urlgen( 'mediagoblin.user_pages.collection_item_confirm_remove', user=item.in_collection.get_creator.username, diff --git a/mediagoblin/admin/routing.py b/mediagoblin/templates/mediagoblin/utils/report.html index 29515f12..3829de97 100644 --- a/mediagoblin/admin/routing.py +++ b/mediagoblin/templates/mediagoblin/utils/report.html @@ -1,3 +1,4 @@ +{# # GNU MediaGoblin -- federated, autonomous media hosting # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. # @@ -13,8 +14,15 @@ # # 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/>. +#} -admin_routes = [ - ('mediagoblin.admin.panel', - '/panel', - 'mediagoblin.admin.views:admin_processing_panel')] +{% block report_content -%} + <p> + <a href="{{ request.urlgen('mediagoblin.user_pages.media_home.report_media', + user=media.get_uploader.username, + media=media.slug_or_id) }}" + class="button_action" id="button_reportmedia" title="Report media"> + {% trans %}Report media{% endtrans %} + </a> + </p> +{% endblock %} diff --git a/mediagoblin/tests/test_api.py b/mediagoblin/tests/test_api.py index 89cf1026..4e0cbd8f 100644 --- a/mediagoblin/tests/test_api.py +++ b/mediagoblin/tests/test_api.py @@ -35,7 +35,8 @@ class TestAPI(object): self.db = mg_globals.database self.user_password = u'4cc355_70k3N' - self.user = fixture_add_user(u'joapi', self.user_password) + self.user = fixture_add_user(u'joapi', self.user_password, + privileges=[u'active',u'uploader']) def login(self, test_app): test_app.post( diff --git a/mediagoblin/tests/test_auth.py b/mediagoblin/tests/test_auth.py index d2e01e44..1bbc3d01 100644 --- a/mediagoblin/tests/test_auth.py +++ b/mediagoblin/tests/test_auth.py @@ -84,22 +84,26 @@ def test_register_views(test_app): template.clear_test_template_context() response = test_app.post( '/auth/register/', { - 'username': u'happygirl', - 'password': 'iamsohappy', - 'email': 'happygrrl@example.org'}) + 'username': u'angrygirl', + 'password': 'iamsoangry', + 'email': 'angrygrrl@example.org'}) response.follow() ## Did we redirect to the proper page? Use the right template? - assert urlparse.urlsplit(response.location)[2] == '/u/happygirl/' + assert urlparse.urlsplit(response.location)[2] == '/u/angrygirl/' assert 'mediagoblin/user_pages/user_nonactive.html' in template.TEMPLATE_TEST_CONTEXT ## Make sure user is in place new_user = mg_globals.database.User.query.filter_by( - username=u'happygirl').first() + username=u'angrygirl').first() assert new_user - assert new_user.status == u'needs_email_verification' - assert new_user.email_verified == False + ## Make sure that the proper privileges are granted on registration + + assert new_user.has_privilege(u'commenter') + assert new_user.has_privilege(u'uploader') + assert new_user.has_privilege(u'reporter') + assert not new_user.has_privilege(u'active') ## Make sure user is logged in request = template.TEMPLATE_TEST_CONTEXT[ 'mediagoblin/user_pages/user_nonactive.html']['request'] @@ -108,7 +112,7 @@ def test_register_views(test_app): ## Make sure we get email confirmation, and try verifying assert len(mail.EMAIL_TEST_INBOX) == 1 message = mail.EMAIL_TEST_INBOX.pop() - assert message['To'] == 'happygrrl@example.org' + assert message['To'] == 'angrygrrl@example.org' email_context = template.TEMPLATE_TEST_CONTEXT[ 'mediagoblin/auth/verification_email.txt'] assert email_context['verification_url'] in message.get_payload(decode=True) @@ -130,10 +134,8 @@ def test_register_views(test_app): # assert context['verification_successful'] == True # TODO: Would be good to test messages here when we can do so... new_user = mg_globals.database.User.query.filter_by( - username=u'happygirl').first() + username=u'angrygirl').first() assert new_user - assert new_user.status == u'needs_email_verification' - assert new_user.email_verified == False ## Verify the email activation works template.clear_test_template_context() @@ -144,10 +146,8 @@ def test_register_views(test_app): # assert context['verification_successful'] == True # TODO: Would be good to test messages here when we can do so... new_user = mg_globals.database.User.query.filter_by( - username=u'happygirl').first() + username=u'angrygirl').first() assert new_user - assert new_user.status == u'active' - assert new_user.email_verified == True # Uniqueness checks # ----------------- @@ -155,9 +155,9 @@ def test_register_views(test_app): template.clear_test_template_context() response = test_app.post( '/auth/register/', { - 'username': u'happygirl', - 'password': 'iamsohappy2', - 'email': 'happygrrl2@example.org'}) + 'username': u'angrygirl', + 'password': 'iamsoangry2', + 'email': 'angrygrrl2@example.org'}) context = template.TEMPLATE_TEST_CONTEXT[ 'mediagoblin/auth/register.html'] @@ -172,7 +172,7 @@ def test_register_views(test_app): template.clear_test_template_context() response = test_app.post( '/auth/forgot_password/', - {'username': u'happygirl'}) + {'username': u'angrygirl'}) response.follow() ## Did we redirect to the proper page? Use the right template? @@ -182,7 +182,7 @@ def test_register_views(test_app): ## Make sure link to change password is sent by email assert len(mail.EMAIL_TEST_INBOX) == 1 message = mail.EMAIL_TEST_INBOX.pop() - assert message['To'] == 'happygrrl@example.org' + assert message['To'] == 'angrygrrl@example.org' email_context = template.TEMPLATE_TEST_CONTEXT[ 'mediagoblin/plugins/basic_auth/fp_verification_email.txt'] #TODO - change the name of verification_url to something forgot-password-ish @@ -212,7 +212,7 @@ def test_register_views(test_app): template.clear_test_template_context() response = test_app.post( '/auth/forgot_password/verify/', { - 'password': 'iamveryveryhappy', + 'password': 'iamveryveryangry', 'token': parsed_get_params['token']}) response.follow() assert 'mediagoblin/auth/login.html' in template.TEMPLATE_TEST_CONTEXT @@ -221,8 +221,8 @@ def test_register_views(test_app): template.clear_test_template_context() response = test_app.post( '/auth/login/', { - 'username': u'happygirl', - 'password': 'iamveryveryhappy'}) + 'username': u'angrygirl', + 'password': 'iamveryveryangry'}) # User should be redirected response.follow() @@ -235,7 +235,7 @@ def test_authentication_views(test_app): Test logging in and logging out """ # Make a new user - test_user = fixture_add_user(active_user=False) + test_user = fixture_add_user() # Get login @@ -332,7 +332,6 @@ def test_authentication_views(test_app): 'next' : '/u/chris/'}) assert urlparse.urlsplit(response.location)[2] == '/u/chris/' - @pytest.fixture() def authentication_disabled_app(request): return get_app( @@ -344,6 +343,7 @@ def authentication_disabled_app(request): def test_authentication_disabled_app(authentication_disabled_app): # app.auth should = false + assert mg_globals assert mg_globals.app.auth is False # Try to visit register page diff --git a/mediagoblin/tests/test_edit.py b/mediagoblin/tests/test_edit.py index 4740bd2a..4f44e0b9 100644 --- a/mediagoblin/tests/test_edit.py +++ b/mediagoblin/tests/test_edit.py @@ -27,7 +27,8 @@ class TestUserEdit(object): def setup(self): # set up new user self.user_password = u'toast' - self.user = fixture_add_user(password = self.user_password) + self.user = fixture_add_user(password = self.user_password, + privileges=[u'active']) def login(self, test_app): test_app.post( @@ -52,7 +53,8 @@ class TestUserEdit(object): # deleted too. Perhaps in submission test? #Restore user at end of test - self.user = fixture_add_user(password = self.user_password) + self.user = fixture_add_user(password = self.user_password, + privileges=[u'active']) self.login(test_app) @@ -80,7 +82,8 @@ class TestUserEdit(object): assert test_user.url == u'http://dustycloud.org/' # change a different user than the logged in (should fail with 403) - fixture_add_user(username=u"foo") + fixture_add_user(username=u"foo", + privileges=[u'active']) res = test_app.post( '/u/foo/edit/', { 'bio': u'I love toast!', diff --git a/mediagoblin/tests/test_modelmethods.py b/mediagoblin/tests/test_modelmethods.py index 427aa47c..86513c76 100644 --- a/mediagoblin/tests/test_modelmethods.py +++ b/mediagoblin/tests/test_modelmethods.py @@ -18,7 +18,7 @@ # methods, and so it makes sense to test them here. from mediagoblin.db.base import Session -from mediagoblin.db.models import MediaEntry +from mediagoblin.db.models import MediaEntry, User, Privilege from mediagoblin.tests.tools import fixture_add_user @@ -47,7 +47,7 @@ class TestMediaEntrySlugs(object): entry.id = this_id entry.uploader = uploader or self.chris_user.id entry.media_type = u'image' - + if save: entry.save() @@ -99,7 +99,7 @@ class TestMediaEntrySlugs(object): u"Beware, I exist!!", this_id=9000, save=False) entry.generate_slug() assert entry.slug == u"beware-i-exist-test" - + _real_test() def test_existing_slug_cant_use_id_extra_junk(self, test_app): @@ -151,6 +151,44 @@ class TestMediaEntrySlugs(object): qbert_entry.generate_slug() assert qbert_entry.slug is None +class TestUserHasPrivilege: + def _setup(self): + fixture_add_user(u'natalie', + privileges=[u'admin',u'moderator',u'active']) + fixture_add_user(u'aeva', + privileges=[u'moderator',u'active']) + self.natalie_user = User.query.filter( + User.username==u'natalie').first() + self.aeva_user = User.query.filter( + User.username==u'aeva').first() + + def test_privilege_added_correctly(self, test_app): + self._setup() + admin = Privilege.query.filter( + Privilege.privilege_name == u'admin').one() + # first make sure the privileges were added successfully + + assert admin in self.natalie_user.all_privileges + assert admin not in self.aeva_user.all_privileges + + def test_user_has_privilege_one(self, test_app): + self._setup() + + # then test out the user.has_privilege method for one privilege + assert not self.natalie_user.has_privilege(u'commenter') + assert self.aeva_user.has_privilege(u'active') + + + def test_user_has_privileges_multiple(self, test_app): + self._setup() + + # when multiple args are passed to has_privilege, the method returns + # True if the user has ANY of the privileges + assert self.natalie_user.has_privilege(u'admin',u'commenter') + assert self.aeva_user.has_privilege(u'moderator',u'active') + assert not self.natalie_user.has_privilege(u'commenter',u'uploader') + + def test_media_data_init(test_app): Session.rollback() @@ -165,3 +203,4 @@ def test_media_data_init(test_app): obj_in_session += 1 print repr(obj) assert obj_in_session == 0 + diff --git a/mediagoblin/tests/test_moderation.py b/mediagoblin/tests/test_moderation.py new file mode 100644 index 00000000..e7a0ebef --- /dev/null +++ b/mediagoblin/tests/test_moderation.py @@ -0,0 +1,242 @@ +# 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 pytest + +from mediagoblin.tests.tools import (fixture_add_user, + fixture_add_comment_report, fixture_add_comment) +from mediagoblin.db.models import User, CommentReport, MediaComment, UserBan +from mediagoblin.tools import template, mail +from webtest import AppError + +class TestModerationViews: + @pytest.fixture(autouse=True) + def _setup(self, test_app): + self.test_app = test_app + + fixture_add_user(u'admin', + privileges=[u'admin',u'active']) + fixture_add_user(u'moderator', + privileges=[u'moderator',u'active']) + fixture_add_user(u'regular', + privileges=[u'active',u'commenter']) + self.query_for_users() + + def login(self, username): + self.test_app.post( + '/auth/login/', { + 'username': username, + 'password': 'toast'}) + self.query_for_users() + + def logout(self): + self.test_app.get('/auth/logout/') + self.query_for_users() + + def query_for_users(self): + self.admin_user = User.query.filter(User.username==u'admin').first() + self.mod_user = User.query.filter(User.username==u'moderator').first() + self.user = User.query.filter(User.username==u'regular').first() + + def do_post(self, data, *context_keys, **kwargs): + url = kwargs.pop('url', '/submit/') + do_follow = kwargs.pop('do_follow', False) + template.clear_test_template_context() + response = self.test_app.post(url, data, **kwargs) + if do_follow: + response.follow() + context_data = template.TEMPLATE_TEST_CONTEXT + for key in context_keys: + context_data = context_data[key] + return response, context_data + + def testGiveOrTakeAwayPrivileges(self): + self.login(u'admin') + # First, test an admin taking away a privilege from a user + #---------------------------------------------------------------------- + response, context = self.do_post({'privilege_name':u'commenter'}, + url='/mod/users/{0}/privilege/'.format(self.user.username)) + assert response.status == '302 FOUND' + self.query_for_users() + assert not self.user.has_privilege(u'commenter') + + # Then, test an admin giving a privilege to a user + #---------------------------------------------------------------------- + response, context = self.do_post({'privilege_name':u'commenter'}, + url='/mod/users/{0}/privilege/'.format(self.user.username)) + assert response.status == '302 FOUND' + self.query_for_users() + assert self.user.has_privilege(u'commenter') + + # Then, test a mod trying to take away a privilege from a user + # they are not allowed to do this, so this will raise an error + #---------------------------------------------------------------------- + self.logout() + self.login(u'moderator') + + with pytest.raises(AppError) as excinfo: + response, context = self.do_post({'privilege_name':u'commenter'}, + url='/mod/users/{0}/privilege/'.format(self.user.username)) + assert 'Bad response: 403 FORBIDDEN' in str(excinfo) + self.query_for_users() + + assert self.user.has_privilege(u'commenter') + + def testReportResolution(self): + self.login(u'moderator') + + # First, test a moderators taking away a user's privilege in response + # to a reported comment + #---------------------------------------------------------------------- + fixture_add_comment_report(reported_user=self.user) + comment_report = CommentReport.query.filter( + CommentReport.reported_user==self.user).first() + + response = self.test_app.get('/mod/reports/{0}/'.format( + comment_report.id)) + assert response.status == '200 OK' + self.query_for_users() + comment_report = CommentReport.query.filter( + CommentReport.reported_user==self.user).first() + + response, context = self.do_post({'action_to_resolve':[u'takeaway'], + 'take_away_privileges':[u'commenter'], + 'targeted_user':self.user.id}, + url='/mod/reports/{0}/'.format(comment_report.id)) + + self.query_for_users() + comment_report = CommentReport.query.filter( + CommentReport.reported_user==self.user).first() + assert response.status == '302 FOUND' + assert not self.user.has_privilege(u'commenter') + assert comment_report.is_archived_report() is True + + fixture_add_comment_report(reported_user=self.user) + comment_report = CommentReport.query.filter( + CommentReport.reported_user==self.user).first() + + # Then, test a moderator sending an email to a user in response to a + # reported comment + #---------------------------------------------------------------------- + self.query_for_users() + + response, context = self.do_post({'action_to_resolve':[u'sendmessage'], + 'message_to_user':'This is your last warning, regular....', + 'targeted_user':self.user.id}, + url='/mod/reports/{0}/'.format(comment_report.id)) + + self.query_for_users() + comment_report = CommentReport.query.filter( + CommentReport.reported_user==self.user).first() + assert response.status == '302 FOUND' + assert mail.EMAIL_TEST_MBOX_INBOX == [{'to': [u'regular@example.com'], + 'message': 'Content-Type: text/plain; charset="utf-8"\n\ +MIME-Version: 1.0\nContent-Transfer-Encoding: base64\nSubject: Warning from- \ +moderator \nFrom: notice@mediagoblin.example.org\nTo: regular@example.com\n\n\ +VGhpcyBpcyB5b3VyIGxhc3Qgd2FybmluZywgcmVndWxhci4uLi4=\n', + 'from': 'notice@mediagoblin.example.org'}] + assert comment_report.is_archived_report() is True + + # Then test a moderator banning a user AND a moderator deleting the + # offending comment. This also serves as a test for taking multiple + # actions to resolve a report + #---------------------------------------------------------------------- + self.query_for_users() + fixture_add_comment(author=self.user.id, + comment=u'Comment will be removed') + test_comment = MediaComment.query.filter( + MediaComment.author==self.user.id).first() + fixture_add_comment_report(comment=test_comment, + reported_user=self.user) + comment_report = CommentReport.query.filter( + CommentReport.comment==test_comment).filter( + CommentReport.resolved==None).first() + + response, context = self.do_post( + {'action_to_resolve':[u'userban', u'delete'], + 'targeted_user':self.user.id, + 'why_user_was_banned':u'', + 'user_banned_until':u''}, + url='/mod/reports/{0}/'.format(comment_report.id)) + assert response.status == '302 FOUND' + self.query_for_users() + test_user_ban = UserBan.query.filter( + UserBan.user_id == self.user.id).first() + assert test_user_ban is not None + test_comment = MediaComment.query.filter( + MediaComment.author==self.user.id).first() + assert test_comment is None + + # Then, test what happens when a moderator attempts to punish an admin + # from a reported comment on an admin. + #---------------------------------------------------------------------- + fixture_add_comment_report(reported_user=self.admin_user) + comment_report = CommentReport.query.filter( + CommentReport.reported_user==self.admin_user).filter( + CommentReport.resolved==None).first() + + response, context = self.do_post({'action_to_resolve':[u'takeaway'], + 'take_away_privileges':[u'active'], + 'targeted_user':self.admin_user.id}, + url='/mod/reports/{0}/'.format(comment_report.id)) + self.query_for_users() + + assert response.status == '200 OK' + assert self.admin_user.has_privilege(u'active') + + def testAllModerationViews(self): + self.login(u'moderator') + username = self.user.username + self.query_for_users() + fixture_add_comment_report(reported_user=self.admin_user) + response = self.test_app.get('/mod/reports/') + assert response.status == "200 OK" + + response = self.test_app.get('/mod/reports/1/') + assert response.status == "200 OK" + + response = self.test_app.get('/mod/users/') + assert response.status == "200 OK" + + user_page_url = '/mod/users/{0}/'.format(username) + response = self.test_app.get(user_page_url) + assert response.status == "200 OK" + + self.test_app.get('/mod/media/') + assert response.status == "200 OK" + + def testBanUnBanUser(self): + self.login(u'admin') + username = self.user.username + user_id = self.user.id + ban_url = '/mod/users/{0}/ban/'.format(username) + response, context = self.do_post({ + 'user_banned_until':u'', + 'why_user_was_banned':u'Because I said so'}, + url=ban_url) + + assert response.status == "302 FOUND" + user_banned = UserBan.query.filter(UserBan.user_id==user_id).first() + assert user_banned is not None + assert user_banned.expiration_date is None + assert user_banned.reason == u'Because I said so' + + response, context = self.do_post({}, + url=ban_url) + + assert response.status == "302 FOUND" + user_banned = UserBan.query.filter(UserBan.user_id==user_id).first() + assert user_banned is None diff --git a/mediagoblin/tests/test_notifications.py b/mediagoblin/tests/test_notifications.py index e075d475..3bf36f5f 100644 --- a/mediagoblin/tests/test_notifications.py +++ b/mediagoblin/tests/test_notifications.py @@ -38,7 +38,7 @@ class TestNotifications: # TODO: Possibly abstract into a decorator like: # @as_authenticated_user('chris') - self.test_user = fixture_add_user() + self.test_user = fixture_add_user(privileges=[u'active',u'commenter']) self.current_user = None @@ -75,7 +75,10 @@ class TestNotifications: ''' user = fixture_add_user('otherperson', password='nosreprehto', - wants_comment_notification=wants_email) + wants_comment_notification=wants_email, + privileges=[u'active',u'commenter']) + + assert user.wants_comment_notification == wants_email user_id = user.id @@ -124,6 +127,7 @@ otherperson@example.com\n\nSGkgb3RoZXJwZXJzb24sCmNocmlzIGNvbW1lbnRlZCBvbiB5b3VyI else: assert mail.EMAIL_TEST_MBOX_INBOX == [] + # Save the ids temporarily because of DetachedInstanceError notification_id = notification.id comment_id = notification.subject.id @@ -153,7 +157,8 @@ otherperson@example.com\n\nSGkgb3RoZXJwZXJzb24sCmNocmlzIGNvbW1lbnRlZCBvbiB5b3VyI def test_mark_all_comment_notifications_seen(self): """ Test that mark_all_comments_seen works""" - user = fixture_add_user('otherperson', password='nosreprehto') + user = fixture_add_user('otherperson', password='nosreprehto', + privileges=[u'active']) media_entry = fixture_media_entry(uploader=user.id, state=u'processed') diff --git a/mediagoblin/tests/test_oauth2.py b/mediagoblin/tests/test_oauth2.py index 86f9e8cc..957f4e65 100644 --- a/mediagoblin/tests/test_oauth2.py +++ b/mediagoblin/tests/test_oauth2.py @@ -38,7 +38,8 @@ class TestOAuth(object): self.pman = pluginapi.PluginManager() self.user_password = u'4cc355_70k3N' - self.user = fixture_add_user(u'joauth', self.user_password) + self.user = fixture_add_user(u'joauth', self.user_password, + privileges=[u'active']) self.login() diff --git a/mediagoblin/tests/test_openid.py b/mediagoblin/tests/test_openid.py index 0ddfeff9..0424fdda 100644 --- a/mediagoblin/tests/test_openid.py +++ b/mediagoblin/tests/test_openid.py @@ -238,7 +238,7 @@ class TestOpenIDPlugin(object): def test_add_delete(self, openid_plugin_app): """Test adding and deleting openids""" # Add user - test_user = fixture_add_user(password='') + test_user = fixture_add_user(password='', privileges=[u'active']) openid = OpenIDUserURL() openid.openid_url = 'http://real.myopenid.com' openid.user_id = test_user.id diff --git a/mediagoblin/tests/test_persona.py b/mediagoblin/tests/test_persona.py index 7707618b..a1cd30eb 100644 --- a/mediagoblin/tests/test_persona.py +++ b/mediagoblin/tests/test_persona.py @@ -22,6 +22,7 @@ pytest.importorskip("requests") from mediagoblin import mg_globals from mediagoblin.db.base import Session +from mediagoblin.db.models import Privilege from mediagoblin.tests.tools import get_app from mediagoblin.tools import template @@ -112,8 +113,9 @@ class TestPersonaPlugin(object): # Get user and detach from session test_user = mg_globals.database.User.query.filter_by( username=u'chris').first() - test_user.email_verified = True - test_user.status = u'active' + active_privilege = Privilege.query.filter( + Privilege.privilege_name==u'active').first() + test_user.all_privileges.append(active_privilege) test_user.save() test_user = mg_globals.database.User.query.filter_by( username=u'chris').first() diff --git a/mediagoblin/tests/test_privileges.py b/mediagoblin/tests/test_privileges.py new file mode 100644 index 00000000..05829b34 --- /dev/null +++ b/mediagoblin/tests/test_privileges.py @@ -0,0 +1,205 @@ +# 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 pytest +from datetime import date, timedelta +from webtest import AppError + +from mediagoblin.tests.tools import fixture_add_user, fixture_media_entry + +from mediagoblin.db.models import User, UserBan +from mediagoblin.tools import template + +from .resources import GOOD_JPG + +class TestPrivilegeFunctionality: + + @pytest.fixture(autouse=True) + def _setup(self, test_app): + self.test_app = test_app + + fixture_add_user(u'alex', + privileges=[u'admin',u'active']) + fixture_add_user(u'meow', + privileges=[u'moderator',u'active',u'reporter']) + fixture_add_user(u'natalie', + privileges=[u'active']) + self.query_for_users() + + def login(self, username): + self.test_app.post( + '/auth/login/', { + 'username': username, + 'password': 'toast'}) + self.query_for_users() + + def logout(self): + self.test_app.get('/auth/logout/') + self.query_for_users() + + def do_post(self, data, *context_keys, **kwargs): + url = kwargs.pop('url', '/submit/') + do_follow = kwargs.pop('do_follow', False) + template.clear_test_template_context() + response = self.test_app.post(url, data, **kwargs) + if do_follow: + response.follow() + context_data = template.TEMPLATE_TEST_CONTEXT + for key in context_keys: + context_data = context_data[key] + return response, context_data + + def query_for_users(self): + self.admin_user = User.query.filter(User.username==u'alex').first() + self.mod_user = User.query.filter(User.username==u'meow').first() + self.user = User.query.filter(User.username==u'natalie').first() + + def testUserBanned(self): + self.login(u'natalie') + uid = self.user.id + # First, test what happens when a user is banned indefinitely + #---------------------------------------------------------------------- + user_ban = UserBan(user_id=uid, + reason=u'Testing whether user is banned', + expiration_date=None) + user_ban.save() + + response = self.test_app.get('/') + assert response.status == "200 OK" + assert "You are Banned" in response.body + # Then test what happens when that ban has an expiration date which + # hasn't happened yet + #---------------------------------------------------------------------- + user_ban = UserBan.query.get(uid) + user_ban.delete() + user_ban = UserBan(user_id=uid, + reason=u'Testing whether user is banned', + expiration_date= date.today() + timedelta(days=20)) + user_ban.save() + + response = self.test_app.get('/') + assert response.status == "200 OK" + assert "You are Banned" in response.body + + # Then test what happens when that ban has an expiration date which + # has already happened + #---------------------------------------------------------------------- + user_ban = UserBan.query.get(uid) + user_ban.delete() + exp_date = date.today() - timedelta(days=20) + user_ban = UserBan(user_id=uid, + reason=u'Testing whether user is banned', + expiration_date= exp_date) + user_ban.save() + + response = self.test_app.get('/') + assert response.status == "302 FOUND" + assert not "You are Banned" in response.body + + def testVariousPrivileges(self): + # The various actions that require privileges (ex. reporting, + # commenting, moderating...) are tested in other tests. This method + # will be used to ensure that those actions are impossible for someone + # without the proper privileges. + # For other tests that show what happens when a user has the proper + # privileges, check out: + # tests/test_moderation.py moderator + # tests/test_notifications.py commenter + # tests/test_reporting.py reporter + # tests/test_submission.py uploader + #---------------------------------------------------------------------- + self.login(u'natalie') + + # First test the get and post requests of submission/uploading + #---------------------------------------------------------------------- + with pytest.raises(AppError) as excinfo: + response = self.test_app.get('/submit/') + assert 'Bad response: 403 FORBIDDEN' in str(excinfo) + + + with pytest.raises(AppError) as excinfo: + response = self.do_post({'upload_files':[('file',GOOD_JPG)], + 'title':u'Normal Upload 1'}, + url='/submit/') + assert 'Bad response: 403 FORBIDDEN' in str(excinfo) + + # Test that a user cannot comment without the commenter privilege + #---------------------------------------------------------------------- + self.query_for_users() + + media_entry = fixture_media_entry(uploader=self.admin_user.id, + state=u'processed') + + media_entry_id = media_entry.id + media_uri_id = '/u/{0}/m/{1}/'.format(self.admin_user.username, + media_entry.id) + media_uri_slug = '/u/{0}/m/{1}/'.format(self.admin_user.username, + media_entry.slug) + response = self.test_app.get(media_uri_slug) + assert not "Add a comment" in response.body + + self.query_for_users() + with pytest.raises(AppError) as excinfo: + response = self.test_app.post( + media_uri_id + 'comment/add/', + {'comment_content': u'Test comment #42'}) + assert 'Bad response: 403 FORBIDDEN' in str(excinfo) + + # Test that a user cannot report without the reporter privilege + #---------------------------------------------------------------------- + with pytest.raises(AppError) as excinfo: + response = self.test_app.get(media_uri_slug+"report/") + assert 'Bad response: 403 FORBIDDEN' in str(excinfo) + + with pytest.raises(AppError) as excinfo: + response = self.do_post( + {'report_reason':u'Testing Reports #1', + 'reporter_id':u'3'}, + url=(media_uri_slug+"report/")) + assert 'Bad response: 403 FORBIDDEN' in str(excinfo) + + # Test that a user cannot access the moderation pages w/o moderator + # or admin privileges + #---------------------------------------------------------------------- + with pytest.raises(AppError) as excinfo: + response = self.test_app.get("/mod/users/") + assert 'Bad response: 403 FORBIDDEN' in str(excinfo) + + with pytest.raises(AppError) as excinfo: + response = self.test_app.get("/mod/reports/") + assert 'Bad response: 403 FORBIDDEN' in str(excinfo) + + with pytest.raises(AppError) as excinfo: + response = self.test_app.get("/mod/media/") + assert 'Bad response: 403 FORBIDDEN' in str(excinfo) + + with pytest.raises(AppError) as excinfo: + response = self.test_app.get("/mod/users/1/") + assert 'Bad response: 403 FORBIDDEN' in str(excinfo) + + with pytest.raises(AppError) as excinfo: + response = self.test_app.get("/mod/reports/1/") + assert 'Bad response: 403 FORBIDDEN' in str(excinfo) + + self.query_for_users() + + with pytest.raises(AppError) as excinfo: + response, context = self.do_post({'action_to_resolve':[u'takeaway'], + 'take_away_privileges':[u'active'], + 'targeted_user':self.admin_user.id}, + url='/mod/reports/1/') + self.query_for_users() + assert 'Bad response: 403 FORBIDDEN' in str(excinfo) diff --git a/mediagoblin/tests/test_reporting.py b/mediagoblin/tests/test_reporting.py new file mode 100644 index 00000000..a154a061 --- /dev/null +++ b/mediagoblin/tests/test_reporting.py @@ -0,0 +1,167 @@ +# 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 pytest + +from mediagoblin.tools import template +from mediagoblin.tests.tools import (fixture_add_user, fixture_media_entry, + fixture_add_comment, fixture_add_comment_report) +from mediagoblin.db.models import (MediaReport, CommentReport, User, + MediaComment) + + +class TestReportFiling: + @pytest.fixture(autouse=True) + def _setup(self, test_app): + self.test_app = test_app + + fixture_add_user(u'allie', + privileges=[u'reporter',u'active']) + fixture_add_user(u'natalie', + privileges=[u'active', u'moderator']) + + def login(self, username): + self.test_app.post( + '/auth/login/', { + 'username': username, + 'password': 'toast'}) + + def logout(self): + self.test_app.get('/auth/logout/') + + def do_post(self, data, *context_keys, **kwargs): + url = kwargs.pop('url', '/submit/') + do_follow = kwargs.pop('do_follow', False) + template.clear_test_template_context() + response = self.test_app.post(url, data, **kwargs) + if do_follow: + response.follow() + context_data = template.TEMPLATE_TEST_CONTEXT + for key in context_keys: + context_data = context_data[key] + return response, context_data + + def query_for_users(self): + return (User.query.filter(User.username==u'allie').first(), + User.query.filter(User.username==u'natalie').first()) + + def testMediaReports(self): + self.login(u'allie') + allie_user, natalie_user = self.query_for_users() + allie_id = allie_user.id + + media_entry = fixture_media_entry(uploader=natalie_user.id, + state=u'processed') + + mid = media_entry.id + media_uri_slug = '/u/{0}/m/{1}/'.format(natalie_user.username, + media_entry.slug) + + response = self.test_app.get(media_uri_slug + "report/") + assert response.status == "200 OK" + + response, context = self.do_post( + {'report_reason':u'Testing Media Report', + 'reporter_id':unicode(allie_id)},url= media_uri_slug + "report/") + + assert response.status == "302 FOUND" + + media_report = MediaReport.query.first() + + allie_user, natalie_user = self.query_for_users() + assert media_report is not None + assert media_report.report_content == u'Testing Media Report' + assert media_report.reporter_id == allie_id + assert media_report.reported_user_id == natalie_user.id + assert media_report.created is not None + assert media_report.discriminator == 'media_report' + + def testCommentReports(self): + self.login(u'allie') + allie_user, natalie_user = self.query_for_users() + allie_id = allie_user.id + + media_entry = fixture_media_entry(uploader=natalie_user.id, + state=u'processed') + mid = media_entry.id + fixture_add_comment(media_entry=mid, + author=natalie_user.id) + comment = MediaComment.query.first() + + comment_uri_slug = '/u/{0}/m/{1}/c/{2}/'.format(natalie_user.username, + media_entry.slug, + comment.id) + + response = self.test_app.get(comment_uri_slug + "report/") + assert response.status == "200 OK" + + response, context = self.do_post({ + 'report_reason':u'Testing Comment Report', + 'reporter_id':unicode(allie_id)},url= comment_uri_slug + "report/") + + assert response.status == "302 FOUND" + + comment_report = CommentReport.query.first() + + allie_user, natalie_user = self.query_for_users() + assert comment_report is not None + assert comment_report.report_content == u'Testing Comment Report' + assert comment_report.reporter_id == allie_id + assert comment_report.reported_user_id == natalie_user.id + assert comment_report.created is not None + assert comment_report.discriminator == 'comment_report' + + def testArchivingReports(self): + self.login(u'natalie') + allie_user, natalie_user = self.query_for_users() + allie_id, natalie_id = allie_user.id, natalie_user.id + + fixture_add_comment(author=allie_user.id, + comment=u'Comment will be removed') + test_comment = MediaComment.query.filter( + MediaComment.author==allie_user.id).first() + fixture_add_comment_report(comment=test_comment, + reported_user=allie_user, + report_content=u'Testing Archived Reports #1', + reporter=natalie_user) + comment_report = CommentReport.query.filter( + CommentReport.reported_user==allie_user).first() + + assert comment_report.report_content == u'Testing Archived Reports #1' + response, context = self.do_post( + {'action_to_resolve':[u'userban', u'delete'], + 'targeted_user':allie_user.id, + 'resolution_content':u'This is a test of archiving reports.'}, + url='/mod/reports/{0}/'.format(comment_report.id)) + + assert response.status == "302 FOUND" + allie_user, natalie_user = self.query_for_users() + + archived_report = CommentReport.query.filter( + CommentReport.reported_user==allie_user).first() + + assert CommentReport.query.count() != 0 + assert archived_report is not None + assert archived_report.report_content == u'Testing Archived Reports #1' + assert archived_report.reporter_id == natalie_id + assert archived_report.reported_user_id == allie_id + assert archived_report.created is not None + assert archived_report.resolved is not None + assert archived_report.result == u'''This is a test of archiving reports. +natalie banned user allie indefinitely. +natalie deleted the comment.''' + assert archived_report.discriminator == 'comment_report' + diff --git a/mediagoblin/tests/test_submission.py b/mediagoblin/tests/test_submission.py index 7f4e8086..5d42c5a5 100644 --- a/mediagoblin/tests/test_submission.py +++ b/mediagoblin/tests/test_submission.py @@ -47,12 +47,22 @@ class TestSubmission: # TODO: Possibly abstract into a decorator like: # @as_authenticated_user('chris') - test_user = fixture_add_user() - - self.test_user = test_user + fixture_add_user(privileges=[u'active',u'uploader', u'commenter']) self.login() + def our_user(self): + """ + Fetch the user we're submitting with. Every .get() or .post() + invalidates the session; this is a hacky workaround. + """ + #### FIXME: Pytest collects this as a test and runs this. + #### ... it shouldn't. At least it passes, but that's + #### totally stupid. + #### Also if we found a way to make this run it should be a + #### property. + return User.query.filter(User.username==u'chris').first() + def login(self): self.test_app.post( '/auth/login/', { @@ -98,10 +108,10 @@ class TestSubmission: def check_normal_upload(self, title, filename): response, context = self.do_post({'title': title}, do_follow=True, **self.upload_data(filename)) - self.check_url(response, '/u/{0}/'.format(self.test_user.username)) + self.check_url(response, '/u/{0}/'.format(self.our_user().username)) assert 'mediagoblin/user_pages/user.html' in context # Make sure the media view is at least reachable, logged in... - url = '/u/{0}/m/{1}/'.format(self.test_user.username, + url = '/u/{0}/m/{1}/'.format(self.our_user().username, title.lower().replace(' ', '-')) self.test_app.get(url) # ... and logged out too. @@ -148,7 +158,7 @@ class TestSubmission: response, context = self.do_post({'title': u'Normal upload 3 (pdf)'}, do_follow=True, **self.upload_data(GOOD_PDF)) - self.check_url(response, '/u/{0}/'.format(self.test_user.username)) + self.check_url(response, '/u/{0}/'.format(self.our_user().username)) assert 'mediagoblin/user_pages/user.html' in context def test_default_upload_limits(self): @@ -264,7 +274,7 @@ class TestSubmission: # render and post to the edit page. edit_url = request.urlgen( 'mediagoblin.edit.edit_media', - user=self.test_user.username, media_id=media_id) + user=self.our_user().username, media_id=media_id) self.test_app.get(edit_url) self.test_app.post(edit_url, {'title': u'Balanced Goblin', @@ -277,7 +287,7 @@ class TestSubmission: self.check_comments(request, media_id, 0) comment_url = request.urlgen( 'mediagoblin.user_pages.media_post_comment', - user=self.test_user.username, media_id=media_id) + user=self.our_user().username, media_id=media_id) response = self.do_post({'comment_content': 'i love this test'}, url=comment_url, do_follow=True)[0] self.check_comments(request, media_id, 1) @@ -286,7 +296,7 @@ class TestSubmission: # --------------------------------------------------- delete_url = request.urlgen( 'mediagoblin.user_pages.media_confirm_delete', - user=self.test_user.username, media_id=media_id) + user=self.our_user().username, media_id=media_id) # Empty data means don't confirm response = self.do_post({}, do_follow=True, url=delete_url)[0] media = self.check_media(request, {'title': u'Balanced Goblin'}, 1) @@ -359,7 +369,7 @@ class TestSubmission: # they'll be caught as failures during the processing step. response, context = self.do_post({'title': title}, do_follow=True, **self.upload_data(filename)) - self.check_url(response, '/u/{0}/'.format(self.test_user.username)) + self.check_url(response, '/u/{0}/'.format(self.our_user().username)) entry = mg_globals.database.MediaEntry.query.filter_by(title=title).first() assert entry.state == 'failed' assert entry.fail_error == u'mediagoblin.processing:BadMediaFail' diff --git a/mediagoblin/tests/tools.py b/mediagoblin/tests/tools.py index 98361adc..060dfda9 100644 --- a/mediagoblin/tests/tools.py +++ b/mediagoblin/tests/tools.py @@ -25,7 +25,7 @@ from webtest import TestApp from mediagoblin import mg_globals from mediagoblin.db.models import User, MediaEntry, Collection, MediaComment, \ - CommentSubscription, CommentNotification + CommentSubscription, CommentNotification, Privilege, CommentReport from mediagoblin.tools import testing from mediagoblin.init.config import read_mediagoblin_config from mediagoblin.db.base import Session @@ -33,6 +33,8 @@ from mediagoblin.meddleware import BaseMeddleware from mediagoblin.auth import gen_password_hash from mediagoblin.gmg_commands.dbupdate import run_dbupdate +from datetime import datetime + MEDIAGOBLIN_TEST_DB_NAME = u'__mediagoblin_tests__' TEST_SERVER_CONFIG = pkg_resources.resource_filename( @@ -133,7 +135,6 @@ def get_app(request, paste_config=None, mgoblin_config=None): mg_globals.app.meddleware.insert(0, TestingMeddleware(mg_globals.app)) app = TestApp(test_app) - return app @@ -170,7 +171,7 @@ def assert_db_meets_expected(db, expected): def fixture_add_user(username=u'chris', password=u'toast', - active_user=True, wants_comment_notification=True): + privileges=[], wants_comment_notification=True): # Reuse existing user or create a new one test_user = User.query.filter_by(username=username).first() if test_user is None: @@ -179,14 +180,13 @@ def fixture_add_user(username=u'chris', password=u'toast', test_user.email = username + u'@example.com' if password is not None: test_user.pw_hash = gen_password_hash(password) - if active_user: - test_user.email_verified = True - test_user.status = u'active' - test_user.wants_comment_notification = wants_comment_notification + for privilege in privileges: + query = Privilege.query.filter(Privilege.privilege_name==privilege) + if query.count(): + test_user.all_privileges.append(query.one()) test_user.save() - # Reload test_user = User.query.filter_by(username=username).first() @@ -314,3 +314,32 @@ def fixture_add_comment(author=None, media_entry=None, comment=None): return comment +def fixture_add_comment_report(comment=None, reported_user=None, + reporter=None, created=None, report_content=None): + if comment is None: + comment = fixture_add_comment() + + if reported_user is None: + reported_user = fixture_add_user() + + if reporter is None: + reporter = fixture_add_user() + + if created is None: + created=datetime.now() + + if report_content is None: + report_content = \ + 'Auto-generated test report' + + comment_report = CommentReport(comment=comment, + reported_user = reported_user, + reporter = reporter, + created = created, + report_content=report_content) + + comment_report.save() + + Session.expunge(comment_report) + + return comment_report diff --git a/mediagoblin/tools/response.py b/mediagoblin/tools/response.py index b0401e08..cd99a230 100644 --- a/mediagoblin/tools/response.py +++ b/mediagoblin/tools/response.py @@ -21,6 +21,8 @@ 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, User +from datetime import date class Response(wz_Response): """Set default response mimetype to HTML, otherwise we get text/plain""" @@ -50,7 +52,8 @@ def render_400(request, err_msg=None): _ = pass_to_ugettext title = _("Bad Request") if err_msg is None: - err_msg = _("The request sent to the server is invalid, please double check it") + err_msg = _("The request sent to the server is invalid, \ +please double check it") return render_error(request, 400, title, err_msg) @@ -71,6 +74,21 @@ 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) + if (user_ban.expiration_date is not None and + date.today()>user_ban.expiration_date): + + user_ban.delete() + return redirect(request, + 'index') + 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 @@ -126,7 +144,7 @@ def json_response(serializable, _disable_cors=False, *args, **kw): Any extra arguments and keyword arguments are passed to the Response.__init__ method. ''' - + response = wz_Response(json.dumps(serializable), *args, content_type='application/json', **kw) if not _disable_cors: diff --git a/mediagoblin/user_pages/forms.py b/mediagoblin/user_pages/forms.py index ac8084c5..eb786f47 100644 --- a/mediagoblin/user_pages/forms.py +++ b/mediagoblin/user_pages/forms.py @@ -49,3 +49,15 @@ class MediaCollectForm(wtforms.Form): description=_("""You can use <a href="http://daringfireball.net/projects/markdown/basics" target="_blank"> Markdown</a> for formatting.""")) + +class CommentReportForm(wtforms.Form): + report_reason = wtforms.TextAreaField( + _('Reason for Reporting'), + [wtforms.validators.Required()]) + reporter_id = wtforms.HiddenField('') + +class MediaReportForm(wtforms.Form): + report_reason = wtforms.TextAreaField( + _('Reason for Reporting'), + [wtforms.validators.Required()]) + reporter_id = wtforms.HiddenField('') diff --git a/mediagoblin/user_pages/lib.py b/mediagoblin/user_pages/lib.py index 2f47e4b1..80eb30bd 100644 --- a/mediagoblin/user_pages/lib.py +++ b/mediagoblin/user_pages/lib.py @@ -19,7 +19,9 @@ from mediagoblin.tools.template import render_template from mediagoblin.tools.translate import pass_to_ugettext as _ from mediagoblin import mg_globals from mediagoblin.db.base import Session -from mediagoblin.db.models import CollectionItem +from mediagoblin.db.models import (CollectionItem, MediaReport, CommentReport, + MediaComment, MediaEntry) +from mediagoblin.user_pages import forms as user_forms def send_comment_email(user, comment, media, request): @@ -75,3 +77,44 @@ def add_media_to_collection(collection, media, note=None, commit=True): if commit: Session.commit() + +def build_report_object(report_form, media_entry=None, comment=None): + """ + This function is used to convert a form object (from a User filing a + report) into either a MediaReport or CommentReport object. + + :param report_form A MediaReportForm or a CommentReportForm object + with valid information from a POST request. + :param media_entry A MediaEntry object. The MediaEntry being repo- + -rted by a MediaReport. In a CommentReport, + this will be None. + :param comment A MediaComment object. The MediaComment being + reported by a CommentReport. In a MediaReport + this will be None. + + :returns A MediaReport object if a valid MediaReportForm is + passed as kwarg media_entry. This MediaReport has + not been saved. + :returns A CommentReport object if a valid CommentReportForm + is passed as kwarg comment. This CommentReport + has not been saved. + :returns None if the form_dict is invalid. + """ + + if report_form.validate() and comment is not None: + report_object = CommentReport() + report_object.comment_id = comment.id + report_object.reported_user_id = MediaComment.query.get( + comment.id).get_author.id + elif report_form.validate() and media_entry is not None: + report_object = MediaReport() + report_object.media_entry_id = media_entry.id + report_object.reported_user_id = MediaEntry.query.get( + media_entry.id).get_uploader.id + else: + return None + + report_object.report_content = report_form.report_reason.data + report_object.reporter_id = report_form.reporter_id.data + return report_object + diff --git a/mediagoblin/user_pages/routing.py b/mediagoblin/user_pages/routing.py index b1dde397..f0f4d8b7 100644 --- a/mediagoblin/user_pages/routing.py +++ b/mediagoblin/user_pages/routing.py @@ -23,6 +23,10 @@ add_route('mediagoblin.user_pages.media_home', '/u/<string:user>/m/<string:media>/', 'mediagoblin.user_pages.views:media_home') +add_route('mediagoblin.user_pages.media_home.report_media', + '/u/<string:user>/m/<string:media>/report/', + 'mediagoblin.user_pages.views:file_a_report') + add_route('mediagoblin.user_pages.media_confirm_delete', '/u/<string:user>/m/<int:media_id>/confirm-delete/', 'mediagoblin.user_pages.views:media_confirm_delete') @@ -44,6 +48,10 @@ add_route('mediagoblin.user_pages.media_home.view_comment', '/u/<string:user>/m/<string:media>/c/<int:comment>/', 'mediagoblin.user_pages.views:media_home') +add_route('mediagoblin.user_pages.media_home.report_comment', + '/u/<string:user>/m/<string:media>/c/<int:comment>/report/', + 'mediagoblin.user_pages.views:file_a_report') + # User's tags gallery add_route('mediagoblin.user_pages.user_tag_gallery', '/u/<string:user>/tag/<string:tag>/', diff --git a/mediagoblin/user_pages/views.py b/mediagoblin/user_pages/views.py index 974cb3c6..73823e4d 100644 --- a/mediagoblin/user_pages/views.py +++ b/mediagoblin/user_pages/views.py @@ -27,13 +27,16 @@ from mediagoblin.tools.text import cleaned_markdown_conversion from mediagoblin.tools.translate import pass_to_ugettext as _ from mediagoblin.tools.pagination import Pagination from mediagoblin.user_pages import forms as user_forms -from mediagoblin.user_pages.lib import add_media_to_collection +from mediagoblin.user_pages.lib import (send_comment_email, + add_media_to_collection, build_report_object) from mediagoblin.notifications import trigger_notification, \ add_comment_subscription, mark_comment_notification_seen + from mediagoblin.decorators import (uses_pagination, get_user_media_entry, - get_media_entry_by_id, + get_media_entry_by_id, user_has_privilege, user_not_banned, require_active_login, user_may_delete_media, user_may_alter_collection, - get_user_collection, get_user_collection_item, active_user_from_url) + get_user_collection, get_user_collection_item, active_user_from_url, + get_optional_media_comment_by_id, allow_reporting) from werkzeug.contrib.atom import AtomFeed from werkzeug.exceptions import MethodNotAllowed @@ -43,14 +46,14 @@ from werkzeug.wrappers import Response _log = logging.getLogger(__name__) _log.setLevel(logging.DEBUG) - +@user_not_banned @uses_pagination def user_home(request, page): """'Homepage' of a User()""" user = User.query.filter_by(username=request.matchdict['user']).first() if not user: return render_404(request) - elif user.status != u'active': + elif not user.has_privilege(u'active'): return render_to_response( request, 'mediagoblin/user_pages/user_nonactive.html', @@ -79,7 +82,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 +117,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): @@ -154,7 +157,7 @@ def media_home(request, media, page, **kwargs): @get_media_entry_by_id -@require_active_login +@user_has_privilege(u'commenter') def media_post_comment(request, media): """ recieves POST from a MediaEntry() comment form, saves the comment. @@ -165,7 +168,6 @@ def media_post_comment(request, media): comment = request.db.MediaComment() comment.media_entry = media.id comment.author = request.user.id - print request.form['comment_content'] comment.content = unicode(request.form['comment_content']) # Show error message if commenting is disabled. @@ -205,6 +207,7 @@ def media_preview_comment(request): return Response(json.dumps(cleancomment)) +@user_not_banned @get_media_entry_by_id @require_active_login def media_collect(request, media): @@ -316,7 +319,7 @@ def media_confirm_delete(request, media): _("The media was not deleted because you didn't check that you were sure.")) return redirect_obj(request, media) - if ((request.user.is_admin and + if ((request.user.has_privilege(u'admin') and request.user.id != media.uploader)): messages.add_message( request, messages.WARNING, @@ -329,7 +332,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): @@ -359,7 +362,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""" @@ -402,7 +405,7 @@ def collection_item_confirm_remove(request, collection_item): return redirect_obj(request, collection) - if ((request.user.is_admin and + if ((request.user.has_privilege(u'admin') and request.user.id != collection_item.in_collection.creator)): messages.add_message( request, messages.WARNING, @@ -450,7 +453,7 @@ def collection_confirm_delete(request, collection): return redirect_obj(request, collection) - if ((request.user.is_admin and + if ((request.user.has_privilege(u'admin') and request.user.id != collection.creator)): messages.add_message( request, messages.WARNING, @@ -472,9 +475,8 @@ def atom_feed(request): generates the atom feed with the newest images """ user = User.query.filter_by( - username = request.matchdict['user'], - status = u'active').first() - if not user: + username = request.matchdict['user']).first() + if not user or not user.has_privilege(u'active'): return render_404(request) cursor = MediaEntry.query.filter_by( @@ -535,9 +537,8 @@ def collection_atom_feed(request): generates the atom feed with the newest images from a collection """ user = User.query.filter_by( - username = request.matchdict['user'], - status = u'active').first() - if not user: + username = request.matchdict['user']).first() + if not user or not user.has_privilege(u'active'): return render_404(request) collection = Collection.query.filter_by( @@ -599,7 +600,6 @@ def collection_atom_feed(request): return feed.get_response() - @require_active_login def processing_panel(request): """ @@ -611,7 +611,7 @@ def processing_panel(request): # # Make sure we have permission to access this user's panel. Only # admins and this user herself should be able to do so. - if not (user.id == request.user.id or request.user.is_admin): + if not (user.id == request.user.id or request.user.has_privilege(u'admin')): # No? Simply redirect to this user's homepage. return redirect( request, 'mediagoblin.user_pages.user_home', @@ -643,3 +643,44 @@ def processing_panel(request): 'processing_entries': processing_entries, 'failed_entries': failed_entries, 'processed_entries': processed_entries}) + +@allow_reporting +@get_user_media_entry +@user_has_privilege(u'reporter') +@get_optional_media_comment_by_id +def file_a_report(request, media, comment): + """ + This view handles the filing of a MediaReport or a CommentReport. + """ + if comment is not None: + if not comment.get_media_entry.id == media.id: + return render_404(request) + + form = user_forms.CommentReportForm(request.form) + context = {'media': media, + 'comment':comment, + 'form':form} + else: + form = user_forms.MediaReportForm(request.form) + context = {'media': media, + 'form':form} + form.reporter_id.data = request.user.id + + + if request.method == "POST": + report_object = build_report_object(form, + media_entry=media, + comment=comment) + + # if the object was built successfully, report_table will not be None + if report_object: + report_object.save() + return redirect( + request, + 'index') + + + return render_to_response( + request, + 'mediagoblin/user_pages/report.html', + context) diff --git a/mediagoblin/views.py b/mediagoblin/views.py index 6acd7e96..1d7be813 100644 --- a/mediagoblin/views.py +++ b/mediagoblin/views.py @@ -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').\ @@ -44,3 +44,7 @@ def simple_template_render(request): template_name = request.matchdict['template'] return render_to_response( request, template_name, {}) + +def terms_of_service(request): + return render_to_response(request, + 'mediagoblin/terms_of_service.html', {}) |