aboutsummaryrefslogtreecommitdiffstats
path: root/mediagoblin
diff options
context:
space:
mode:
authorChristopher Allan Webber <cwebber@dustycloud.org>2013-10-07 15:48:33 -0500
committerChristopher Allan Webber <cwebber@dustycloud.org>2013-10-07 15:48:33 -0500
commit56c4ad89ebef32bd5a40c00d987811ce4501ce22 (patch)
tree7523b2b274ce512093aed3914520275c80fe7e8f /mediagoblin
parent345b958871c48df56b8d1acf9516bb9780aa6701 (diff)
parent0a24db84c5769149537b0d9bd8e3a88fe8e9522a (diff)
downloadmediagoblin-56c4ad89ebef32bd5a40c00d987811ce4501ce22.tar.lz
mediagoblin-56c4ad89ebef32bd5a40c00d987811ce4501ce22.tar.xz
mediagoblin-56c4ad89ebef32bd5a40c00d987811ce4501ce22.zip
Merge remote-tracking branch 'refs/remotes/tilly-q/OPW-Moderation-Update'
Conflicts: mediagoblin/templates/mediagoblin/user_pages/user.html mediagoblin/tests/test_auth.py mediagoblin/tests/test_submission.py
Diffstat (limited to 'mediagoblin')
-rw-r--r--mediagoblin/admin/views.py48
-rw-r--r--mediagoblin/auth/tools.py12
-rw-r--r--mediagoblin/auth/views.py23
-rw-r--r--mediagoblin/config_spec.ini3
-rw-r--r--mediagoblin/db/migrations.py202
-rw-r--r--mediagoblin/db/mixin.py1
-rw-r--r--mediagoblin/db/models.py239
-rw-r--r--mediagoblin/db/util.py1
-rw-r--r--mediagoblin/decorators.py130
-rw-r--r--mediagoblin/edit/lib.py2
-rw-r--r--mediagoblin/edit/views.py6
-rw-r--r--mediagoblin/gmg_commands/users.py18
-rw-r--r--mediagoblin/moderation/__init__.py (renamed from mediagoblin/admin/__init__.py)0
-rw-r--r--mediagoblin/moderation/forms.py148
-rw-r--r--mediagoblin/moderation/routing.py38
-rw-r--r--mediagoblin/moderation/tools.py217
-rw-r--r--mediagoblin/moderation/views.py219
-rw-r--r--mediagoblin/routing.py6
-rw-r--r--mediagoblin/static/css/base.css81
-rw-r--r--mediagoblin/static/images/icon_clipboard.pngbin0 -> 682 bytes
-rw-r--r--mediagoblin/static/images/icon_clipboard_alert.pngbin0 -> 647 bytes
-rw-r--r--mediagoblin/static/js/setup_report_forms.js67
-rw-r--r--mediagoblin/submit/views.py3
-rw-r--r--mediagoblin/templates/mediagoblin/banned.html35
-rw-r--r--mediagoblin/templates/mediagoblin/base.html36
-rw-r--r--mediagoblin/templates/mediagoblin/moderation/media_panel.html (renamed from mediagoblin/templates/mediagoblin/admin/panel.html)5
-rw-r--r--mediagoblin/templates/mediagoblin/moderation/report.html154
-rw-r--r--mediagoblin/templates/mediagoblin/moderation/report_panel.html200
-rw-r--r--mediagoblin/templates/mediagoblin/moderation/user.html204
-rw-r--r--mediagoblin/templates/mediagoblin/moderation/user_panel.html97
-rw-r--r--mediagoblin/templates/mediagoblin/terms_of_service.html291
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/collection.html2
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/collection_list.html2
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/media.html21
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/report.html83
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/user.html2
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/user_nonactive.html2
-rw-r--r--mediagoblin/templates/mediagoblin/utils/collection_gallery.html2
-rw-r--r--mediagoblin/templates/mediagoblin/utils/report.html (renamed from mediagoblin/admin/routing.py)16
-rw-r--r--mediagoblin/tests/test_api.py3
-rw-r--r--mediagoblin/tests/test_auth.py48
-rw-r--r--mediagoblin/tests/test_edit.py9
-rw-r--r--mediagoblin/tests/test_modelmethods.py45
-rw-r--r--mediagoblin/tests/test_moderation.py242
-rw-r--r--mediagoblin/tests/test_notifications.py11
-rw-r--r--mediagoblin/tests/test_oauth2.py3
-rw-r--r--mediagoblin/tests/test_openid.py2
-rw-r--r--mediagoblin/tests/test_persona.py6
-rw-r--r--mediagoblin/tests/test_privileges.py205
-rw-r--r--mediagoblin/tests/test_reporting.py167
-rw-r--r--mediagoblin/tests/test_submission.py30
-rw-r--r--mediagoblin/tests/tools.py45
-rw-r--r--mediagoblin/tools/response.py22
-rw-r--r--mediagoblin/user_pages/forms.py12
-rw-r--r--mediagoblin/user_pages/lib.py45
-rw-r--r--mediagoblin/user_pages/routing.py8
-rw-r--r--mediagoblin/user_pages/views.py85
-rw-r--r--mediagoblin/views.py8
58 files changed, 3406 insertions, 206 deletions
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
new file mode 100644
index 00000000..6f94498b
--- /dev/null
+++ b/mediagoblin/static/images/icon_clipboard.png
Binary files differ
diff --git a/mediagoblin/static/images/icon_clipboard_alert.png b/mediagoblin/static/images/icon_clipboard_alert.png
new file mode 100644
index 00000000..952c588d
--- /dev/null
+++ b/mediagoblin/static/images/icon_clipboard_alert.png
Binary files differ
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">&#9660;</a>
<a href="javascript:;" class="button_action header_dropdown_up">&#9650;</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>
+ &middot;
+ <a href="{{ request.urlgen('mediagoblin.moderation.users') }}">
+ {%- trans %}User management panel{% endtrans -%}
+ </a>
+ &middot;
+ <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 %} &mdash; {{ 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 %} &mdash; {{ 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 %} &mdash; {{ 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 %}
+ &mdash; BANNED until {{ user_banned.expiration_date }}
+ {% elif user_banned %}
+ &mdash; 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>
+ &middot;
+ <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 %} &mdash; {{ 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', {})