Permalink
Browse files

merge async branch

  • Loading branch information...
jlongster committed Dec 13, 2011
2 parents ffe0543 + 75237c6 commit 1352b1126d1ad5e55603dd7bfb3c2e3a4396ac4d
Showing with 297 additions and 298 deletions.
  1. +1 −0 .gitignore
  2. +39 −0 apps/news/README
  3. +51 −0 apps/news/newsletters.py
  4. +128 −0 apps/news/tasks.py
  5. +2 −1 apps/news/urls.py
  6. +59 −195 apps/news/views.py
  7. +0 −102 responsys.py
  8. +17 −0 settings.py
View
@@ -11,3 +11,4 @@ build.py
*-all.css
*-min.js
*-all.js
+.#*
View
@@ -1,6 +1,35 @@
This "news" app provides a service for managing Mozilla newsletters.
+Available newsletters:
+
+* mozilla-and-you
+* mobile
+* beta
+* aurora
+* about-mozilla
+* drumbeat
+* addons
+* hacks
+* labs
+* qa-news
+* student-reps
+* about-standards
+* mobile-addon-dev
+* addon-dev
+* join-mozilla
+* mozilla-phone
+* app-dev
+
+If 'auth-required' is specified, a token must be suffixed onto the API
+URL, such as:
+
+/news/user/<token>/
+
+This is a user-specific token given away by the email backend or
+basket in some manner (i.e. emailed to the user from basket). This
+token allows clients to do more powerful things with the user.
+
The following URLs are available (assuming "/news" is app url):
/news/subscribe
@@ -56,3 +85,13 @@ The following URLs are available (assuming "/news" is app url):
fields. Note that the user is only subscribed to "newsletters" after
this, meaning the user will be unsubscribed to all other
newsletters. "optin" should be Y or N and opts in/out the user.
+
+/news/debug-user
+
+ method: GET
+ fields: email, supertoken
+
+ This is the same as a GET request to /user, except that you must
+ pass in the email and a supertoken as GET params. The supertoken is
+ a special token that should never be made public and lets devs debug
+ users to make sure they were entered into the system correctly.
View
@@ -0,0 +1,51 @@
+"""This data provides an official list of newsletters and tracks
+backend-specific data for working with them in the email provider.
+
+It's used to lookup the backend-specific newsletter name from a
+generic one passed by the user. This decouples the API from any
+specific email provider."""
+
+__all__ = ('newsletter_field', 'newsletter_name', 'newsletter_fields', 'newsletter_names')
+
+newsletters = {
+ 'mozilla-and-you': 'MOZILLA_AND_YOU',
+ 'mobile': 'ABOUT_MOBILE',
+ 'beta': 'FIREFOX_BETA_NEWS',
+ 'aurora': 'AURORA',
+ 'about-mozilla': 'ABOUT_MOZILLA',
+ 'drumbeat': 'DRUMBEAT_NEWS_GROUP',
+ 'addons': 'ABOUT_ADDONS',
+ 'hacks': 'ABOUT_HACKS',
+ 'labs': 'ABOUT_LABS',
+ 'qa-news': 'QA_NEWS',
+ 'student-reps': 'STUDENT_REPS',
+ 'about-standards': 'ABOUT_STANDARDS',
+ 'mobile-addon-dev': 'MOBILE_ADDON_DEV',
+ 'addon-dev': 'ADD_ONS',
+ 'join-mozilla': 'JOIN_MOZILLA',
+ 'mozilla-phone': 'MOZILLA_PHONE',
+ 'app-dev': 'APP_DEV',
+ 'moz-spaces': 'MOZ_SPACE',
+}
+
+
+def newsletter_field(name):
+ """Lookup the backend-specific field for the newsletter"""
+ return newsletters.get(name)
+
+
+def newsletter_name(field):
+ """Lookup the generic name for this newsletter field"""
+ for k, v in newsletters.iteritems():
+ if v == field:
+ return k
+
+
+def newsletter_names():
+ """Get a list of all the availble newsletters"""
+ return newsletters.keys()
+
+
+def newsletter_fields():
+ """Get a list of all the newsletter backend-specific fields"""
+ return newsletters.values()
View
@@ -0,0 +1,128 @@
+import uuid
+from datetime import date
+from urllib2 import URLError
+
+from django.conf import settings
+from celery.task import task
+
+from responsys import Responsys, NewsletterException, UnauthorizedException
+from models import Subscriber
+from newsletters import *
+
+
+# A few constants to indicate the type of action to take
+# on a user with a list of newsletters
+SUBSCRIBE=1
+UNSUBSCRIBE=2
+SET=3
+
+
+def parse_newsletters(record, type, newsletters):
+ """Utility function to take a list of newsletters and according
+ the type of action (subscribe, unsubscribe, and set) set the
+ appropriate flags in `record` which is a dict of parameters that
+ will be sent to Responsys."""
+
+ newsletters = [x.strip() for x in newsletters.split(',')]
+
+ if type == SUBSCRIBE or type == SET:
+ # Subscribe the user to these newsletters
+ for nl in newsletters:
+ name = newsletter_field(nl)
+ if name:
+ record['%s_FLG' % name] = 'Y'
+ record['%s_DATE' % name] = date.today().strftime('%Y-%m-%d')
+
+
+ if type == UNSUBSCRIBE or type == SET:
+ # Unsubscribe the user to these newsletters
+ unsubs = newsletters
+
+ if type == SET:
+ # Unsubscribe to the inversion of these newsletters
+ subs = set(newsletters)
+ all = set(newsletter_names())
+ unsubs = all.difference(subs)
+
+ for nl in unsubs:
+ name = newsletter_field(nl)
+ if name:
+ record['%s_FLG' % name] = 'N'
+ record['%s_DATE' % name] = date.today().strftime('%Y-%m-%d')
+
+
+@task(default_retry_delay=60) # retry in 1 minute on failure
+def update_user(data, authed_email, type):
+ """Task for updating user's preferences and newsletters.
+
+ ``authed_email`` is the email for the user pulled from the database
+ with their token, if exists."""
+
+ log = update_user.get_logger()
+
+ # Validate parameters
+ if not authed_email and 'email' not in data:
+ log.error('No user or email provided')
+
+ # Parse the parameters
+ record = {'EMAIL_ADDRESS_': data['email'],
+ 'EMAIL_PERMISSION_STATUS_': 'I'}
+
+ extra_fields = {
+ 'format': 'EMAIL_FORMAT_',
+ 'country': 'COUNTRY_',
+ 'lang': 'LANGUAGE_ISO2',
+ 'locale': 'LANG_LOCALE',
+ 'source_url': 'SOURCE_URL'
+ }
+
+ # Optionally add more fields
+ for field in extra_fields.keys():
+ if field in data:
+ record[extra_fields[field]] = data[field]
+
+ # Set the newsletter flags in the record
+ parse_newsletters(record, type, data.get('newsletters', ''))
+
+ # Get the user or create them
+ (sub, created) = Subscriber.objects.get_or_create(email=record['EMAIL_ADDRESS_'])
+
+ # Update the token if it's a new user or they aren't simply
+ # subscribing from a newsletter form (tokens are one-time use)
+ if created or type != SUBSCRIBE:
+ sub.token = str(uuid.uuid4())
+ record['TOKEN'] = sub.token
+ sub.save()
+
+ # Submit the final data to responsys
+ try:
+ rs = Responsys()
+ rs.login(settings.RESPONSYS_USER, settings.RESPONSYS_PASS)
+
+ if authed_email and record['EMAIL_ADDRESS_'] != authed_email:
+ # Email has changed, we need to delete the previous user
+ rs.delete_list_members(authed_email,
+ settings.RESPONSYS_FOLDER,
+ settings.RESPONSYS_LIST)
+
+ rs.merge_list_members(settings.RESPONSYS_FOLDER,
+ settings.RESPONSYS_LIST,
+ record.keys(),
+ record.values())
+
+ # Trigger the welcome event unless it is suppressed
+ if data.get('trigger_welcome', False) == 'Y':
+ rs.trigger_custom_event(record['EMAIL_ADDRESS_'],
+ settings.RESPONSYS_FOLDER,
+ settings.RESPONSYS_LIST,
+ 'New_Signup_Welcome')
+
+ rs.logout()
+ except URLError, e:
+ # URL timeout, try again
+ update_user.retry(exc=e)
+ except NewsletterException, e:
+ log.error('NewsletterException: %s' % e.message)
+ except UnauthorizedException, e:
+ log.error('Responsys auth failure')
+
View
@@ -1,12 +1,13 @@
from django.conf.urls.defaults import *
from views import (subscribe, unsubscribe, user,
- delete_user, custom_unsub_reason)
+ delete_user, debug_user, custom_unsub_reason)
urlpatterns = patterns('',
url('^subscribe/$', subscribe),
url('^unsubscribe/(.*)/$', unsubscribe),
url('^user/(.*)/$', user),
url('^delete/(.*)/$', delete_user),
+ url('^debug-user/$', debug_user),
url('^custom_unsub_reason/$', custom_unsub_reason)
)
Oops, something went wrong.

0 comments on commit 1352b11

Please sign in to comment.