Permalink
Browse files

Sync DJs via member API (issue #2, issue #3)

  • Loading branch information...
1 parent d49900a commit 66a4151341d0134da41a1da44df2ce30720aa9a0 @kumar303 kumar303 committed Dec 3, 2012
Showing with 422 additions and 10 deletions.
  1. +6 −2 app.yaml
  2. +71 −0 auth/cron.py
  3. +5 −0 auth/models.py
  4. +75 −0 auth/tasks.py
  5. +127 −0 auth/tests/test_cron.py
  6. +117 −0 auth/tests/test_tasks.py
  7. +9 −1 auth/urls.py
  8. +3 −0 cron.yaml
  9. +2 −0 settings.py
  10. +7 −7 urls.py
View
@@ -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.
View
@@ -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)
View
@@ -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.
View
@@ -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')
@@ -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')
Oops, something went wrong.

0 comments on commit 66a4151

Please sign in to comment.