Commit 691d3ea8 by Carlos Andrés Rocha

Added functionality to split each list into static segments

Also only subscribe people who are not already subscribed
parent 65a0255f
import logging import logging
import math
import random
import itertools import itertools
from itertools import chain
from optparse import make_option from optparse import make_option
from collections import namedtuple from collections import namedtuple
...@@ -28,6 +31,10 @@ class Command(BaseCommand): ...@@ -28,6 +31,10 @@ class Command(BaseCommand):
help='mailchimp list id'), help='mailchimp list id'),
make_option('--course', action='store', dest='course_id', make_option('--course', action='store', dest='course_id',
help='xmodule course_id'), help='xmodule course_id'),
make_option('--segments', action='store', dest='segments',
default=0, type=int,
help='number of static random segments to create'),
) )
def parse_options(self, options): def parse_options(self, options):
...@@ -40,10 +47,11 @@ class Command(BaseCommand): ...@@ -40,10 +47,11 @@ class Command(BaseCommand):
if not options['course_id']: if not options['course_id']:
raise CommandError('missing course id') raise CommandError('missing course id')
return options['key'], options['list_id'], options['course_id'] return (options['key'], options['list_id'],
options['course_id'], options['segments'])
def handle(self, *args, **options): def handle(self, *args, **options):
key, list_id, course_id = self.parse_options(options) key, list_id, course_id, nsegments = self.parse_options(options)
log.info('Syncronizing email list for {0}'.format(course_id)) log.info('Syncronizing email list for {0}'.format(course_id))
...@@ -55,18 +63,25 @@ class Command(BaseCommand): ...@@ -55,18 +63,25 @@ class Command(BaseCommand):
non_subscribed = unsubscribed.union(cleaned) non_subscribed = unsubscribed.union(cleaned)
enrolled = get_enrolled_students(course_id) enrolled = get_enrolled_students(course_id)
active = enrolled.all().exclude(user__email__in=non_subscribed)
data = get_student_data(course_id, active) exclude = subscribed.union(non_subscribed)
to_subscribe = get_student_data(enrolled, exclude=exclude)
log.info(len(to_subscribe))
update_merge_tags(mailchimp, list_id, data) tag_names = set(chain.from_iterable(d.keys() for d in to_subscribe))
update_data(mailchimp, list_id, data) update_merge_tags(mailchimp, list_id, tag_names)
subscribe_with_data(mailchimp, list_id, to_subscribe)
enrolled_emails = set(enrolled.values_list('user__email', flat=True)) enrolled_emails = set(enrolled.values_list('user__email', flat=True))
non_enrolled_emails = list(subscribed.difference(enrolled_emails)) non_enrolled_emails = list(subscribed.difference(enrolled_emails))
unsubscribe(mailchimp, list_id, non_enrolled_emails) unsubscribe(mailchimp, list_id, non_enrolled_emails)
subscribed = subscribed.union(set(d['EMAIL'] for d in to_subscribe))
make_segments(mailchimp, list_id, nsegments, subscribed)
def connect_mailchimp(key, list_id, course_id): def connect_mailchimp(key, list_id, course_id):
mailchimp = MailSnake(key) mailchimp = MailSnake(key)
...@@ -85,7 +100,7 @@ def connect_mailchimp(key, list_id, course_id): ...@@ -85,7 +100,7 @@ def connect_mailchimp(key, list_id, course_id):
# check that we are connecting to the correct list # check that we are connecting to the correct list
parts = course_id.replace('_', ' ').replace('/', ' ').split() parts = course_id.replace('_', ' ').replace('/', ' ').split()
count = sum(1 for p in parts if p in list_name) count = sum(1 for p in parts if p in list_name)
if count != 4: if count < 3:
log.info(course_id) log.info(course_id)
log.info(list_name) log.info(list_name)
raise CommandError('course_id does not match list name') raise CommandError('course_id does not match list name')
...@@ -93,11 +108,13 @@ def connect_mailchimp(key, list_id, course_id): ...@@ -93,11 +108,13 @@ def connect_mailchimp(key, list_id, course_id):
return mailchimp return mailchimp
def get_student_data(course_id, students): def get_student_data(students, exclude=None):
# To speed the query, we won't retrieve the full User object, only # To speed the query, we won't retrieve the full User object, only
# two of its values. The namedtuple simulates the User object. # two of its values. The namedtuple simulates the User object.
FakeUser = namedtuple('Fake', 'id username') FakeUser = namedtuple('Fake', 'id username')
exclude = exclude if exclude else set()
def make(v): def make(v):
e = {'EMAIL': v['user__email'], e = {'EMAIL': v['user__email'],
'FULLNAME': v['name'].title()} 'FULLNAME': v['name'].title()}
...@@ -109,7 +126,8 @@ def get_student_data(course_id, students): ...@@ -109,7 +126,8 @@ def get_student_data(course_id, students):
fields = 'user__email', 'name', 'user_id', 'user__username' fields = 'user__email', 'name', 'user_id', 'user__username'
values = students.values(*fields) values = students.values(*fields)
return [make(s) for s in values] exclude_func = lambda s: s['user__email'] in exclude
return [make(s) for s in values if not exclude_func(s)]
def get_enrolled_students(course_id): def get_enrolled_students(course_id):
...@@ -158,17 +176,13 @@ def unsubscribe(mailchimp, list_id, emails): ...@@ -158,17 +176,13 @@ def unsubscribe(mailchimp, list_id, emails):
log.debug(result) log.debug(result)
def update_merge_tags(mailchimp, list_id, data): def update_merge_tags(mailchimp, list_id, tag_names):
names = set()
for row in data:
names.update(row.keys())
mc_vars = mailchimp.listMergeVars(id=list_id) mc_vars = mailchimp.listMergeVars(id=list_id)
mc_names = set(v['name'] for v in mc_vars) mc_names = set(v['name'] for v in mc_vars)
mc_merge = mailchimp.listMergeVarAdd mc_merge = mailchimp.listMergeVarAdd
for name in names: for name in tag_names:
tag = name_to_tag(name) tag = name_to_tag(name)
# verify FULLNAME is present # verify FULLNAME is present
...@@ -192,9 +206,9 @@ def update_merge_tags(mailchimp, list_id, data): ...@@ -192,9 +206,9 @@ def update_merge_tags(mailchimp, list_id, data):
log.debug(result) log.debug(result)
def update_data(mailchimp, list_id, data): def subscribe_with_data(mailchimp, list_id, user_data):
format_entry = lambda e: {name_to_tag(k): v for k, v in e.iteritems()} format_entry = lambda e: {name_to_tag(k): v for k, v in e.iteritems()}
formated_data = list(format_entry(e) for e in data) formated_data = list(format_entry(e) for e in user_data)
# send the updates in batches of a fixed size # send the updates in batches of a fixed size
for batch in batches(formated_data, BATCH_SIZE): for batch in batches(formated_data, BATCH_SIZE):
...@@ -205,6 +219,31 @@ def update_data(mailchimp, list_id, data): ...@@ -205,6 +219,31 @@ def update_data(mailchimp, list_id, data):
log.debug(result) log.debug(result)
def make_segments(mailchimp, list_id, count, emails):
if count > 0:
# reset segments
segments = mailchimp.listStaticSegments(id=list_id)
for s in segments:
if s['name'].startswith('random'):
mailchimp.listStaticSegmentDel(id=list_id, seg_id=s['id'])
# shuffle and split emails
emails = list(emails)
random.shuffle(emails)
chunk_size = int(math.ceil(float(len(emails))/count))
chunks = list(chunk(emails, chunk_size))
# create segments and add emails
for n in xrange(count):
name = 'random_{0:002}'.format(n)
seg_id = mailchimp.listStaticSegmentAdd(id=list_id, name=name)
for batch in batches(chunks[n], BATCH_SIZE):
mailchimp.listStaticSegmentMembersAdd(id=list_id,
seg_id=seg_id,
batch=batch)
def name_to_tag(name): def name_to_tag(name):
return (name[:10] if len(name) > 10 else name).replace(' ', '_').strip() return (name[:10] if len(name) > 10 else name).replace(' ', '_').strip()
...@@ -212,3 +251,8 @@ def name_to_tag(name): ...@@ -212,3 +251,8 @@ def name_to_tag(name):
def batches(iterable, size): def batches(iterable, size):
slices = range(0, len(iterable), size) slices = range(0, len(iterable), size)
return [iterable[slice(i, i + size)] for i in slices] return [iterable[slice(i, i + size)] for i in slices]
def chunk(l, n):
for i in xrange(0, len(l), n):
yield l[i:i+n]
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment