Newsletter: Capture emails from interested users on registration

Adds an an opt-in checkbox to the registration flow for the
upvoted newsletter, a project Alexis and Heath are working on.

This does not associate any data with the user's account, it
just sends their email address to the campaign monitor API if
they opted in.
This commit is contained in:
umbrae
2015-02-27 17:19:00 -08:00
parent 76555a9d4a
commit 04c2ac2d90
6 changed files with 132 additions and 9 deletions

View File

@@ -39,6 +39,8 @@ paypal_webhook =
coinbase_webhook =
# secret for communicating with RedditGifts (optional payment processor)
redditgifts_webhook =
# The campaign monitor API key for the newsletter
newsletter_api_key =
[DEFAULT]
############################################ SITE-SPECIFIC OPTIONS
@@ -287,6 +289,10 @@ embedly_api_key =
autoexpand_media_types = liveupdate
############################################ NEWSLETTER
# The list ID within campaign monitor to be altering
newsletter_list_id =
############################################ QUOTAS
# quota for various types of relations creatable in subreddits
sr_banned_quota = 10000

View File

@@ -97,7 +97,7 @@ from r2.lib.db import queries
from r2.lib import media
from r2.lib.db import tdb_cassandra
from r2.lib import promote
from r2.lib import tracking, emailer
from r2.lib import tracking, emailer, newsletter
from r2.lib.subreddit_search import search_reddits
from r2.lib.log import log_text
from r2.lib.filters import safemarkdown
@@ -278,11 +278,19 @@ class ApiController(RedditController):
return {}
@csrf_exempt
@json_validate(email=ValidEmail("email"))
def POST_check_email(self, responder, email):
@json_validate(email=ValidEmail("email"),
newsletter_subscribe=VBoolean("newsletter_subscribe", default=False))
def POST_check_email(self, responder, email, newsletter_subscribe):
"""
Check whether an email is valid.
Check whether an email is valid. Allows blank emails.
Additionally checks if a newsletter is requested, and will be strict
on blank emails if so.
"""
if feature.is_enabled('newsletter') and newsletter_subscribe and not email:
c.errors.add(errors.NEWSLETTER_NO_EMAIL, field="email")
responder.has_errors("email", errors.NEWSLETTER_NO_EMAIL)
return
if not (responder.has_errors("email", errors.BAD_EMAIL)):
# Pylons does not handle 204s correctly.
@@ -650,6 +658,16 @@ class ApiController(RedditController):
responder.has_errors('ratelimit', errors.RATELIMIT) or
(not g.disable_captcha and bad_captcha)):
newsletter_subscribe = False
if feature.is_enabled('newsletter'):
# Todo: add to validatedForm when feature is released
vnewsletter = VBoolean('newsletter_subscribe', default=False)
newsletter_subscribe = vnewsletter.run(request.params.get('newsletter_subscribe'))
if newsletter_subscribe and not email:
c.errors.add(errors.NEWSLETTER_NO_EMAIL, field="email")
form.has_errors("email", errors.NEWSLETTER_NO_EMAIL)
return
user = register(name, password, request.ip)
VRatelimit.ratelimit(rate_ip = True, prefix = "rate_register_")
@@ -673,6 +691,13 @@ class ApiController(RedditController):
if any(reject):
return
if feature.is_enabled('newsletter'):
if newsletter_subscribe and email:
try:
newsletter.add_subscriber(email, source="register")
except newsletter.NewsletterError as e:
g.log.warning("Failed to subscribe: %r" % e)
self._login(responder, user, rem)
@csrf_exempt

View File

@@ -92,6 +92,7 @@ error_list = dict((
('BAD_EMAILS', _('the following emails are invalid: %(emails)s')),
('NO_EMAILS', _('please enter at least one email address')),
('TOO_MANY_EMAILS', _('please only share to %(num)s emails at a time.')),
('NEWSLETTER_NO_EMAIL', _('where should we send that weekly newsletter?')),
('OVERSOLD', _('that subreddit has already been oversold on %(start)s to %(end)s. Please pick another subreddit or date.')),
('OVERSOLD_DETAIL', _("We have insufficient inventory to fulfill your requested budget, target, and dates. Only %(available)s impressions available on %(target)s from %(start)s to %(end)s.")),
('BAD_DATE', _('please provide a date of the form mm/dd/yyyy')),

75
r2/r2/lib/newsletter.py Normal file
View File

@@ -0,0 +1,75 @@
# 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 pylons import g
import json
import requests
BASE_URL = "https://api.createsend.com/api/v3.1/"
API_KEY = g.secrets['newsletter_api_key']
LIST_ID = g.newsletter_list_id
class NewsletterError(Exception):
pass
def add_subscriber(email, source=""):
"""Given an email, add this user to our upvoted newsletter.
Optionally, also provide a source parameter describing where the subscribe
came from - like "register".
If the email was unable to be added, throws `NewsletterError`. Returns
`True` on success.
This should be used sparingly and outside of high traffic areas, as it
requires a network call.
"""
if not API_KEY or not LIST_ID:
raise NewsletterError("Unable to subscribe user %s to newsletter. "
"API key or list ID not set." % email)
params = {
"EmailAddress": email
}
if source:
params["CustomFields"] = [{"Key": "source", "Value": source}]
try:
r = requests.post(
"%s/subscribers/%s.json" % (BASE_URL, LIST_ID),
json.dumps(params),
timeout=3,
auth=(API_KEY, 'x'),
)
except requests.exception.Timeout:
raise NewsletterError("Unable to subscribe user %s to newsletter. "
"Request timed out." % email)
else:
if r.status_code == 201:
return True
else:
raise NewsletterError("Could not subscribe user %s to "
"newsletter. Status code: %s" %
(email, r.status_code))

View File

@@ -30,6 +30,10 @@
position: relative;
}
.c-submit-group {
margin-top: @input-height-base;
}
.c-radio,
.c-checkbox {
position: relative;

View File

@@ -112,17 +112,29 @@
data-validate-on="change blur">
</%call>
%endif
<div class="c-checkbox c-input-height">
<label for="rem_${op}" class="remember">
<input type="checkbox" name="rem" id="rem_${op}"
tabindex="${tabindex}">
<div class="c-checkbox">
<input type="checkbox" name="rem" id="rem_${op}" tabindex="${tabindex}">
<label for="rem_${op}">
${_('remember me')}
</label>
%if not register:
<a href="/password" class="c-pull-right">${_('reset password')}</a>
%endif
</div>
<div class="c-clearfix">
%if register and feature.is_enabled('newsletter'):
<div class="c-checkbox">
<input type="checkbox" name="newsletter_subscribe" id="newsletter_subscribe" tabindex="${tabindex}"
data-validate-url="/api/check_email.json"
data-validate-on="change blur"
data-validate-with="email"
>
<label for="newsletter_subscribe">
${_('get the best of reddit emailed to you once a week.')}&#32;
<a href="/newsletter" target="_blank">${_('learn more')}</a>
</label>
</div>
%endif
<div class="c-clearfix c-submit-group">
<span class="c-form-throbber"></span>
<button type="submit" class="c-btn c-btn-primary c-pull-right" tabindex="${tabindex}">
${register and _("create account") or _("sign in")}