Award-claiming via one-time links.

This commit is contained in:
Keith Mitchell
2013-03-08 10:49:21 -08:00
committed by Max Goodman
parent db38aad94c
commit fc9abd1dcb
10 changed files with 222 additions and 0 deletions

View File

@@ -100,6 +100,10 @@ def make_map():
mc('/bookmarklets', controller='buttons', action='bookmarklets')
mc('/awards', controller='front', action='awards')
mc('/awards/confirm/:code', controller='front',
action='confirm_award_claim')
mc('/awards/claim/:code', controller='front', action='claim_award')
mc('/awards/received', controller='front', action='received_award')
mc('/i18n', controller='redirect', action='redirect',
dest='http://www.reddit.com/r/i18n')

View File

@@ -1106,6 +1106,39 @@ class FrontController(RedditController, OAuth2ResourceController):
vendor_url=vendor_url,
lounge_md=lounge_md)).render()
@validate(VUser(),
token=VOneTimeToken(AwardClaimToken, "code"))
def GET_confirm_award_claim(self, token):
if not token:
abort(403)
award = Award._by_fullname(token.awardfullname)
trophy = FakeTrophy(c.user, award, token.description, token.url)
content = ConfirmAwardClaim(trophy=trophy, user=c.user.name,
token=token)
return BoringPage(_("claim this award?"), content=content).render()
@validate(VUser(),
token=VOneTimeToken(AwardClaimToken, "code"))
def POST_claim_award(self, token):
if not token:
abort(403)
token.consume()
award = Award._by_fullname(token.awardfullname)
trophy, preexisting = Trophy.claim(c.user, token.uid, award,
token.description, token.url)
redirect = '/awards/received?trophy=' + trophy._id36
if preexisting:
redirect += '&duplicate=true'
self.redirect(redirect)
@validate(trophy=VTrophy('trophy'),
preexisting=VBoolean('duplicate'))
def GET_received_award(self, trophy, preexisting):
content = AwardReceived(trophy=trophy, preexisting=preexisting)
return BoringPage(_("award claim"), content=content).render()
def GET_gold_info(self):
return GoldInfoPage(_("gold"), show_sidebar=False).render()

View File

@@ -3895,6 +3895,12 @@ class ApiHelp(Templated):
class RulesPage(Templated):
pass
class AwardReceived(Templated):
pass
class ConfirmAwardClaim(Templated):
pass
class TimeSeriesChart(Templated):
def __init__(self, id, title, interval, columns, rows,
latest_available_data=None, classes=[]):

View File

@@ -620,6 +620,23 @@ class Account(Thing):
if not self._spam:
AccountsActiveBySR.touch(self, sr)
def get_trophy_id(self, uid):
'''Return the ID of the Trophy associated with the given "uid"
`uid` - The unique identifier for the Trophy to look up
'''
return getattr(self, 'received_trophy_%s' % uid, None)
def set_trophy_id(self, uid, trophy_id):
'''Recored that a user has received a Trophy with "uid"
`uid` - The trophy "type" that the user should only have one of
`trophy_id` - The ID of the corresponding Trophy object
'''
return setattr(self, 'received_trophy_%s' % uid, trophy_id)
class FakeAccount(Account):
_nodb = True
pref_no_profanity = True

View File

@@ -25,6 +25,7 @@ from r2.lib.utils import tup, fetch_things2
from r2.lib.filters import websafe
from r2.lib.log import log_text
from r2.models import Report, Account, Subreddit
from r2.models.token import AwardClaimToken
from _pylibmc import MemcachedError
from pylons import g
@@ -193,6 +194,22 @@ class AdminTools(object):
def admin_list(self):
return list(g.admins)
def create_award_claim_code(self, unique_award_id, award_codename,
description, url):
'''Create a one-time-use claim URL for a user to claim a trophy.
`unique_award_id` - A string that uniquely identifies the kind of
Trophy the user would be claiming.
See: token.py:AwardClaimToken.uid
`award_codename` - The codename of the Award the user will claim
`description` - The description the Trophy will receive
`url` - The URL the Trophy will receive
'''
award = Award._by_codename(award_codename)
token = AwardClaimToken._new(unique_award_id, award, description, url)
return token.confirm_url()
admintools = AdminTools()
def cancel_subscription(subscr_id):

View File

@@ -117,6 +117,16 @@ class Award (Thing):
else:
g.log.debug("%s didn't have %s" % (user, codename))
class FakeTrophy(object):
def __init__(self, recipient, award, description=None, url=None,
cup_info=None):
self._thing2 = award
self._thing1 = recipient
self.description = description
self.url = url
self.cup_info = cup_info
self._id = self._id36 = None
class Trophy(Relation(Account, Award)):
@classmethod
def _new(cls, recipient, award, description = None,
@@ -178,3 +188,18 @@ class Trophy(Relation(Account, Award)):
trophies = Trophy._byID_rel(rel_ids, data=True, eager_load=True,
thing_data=True, return_dict = False)
return trophies
@classmethod
def claim(cls, user, uid, award, description, url):
with g.make_lock("claim_award", str("%s_%s" % (user.name, uid))):
existing_trophy_id = user.get_trophy_id(uid)
if existing_trophy_id:
trophy = cls._byID(existing_trophy_id)
preexisting = True
else:
preexisting = False
trophy = cls._new(user, award, description=description,
url=url)
user.set_trophy_id(uid, trophy._id)
user._commit()
return trophy, preexisting

View File

@@ -26,6 +26,7 @@ from base64 import urlsafe_b64encode
from pycassa.system_manager import ASCII_TYPE, DATE_TYPE, UTF8_TYPE
from pylons import g
from pylons.i18n import _
from r2.lib.db import tdb_cassandra
@@ -532,3 +533,46 @@ class PasswordResetToken(ConsumableToken):
def valid_for_user(self, user):
return (self.email_address == user.email and
self.password == user.password)
class AwardClaimToken(ConsumableToken):
token_size = 20
_ttl = datetime.timedelta(days=30)
_defaults = dict(ConsumableToken._defaults.items() + [
("awardfullname", ""),
("description", ""),
("url", ""),
("uid", "")])
_use_db = True
_connection_pool = "main"
@classmethod
def _new(cls, uid, award, description, url):
'''Create an AwardClaimToken with the given parameters
`uid` - A string that uniquely identifies the kind of
Trophy the user would be claiming.*
`award_codename` - The codename of the Award the user will claim
`description` - The description the Trophy will receive
`url` - The URL the Trophy will receive
*Note that this differs from Award codenames, because it may be
desirable to allow users to have multiple copies of the same Award,
but restrict another aspect of the Trophy. For example, users
are allowed to have multiple Translator awards, but should only get
one for each language, so the `unique_award_id`s for those would be
of the form "i18n_%(language)s"
'''
return super(AwardClaimToken, cls)._new(
awardfullname=award._fullname,
description=description or "",
url=url or "",
uid=uid,
)
def claim_url(self):
return "http://%s/awards/claim/%s" % (g.domain, self._id)
def confirm_url(self):
return "http://%s/awards/confirm/%s" % (g.domain, self._id)

View File

@@ -4297,6 +4297,11 @@ table.lined-table {
margin-top: 4px;
}
.confirm-award-claim .md {
max-width: none;
font-size: 18px;
}
.trophy-table {
width: 100%;
}

View File

@@ -0,0 +1,34 @@
## 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="trophycase.html" import="trophy_info" />
<div class="centered">
%if thing.preexisting:
<h1>${_("you already have that trophy!")}</h1>
%else:
<h1>${_("trophy claimed!")}</h1>
%endif
${trophy_info(thing.trophy, False)}
</div>

View File

@@ -0,0 +1,37 @@
## 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.
###############################################################################
<%!
from r2.lib.filters import safemarkdown
%>
<%namespace file="trophycase.html" import="trophy_info" />
<form action="${thing.token.claim_url()}" method="post"
class="centered confirm-award-claim">
${unsafe(safemarkdown(_("# Claim this award for /u/%s? #") % thing.user))}
${trophy_info(thing.trophy, False)}
<button class="btn" type="submit">${_("Yes, please!")}</button>
</form>