Change moderator adding to an invite system.

This commit is contained in:
Max Goodman
2012-10-01 17:41:16 -07:00
parent 7706f844f3
commit db7b531a71
14 changed files with 241 additions and 86 deletions

View File

@@ -456,7 +456,7 @@ comment_visits_period = 600
agents =
# subreddit ratelimits
sr_banned_quota = 10000
sr_moderator_quota = 10000
sr_moderator_invite_quota = 10000
sr_contributor_quota = 10000
sr_wikibanned_quota = 10000
sr_wikicontributor_quota = 10000

View File

@@ -505,6 +505,7 @@ class ApiController(RedditController, OAuth2ResourceController):
_sr_friend_types = (
'moderator',
'moderator_invite',
'contributor',
'banned',
'wikibanned',
@@ -553,6 +554,7 @@ class ApiController(RedditController, OAuth2ResourceController):
# Log this action
if new and type in self._sr_friend_types:
action = dict(banned='unbanuser', moderator='removemoderator',
moderator_invite='uninvitemoderator',
wikicontributor='removewikicontributor',
wikibanned='wikiunbanned',
contributor='removecontributor').get(type, None)
@@ -584,6 +586,11 @@ class ApiController(RedditController, OAuth2ResourceController):
container = VByName('container').run(container)
if not container:
return
if type == "moderator" and not c.user_is_admin:
# attempts to add moderators now create moderator invites.
type = "moderator_invite"
fn = getattr(container, 'add_' + type)
# The user who made the request must be an admin or a moderator
@@ -612,11 +619,21 @@ class ApiController(RedditController, OAuth2ResourceController):
elif form.has_errors("name", errors.USER_DOESNT_EXIST, errors.NO_USER):
return
if type == "moderator_invite" and container.is_moderator(friend):
c.errors.add(errors.ALREADY_MODERATOR, field="name")
form.set_error(errors.ALREADY_MODERATOR, "name")
return
if type == "moderator":
container.remove_moderator_invite(friend)
new = fn(friend)
# Log this action
if new and type in self._sr_friend_types:
action = dict(banned='banuser', moderator='addmoderator',
action = dict(banned='banuser',
moderator='addmoderator',
moderator_invite='invitemoderator',
wikicontributor='wikicontributor',
contributor='addcontributor',
wikibanned='wikibanned').get(type, None)
@@ -634,14 +651,16 @@ class ApiController(RedditController, OAuth2ResourceController):
cls = dict(friend=FriendList,
moderator=ModList,
moderator_invite=ModList,
contributor=ContributorList,
wikicontributor=WikiMayContributeList,
banned=BannedList, wikibanned=WikiBannedList).get(type)
userlist = cls()
form.set_inputs(name = "")
form.set_html(".status:first", _("added"))
form.set_html(".status:first", userlist.executed_message(type))
if new and cls:
user_row = cls().user_row(friend)
jquery("#" + type + "-table").show(
user_row = userlist.user_row(type, friend)
jquery("." + type + "-table").show(
).find("table").insert_table_rows(user_row)
if new:
@@ -654,6 +673,20 @@ class ApiController(RedditController, OAuth2ResourceController):
c.user.add_friend_note(friend, note)
form.set_html('.status', _("saved"))
@validatedForm(VUser(),
VModhash(),
ip=ValidIP())
@api_doc(api_section.subreddits)
def POST_accept_moderator_invite(self, form, jquery, ip):
if not c.site.remove_moderator_invite(c.user):
return
ModAction.create(c.site, c.user, "acceptmoderatorinvite")
c.site.add_moderator(c.user)
Subreddit.special_reddits(c.user, "moderator", _update=True)
notify_user_added("accept_moderator_invite", c.user, c.user, c.site)
jquery.refresh()
@validatedForm(VUser('curpass', default=''),
VModhash(),
password = VPassword(['curpass', 'curpass']),

View File

@@ -109,6 +109,7 @@ error_list = dict((
('DEVELOPER_ALREADY_ADDED', _('already added')),
('TOO_MANY_DEVELOPERS', _('too many developers')),
('BAD_HASH', _("i don't believe you.")),
('ALREADY_MODERATOR', _('that user is already a moderator')),
))
errors = Storage([(e, e) for e in error_list.keys()])

View File

@@ -520,7 +520,7 @@ class UserListJsonTemplate(ThingJsonTemplate):
def thing_attr(self, thing, attr):
if attr == "users":
res = []
for a in thing.users:
for a in thing.user_rows:
r = a.render()
res.append(r)
return res

View File

@@ -2788,53 +2788,57 @@ class UserList(Templated):
"""base class for generating a list of users"""
form_title = ''
table_title = ''
table_headers = None
type = ''
container_name = ''
cells = ('user', 'sendmessage', 'remove')
_class = ""
destination = "friend"
remove_action = "unfriend"
editable_fn = None
def __init__(self, editable=True):
self.editable = editable
Templated.__init__(self)
def user_row(self, user):
def user_row(self, row_type, user, editable=True):
"""Convenience method for constructing a UserTableItem
instance of the user with type, container_name, etc. of this
UserList instance"""
editable = self.editable
if self.editable_fn and not self.editable_fn(user):
editable = False
return UserTableItem(user, self.type, self.cells, self.container_name,
return UserTableItem(user, row_type, self.cells, self.container_name,
editable, self.remove_action)
@property
def users(self, site = None):
def _user_rows(self, row_type, uids, editable_fn=None):
"""Generates a UserTableItem wrapped list of the Account
objects which should be present in this UserList."""
uids = self.user_ids()
if uids:
users = Account._byID(uids, True, return_dict = False)
return [self.user_row(u) for u in users]
users = Account._byID(uids, True, return_dict = False)
rows = []
for u in users:
editable = editable_fn(u) if editable_fn else self.editable
rows.append(self.user_row(row_type, u, editable))
return rows
else:
return []
@property
def user_rows(self):
return self._user_rows(self.type, self.user_ids())
def user_ids(self):
"""virtual method for fetching the list of ids of the Accounts
to be listing in this UserList instance"""
raise NotImplementedError
def can_remove_self(self):
return False
@property
def container_name(self):
return c.site._fullname
def executed_message(self, row_type):
return _("added")
class FriendList(UserList):
"""Friend list on /pref/friends"""
@@ -2860,13 +2864,13 @@ class FriendList(UserList):
def user_ids(self):
return c.user.friends
def user_row(self, user):
def user_row(self, row_type, user, editable=True):
if not getattr(self, "friend_rels", None):
return UserList.user_row(self, user)
return UserList.user_row(self, row_type, user, editable)
else:
rel = self.friend_rels[user._id]
return UserTableItem(user, self.type, self.cells, self.container_name,
True, self.remove_action, rel)
return UserTableItem(user, row_type, self.cells, self.container_name,
editable, self.remove_action, rel)
@property
def container_name(self):
@@ -2916,23 +2920,35 @@ class ContributorList(UserList):
class ModList(UserList):
"""Moderator list for a reddit."""
type = 'moderator'
remove_self_action = _('leave')
invite_type = 'moderator_invite'
invite_action = 'accept_moderator_invite'
form_title = _('add moderator')
invite_form_title = _('invite moderator')
remove_self_title = _('you are a moderator of this subreddit. %(action)s')
remove_self_confirm = _('stop being a moderator?')
remove_self_final = _('you are no longer a moderator')
@property
def form_title(self):
return _('add moderator')
@property
def table_title(self):
return _("moderators of %(reddit)s") % dict(reddit = c.site.name)
return _("moderators of /r/%(reddit)s") % {"reddit": c.site.name}
def executed_message(self, row_type):
if row_type == "moderator_invite":
return _("invited")
else:
return _("added")
@property
def can_force_add(self):
return c.user_is_admin
@property
def can_remove_self(self):
return c.user_is_loggedin and c.site.is_moderator(c.user)
def editable_fn(self, user):
@property
def has_invite(self):
return c.user_is_loggedin and c.site.is_moderator_invite(c.user)
def moderator_editable(self, user):
if not c.user_is_loggedin:
return False
elif c.user_is_admin:
@@ -2940,6 +2956,14 @@ class ModList(UserList):
else:
return c.site.can_demod(c.user, user)
@property
def user_rows(self):
return self._user_rows(self.type, self.user_ids(), self.moderator_editable)
@property
def invited_user_rows(self):
return self._user_rows(self.invite_type, c.site.moderator_invite_ids(), self.moderator_editable)
def user_ids(self):
return c.site.moderators

View File

@@ -23,15 +23,27 @@
from pylons import request
from pylons.i18n import _
from r2.models import Message
from r2.models import Account, Message
from r2.lib.db import queries
user_added_messages = {
"moderator": {
"moderator_invite": {
"pm": {
"subject": _("you are a moderator"),
"msg": _("you have been added as a moderator to [%(title)s](%(url)s)."),
"subject": _("invitation to moderate %(url)s"),
"msg": _("**gadzooks! you are invited to become a moderator of [%(title)s](%(url)s)!**\n\n"
"*to accept*, visit the [moderators page for %(url)s](%(url)s/about/moderators) and click \"accept\".\n\n"
"*otherwise,* if you did not expect to receive this, you can simply ignore this invitation or report it."),
},
"modmail": {
"subject": _("moderator invited"),
"msg": _("%(user)s has been invited by %(author)s to moderate %(url)s."),
},
},
"accept_moderator_invite": {
"modmail": {
"subject": _("moderator added"),
"msg": _("%(user)s has accepted an invitation to become moderator of %(url)s."),
},
},
"contributor": {
@@ -71,10 +83,12 @@ def notify_user_added(rel_type, author, user, target):
if "pm" in msgs and author != user:
subject = msgs["pm"]["subject"] % d
msg = msgs["pm"]["msg"] % d
if rel_type == "banned":
if not user.has_interacted_with(target):
return
if rel_type == "banned" and not user.has_interacted_with(target):
return
if rel_type in ("banned", "moderator_invite"):
# send the message from the subreddit
item, inbox_rel = Message._new(author, user, subject, msg, request.ip,
sr=target, from_sr=True)
else:
@@ -85,6 +99,12 @@ def notify_user_added(rel_type, author, user, target):
if "modmail" in msgs:
subject = msgs["modmail"]["subject"] % d
msg = msgs["modmail"]["msg"] % d
item, inbox_rel = Message._new(author, target, subject, msg, request.ip,
sr=target, from_sr=True)
if rel_type == "moderator_invite":
modmail_author = Account.system_user()
else:
modmail_author = author
item, inbox_rel = Message._new(modmail_author, target, subject, msg,
request.ip, sr=target)
queries.new_message(item, inbox_rel)

View File

@@ -49,6 +49,7 @@ class ModAction(tdb_cassandra.UuidThing, Printable):
actions = ('banuser', 'unbanuser', 'removelink', 'approvelink',
'removecomment', 'approvecomment', 'addmoderator',
'invitemoderator', 'uninvitemoderator', 'acceptmoderatorinvite',
'removemoderator', 'addcontributor', 'removecontributor',
'editsettings', 'editflair', 'distinguish', 'marknsfw',
'wikibanned', 'wikicontributor', 'wikiunbanned',
@@ -62,6 +63,9 @@ class ModAction(tdb_cassandra.UuidThing, Printable):
'approvecomment': _('approve comment'),
'addmoderator': _('add moderator'),
'removemoderator': _('remove moderator'),
'invitemoderator': _('invite moderator'),
'uninvitemoderator': _('uninvite moderator'),
'acceptmoderatorinvite': _('accept moderator invite'),
'addcontributor': _('add contributor'),
'removecontributor': _('remove contributor'),
'editsettings': _('edit settings'),
@@ -87,6 +91,9 @@ class ModAction(tdb_cassandra.UuidThing, Printable):
'approvecomment': _('approved'),
'addmoderator': _('added moderator'),
'removemoderator': _('removed moderator'),
'invitemoderator': _('invited moderator'),
'uninvitemoderator': _('uninvited moderator'),
'acceptmoderatorinvite': _('accepted moderator invitation'),
'addcontributor': _('added approved contributor'),
'removecontributor': _('removed approved contributor'),
'editsettings': _('edited settings'),

View File

@@ -427,8 +427,9 @@ class Subreddit(Thing, Printable):
elif self.type in ('public', 'restricted', 'archived'):
return True
elif c.user_is_loggedin:
#private requires contributorship
return self.is_contributor(user) or self.is_moderator(user)
return (self.is_contributor(user) or
self.is_moderator(user) or
self.is_moderator_invite(user))
def can_demod(self, bully, victim):
# This works because the is_*() functions return the relation
@@ -1225,6 +1226,7 @@ Subreddit._specials.update(dict(friends = Friends,
class SRMember(Relation(Subreddit, Account)): pass
Subreddit.__bases__ += (
UserRel('moderator', SRMember),
UserRel('moderator_invite', SRMember),
UserRel('contributor', SRMember),
UserRel('subscriber', SRMember, disable_ids_fn=True),
UserRel('banned', SRMember),

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 B

View File

@@ -4879,6 +4879,10 @@ dd { margin-left: 20px; }
background-image: url(../shield.png); /* SPRITE */
}
.moderator.accept-invite .main:before {
background-image: url(../addmoderator.png); /* SPRITE */
}
.titlebox form.leavecontributor-button:before {
background-image: url(../pencil.png); /* SPRITE */
}
@@ -5670,6 +5674,9 @@ tr.gold-accent + tr > td {
.modactions.approvecomment,
.modactions.addmoderator,
.modactions.removemoderator,
.modactions.invitemoderator,
.modactions.uninvitemoderator,
.modactions.acceptmoderatorinvite,
.modactions.addcontributor,
.modactions.removecontributor,
.modactions.editsettings,
@@ -5701,10 +5708,13 @@ tr.gold-accent + tr > td {
.modactions.approvecomment {
background-image: url(../modactions_approvecomment.png); /* SPRITE */
}
.modactions.addmoderator {
.modactions.addmoderator,
.modactions.invitemoderator,
.modactions.acceptmoderatorinvite {
background-image: url(../modactions_addmoderator.png); /* SPRITE */
}
.modactions.removemoderator {
.modactions.removemoderator,
.modactions.uninvitemoderator {
background-image: url(../modactions_removemoderator.png); /* SPRITE */
}
.modactions.addcontributor {

View File

@@ -0,0 +1 @@
addmoderator.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 758 B

After

Width:  |  Height:  |  Size: 16 B

View File

@@ -0,0 +1 @@
addmoderator.png

Before

Width:  |  Height:  |  Size: 758 B

After

Width:  |  Height:  |  Size: 16 B

View File

@@ -0,0 +1,64 @@
## The contents of this file are subject to the Common Public Attribution
## License Version 1.0. (the "License"); you may not use this file except in
## compliance with the License. You may obtain a copy of the License at
## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
## License Version 1.1, but Sections 14 and 15 have been added to cover use of
## software over a computer network and provide for limited attribution for the
## Original Developer. In addition, Exhibit A has been modified to be
## consistent with Exhibit B.
##
## Software distributed under the License is distributed on an "AS IS" basis,
## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
## the specific language governing rights and limitations under the License.
##
## The Original Code is reddit.
##
## The Original Developer is the Initial Developer. The Initial Developer of
## the Original Code is reddit Inc.
##
## All portions of the code written by reddit are Copyright (c) 2006-2012
## reddit Inc. All Rights Reserved.
###############################################################################
<%namespace file="printablebuttons.html" import="ynbutton" />
<%namespace file="userlist.html" import="add_form, userlist"/>
<%namespace file="utils.html" import="error_field"/>
<div class="${thing._class} usertable">
%if thing.can_remove_self:
${ynbutton(op=thing.remove_action,
title=_("leave"),
executed=_("you are no longer a moderator"),
question=_("stop being a moderator?"),
format=_('you are a moderator of this subreddit. %(action)s'),
format_arg='action',
_class=thing.type + ' remove-self',
hidden_data=dict(
id=c.user._fullname,
type=thing.type,
container=thing.container_name))}
%endif
%if thing.has_invite:
${ynbutton(op=thing.invite_action,
title=_("accept"),
executed=_("you are now a moderator. welcome to the team!"),
question=_("become a moderator of %s?" % ("/r/" + c.site.name)),
format=_('you are invited to become a moderator. %(action)s'),
format_arg='action',
_class=thing.type + ' accept-invite')}
%endif
%if thing.can_force_add:
${add_form(thing.form_title, thing.destination, thing.type, thing.container_name)}
%endif
%if thing.editable:
<%call expr="add_form(thing.invite_form_title, thing.destination, thing.invite_type, thing.container_name, verb=_('invite'))">
${error_field("ALREADY_MODERATOR", "name")}
</%call>
%endif
%if thing.editable:
${userlist(_("pending invitations"), thing.invite_type, thing.invited_user_rows, thing.table_headers)}
%endif
${userlist(thing.table_title, thing.type, thing.user_rows, thing.table_headers)}
</div>

View File

@@ -21,57 +21,44 @@
###############################################################################
<%namespace file="utils.html" import="error_field"/>
<%namespace file="printablebuttons.html" import="ynbutton" />
<% from r2.lib.template_helpers import static %>
<div class="${thing._class} usertable">
%if thing.can_remove_self():
${ynbutton(op=thing.remove_action,
title=thing.remove_self_action,
executed=thing.remove_self_final,
question=thing.remove_self_confirm,
format=thing.remove_self_title,
format_arg='action',
_class=thing.type + ' remove-self',
hidden_data=dict(
id=c.user._fullname,
type=thing.type,
container=thing.container_name))}
%endif
<%def name="add_form(title, dest, add_type, container_name, verb=None)">
<form action="/post/${dest}"
method="post" class="pretty-form medium-text"
onsubmit="return post_form(this, '${dest}')"
id="${add_type}">
<h1>${title}</h1>
%if thing.addable:
<form action="/post/${thing.destination}"
method="post" class="pretty-form medium-text"
onsubmit="return post_form(this, '${thing.destination}');"
id="${thing.type}">
<h1>${thing.form_title}</h1>
<input type="hidden" name="action" value="add">
<input type="hidden" name="container" value="${container_name}">
<input type="hidden" name="type" value="${add_type}">
<input type="text" name="name" id="name">
<button class="btn" type="submit">${verb or _("add")}</button>
<span class="status"></span>
${error_field("USER_DOESNT_EXIST", "name")}
%if caller:
${caller.body()}
%endif
</form>
</%def>
<input type="hidden" name="action" value="add"/>
<input type="hidden" name="container" value="${thing.container_name}"/>
<input type="hidden" name="type" value="${thing.type}"/>
<input type="text" name="name" id="name"/>
<button class="btn" type="submit">${_("add")}</button>
<span class="status"></span>
${error_field("USER_DOESNT_EXIST", "name")}
</form>
%endif
<div id="${thing.type}-table"
${"style='display:none'" if not thing.users else ""}>
<h1>
${thing.table_title}
<%def name="userlist(title, row_type, rows, headers=None)">
<div class="${row_type}-table">
<h1>
${title}
</h1>
<table>
%if getattr(thing, "table_headers", None):
%if headers:
<tr>
%for header in thing.table_headers:
%for header in headers:
<th>${header}</th>
%endfor
</tr>
%endif
%if thing.users:
%for item in thing.users:
%if rows:
%for item in rows:
${item}
%endfor
%else:
@@ -79,6 +66,12 @@
%endif
</table>
</div>
</%def>
<div class="${thing._class} usertable">
%if thing.editable:
${add_form(thing.form_title, thing.destination, thing.type, thing.container_name)}
%endif
${userlist(thing.table_title, thing.type, thing.user_rows, thing.table_headers)}
</div>