aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--mediagoblin/admin/views.py48
-rw-r--r--mediagoblin/auth/tools.py10
-rw-r--r--mediagoblin/auth/views.py7
-rw-r--r--mediagoblin/db/migrations.py75
-rw-r--r--mediagoblin/db/mixin.py1
-rw-r--r--mediagoblin/db/models.py213
-rw-r--r--mediagoblin/db/util.py19
-rw-r--r--mediagoblin/decorators.py100
-rw-r--r--mediagoblin/gmg_commands/users.py11
-rw-r--r--mediagoblin/moderation/__init__.py (renamed from mediagoblin/admin/__init__.py)0
-rw-r--r--mediagoblin/moderation/forms.py60
-rw-r--r--mediagoblin/moderation/routing.py35
-rw-r--r--mediagoblin/moderation/tools.py134
-rw-r--r--mediagoblin/moderation/views.py151
-rw-r--r--mediagoblin/routing.py4
-rw-r--r--mediagoblin/static/css/base.css55
-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/submit/views.py3
-rw-r--r--mediagoblin/templates/mediagoblin/banned.html (renamed from mediagoblin/admin/routing.py)16
-rw-r--r--mediagoblin/templates/mediagoblin/base.html5
-rw-r--r--mediagoblin/templates/mediagoblin/moderation/media_panel.html (renamed from mediagoblin/templates/mediagoblin/admin/panel.html)0
-rw-r--r--mediagoblin/templates/mediagoblin/moderation/report.html204
-rw-r--r--mediagoblin/templates/mediagoblin/moderation/report_panel.html122
-rw-r--r--mediagoblin/templates/mediagoblin/moderation/user.html169
-rw-r--r--mediagoblin/templates/mediagoblin/moderation/user_panel.html61
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/media.html13
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/report.html83
-rw-r--r--mediagoblin/templates/mediagoblin/utils/report.html33
-rw-r--r--mediagoblin/tools/response.py15
-rw-r--r--mediagoblin/user_pages/forms.py12
-rw-r--r--mediagoblin/user_pages/lib.py35
-rw-r--r--mediagoblin/user_pages/routing.py8
-rw-r--r--mediagoblin/user_pages/views.py69
-rw-r--r--mediagoblin/views.py4
35 files changed, 1671 insertions, 104 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 579775ff..596a4447 100644
--- a/mediagoblin/auth/tools.py
+++ b/mediagoblin/auth/tools.py
@@ -14,8 +14,10 @@
# 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
@@ -161,6 +163,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 d54762b0..90d6f5cc 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,6 +147,11 @@ def verify_email(request):
if user and user.email_verified is False:
user.status = u'active'
user.email_verified = True
+ user.verification_key = None
+ user.all_privileges.append(
+ Privilege.query.filter(
+ Privilege.privilege_name==u'active').first())
+
user.save()
messages.add_message(
diff --git a/mediagoblin/db/migrations.py b/mediagoblin/db/migrations.py
index fe4ffb3e..53df7954 100644
--- a/mediagoblin/db/migrations.py
+++ b/mediagoblin/db/migrations.py
@@ -26,7 +26,8 @@ from sqlalchemy.sql import and_
from migrate.changeset.constraint import UniqueConstraint
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, User,
+ MediaComment, Privilege, ReportBase)
MIGRATIONS = {}
@@ -297,6 +298,7 @@ def drop_token_related_User_columns(db):
metadata = MetaData(bind=db.bind)
user_table = inspect_table(metadata, 'core__users')
+
verification_key = user_table.columns['verification_key']
fp_verification_key = user_table.columns['fp_verification_key']
fp_token_expire = user_table.columns['fp_token_expire']
@@ -318,6 +320,7 @@ class CommentSubscription_v0(declarative_base()):
user_id = Column(Integer, ForeignKey(User.id), nullable=False)
+
notify = Column(Boolean, nullable=False, default=True)
send_email = Column(Boolean, nullable=False, default=True)
@@ -378,4 +381,74 @@ def pw_hash_nullable(db):
constraint = UniqueConstraint('username', table=user_table)
constraint.create()
+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))
+ __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=False)
+
+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.i
+
+class ArchivedReport_v0(ReportBase_v0):
+ __tablename__ = 'core__reports_archived'
+ __mapper_args__ = {'polymorphic_identity': 'archived_report'}
+
+ id = Column('id',Integer, ForeignKey('core__reports.id'))
+ media_entry_id = Column(Integer, ForeignKey(MediaEntry.id))
+ comment_id = Column(Integer, ForeignKey(MediaComment.id))
+ resolver_id = Column(Integer, ForeignKey(User.id), nullable=False)
+ resolved_time = Column(DateTime)
+ result = Column(UnicodeText)
+
+class UserBan_v0(declarative_base()):
+ __tablename__ = 'core__user_bans'
+ user_id = Column('id',Integer, ForeignKey(User.id), nullable=False,
+ primary_key=True)
+ expiration_date = Column(DateTime)
+ 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)
+
+class PrivilegeUserAssociation_v0(declarative_base()):
+ __tablename__ = 'core__privileges_users'
+ group_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)
+
+@RegisterMigration(14, MIGRATIONS)
+def create_moderation_tables(db):
+ ReportBase_v0.__table__.create(db.bind)
+ CommentReport_v0.__table__.create(db.bind)
+ MediaReport_v0.__table__.create(db.bind)
+ ArchivedReport_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()
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 7448f5ce..32d3135f 100644
--- a/mediagoblin/db/models.py
+++ b/mediagoblin/db/models.py
@@ -30,7 +30,6 @@ from sqlalchemy.sql.expression import desc
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.util import memoized_property
-
from mediagoblin.db.extratypes import PathTupleWithSlashes, JSONEncoded
from mediagoblin.db.base import Base, DictReadAttrProxy
from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, \
@@ -62,17 +61,20 @@ class User(Base, UserMixin):
# the RFC) and because it would be a mess to implement at this
# 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)
+ pw_hash = Column(Unicode, nullable=False)
+ email_verified = Column(Boolean, default=False)
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)
license_preference = Column(Unicode)
+ verification_key = Column(Unicode)
is_admin = Column(Boolean, default=False, nullable=False)
url = Column(Unicode)
bio = Column(UnicodeText) # ??
+ fp_verification_key = Column(Unicode)
+ fp_token_expire = Column(DateTime)
## TODO
# plugin data would be in a separate model
@@ -486,7 +488,6 @@ class ProcessingMetaData(Base):
"""A dict like view on this object"""
return DictReadAttrProxy(self)
-
class CommentSubscription(Base):
__tablename__ = 'core__comment_subscriptions'
id = Column(Integer, primary_key=True)
@@ -574,16 +575,204 @@ class ProcessingNotification(Notification):
'polymorphic_identity': 'processing_notification'
}
+class ReportBase(Base):
+ """
+ This is the basic report table which the other reports are based off of.
+ :keyword reporter_id
+ :keyword report_content
+ :keyword reported_user_id
+ :keyword created
+ :keyword resolved
+ :keyword result
+ :keyword discriminator
+
+ """
+ __tablename__ = 'core__reports'
+ 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))
+ __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.discriminator=='archived_report'
+
+
+class CommentReport(ReportBase):
+ """
+ A class to keep track of reports that have been filed on comments
+ """
+ __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=False)
+ comment = relationship(
+ MediaComment, backref=backref("reports_filed_on",
+ lazy="dynamic",
+ cascade="all, delete-orphan"))
+
+class MediaReport(ReportBase):
+ """
+ A class to keep track of reports that have been filed on media entries
+ """
+ __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=False)
+ media_entry = relationship(
+ MediaEntry,
+ backref=backref("reports_filed_onmod/reports/1/",
+ lazy="dynamic",
+ cascade="all, delete-orphan"))
+
+class ArchivedReport(ReportBase):
+ """
+ A table to keep track of reports that have been resolved
+ """
+ __tablename__ = 'core__reports_archived'
+ __mapper_args__ = {'polymorphic_identity': 'archived_report'}
+ id = Column('id',Integer, ForeignKey('core__reports.id'),
+ primary_key=True)
+
+ media_entry_id = Column(Integer, ForeignKey(MediaEntry.id))
+ media_entry = relationship(
+ MediaEntry,
+ backref=backref("past_reports_filed_on",
+ lazy="dynamic"))
+ comment_id = Column(Integer, ForeignKey(MediaComment.id))
+ comment = relationship(
+ MediaComment, backref=backref("past_reports_filed_on",
+ lazy="dynamic"))
+
+ resolver_id = Column(Integer, ForeignKey(User.id), nullable=False)
+ resolver = relationship(
+ User,
+ backref=backref("reports_resolved_by",
+ lazy="dynamic",
+ cascade="all, delete-orphan"),
+ primaryjoin="User.id==ArchivedReport.resolver_id")
+
+ resolved = Column(DateTime)
+ result = Column(UnicodeText)
+
+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(DateTime)
+ 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)
+
+ def is_admin_or_moderator(self):
+ '''
+ This method is necessary to check if a user is able to take moderation
+ actions.
+ '''
+
+ return (self.privilege_name==u'admin' or
+ self.privilege_name==u'moderator')
+
+class PrivilegeUserAssociation(Base):
+ '''
+ This table holds the many-to-many relationship between User and Privilege
+ '''
+
+ __tablename__ = 'core__privileges_users'
+
+ privilege_id = Column(
+ 'core__privilege_id',
+ Integer,
+ ForeignKey(User.id),
+ primary_key=True)
+ user_id = Column(
+ 'core__user_id',
+ Integer,
+ ForeignKey(Privilege.id),
+ primary_key=True)
with_polymorphic(
Notification,
[ProcessingNotification, CommentNotification])
+
MODELS = [
- User, 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, ReportBase,
+ CommentReport, MediaReport, UserBan, Privilege, PrivilegeUserAssociation,
+ ArchivedReport, Notification, CommentNotification,
+ ProcessingNotification, CommentSubscription]
"""
Foundations are the default rows that are created immediately after the tables
@@ -599,7 +788,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..2a99d467 100644
--- a/mediagoblin/db/util.py
+++ b/mediagoblin/db/util.py
@@ -15,7 +15,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from mediagoblin.db.base import Session
-from mediagoblin.db.models import MediaEntry, Tag, MediaTag, Collection
+from mediagoblin.db.models import (MediaEntry, Tag, MediaTag, Collection, \
+ User, Privilege, FOUNDATIONS)
##########################
@@ -67,6 +68,22 @@ 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
+def user_privileges_to_dictionary(user_id):
+ """
+ This function accepts a users id and returns a dictionary of True or False
+ values for each privilege the user does or does not have. This allows for
+ easier referencing of a user's privileges inside templates.
+ """
+ privilege_dictionary = {}
+ user = User.query.get(user_id)
+ users_privileges = [p_item.privilege_name for p_item in user.all_privileges]
+ for privilege_name in FOUNDATIONS[Privilege]:
+ privilege_name = privilege_name[0]
+ if privilege_name in users_privileges:
+ privilege_dictionary[privilege_name]=True
+ else:
+ privilege_dictionary[privilege_name]=False
+ return privilege_dictionary
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 ca7be53c..c9a1a78c 100644
--- a/mediagoblin/decorators.py
+++ b/mediagoblin/decorators.py
@@ -18,10 +18,12 @@ from functools import wraps
from urlparse import urljoin
from werkzeug.exceptions import Forbidden, NotFound
+from werkzeug.urls import url_quote
from mediagoblin import mg_globals as mgg
from mediagoblin import messages
-from mediagoblin.db.models import MediaEntry, User
+from mediagoblin.db.models import MediaEntry, User, MediaComment,
+ UserBan
from mediagoblin.tools.response import redirect, render_404
from mediagoblin.tools.translate import pass_to_ugettext as _
@@ -64,6 +66,26 @@ def active_user_from_url(controller):
return wrapper
+def user_has_privilege(privilege_name):
+
+ def user_has_privilege_decorator(controller):
+ @wraps(controller)
+ def wrapper(request, *args, **kwargs):
+ user_id = request.user.id
+ privileges_of_user = Privilege.query.filter(
+ Privilege.all_users.any(
+ User.id==user_id))
+ if UserBan.query.filter(UserBan.user_id==user_id).count():
+ return render_user_banned(request)
+ elif not privileges_of_user.filter(
+ Privilege.privilege_name==privilege_name).count():
+ raise Forbidden()
+
+ return controller(request, *args, **kwargs)
+
+ return wrapper
+ return user_has_privilege_decorator
+
def user_may_delete_media(controller):
"""
@@ -227,17 +249,6 @@ def get_media_entry_by_id(controller):
return wrapper
-def get_workbench(func):
- """Decorator, passing in a workbench as kwarg which is cleaned up afterwards"""
-
- @wraps(func)
- def new_func(*args, **kwargs):
- with mgg.workbench_manager.create() as workbench:
- return func(*args, workbench=workbench, **kwargs)
-
- return new_func
-
-
def allow_registration(controller):
""" Decorator for if registration is enabled"""
@wraps(controller)
@@ -253,6 +264,22 @@ def allow_registration(controller):
return wrapper
+def get_media_comment_by_id(controller):
+ """
+ Pass in a MediaComment based off of a url component
+ """
+ @wraps(controller)
+ def wrapper(request, *args, **kwargs):
+ comment = MediaComment.query.filter_by(
+ id=request.matchdict['comment']).first()
+ # Still no media? Okay, 404.
+ if not comment:
+ return render_404(request)
+
+ return controller(request, comment=comment, *args, **kwargs)
+
+ return wrapper
+
def auth_enabled(controller):
"""Decorator for if an auth plugin is enabled"""
@@ -264,7 +291,56 @@ def auth_enabled(controller):
messages.WARNING,
_('Sorry, authentication is disabled on this instance.'))
return redirect(request, 'index')
+ return controller(request, *args, **kwargs)
+
+ return wrapper
+
+def get_workbench(func):
+ """Decorator, passing in a workbench as kwarg which is cleaned up afterwards"""
+ @wraps(func)
+ def new_func(*args, **kwargs):
+ with mgg.workbench_manager.create() as workbench:
+ return func(*args, workbench=workbench, **kwargs)
+ return new_func
+
+def require_admin_or_moderator_login(controller):
+ """
+ Require an login from an administrator or a moderator.
+ """
+ @wraps(controller)
+ def new_controller_func(request, *args, **kwargs):
+ admin_privilege = Privilege.one({'privilege_name':u'admin'})
+ moderator_privilege = Privilege.one({'privilege_name':u'moderator'})
+ if request.user and \
+ not admin_privilege in request.user.all_privileges and \
+ not moderator_privilege in request.user.all_privileges:
+
+ raise Forbidden()
+ elif not request.user:
+ next_url = urljoin(
+ 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 user_not_banned(controller):
+ """
+ Requires that the user has not been banned. Otherwise redirects to the page
+ explaining why they have been banned
+ """
+ @wraps(controller)
+ def wrapper(request, *args, **kwargs):
+ if request.user:
+ user_banned = UserBan.query.get(request.user.id)
+ if user_banned:
+ return render_user_banned(request)
+ return controller(request, *args, **kwargs)
+
return wrapper
+
diff --git a/mediagoblin/gmg_commands/users.py b/mediagoblin/gmg_commands/users.py
index e44b0aa9..ad8263e7 100644
--- a/mediagoblin/gmg_commands/users.py
+++ b/mediagoblin/gmg_commands/users.py
@@ -55,6 +55,13 @@ def adduser(args):
entry.pw_hash = auth.gen_password_hash(args.password)
entry.status = u'active'
entry.email_verified = True
+ default_privileges = [
+ db.Privilege.one({'privilege_name':u'commenter'}),
+ db.Privilege.one({'privilege_name':u'uploader'}),
+ db.Privilege.one({'privilege_name':u'reporter'}),
+ db.Privilege.one({'privilege_name':u'active'})
+]
+ entry.all_privileges = default_privileges
entry.save()
print "User created (and email marked as verified)"
@@ -75,6 +82,10 @@ def makeadmin(args):
username=unicode(args.username.lower())).one()
if user:
user.is_admin = True
+ user.all_privileges.append(
+ db.Privilege.one({
+ 'privilege_name':u'admin'})
+ )
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..718cd8fa
--- /dev/null
+++ b/mediagoblin/moderation/forms.py
@@ -0,0 +1,60 @@
+# 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()
+
+
+class PrivilegeAddRemoveForm(wtforms.Form):
+ privilege_name = wtforms.HiddenField('',[wtforms.validators.required()])
+
+class ReportResolutionForm(wtforms.Form):
+ 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()
+
diff --git a/mediagoblin/moderation/routing.py b/mediagoblin/moderation/routing.py
new file mode 100644
index 00000000..f177c32a
--- /dev/null
+++ b/mediagoblin/moderation/routing.py
@@ -0,0 +1,35 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+moderation_routes = [
+ ('mediagoblin.moderation.media_panel',
+ '/media/',
+ 'mediagoblin.moderation.views:moderation_media_processing_panel'),
+ ('mediagoblin.moderation.users',
+ '/users/',
+ 'mediagoblin.moderation.views:moderation_users_panel'),
+ ('mediagoblin.moderation.reports',
+ '/reports/',
+ 'mediagoblin.moderation.views:moderation_reports_panel'),
+ ('mediagoblin.moderation.users_detail',
+ '/users/<string:user>/',
+ 'mediagoblin.moderation.views:moderation_users_detail'),
+ ('mediagoblin.moderation.give_or_take_away_privilege',
+ '/users/<string:user>/privilege/',
+ 'mediagoblin.moderation.views:give_or_take_away_privilege'),
+ ('mediagoblin.moderation.reports_detail',
+ '/reports/<int:report_id>/',
+ 'mediagoblin.moderation.views:moderation_reports_detail')]
diff --git a/mediagoblin/moderation/tools.py b/mediagoblin/moderation/tools.py
new file mode 100644
index 00000000..25e5dc63
--- /dev/null
+++ b/mediagoblin/moderation/tools.py
@@ -0,0 +1,134 @@
+# 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, ArchivedReport, 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 _
+import sys, traceback
+
+def take_punitive_actions(request, form, report, user):
+ message_body =''
+ try:
+
+ # 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:
+ privilege = Privilege.one({u'privilege_name':privilege_name})
+ form.resolution_content.data += \
+ u"<br>%s took away %s\'s %s privileges" % (
+ request.user.username,
+ user.username,
+ privilege.privilege_name)
+ user.all_privileges.remove(privilege)
+
+ # 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:
+ reason = form.resolution_content.data + \
+ "<br>"+request.user.username
+ user_ban = UserBan(
+ user_id=form.targeted_user.data,
+ expiration_date=form.user_banned_until.data,
+ reason= form.why_user_was_banned.data
+ )
+ Session.add(user_ban)
+
+ if form.user_banned_until.data is not None:
+ form.resolution_content.data += \
+ u"<br>%s banned user %s until %s." % (
+ request.user.username,
+ user.username,
+ form.user_banned_until.data)
+ else:
+ form.resolution_content.data += \
+ u"<br>%s banned user %s indefinitely." % (
+ request.user.username,
+ user.username)
+
+ # 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"<br>%s sent a warning email to the offender." % (
+ request.user.username)
+
+ archive = ArchivedReport(
+ reporter_id=report.reporter_id,
+ report_content=report.report_content,
+ reported_user_id=report.reported_user_id,
+ created=report.created,
+ resolved=datetime.now(),
+ resolver_id=request.user.id
+ )
+
+ 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"<br>%s deleted the comment" % (
+ request.user.username)
+ elif u'delete' in form.action_to_resolve.data and \
+ report.is_media_entry_report():
+ deleted_media = report.media_entry
+ Session.delete(deleted_media)
+ form.resolution_content.data += \
+ u"<br>%s deleted the media entry" % (
+ request.user.username)
+
+ # If the moderator didn't delete the content we then attach the
+ # content to the archived report. We also have to actively delete the
+ # old report, since it won't be deleted by cascading.
+ elif report.is_comment_report():
+ archive.comment_id = report.comment_id
+ Session.delete(report)
+ elif report.is_media_entry_report():
+ archive.media_entry_id = report.media_entry.id
+ Session.delete(report)
+
+
+ archive.result=form.resolution_content.data
+ Session.add(archive)
+ 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)
+ except:
+#TODO make a more effective and specific try except statement. To account for
+# incorrect value addition my moderators
+ print sys.exc_info()[0]
+ print sys.exc_info()[1]
+ traceback.print_tb(sys.exc_info()[2])
+ Session.rollback()
+ return redirect(
+ request,
+ 'mediagoblin.moderation.reports_detail',
+ report_id=report.id)
diff --git a/mediagoblin/moderation/views.py b/mediagoblin/moderation/views.py
new file mode 100644
index 00000000..67928927
--- /dev/null
+++ b/mediagoblin/moderation/views.py
@@ -0,0 +1,151 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from werkzeug.exceptions import Forbidden
+
+from mediagoblin.db.models import (MediaEntry, User, MediaComment, \
+ CommentReport, ReportBase, Privilege, \
+ UserBan, ArchivedReport)
+from mediagoblin.db.util import user_privileges_to_dictionary
+from mediagoblin.decorators import (require_admin_or_moderator_login, \
+ active_user_from_url)
+from mediagoblin.tools.response import render_to_response, redirect
+from mediagoblin.moderation import forms as moderation_forms
+from mediagoblin.moderation.tools import take_punitive_actions
+from datetime import datetime
+
+@require_admin_or_moderator_login
+def moderation_media_processing_panel(request):
+ '''
+ Show the global media processing panel for this instance
+ '''
+ processing_entries = MediaEntry.query.filter_by(state = u'processing').\
+ order_by(MediaEntry.created.desc())
+
+ # Get media entries which have failed to process
+ failed_entries = MediaEntry.query.filter_by(state = u'failed').\
+ order_by(MediaEntry.created.desc())
+
+ processed_entries = MediaEntry.query.filter_by(state = u'processed').\
+ order_by(MediaEntry.created.desc()).limit(10)
+
+ # Render to response
+ return render_to_response(
+ request,
+ 'mediagoblin/moderation/media_panel.html',
+ {'processing_entries': processing_entries,
+ 'failed_entries': failed_entries,
+ 'processed_entries': processed_entries})
+
+@require_admin_or_moderator_login
+def moderation_users_panel(request):
+ '''
+ Show the global panel for monitoring users in this instance
+ '''
+ user_list = User.query
+
+ return render_to_response(
+ request,
+ 'mediagoblin/moderation/user_panel.html',
+ {'user_list': user_list})
+
+@require_admin_or_moderator_login
+def moderation_users_detail(request):
+ '''
+ Shows details about a particular user.
+ '''
+ user = User.query.filter_by(username=request.matchdict['user']).first()
+ active_reports = user.reports_filed_on.filter(
+ ReportBase.discriminator!='archived_report').limit(5)
+ closed_reports = user.reports_filed_on.filter(
+ ReportBase.discriminator=='archived_report').all()
+ privileges = Privilege.query
+ user_banned = UserBan.query.get(user.id)
+ user_privileges = user_privileges_to_dictionary(user.id)
+ requesting_user_privileges = user_privileges_to_dictionary(request.user.id)
+
+ return render_to_response(
+ request,
+ 'mediagoblin/moderation/user.html',
+ {'user':user,
+ 'privileges': privileges,
+ 'requesting_user_privileges':requesting_user_privileges,
+ 'reports':active_reports,
+ 'user_banned':user_banned})
+
+@require_admin_or_moderator_login
+def moderation_reports_panel(request):
+ '''
+ Show the global panel for monitoring reports filed against comments or
+ media entries for this instance.
+ '''
+ report_list = ReportBase.query.filter(
+ ReportBase.discriminator!="archived_report").order_by(
+ ReportBase.created.desc()).limit(10)
+ closed_report_list = ReportBase.query.filter(
+ ReportBase.discriminator=="archived_report").order_by(
+ ReportBase.created.desc()).limit(10)
+
+ # Render to response
+ return render_to_response(
+ request,
+ 'mediagoblin/moderation/report_panel.html',
+ {'report_list':report_list,
+ 'closed_report_list':closed_report_list})
+
+@require_admin_or_moderator_login
+def moderation_reports_detail(request):
+ """
+ This is the page an admin or moderator goes to see the details of a report.
+ The report can be resolved or unresolved. This is also the page that a mod-
+ erator would go to to take an action to resolve a report.
+ """
+ form = moderation_forms.ReportResolutionForm(request.form)
+ report = ReportBase.query.get(request.matchdict['report_id'])
+
+ 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():
+ 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})
+
+@require_admin_or_moderator_login
+@active_user_from_url
+def give_or_take_away_privilege(request, url_user):
+ '''
+ A form action to give or take away a particular privilege from a user
+ '''
+ form = moderation_forms.PrivilegeAddRemoveForm(request.form)
+ if request.method == "POST" and form.validate():
+ privilege = Privilege.one({'privilege_name':form.privilege_name.data})
+ if privilege in url_user.all_privileges:
+ url_user.all_privileges.remove(privilege)
+ else:
+ url_user.all_privileges.append(privilege)
+ url_user.save()
+ return redirect(
+ request,
+ 'mediagoblin.moderation.users_detail',
+ user=url_user.username)
diff --git a/mediagoblin/routing.py b/mediagoblin/routing.py
index 986eb2ed..c9377ad4 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
@@ -28,7 +28,7 @@ _log = logging.getLogger(__name__)
def get_url_map():
add_route('index', '/', 'mediagoblin.views:root_view')
mount('/auth', auth_routes)
- mount('/a', admin_routes)
+ mount('/mod', moderation_routes)
import mediagoblin.submit.routing
import mediagoblin.user_pages.routing
diff --git a/mediagoblin/static/css/base.css b/mediagoblin/static/css/base.css
index 84d274d1..338828d2 100644
--- a/mediagoblin/static/css/base.css
+++ b/mediagoblin/static/css/base.css
@@ -351,40 +351,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 +408,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 +615,38 @@ table.media_panel th {
text-align: left;
}
+/* admin 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 */
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/submit/views.py b/mediagoblin/submit/views.py
index 3f9d5b2d..7c0708ed 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/admin/routing.py b/mediagoblin/templates/mediagoblin/banned.html
index 29515f12..4eda0540 100644
--- a/mediagoblin/admin/routing.py
+++ b/mediagoblin/templates/mediagoblin/banned.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/>.
+#}
+{% extends "mediagoblin/base.html" %}
-admin_routes = [
- ('mediagoblin.admin.panel',
- '/panel',
- 'mediagoblin.admin.views:admin_processing_panel')]
+{% block title %}You are Banned.{% endblock %}
+
+{% block mediagoblin_content %}
+ <img class="right_align" src="{{ request.staticdirect('/images/404.png') }}"
+ alt="{% trans %}Image of goblin stressing out{% endtrans %}" />
+ <h1>You have been banned until {{ expiration_date }}</h1>
+ <p>{{ reason|safe }}</p>
+ <div class="clear"></div>
+{% endblock %}
diff --git a/mediagoblin/templates/mediagoblin/base.html b/mediagoblin/templates/mediagoblin/base.html
index 1fc4467c..575ddf42 100644
--- a/mediagoblin/templates/mediagoblin/base.html
+++ b/mediagoblin/templates/mediagoblin/base.html
@@ -112,9 +112,12 @@
{% if request.user.is_admin %}
<p>
<span class="dropdown_title">Admin powers:</span>
- <a href="{{ request.urlgen('mediagoblin.admin.panel') }}">
+ <a href="{{ request.urlgen('mediagoblin.moderation.media_panel') }}">
{%- trans %}Media processing panel{% endtrans -%}
</a>
+ <a href="{{ request.urlgen('mediagoblin.moderation.users') }}">
+ {%- trans %}User management panel{% endtrans -%}
+ </a>
</p>
{% endif %}
{% include 'mediagoblin/fragments/header_notifications.html' %}
diff --git a/mediagoblin/templates/mediagoblin/admin/panel.html b/mediagoblin/templates/mediagoblin/moderation/media_panel.html
index 1c3c866e..1c3c866e 100644
--- a/mediagoblin/templates/mediagoblin/admin/panel.html
+++ b/mediagoblin/templates/mediagoblin/moderation/media_panel.html
diff --git a/mediagoblin/templates/mediagoblin/moderation/report.html b/mediagoblin/templates/mediagoblin/moderation/report.html
new file mode 100644
index 00000000..b912c712
--- /dev/null
+++ b/mediagoblin/templates/mediagoblin/moderation/report.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 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() or
+ (report.is_archived_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() or
+ (report.is_archived_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.reporter.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() %}
+ <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() {
+ hidden_input_names = {
+ 'takeaway':['take_away_privileges'],
+ 'userban':['user_banned_until','why_user_was_banned'],
+ 'sendmessage':['message_to_user']
+}
+
+ $('form#resolution_form').hide()
+ $('#user_banned_until').val("YYYY-MM-DD")
+ $('#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();
+ });
+ });
+/* $.each(hidden_input_names, function(key,name){
+ if ($.inArray(key, $('ul#action_to_resolve li input:checked').val())){
+ $.each(hidden_input_names[key], function(index,name){
+ $('#'+name).show();
+ $('label[for='+name+']').show();
+ });
+ } else {
+ $.each(hidden_input_names[key], function(index,name){
+ $('#'+name).hide();
+ $('label[for='+name+']').hide();
+ });
+ }
+ });*/
+ });
+ $("#user_banned_until").focus(function() {
+ $(this).val("");
+ $(this).unbind('focus');
+ });
+ $("#submit_this_report").click(function(){
+ if ($("#user_banned_until").val() == 'YYYY-MM-DD'){
+ $("#user_banned_until").val("");
+ }
+ });
+ });
+ </script>
+ {% else %}
+ <h2><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") }}
+ {% autoescape False %}
+ <p>{{ report.result }}</p>
+ {% endautoescape %}
+ {% 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..f3840e29
--- /dev/null
+++ b/mediagoblin/templates/mediagoblin/moderation/report_panel.html
@@ -0,0 +1,122 @@
+{#
+# 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 %}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() %}
+ <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() %}
+ <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 }}</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..3fb65063
--- /dev/null
+++ b/mediagoblin/templates/mediagoblin/moderation/user.html
@@ -0,0 +1,169 @@
+{#
+# 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 %}
+ {%- if user -%}
+ {%- trans username=user.username -%}
+ User: {{ username }}
+ {%- endtrans %} &mdash; {{ super() }}
+ {%- else -%}
+ {{ super() }}
+ {%- endif -%}
+{% 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 user.status == "needs_email_verification" %}
+ <div class="form_box">
+ <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>
+
+ <p>
+ {% trans login_url=request.urlgen('mediagoblin.auth.login') -%}
+ If you are that person but you've lost your verification email, you can
+ <a href="{{ login_url }}">log in</a> and resend it.
+ {%- endtrans %}
+ </p>
+ </div>
+
+ {# Active(?) (or at least verified at some point) user, horray! #}
+ {% else %}
+ <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.is_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 %}
+ <a class="right_align">{{ user.username }}'s report history</a>
+ <span class=clear></span>
+ <h2>{{ user.username }}'s Privileges</h2>
+ <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 requesting_user_privileges.admin%}
+ <td>{% if privilege in user.all_privileges %}
+ <input type=submit id="{{ privilege.privilege_name }}" class=submit_button value ="-" />{% else %}
+ <input type=submit id="{{ privilege.privilege_name }}" class=submit_button 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'));
+ });
+});
+ </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..6762a844
--- /dev/null
+++ b/mediagoblin/templates/mediagoblin/moderation/user_panel.html
@@ -0,0 +1,61 @@
+{#
+# 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() %}
+ <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/user_pages/media.html b/mediagoblin/templates/mediagoblin/user_pages/media.html
index c16e4c78..b10ef3be 100644
--- a/mediagoblin/templates/mediagoblin/user_pages/media.html
+++ b/mediagoblin/templates/mediagoblin/user_pages/media.html
@@ -142,6 +142,17 @@
{{ comment.content_html }}
{%- endautoescape %}
</div>
+ <div>
+ <a {% if not request.user -%}
+ href="{{ request.urlgen('mediagoblin.auth.login') }}"
+ {%- else %}
+ href="{{ request.urlgen('mediagoblin.user_pages.media_home.report_comment',
+ user=media.get_uploader.username,
+ media=media.slug_or_id,
+ comment=comment.id) }}"
+ {%- endif %}>
+ {% trans %} Report {% endtrans %}</a>
+ </div>
</li>
{% endfor %}
</ul>
@@ -166,6 +177,8 @@
{% include "mediagoblin/utils/collections.html" %}
+ {% include "mediagoblin/utils/report.html" %}
+
{% include "mediagoblin/utils/license.html" %}
{% include "mediagoblin/utils/exif.html" %}
diff --git a/mediagoblin/templates/mediagoblin/user_pages/report.html b/mediagoblin/templates/mediagoblin/user_pages/report.html
new file mode 100644
index 00000000..cd5e6f59
--- /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/utils/report.html b/mediagoblin/templates/mediagoblin/utils/report.html
new file mode 100644
index 00000000..4108cd82
--- /dev/null
+++ b/mediagoblin/templates/mediagoblin/utils/report.html
@@ -0,0 +1,33 @@
+{#
+# 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/>.
+#}
+
+{% block report_content -%}
+ <p>
+ <a
+ {% if not request.user -%}
+ href="{{ request.urlgen('mediagoblin.auth.login') }}"
+ {% else %}
+ href="{{ request.urlgen('mediagoblin.user_pages.media_home.report_media',
+ user=media.get_uploader.username,
+ media=media.slug_or_id) }}"
+ {% endif %}
+ class="button_action" id="button_reportmedia" title="Report media">
+ {% trans %}Report media{% endtrans %}
+ </a>
+ </p>
+{% endblock %}
diff --git a/mediagoblin/tools/response.py b/mediagoblin/tools/response.py
index 0be1f835..8d9c02d4 100644
--- a/mediagoblin/tools/response.py
+++ b/mediagoblin/tools/response.py
@@ -19,6 +19,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 datetime
class Response(wz_Response):
"""Set default response mimetype to HTML, otherwise we get text/plain"""
@@ -62,6 +64,19 @@ 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 datetime.now()>user_ban.expiration_date:
+ user_ban.delete()
+ redirect(request,
+ 'mediagoblin.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
diff --git a/mediagoblin/user_pages/forms.py b/mediagoblin/user_pages/forms.py
index 9a193680..d83338e9 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">
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..7f03fcd3 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,34 @@ 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 should be a MediaReportForm or a CommentReportForm
+ object
+ :param
+
+ :returns either of MediaReport or a CommentReport object that has not been
+ saved. In case of an improper form_dict, returns None
+ """
+
+ 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 9cb665b5..adccda9e 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')
@@ -40,6 +44,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_comment_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 596d4c20..06ea0ab0 100644
--- a/mediagoblin/user_pages/views.py
+++ b/mediagoblin/user_pages/views.py
@@ -19,20 +19,23 @@ import datetime
from mediagoblin import messages, mg_globals
from mediagoblin.db.models import (MediaEntry, MediaTag, Collection,
- CollectionItem, User)
+ CollectionItem, User, MediaComment,
+ CommentReport, MediaReport)
from mediagoblin.tools.response import render_to_response, render_404, \
redirect, redirect_obj
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)
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,
require_active_login, user_may_delete_media, user_may_alter_collection,
- get_user_collection, get_user_collection_item, active_user_from_url)
+ get_user_collection, get_user_collection_item, active_user_from_url,
+ get_media_comment_by_id, user_not_banned)
from werkzeug.contrib.atom import AtomFeed
from werkzeug.exceptions import MethodNotAllowed
@@ -41,7 +44,7 @@ from werkzeug.exceptions import MethodNotAllowed
_log = logging.getLogger(__name__)
_log.setLevel(logging.DEBUG)
-
+@user_not_banned
@uses_pagination
def user_home(request, page):
"""'Homepage' of a User()"""
@@ -80,7 +83,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):
@@ -115,7 +118,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):
@@ -156,6 +159,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.
@@ -192,7 +196,7 @@ def media_post_comment(request, media):
return redirect_obj(request, media)
-
+@user_not_banned
@get_media_entry_by_id
@require_active_login
def media_collect(request, media):
@@ -271,6 +275,7 @@ def media_collect(request, media):
#TODO: Why does @user_may_delete_media not implicate @require_active_login?
+@user_not_banned
@get_media_entry_by_id
@require_active_login
@user_may_delete_media
@@ -307,7 +312,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):
@@ -337,7 +342,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"""
@@ -393,7 +398,7 @@ def collection_item_confirm_remove(request, collection_item):
{'collection_item': collection_item,
'form': form})
-
+@user_not_banned
@get_user_collection
@require_active_login
@user_may_alter_collection
@@ -577,7 +582,7 @@ def collection_atom_feed(request):
return feed.get_response()
-
+@user_not_banned
@require_active_login
def processing_panel(request):
"""
@@ -621,3 +626,43 @@ def processing_panel(request):
'processing_entries': processing_entries,
'failed_entries': failed_entries,
'processed_entries': processed_entries})
+
+@require_active_login
+@get_user_media_entry
+@user_has_privilege(u'reporter')
+def file_a_report(request, media, comment=None):
+ if comment is not None:
+ form = user_forms.CommentReportForm(request.form)
+ form.reporter_id.data = request.user.id
+ context = {'media': media,
+ 'comment':comment,
+ 'form':form}
+ else:
+ form = user_forms.MediaReportForm(request.form)
+ form.reporter_id.data = request.user.id
+ context = {'media': media,
+ 'form':form}
+
+ if request.method == "POST":
+ report_table = build_report_object(form,
+ media_entry=media,
+ comment=comment)
+
+ # if the object was built successfully, report_table will not be None
+ if report_table:
+ report_table.save()
+ return redirect(
+ request,
+ 'index')
+
+
+ return render_to_response(
+ request,
+ 'mediagoblin/user_pages/report.html',
+ context)
+
+@require_active_login
+@get_user_media_entry
+@get_media_comment_by_id
+def file_a_comment_report(request, media, comment):
+ return file_a_report(request, comment=comment)
diff --git a/mediagoblin/views.py b/mediagoblin/views.py
index 6acd7e96..595f2725 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').\