From ae1296cf876170dd27bb3a237e9bd6a7bb492c8a Mon Sep 17 00:00:00 2001 From: MelissaCole Date: Tue, 21 Jul 2015 16:21:14 -0700 Subject: [PATCH] Create quarantined subreddits content gate When a user visits a quarantined subreddit for the first time, they are presented with a content gate so that they can choose to optin to view the subreddit's content. This preference is remembered per subreddit. If a user is logged out, it requires the user to log in before validating that the user has chosen to opt in to view the subreddit. --- r2/r2/config/routing.py | 1 + r2/r2/controllers/post.py | 27 ++++++++++++++++++++ r2/r2/controllers/reddit_base.py | 12 ++++----- r2/r2/lib/pages/pages.py | 7 +++++ r2/r2/models/account.py | 30 ++++++++++++++++++++++ r2/r2/models/subreddit.py | 21 +++++++++++---- r2/r2/templates/quarantine.html | 44 ++++++++++++++++++++++++++++++++ 7 files changed, 130 insertions(+), 12 deletions(-) create mode 100644 r2/r2/templates/quarantine.html diff --git a/r2/r2/config/routing.py b/r2/r2/config/routing.py index 012d9cd12..c78277906 100644 --- a/r2/r2/config/routing.py +++ b/r2/r2/config/routing.py @@ -70,6 +70,7 @@ def make_map(): mc('/submit', controller='front', action='submit') mc('/over18', controller='post', action='over18') + mc('/quarantine', controller='post', action='quarantine') mc('/rules', controller='front', action='rules') mc('/sup', controller='front', action='sup') diff --git a/r2/r2/controllers/post.py b/r2/r2/controllers/post.py index 88e9ed53e..980663d7b 100644 --- a/r2/r2/controllers/post.py +++ b/r2/r2/controllers/post.py @@ -81,6 +81,16 @@ class PostController(ApiController): return BoringPage(_("over 18?"), content=Over18(), show_sidebar=False).render() + @validate( + dest=VDestination(default='/'), + ) + def GET_quarantine(self, dest): + sr = UrlParser(dest).get_subreddit() + return BoringPage(_("opt in to potentially offensive content?"), + content=Quarantine(sr.name), + show_sidebar=False, + ).render() + @validate(VModhash(fatal=False), over18 = nop('over18'), dest = VDestination(default = '/')) @@ -100,6 +110,23 @@ class PostController(ApiController): delete_over18_cookie() return self.redirect('/') + @validate( + VModhash(), + sr=VSRByName('sr_name'), + accept=VBoolean('accept'), + dest=VDestination(default='/'), + ) + def POST_quarantine(self, sr, accept, dest): + if not c.user_is_loggedin: + return self.redirect(dest) + + if accept: + QuarantinedSubredditOptInsByAccount.opt_in(c.user, sr) + return self.redirect(dest) + else: + QuarantinedSubredditOptInsByAccount.opt_out(c.user, sr) + return self.redirect('/') + @csrf_exempt @validate(msg_hash = nop('x')) def POST_optout(self, msg_hash): diff --git a/r2/r2/controllers/reddit_base.py b/r2/r2/controllers/reddit_base.py index 5b339b552..bca459939 100644 --- a/r2/r2/controllers/reddit_base.py +++ b/r2/r2/controllers/reddit_base.py @@ -1668,13 +1668,11 @@ class RedditController(OAuth2ResourceController): # do not leak the existence of multis via 403. self.abort404() elif not c.site.is_exposed(c.user): - errpage = pages.RedditError( - strings.quarantine_subreddit_title, - strings.quarantine_subreddit_message, - image="subreddit-banned.png", - ) - request.environ['usable_error_content'] = errpage.render() - self.abort403() + if not c.user_is_loggedin: + return self.intermediate_redirect('/login', sr_path=False) + else: + return self.intermediate_redirect("/quarantine", sr_path=False) + elif c.site.type == 'gold_only' and not (c.user.gold or c.user.gold_charter): public_description = c.site.public_description errpage = pages.RedditError( diff --git a/r2/r2/lib/pages/pages.py b/r2/r2/lib/pages/pages.py index 1c0566cc9..a876a7ae5 100644 --- a/r2/r2/lib/pages/pages.py +++ b/r2/r2/lib/pages/pages.py @@ -2457,6 +2457,13 @@ class Over18(Templated): """The creepy 'over 18' check page for nsfw content.""" pass + +class Quarantine(Templated): + """The opt in page for viewing quarantined content.""" + def __init__(self, sr_name): + Templated.__init__(self, sr_name=sr_name) + + class SubredditTopBar(CachedTemplate): """The horizontal strip at the top of most pages for navigating diff --git a/r2/r2/models/account.py b/r2/r2/models/account.py index 8ece7b4d2..d924b81ff 100644 --- a/r2/r2/models/account.py +++ b/r2/r2/models/account.py @@ -1076,3 +1076,33 @@ class SubredditParticipationByAccount(tdb_cassandra.DenormalizedRelation): @classmethod def mark_participated(cls, account, subreddit): cls.create(account, [subreddit]) + + +class QuarantinedSubredditOptInsByAccount(tdb_cassandra.DenormalizedRelation): + _use_db = True + _last_modified_name = 'QuarantineSubredditOptin' + _read_consistency_level = tdb_cassandra.CL.QUORUM + _write_consistency_level = tdb_cassandra.CL.QUORUM + _connection_pool = 'main' + _views = [] + + @classmethod + def value_for(cls, thing1, thing2): + return datetime.now(g.tz) + + @classmethod + def opt_in(cls, account, subreddit): + if subreddit.quarantine: + cls.create(account, subreddit) + + @classmethod + def opt_out(cls, account, subreddit): + cls.destroy(account, subreddit) + + @classmethod + def is_opted_in(cls, user, subreddit): + try: + r = cls.fast_query(user, [subreddit]) + except tdb_cassandra.NotFound: + return False + return (user, subreddit) in r diff --git a/r2/r2/models/subreddit.py b/r2/r2/models/subreddit.py index f1dd4e964..d0f3aef62 100644 --- a/r2/r2/models/subreddit.py +++ b/r2/r2/models/subreddit.py @@ -37,7 +37,12 @@ from pylons import c, g, request from pylons.i18n import _, N_ from r2.lib.db.thing import Thing, Relation, NotFound -from account import Account, AccountsActiveBySR, FakeAccount +from account import ( + Account, + AccountsActiveBySR, + FakeAccount, + QuarantinedSubredditOptInsByAccount, +) from printable import Printable from r2.lib.db.userrel import UserRel, MigratingUserRel from r2.lib.db.operators import lower, or_, and_, not_, desc @@ -793,7 +798,7 @@ class Subreddit(Thing, Printable, BaseSite): def can_view(self, user): if c.user_is_admin: return True - + if self.spammy() or not self.is_exposed(user): return False elif self.type in ('public', 'restricted', @@ -811,9 +816,15 @@ class Subreddit(Thing, Printable, BaseSite): self.is_moderator_invite(user)) def is_exposed(self, user): - """Checks if visible to user based on the quarantine attribute.""" - return not self.quarantine or (c.user_is_loggedin and - self.is_subscriber(user)) + if c.user_is_admin: + return True + elif not self.quarantine: + return True + elif (c.user_is_loggedin and + QuarantinedSubredditOptInsByAccount.is_opted_in(user, self)): + return True + + return False def can_demod(self, bully, victim): bully_rel = self.get_moderator(bully) diff --git a/r2/r2/templates/quarantine.html b/r2/r2/templates/quarantine.html new file mode 100644 index 000000000..42009a52b --- /dev/null +++ b/r2/r2/templates/quarantine.html @@ -0,0 +1,44 @@ +## 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 %> + +
+

+ ${_("the following content is questionable")} +

+ + +
+ + +

+ ${_("are you willing to see this questionable content?")} +

+

+ + +

+
+
+