Timeouts: Add modal for forbidden actions.

Works similarly to the 'login-required' modal.  Links that perform actions that are
forbidden when 'in timeout' are given the 'access-required' class, which triggers
the modal if the current user is in timeout.  Some links additionally needed to have
their default click handlers edited to exit early in this case (e.g. the ynbutton's toggle function).
This commit is contained in:
Matt Lee
2015-08-16 15:29:25 -04:00
committed by xiongchiamiov
parent 9067143175
commit 9c653b4c6b
20 changed files with 216 additions and 39 deletions

View File

@@ -493,6 +493,7 @@ module["reddit"] = LocalizedModule("reddit.js",
"popup.js",
"login.js",
"locked.js",
"timeouts.js",
"newsletter.js",
"flair.js",
"interestbar.js",

View File

@@ -398,6 +398,16 @@ class Reddit(Templated):
self.toolbars = self.build_toolbars()
if c.user_is_loggedin and c.user.in_timeout:
hook = hooks.get_hook('timeouts.fetch_expiration')
expires = hook.call_until_return(user=c.user)
if expires:
now = datetime.datetime.now(g.tz)
expire_date = expires - now
self.timeout_days_remaining = expire_date.days + 1
else:
self.timeout_days_remaining = 0
has_style_override = (c.user.pref_default_theme_sr and
feature.is_enabled('stylesheets_everywhere') and
c.user.pref_enable_default_themes)
@@ -442,24 +452,24 @@ class Reddit(Templated):
buttons = []
buttons.append(NamedButton("wikirecentrevisions",
css_class="wikiaction-revisions",
css_class="wikiaction-revisions access-required",
dest="/wiki/revisions"))
buttons.append(NamedButton("wikipageslist",
css_class="wikiaction-pages",
css_class="wikiaction-pages access-required",
dest="/wiki/pages"))
if moderator:
buttons += [NamedButton('wikibanned', css_class='reddit-ban',
buttons += [NamedButton('wikibanned', css_class='reddit-ban access-required',
dest='/about/wikibanned'),
NamedButton('wikicontributors',
css_class='reddit-contributors',
css_class='reddit-contributors access-required',
dest='/about/wikicontributors')
]
return SideContentBox(_('wiki tools'),
[NavMenu(buttons,
type="flat_vert",
css_class="icon-menu",
css_class="icon-menu access-required",
separator="")],
_id="wikiactions",
collapsible=True)
@@ -473,43 +483,43 @@ class Reddit(Templated):
if is_single_subreddit and is_moderator_with_perms('config'):
buttons.append(NavButton(menu.community_settings,
css_class="reddit-edit",
css_class="reddit-edit access-required",
dest="edit"))
buttons.append(NavButton(menu.edit_stylesheet,
css_class="edit-stylesheet",
css_class="edit-stylesheet access-required",
dest="stylesheet"))
if is_moderator_with_perms('mail'):
buttons.append(NamedButton("modmail",
dest="message/inbox",
css_class="moderator-mail"))
css_class="moderator-mail access-required"))
if is_single_subreddit:
if is_moderator_with_perms('access'):
buttons.append(NamedButton("moderators",
css_class="reddit-moderators"))
css_class="reddit-moderators access-required"))
if not c.site.hide_contributors:
buttons.append(NavButton(
menu.contributors,
"contributors",
css_class="reddit-contributors"))
css_class="reddit-contributors access-required"))
buttons.append(NamedButton("traffic", css_class="reddit-traffic"))
buttons.append(NamedButton("traffic", css_class="reddit-traffic access-required"))
if is_moderator_with_perms('posts'):
buttons += [NamedButton("modqueue", css_class="reddit-modqueue"),
NamedButton("reports", css_class="reddit-reported"),
NamedButton("spam", css_class="reddit-spam"),
NamedButton("edited", css_class="reddit-edited")]
buttons += [NamedButton("modqueue", css_class="reddit-modqueue access-required"),
NamedButton("reports", css_class="reddit-reported access-required"),
NamedButton("spam", css_class="reddit-spam access-required"),
NamedButton("edited", css_class="reddit-edited access-required")]
if is_single_subreddit:
if is_moderator_with_perms('access'):
buttons.append(NamedButton("banned", css_class="reddit-ban"))
buttons.append(NamedButton("banned", css_class="reddit-ban access-required"))
if is_moderator_with_perms('access', 'mail'):
buttons.append(NamedButton("muted", css_class="reddit-mute"))
buttons.append(NamedButton("muted", css_class="reddit-mute access-required"))
if is_moderator_with_perms('flair'):
buttons.append(NamedButton("flair", css_class="reddit-flair"))
buttons.append(NamedButton("flair", css_class="reddit-flair access-required"))
if is_single_subreddit and is_moderator_with_perms('config'):
# append automod button if they have an AutoMod configuration
@@ -518,15 +528,15 @@ class Reddit(Templated):
buttons.append(NamedButton(
"automod",
dest="../wiki/edit/config/automoderator",
css_class="reddit-automod",
css_class="reddit-automod access-required",
))
except tdb_cassandra.NotFound:
pass
buttons.append(NamedButton("log", css_class="reddit-moderationlog"))
buttons.append(NamedButton("log", css_class="reddit-moderationlog access-required"))
if is_moderator_with_perms('posts'):
buttons.append(
NamedButton("unmoderated", css_class="reddit-unmoderated"))
NamedButton("unmoderated", css_class="reddit-unmoderated access-required"))
return SideContentBox(_('moderation tools'),
[NavMenu(buttons,
@@ -729,12 +739,14 @@ class Reddit(Templated):
more_text = _("about moderation team")
mod_href = c.site.path + 'about/moderators'
if '/r/%s' % c.site.name == g.admin_message_acct:
is_admin_sr = '/r/%s' % c.site.name == g.admin_message_acct
if is_admin_sr:
label = _('message the admins')
else:
label = _('message the moderators')
helplink = ("/message/compose?to=%%2Fr%%2F%s" % c.site.name,
label)
label, is_admin_sr)
ps.append(SideContentBox(_('moderators'),
moderators[:sidebar_list_length],
helplink = helplink,
@@ -2541,10 +2553,17 @@ class BannedInterstitial(Interstitial):
class BannedUserInterstitial(BannedInterstitial):
"""The banned user message."""
"""The message shown when viewing a banned user profile."""
pass
class InTimeoutInterstitial(BannedInterstitial):
"""The message shown to a user in timeout."""
def __init__(self, timeout_days_remaining=0):
self.timeout_days_remaining = timeout_days_remaining
BannedInterstitial.__init__(self)
class PrivateInterstitial(Interstitial):
"""The interstitial shown on private subreddits."""
pass

View File

@@ -143,6 +143,7 @@ def header_url(url):
def js_config(extra_config=None):
logged = c.user_is_loggedin and c.user.name
user_id = c.user_is_loggedin and c.user._id
user_in_timeout = c.user_is_loggedin and c.user.in_timeout
gold = bool(logged and c.user.gold)
controller_name = request.environ['pylons.routes_dict']['controller']
action_name = request.environ['pylons.routes_dict']['action']
@@ -178,6 +179,8 @@ def js_config(extra_config=None):
"logged": logged,
# logged in user's id
"user_id": user_id,
# is user in timeout?
"user_in_timeout": user_in_timeout,
# the subreddit's name (for posts)
"post_site": cur_subreddit,
# the user's voting hash

View File

@@ -865,6 +865,12 @@ class Subreddit(Thing, Printable, BaseSite):
else:
return self.is_allowed_to_view(user)
def is_banned(self, user):
if user and user.in_timeout:
return True
else:
return super(Subreddit, self).is_banned(user)
def is_allowed_to_view(self, user):
"""Returns whether user can view based on permissions and settings"""
if self.type in ('public', 'restricted',

View File

@@ -16,6 +16,11 @@ r.actionForm = {
toggleActionForm: function(e) {
var el = e.target;
var $el = $(el);
if (r.timeouts.isLinkRestricted(el)) {
return;
}
var $thing = $el.thing();
var $thingForm = $thing.find('> .entry .action-form');
var formSelector = $el.data('action-form');

View File

@@ -128,6 +128,7 @@ $(function() {
r.newsletter.ui.init()
r.cachePoisoning.init()
r.locked.init();
r.timeouts.init();
} catch (err) {
r.sendError('Error during base.js init', err.toString());
}

View File

@@ -83,6 +83,10 @@ $(function() {
}
function openFlairSelector(e) {
if (r.config.user_in_timeout) {
return false;
}
close_menus(e);
var button = this;

View File

@@ -75,6 +75,10 @@ r.gold = {
},
_toggleThingGoldForm: function (e) {
if (r.config.user_in_timeout) {
return;
}
var $link = $(e.target);
var $thing = $link.thing();
var thingFullname = $link.thing_id();

View File

@@ -248,6 +248,10 @@ function toggle_label (elem, callback, cancelback) {
}
function toggle(elem, callback, cancelback) {
if (r.config.user_in_timeout) {
return false;
}
r.analytics.breadcrumbs.storeLastClick(elem)
var self = $(elem).parent().addBack().filter(".option");
@@ -845,6 +849,10 @@ function cancel_usertext(elem) {
}
function reply(elem) {
if (r.config.user_in_timeout) {
return;
}
var form = comment_reply_for_elem(elem);
// quote any selected text and put it in the textarea if it's empty

View File

@@ -0,0 +1,50 @@
/*
If the current user is 'in timeout', show a modal on restricted actions.
requires r.config (base.js)
requires r.ui.Popup (popup.js)
*/
!function(r) {
// initialized early so click handlers can be bound on declaration
r.timeouts = {};
_.extend(r.timeouts, {
init: function() {
if (!r.config.user_in_timeout) { return; }
$('body').on('click', '.access-required', this._handleClick);
},
getPopup: function() {
// gets the cached popup instance if available, otherwise creates it.
if (this._popup) { return this._popup; }
var content = $('#access-popup').html();
var popup = new r.ui.Popup({
size: 'large',
content: content,
className: 'access-denied-modal',
});
popup.$.on('click', '.interstitial .c-btn', this._handleModalClick);
this._popup = popup;
return popup;
},
_handleClick: function onClick(e) {
this.getPopup()
.show();
return false;
}.bind(r.timeouts),
_handleModalClick: function onClick(e) {
this.getPopup()
.hide();
return false;
}.bind(r.timeouts),
isLinkRestricted: function(el) {
return $(el).hasClass('access-required') && r.config.user_in_timeout;
},
});
}(r);

View File

@@ -36,7 +36,7 @@
$(document.body).on('click', '.arrow', function vote(e) {
var $el = $(this);
if (!r.config.logged) {
if (!r.config.logged || r.config.user_in_timeout) {
return;
}

View File

@@ -0,0 +1,56 @@
## 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-2015
## reddit Inc. All Rights Reserved.
###############################################################################
<%!
from r2.lib.template_helpers import static
%>
<%namespace file="utils.html" import="_mdf"/>
<%inherit file="interstitial.html"/>
<%def name="interstitial_image_attrs()">
src="${static('interstitial-image-banned.png')}"
alt="${_('banned')}"
</%def>
<%def name="interstitial_title()">
%if thing.timeout_days_remaining:
${_("You have been temporarily banned from Reddit")}
%else:
${_("You have been banned from Reddit")}
%endif
</%def>
<%def name="interstitial_message()">
%if thing.message:
${parent.interstitial_message()}
%elif thing.timeout_days_remaining:
<%
days = ungettext('day', 'days', thing.timeout_days_remaining)
%>
${_mdf("You may not participate for %(num)s %(days)s.",
num=thing.timeout_days_remaining, days=days)}
%else:
${_("You may not participate.")}
%endif
</%def>

View File

@@ -215,8 +215,9 @@ ${self.RenderPrintable()}
arrow_class = arrow_type + ("mod" if mod else "")
login_class = "login-required" if not g.read_only_mode else ""
archived_class = "archived" if not getattr(thing, 'votable', True) else ""
access_class = "access-required"
%>
<div ${classes("arrow", arrow_class, login_class, archived_class)}
<div ${classes("arrow", arrow_class, login_class, archived_class, access_class)}
%if not g.read_only_mode:
role="button"
% if dir > 0:

View File

@@ -56,7 +56,7 @@
%if thing.show_report:
<li class="report-button">
<a href="javascript:void(0)" class="action-thing" data-action-form="#report-action-form">
<a href="javascript:void(0)" class="action-thing access-required" data-action-form="#report-action-form">
${_("report")}
</a>
</li>
@@ -139,7 +139,7 @@
<li class="give-gold-button">
<a href="/gold?goldtype=gift&months=1&thing=${thing.thing._fullname}"
title="${_("give reddit gold in appreciation of this post.")}"
class="give-gold login-required"
class="give-gold login-required access-required"
>${_("give gold")}</a>
</li>
% endif
@@ -412,7 +412,7 @@
%if not getattr(thing, "suppress_reply_buttons", False) and (thing.can_reply or thing.locked):
<li class="reply-button">
%if thing.can_reply:
${self.simple_button(_("reply {verb}"), "reply")}
${self.simple_button(_("reply {verb}"), "reply", "access-required")}
%else:
${self.simple_button(_("reply {verb}"), "void", css_class="locked-error")}
%endif
@@ -491,7 +491,7 @@
%endif
%if thing.can_reply:
<li>
${self.simple_button(_("reply {verb}"), "reply")}
${self.simple_button(_("reply {verb}"), "reply", "access-required")}
</li>
%endif
</%def>
@@ -561,7 +561,7 @@
<%
if question is None:
question = _("are you sure?")
link = ('<a href="#" class="togglebutton" onclick="return toggle(this)">'
link = ('<a href="#" class="togglebutton access-required" onclick="return toggle(this)">'
+ conditional_websafe(title) + '</a>')
link = conditional_websafe(format) % {format_arg : link}
link = unsafe(link.replace(" <", "&#32;<").replace("> ", ">&#32;"))

View File

@@ -158,7 +158,7 @@
%if hasattr(thing, "goldlink"):
<div class="giftgold">
<a href="${thing.goldlink}">
<a href="${thing.goldlink}" class="access-required">
${thing.giftmsg}
</a>
</div>
@@ -168,7 +168,9 @@
%if not thing.viewing_self:
<img src="${static('mailgray.png')}"/>
&#32;
${plain_link(_("send a private message"), "/message/compose/?to=%s" % thing.user.name)}
<a href="${"/message/compose/?to=%s" % thing.user.name}" class="access-required">
${_("send a private message")}
</a>
%endif
<span class="age">

View File

@@ -24,7 +24,14 @@
from r2.config import feature
from r2.lib.template_helpers import add_sr, static, join_urls, class_dict, get_domain
from r2.lib.filters import unsafe, scriptsafe_dumps
from r2.lib.pages import SearchForm, ClickGadget, SideContentBox, Login, ListingChooser
from r2.lib.pages import (
SearchForm,
ClickGadget,
SideContentBox,
Login,
ListingChooser,
InTimeoutInterstitial,
)
from r2.lib import tracking
from pylons import request
from r2.lib.strings import strings
@@ -174,6 +181,11 @@
<%include file="prefoptions.html" />
</script>
%endif
%if c.user_is_loggedin and c.user.in_timeout:
<script id="access-popup" type="text/template">
${InTimeoutInterstitial(timeout_days_remaining=thing.timeout_days_remaining)}
</script>
%endif
%endif
% if c.secure:
## Pixel to pick up HSTS policies from the base domain

View File

@@ -125,10 +125,12 @@
mail_img_class = 'nohavemail'
mail_img_title = "no new mod mail"
mail_path = "/message/moderator/"
css_class = "%s access-required" % mail_img_class
%>
${plain_link(_("mod messages"), path=mail_path,
title = mail_img_title, _sr_path = False,
_id = "modmail", _class=mail_img_class)}
_id = "modmail", _class=css_class)}
${separator("|")}
%endif
%endif

View File

@@ -25,7 +25,7 @@
<div class="sidebox ${thing.css_class}${' disabled' if thing.disabled else ''}">
<div class="morelink">
${plain_link(thing.title, thing.link, _sr_path=thing.sr_path,
_class='login-required' if thing.show_cover else None, nocname=thing.nocname,
_class='login-required access-required' if thing.show_cover else None, nocname=thing.nocname,
target=thing.target)}
<div class="nub"> </div>
</div>
@@ -34,7 +34,7 @@
%if thing.show_icon:
<div class="spacer">
${plain_link('', thing.link, _sr_path=thing.sr_path,
_class='login-required' if thing.show_cover else None, nocname=thing.nocname)}
_class='login-required access-required' if thing.show_cover else None, nocname=thing.nocname)}
%else:
<div class="spacer no-icon">
%endif

View File

@@ -23,7 +23,10 @@
<%namespace file="utils.html" import="tags"/>
<div class="sidecontentbox ${thing.extra_class or ''} ${'collapsible' if thing.collapsible else ''}" ${tags(_id=thing._id)}>
%if thing.helplink:
<a class="helplink" href="${thing.helplink[0]}">
<%
access_required = len(thing.helplink) < 3 or not thing.helplink[2]
%>
<a class="helplink ${'access-required' if access_required else ''}" href="${thing.helplink[0]}">
${thing.helplink[1]}
</a>
%endif

View File

@@ -54,7 +54,7 @@
${flair(thing, enabled=thing.force_show_flair)}
%endif
%if thing.include_flair_selector:
(<a class="flairselectbtn" data-name="${thing.name}" href="javascript://void(0)">${_('edit')}</a>)
(<a class="flairselectbtn access-required" data-name="${thing.name}" href="javascript://void(0)">${_('edit')}</a>)
<div class="flairselector drop-choices"></div>
%endif
<span class="userattrs">