Skip to content

Commit

Permalink
Sync DJs via member API (issue #2, issue #3)
Browse files Browse the repository at this point in the history
  • Loading branch information
kumar303 committed Dec 12, 2012
1 parent d49900a commit 66a4151
Show file tree
Hide file tree
Showing 10 changed files with 422 additions and 10 deletions.
8 changes: 6 additions & 2 deletions app.yaml
Expand Up @@ -35,10 +35,14 @@ handlers:
static_dir: media
expiration: 30m

# restrict public access to task queue URL handlers
# restrict public access to playlist task queue URL handlers
- url: /playlists/task/.*
script: main.py
# script: main.application
login: admin

# restrict public access to auth task queue URL handlers
- url: /auth/task/.*
script: main.py
login: admin

# For everything else, we always want to use HTTPS.
Expand Down
71 changes: 71 additions & 0 deletions auth/cron.py
@@ -0,0 +1,71 @@
import base64
from datetime import datetime
import logging
import re
import xml.etree.ElementTree as ET

from django.core.urlresolvers import reverse
from django.utils import simplejson as json

from google.appengine.api import taskqueue, urlfetch

from auth.models import UserSync
from common import dbconfig
from common.utilities import cronjob


log = logging.getLogger()
ts = re.compile('''(?P<year>\d{4})(?P<mon>\d{2})(?P<day>\d{2})
(?P<hr>\d{2})(?P<min>\d{2})(?P<sec>\d{2})''',
re.VERBOSE)


@cronjob
def sync_users(request):
authstr = 'Basic %s' % base64.b64encode('%s:%s' %
(dbconfig['chirpradio.member_api.user'],
dbconfig['chirpradio.member_api.password']))
resp = urlfetch.fetch(dbconfig['chirpradio.member_api.url'],
headers={'Authorization': authstr})
if resp.status_code != 200:
log.error(resp.content)
raise ValueError('live site XML returned %s' % resp.status_code)
root = ET.fromstring(resp.content)

ts_parts = ts.match(root.find('last_updated').text)
ts_parts = [int(x) for x in ts_parts.groups()]
last_update = datetime(*ts_parts)
log.info('chirpradio data last updated: %s' % last_update)

try:
sync = UserSync.all()[0]
last_sync = sync.last_sync
except IndexError:
sync = UserSync()
# Set to a random date in the past to force a new sync.
last_sync = datetime(2012, 1, 1)
if last_sync >= last_update:
log.info('No need for sync')
return

stats = {'synced': 0, 'deactivated': 0}

for vol in root.iterfind('current_volunteers/volunteer'):
user = {'name_first': vol.find('name/first').text,
'name_last': vol.find('name/last').text,
'nick': vol.find('name/nick').text,
'member_id': int(vol.find('member_id').text),
'email': vol.find('email').text}
taskqueue.add(url=reverse('auth.tasks.sync_user'),
params={'user': json.dumps(user)})
stats['synced'] += 1

for vol in root.iterfind('suspended_volunteers/volunteer'):
taskqueue.add(url=reverse('auth.tasks.deactivate_user'),
params={'external_id': vol.find('member_id').text})
stats['deactivated'] += 1

sync.last_sync = last_update
sync.put()
log.info('Sync finished. Sychronized: %s(synced)s; '
'deactivated: %(deactivated)s' % stats)
5 changes: 5 additions & 0 deletions auth/models.py
Expand Up @@ -128,6 +128,11 @@ def effective_dj_name(self):
setattr(User, property_name, property(prop_fn))


class UserSync(db.Model):
"""A log to track when users were synced with the live site."""
last_sync = db.DateTimeProperty()


#############################################################################

# Test key used to HMAC-sign security tokens.
Expand Down
75 changes: 75 additions & 0 deletions auth/tasks.py
@@ -0,0 +1,75 @@
import logging

from django import http
from django.utils import simplejson as json

from auth import roles
from auth.models import User

log = logging.getLogger()


def sync_user(request):
user = request.POST.get('user')
if not user:
return http.HttpResponseBadRequest()
user = json.loads(user)

qs = User.all().filter('external_id =', user['member_id'])
users = qs.fetch(1)
dj_user = None
if len(users):
dj_user = users[0]
else:
# No previously sync'd user exists.
# Let's check by email to see if an old
# user exists with the same email.
qs = (User.all().filter('email =', user['email'])
.filter('external_id =', None))
users = qs.fetch(1)
if len(users):
log.info('Linking user %s to ID %s' %
(user['email'], user['member_id']))
dj_user = users[0]

fields = {
'first_name': user['name_first'],
'last_name': user['name_last'],
'email': user['email'],
'dj_name': user['nick'],
'external_id': user['member_id'],
'is_superuser': False,
'is_active': True,
}
if not dj_user:
fields['roles'] = [roles.DJ]
dj_user = User(**fields)
else:
for k, v in fields.items():
setattr(dj_user, k, v)
if roles.DJ not in dj_user.roles:
dj_user.roles.append(roles.DJ)
dj_user.put()

return http.HttpResponse('OK')


def deactivate_user(request):
id = request.POST.get('external_id')
if not id:
log.info('external_id not found in POST')
return http.HttpResponseBadRequest()
qs = User.all().filter('external_id =', int(id))
users = qs.fetch(1)
if not len(users):
log.info('no user exists with external_id %s' % id)
# This is okay. We'll deactivate them next time.
# Return a 200 here otherwise the task will be retried.
return http.HttpResponse('No one deactivated')

dj_user = users[0]
dj_user.is_active = False
dj_user.put()
log.info('Deactivated user %s %s' % (dj_user, dj_user.email))

return http.HttpResponse('OK')
127 changes: 127 additions & 0 deletions auth/tests/test_cron.py
@@ -0,0 +1,127 @@
from datetime import date, datetime
from textwrap import dedent

from django.core.urlresolvers import reverse
from django.test import TestCase
from django.utils import simplejson as json

import fudge
from fudge.inspector import arg
from nose.tools import eq_

from auth.models import UserSync
from common import dbconfig


XML = """
<volunteers>
<last_updated>20121202005516</last_updated>
<current_volunteers>
<volunteer>
<name>
<first>Ivan</first>
<last>Krsti\xc4\x87</last>
<nick>DJ Krsti\xc4\x87</nick>
</name>
<member_id>1</member_id>
<email>person@chirpradio.org</email>
<phone>
<home></home>
<cell></cell>
<work></work>
</phone>
<avatar>http://volunteers.chirpradio.dev/_/i/volunteer_images/mikeg.jpeg</avatar>
<urls>
<twitter></twitter>
<facebook></facebook>
<website></website>
</urls>
<bio><![CDATA[
(DJ bio text).]]></bio>
</volunteer>
</current_volunteers>
</volunteers>
"""


class TestSyncUsers(TestCase):

def setUp(self):
dbconfig['chirpradio.member_api.url'] = 'http://waaatjusttesting.com/api'
dbconfig['chirpradio.member_api.user'] = 'example_user'
dbconfig['chirpradio.member_api.password'] = 'example_pw'
self.url = reverse('auth.cron.sync_users')

def tearDown(self):
for ob in UserSync.all():
ob.delete()

@fudge.patch('auth.cron.urlfetch.fetch')
@fudge.patch('auth.cron.taskqueue')
def test_sync(self, fetch, tq):
(fetch.expects_call().returns_fake()
.has_attr(status_code=200,
content=XML))

def user_data(data):
user = json.loads(data['user'])
eq_(user['name_first'], 'Ivan')
eq_(user['name_last'], u'Krsti\u0107')
eq_(user['member_id'], 1)
eq_(user['nick'], u'DJ Krsti\u0107')
eq_(user['email'], 'person@chirpradio.org')
return True

(tq.expects('add')
.with_args(
url=reverse('auth.tasks.sync_user'),
params=arg.passes_test(user_data),
))

res = self.client.post(self.url,
HTTP_X_APPENGINE_CRON='true')
eq_(res.status_code, 200)
eq_(UserSync.all()[0].last_sync.timetuple()[0:3],
date(2012, 12, 2).timetuple()[0:3])

@fudge.patch('auth.cron.urlfetch.fetch')
@fudge.patch('auth.cron.taskqueue')
def test_no_need_for_sync(self, fetch, tq):
(fetch.expects_call().returns_fake()
.has_attr(status_code=200,
content=XML))
sync = UserSync()
sync.last_sync = datetime(2012, 12, 2, 0, 55, 16) # last sync exact
sync.put()

self.client.post(self.url, HTTP_X_APPENGINE_CRON='true')

@fudge.patch('auth.cron.urlfetch.fetch')
@fudge.patch('auth.cron.taskqueue')
def test_deactivate(self, fetch, tq):
suspended = dedent("""\
<volunteers>
<last_updated>20121202005516</last_updated>
<suspended_volunteers>
<volunteer>
<name>
<first>Ivan</first>
<last>Krsti\xc4\x87</last>
<nick>DJ Krsti\xc4\x87</nick>
</name>
<member_id>1</member_id>
<email>person@chirpradio.org</email>
</volunteer>
</suspended_volunteers>
</volunteers>
""")
(fetch.expects_call().returns_fake()
.has_attr(status_code=200,
content=suspended))
(tq.expects('add')
.with_args(
url=reverse('auth.tasks.deactivate_user'),
params={'external_id': '1'},
))

self.client.post(self.url, HTTP_X_APPENGINE_CRON='true')

0 comments on commit 66a4151

Please sign in to comment.