Skip to content
This repository has been archived by the owner on Jan 13, 2022. It is now read-only.

Bug 742951 - changing email address #84

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ The following URLs are available (assuming "/news" is app url):
returns: {
status: ok,
email: <email>,
pending_email: <newemail>, # only if an email change is pending
format: <format>,
country: <country>,
lang: <lang>,
Expand Down Expand Up @@ -183,7 +184,7 @@ The following URLs are available (assuming "/news" is app url):
Returns information about all of the available newsletters::

method: GET

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol

fiends: *none*
fields: *none*
returns: {
status: ok,
newsletters: {
Expand Down Expand Up @@ -248,6 +249,7 @@ The following URLs are available (assuming "/news" is app url):
'status': 'error', # errors talking to ET, see next field
'desc': 'error message' # details if status is error
'email': 'email@address',
'pending_email': <newemail>, # only if an email change is pending
'format': 'T'|'H',
'country': country code,
'lang': language code,
Expand Down Expand Up @@ -282,3 +284,44 @@ The following URLs are available (assuming "/news" is app url):
language and format.

If the email provided is not known, a 404 status is returned.

/news/start-email-change/
-------------------------

This asks to change the email address where a user receives their
newsletters.

method: POST
fields: token (in URL, e.g. /news/start-email-change/<token>/)
email (in POST data) - the desired new email address

This will send an email message to the new email address with a link that
the user needs to click in order to confirm the change. The link will
timeout in 48 hours.

If a change is already pending, the confirmation email message will be
sent again, and the timeout will be extended to 48 hours from the
current time.

See /news/confirm-email-change/ for confirming the change.

/news/confirm-email-change/
---------------------------

This takes a change key from the link provided in a message for confirming
a change of email address, and if the change key is valid, executes the
change.

method: POST
fields: change_key (in URL)
returns: { status: ok } on success
{ status: error, desc: <desc> } on error

The change key is passed in the URL: ``/news/confirm-email-change/CHANGEKEY/``.

The change is implemented by changing the email address on the user's
ET record.

Also, if the email address being changed to already has an ET record,
any subscriptions on that record are merged into the user's ET record, and
then the other ET record is deleted.
8 changes: 7 additions & 1 deletion news/admin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.contrib import admin, messages

from .models import APIUser, FailedTask, Newsletter, Subscriber
from .models import APIUser, EmailChangeRequest, FailedTask, Newsletter, Subscriber


class APIUserAdmin(admin.ModelAdmin):
Expand All @@ -19,6 +19,12 @@ class SubscriberAdmin(admin.ModelAdmin):
admin.site.register(Subscriber, SubscriberAdmin)


class EmailChangeRequestAdmin(admin.ModelAdmin):
list_display = ('subscriber', 'new_email', 'is_expired')
raw_id_fields = ('subscriber',)
admin.site.register(EmailChangeRequest, EmailChangeRequestAdmin)


class NewsletterAdmin(admin.ModelAdmin):
fields = ('title', 'slug', 'vendor_id', 'welcome', 'confirm_message',
'description', 'languages', 'show', 'order', 'active',
Expand Down
13 changes: 13 additions & 0 deletions news/backends/exacttarget.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,18 @@ def data_ext(self):

@logged_in
def trigger_send(self, send_name, fields):
"""
Trigger a send of a pre-defined trigger message, with parameters
that can be used by Exact Target in generating the message sent.

:param str send_name: Name of message to send
:param str fields['EMAIL_ADDRESS_']: Address to send to
:param str fields['TOKEN']: Token of the user we're sending to
:param str fields['EMAIL_FORMAT_']: 'H' to send HTML, else text

Additional values in ``fields`` are passed as attributes.
"""
# http://help.exacttarget.com/en/technical_library/web_service_guide/objects/triggeredsend/
send = self.create('TriggeredSend')
defn = send.TriggeredSendDefinition

Expand All @@ -380,6 +392,7 @@ def trigger_send(self, send_name, fields):
defn.CustomerKey = send_name
defn.TriggeredSendStatus = status.Active

# http://help.exacttarget.com/en/technical_library/web_service_guide/objects/subscriber/
sub = self.create('Subscriber')
sub.EmailAddress = fields.pop('EMAIL_ADDRESS_')
sub.SubscriberKey = fields['TOKEN']
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models


class Migration(SchemaMigration):

def forwards(self, orm):
# Adding model 'EmailChangeRequest'
db.create_table(u'news_emailchangerequest', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('when', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now)),
('subscriber', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['news.Subscriber'])),
('new_email', self.gf('django.db.models.fields.EmailField')(max_length=75)),
('change_key', self.gf('django.db.models.fields.CharField')(default='7138f993-c321-4a22-b5e1-fb21c2d83222', max_length=40, db_index=True)),
))
db.send_create_signal(u'news', ['EmailChangeRequest'])

# Adding unique constraint on 'Subscriber', fields ['token']
db.create_unique(u'news_subscriber', ['token'])


def backwards(self, orm):
# Removing unique constraint on 'Subscriber', fields ['token']
db.delete_unique(u'news_subscriber', ['token'])

# Deleting model 'EmailChangeRequest'
db.delete_table(u'news_emailchangerequest')


models = {
u'news.apiuser': {
'Meta': {'object_name': 'APIUser'},
'api_key': ('django.db.models.fields.CharField', [], {'default': "'b2eb6e1c-287f-402b-8c32-29f592506896'", 'max_length': '40', 'db_index': 'True'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '256'})
},
u'news.emailchangerequest': {
'Meta': {'object_name': 'EmailChangeRequest'},
'change_key': ('django.db.models.fields.CharField', [], {'default': "'987558cb-6fdf-4867-b161-8fb2f96ecc78'", 'max_length': '40', 'db_index': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'new_email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}),
'subscriber': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['news.Subscriber']"}),
'when': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'})
},
u'news.failedtask': {
'Meta': {'object_name': 'FailedTask'},
'args': ('jsonfield.fields.JSONField', [], {'default': '[]'}),
'einfo': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}),
'exc': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'kwargs': ('jsonfield.fields.JSONField', [], {'default': '{}'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'task_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
'when': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'})
},
u'news.newsletter': {
'Meta': {'ordering': "['order']", 'object_name': 'Newsletter'},
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'confirm_message': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
'description': ('django.db.models.fields.CharField', [], {'max_length': '256', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'languages': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'requires_double_optin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'show': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'vendor_id': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'welcome': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'})
},
u'news.subscriber': {
'Meta': {'object_name': 'Subscriber'},
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'primary_key': 'True'}),
'token': ('django.db.models.fields.CharField', [], {'default': "'7d86efb8-5356-4d9b-b935-7cdc0f436eb1'", 'unique': 'True', 'max_length': '40', 'db_index': 'True'})
}
}

complete_apps = ['news']
31 changes: 30 additions & 1 deletion news/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,14 @@ def get_and_sync(self, email, token):
class Subscriber(models.Model):
email = models.EmailField(primary_key=True)
token = models.CharField(max_length=40, default=lambda: str(uuid4()),
db_index=True)
db_index=True,
unique=True)

objects = SubscriberManager()

def __unicode__(self):
return self.email


class Newsletter(models.Model):
slug = models.SlugField(
Expand Down Expand Up @@ -186,3 +190,28 @@ def retry(self):
new_task.apply_async()
# Forget the old task
self.delete()


class EmailChangeRequest(models.Model):
"""A user request to change their email address"""
when = models.DateTimeField(default=now,
help_text="When they requested the change")
subscriber = models.ForeignKey(Subscriber)
new_email = models.EmailField()
change_key = models.CharField(max_length=40,
default=lambda: str(uuid4()),
db_index=True,
help_text="Random string user will use to confirm the email change")

def is_expired(self, delete=False):
"""Return True if change has timed out without being confirmed.

If ``delete`` is true and change has timed out,
delete it from the database before returning.
"""
from news.tasks import TIME_TO_CONFIRM_EMAIL_CHANGE
elapsed = now() - self.when
expired = elapsed > TIME_TO_CONFIRM_EMAIL_CHANGE
if expired and delete:
self.delete()
return expired
16 changes: 8 additions & 8 deletions news/newsletters.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def _newsletters():
The returned data structure looks like::

{
'by_name': {
'by_slug': {
'newsletter_name_1': a Newsletter object,
'newsletter_name_2': another Newsletter object,
},
Expand All @@ -43,21 +43,21 @@ def _newsletters():


def _get_newsletters_data():
by_name = {}
by_slug = {}
by_vendor_id = {}
for nl in Newsletter.objects.all():
by_name[nl.slug] = nl
by_slug[nl.slug] = nl
by_vendor_id[nl.vendor_id] = nl
return {
'by_name': by_name,
'by_slug': by_slug,
'by_vendor_id': by_vendor_id,
}


def newsletter_field(name):
"""Lookup the backend-specific field (vendor ID) for the newsletter"""
try:
return _newsletters()['by_name'][name].vendor_id
return _newsletters()['by_slug'][name].vendor_id
except KeyError:
return None

Expand All @@ -75,12 +75,12 @@ def newsletter_slugs():
Get a list of all the available newsletters.
Returns a list of their slugs.
"""
return _newsletters()['by_name'].keys()
return _newsletters()['by_slug'].keys()


def slug_to_vendor_id(slug):
"""Given a newsletter's slug, return its vendor_id"""
return _newsletters()['by_name'][slug].vendor_id
return _newsletters()['by_slug'][slug].vendor_id


def newsletter_fields():
Expand All @@ -94,7 +94,7 @@ def newsletter_languages():
supported by newsletters.
"""
lang_set = set()
for newsletter in _newsletters()['by_name'].values():
for newsletter in _newsletters()['by_slug'].values():
lang_set |= set(newsletter.language_list)
return lang_set

Expand Down
Loading