mirror of
https://github.com/reddit-archive/reddit.git
synced 2026-01-28 16:28:01 -05:00
Award-claiming via one-time links.
This commit is contained in:
committed by
Max Goodman
parent
db38aad94c
commit
fc9abd1dcb
@@ -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')
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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=[]):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -4297,6 +4297,11 @@ table.lined-table {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.confirm-award-claim .md {
|
||||
max-width: none;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.trophy-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
34
r2/r2/templates/awardreceived.html
Normal file
34
r2/r2/templates/awardreceived.html
Normal 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>
|
||||
37
r2/r2/templates/confirmawardclaim.html
Normal file
37
r2/r2/templates/confirmawardclaim.html
Normal 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>
|
||||
Reference in New Issue
Block a user