Skip to content

Commit

Permalink
Merge branch 'master' into scalability
Browse files Browse the repository at this point in the history
Conflicts:
	app.py
  • Loading branch information
beaugunderson committed Jan 28, 2014
2 parents a783c00 + 79c4873 commit 99c27b6
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 131 deletions.
78 changes: 43 additions & 35 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
from flask import Flask, request, render_template, url_for
import random
import urlparse

import twilio.twiml
from twilio import TwilioRestException
from utils import load_data
from models import log_call, aggregate_stats
from utils import get_database, play_or_say, locate_member_ids

from flask import Flask, request, render_template, url_for
from flask.ext.jsonpify import jsonify
from twilio import TwilioRestException

from models import aggregate_stats # , log_call
from utils import get_database, play_or_say
from political_data import PoliticalData

app = Flask(__name__)
app.config.from_object('config.ConfigProduction')
Expand All @@ -15,17 +18,17 @@

call_methods = ['GET', 'POST']

campaigns, legislators, districts = load_data()
defaults_campaign = campaigns['default']
data = PoliticalData()
defaults_campaign = data.campaigns['default']


def full_url_for(route, **kwds):
return urlparse.urljoin(app.config['APPLICATION_ROOT'],
url_for(route, **kwds))
url_for(route, **kwds))


def get_campaign(cid):
return dict(defaults_campaign, **campaigns[cid])
return dict(defaults_campaign, **data.campaigns[cid])


def parse_params(r):
Expand All @@ -47,9 +50,9 @@ def parse_params(r):
params['repIds'] = campaign['repIds']

# get representative's id by zip code
if (params['zipcode'] is not None) and len(params['repIds']) == 0:
params['repIds'] = locate_member_ids(
params['zipcode'], campaign, districts, legislators)
if params['zipcode'] and params['repIds']:
params['repIds'] = data.locate_member_ids(
params['zipcode'], campaign)

if 'random_choice' in campaign:
# pick a random choice among a selected set of members
Expand All @@ -68,21 +71,18 @@ def intro_zip_gather(params, campaign):

def zip_gather(resp, params, campaign):
with resp.gather(numDigits=5, method="POST",
action=url_for("zip_parse", **params)) as g:
action=url_for("zip_parse", **params)) as g:
play_or_say(g, campaign['msg_ask_zip'])

return str(resp)


def dialing_config(params):
dc = dict(
return dict(
timeLimit=app.config['TW_TIME_LIMIT'],
timeout=app.config['TW_TIMEOUT'],
hangupOnStar=True, # allow the user to hangup and move onto next call
action=url_for('call_complete', **params) # on complete
)

return dc
hangupOnStar=True, # allow the user to hangup and move onto next call
action=url_for('call_complete', **params))


def make_calls(params, campaign):
Expand All @@ -96,7 +96,7 @@ def make_calls(params, campaign):
n_reps = len(params['repIds'])

play_or_say(resp, campaign['msg_call_block_intro'],
n_reps=n_reps, many_reps=n_reps > 1)
n_reps=n_reps, many_reps=n_reps > 1)

resp.redirect(url_for('make_single_call', call_index=0, **params))

Expand All @@ -112,15 +112,15 @@ def _make_calls():

@app.route('/create', methods=call_methods)
def call_user():
'''
"""
Makes a phone call to a user.
Required Params:
userPhone
campaignId
Optional Params:
zipcode
repIds
'''
"""
# parse the info needed to make the call
params, campaign = parse_params(request)

Expand All @@ -132,8 +132,8 @@ def call_user():
url=full_url_for("connection", **params),
timeLimit=app.config['TW_TIME_LIMIT'],
timeout=app.config['TW_TIMEOUT'],
status_callback=full_url_for("call_complete_status", **params),
)
status_callback=full_url_for("call_complete_status", **params))

result = jsonify(message=call.status, debugMode=app.debug)
result.status_code = 200 if call.status != 'failed' else 500
except TwilioRestException, err:
Expand All @@ -151,7 +151,7 @@ def connection():
campaignId
Optional Params:
zipcode
repIds (if not present - go to incoming_call flow and asked for zipcode)
repIds (if not present go to incoming_call flow and asked for zipcode)
"""
params, campaign = parse_params(request)

Expand Down Expand Up @@ -193,17 +193,20 @@ def zip_parse():
Required Params: campaignId, Digits
"""
params, campaign = parse_params(request)
zipcode = request.values.get('Digits', "")
repIds = locate_member_ids(zipcode, campaign, districts, legislators)
zipcode = request.values.get('Digits', '')
rep_ids = data.locate_member_ids(zipcode, campaign)

if app.debug:
print 'DEBUG: zipcode = {}'.format(zipcode)

if len(repIds) == 0:
if not rep_ids:
resp = twilio.twiml.Response()
play_or_say(resp, campaign['msg_invalid_zip'])

return zip_gather(resp, params, campaign)

params['zipcode'] = zipcode
params['repIds'] = repIds
params['repIds'] = rep_ids

return make_calls(params, campaign)

Expand All @@ -213,20 +216,25 @@ def make_single_call():
params, campaign = parse_params(request)
i = int(request.values.get('call_index', 0))
params['call_index'] = i
member = legislators.ix[params['repIds'][i]]
member = [l for l in data.legislators
if l['bioguide_id'] == params['repIds'][i]][0]
congress_phone = member['phone']
full_name = unicode("{} {}".format(
member['firstname'], member['lastname']), 'utf8')

resp = twilio.twiml.Response()

if 'voted_with_list' in campaign and \
(params['repIds'][i] in campaign['voted_with_list']):
params['repIds'][i] in campaign['voted_with_list']:
play_or_say(
resp, campaign['msg_repo_intro_voted_with'], name=full_name)
else:
play_or_say(resp, campaign['msg_rep_intro'], name=full_name)

if app.debug:
print 'DEBUG: Call #{}, {} ({}) from make_single_call()'.format(
i, full_name, congress_phone)

resp.dial(congress_phone, **dialing_config(params))

return str(resp)
Expand All @@ -247,7 +255,7 @@ def call_complete():
play_or_say(resp, campaign['msg_final_thanks'])
else:
# call the next representative
params['call_index'] = i + 1 # increment the call counter
params['call_index'] = i + 1 # increment the call counter

play_or_say(resp, campaign['msg_between_thanks'])

Expand All @@ -260,13 +268,12 @@ def call_complete():
def call_complete_callback():
# asynch callback from twilio on call complete
params, campaign = parse_params(request)

return jsonify(dict(
phoneNumber=request.values.get('To', ''),
callStatus=request.values.get('CallStatus', 'unknown'),
repIds=params['repIds'],
campaignId=params['campaignId'],
))

campaignId=params['campaignId']))


@app.route('/demo')
Expand All @@ -278,6 +285,7 @@ def demo():
def stats():
pwd = request.values.get('password', None)
campaign = get_campaign(request.values.get('campaignId', 'default'))

if pwd == app.config['SECRET_KEY']:
return jsonify(aggregate_stats(campaign['id']))
else:
Expand Down
15 changes: 7 additions & 8 deletions data/campaigns.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,11 @@
number: 415-419-8672
target_house: true
target_senate: true
target_house_first: true
msg_intro: Hi. Welcome to call congress.
msg_intro_confirm: "Press star to start."
msg_ask_zip: Please enter your zip code so we can lookup your Congress person.
msg_invalid_zip: "Sorry, that zip code didn't work. Please try again."
msg_call_block_intro: "{{#many_reps}}We'll now connect you to {{n_reps}} representatives. Press star for next rep.{{/many_reps}}"
target_house_first: false
msg_intro: http://tfrce.s3.amazonaws.com/sft/sft_msg_intro.mp3
msg_ask_zip: http://tfrce.s3.amazonaws.com/sft/sft_ask_zip.mp3
msg_invalid_zip: http://tfrce.s3.amazonaws.com/sft/sft_invalid_zip.mp3
msg_call_block_intro: http://tfrce.s3.amazonaws.com/sft/sft_call_block_intro.mp3
msg_rep_intro: "We're now connecting you to {{name}}"
msg_between_thanks: You're doing great - here's the next call.
msg_final_thanks: Thank you!
msg_between_thanks: http://tfrce.s3.amazonaws.com/sft/sft_between_thanks.mp3
msg_final_thanks: http://tfrce.s3.amazonaws.com/sft/sft_msg_final_thanks.mp3
30 changes: 17 additions & 13 deletions models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,23 @@
from datetime import datetime
import hashlib
import logging
import pandas as pd

#valid_users = pd.Series(open('data/users.txt').readlines()).str.strip()

db = SQLAlchemy()


def hash_phone(number):
# takes phone number and returns 64 charachter string
return hashlib.sha256(number).hexdigest()


def to_dict(model):
d = model.__dict__
d.pop('_sa_instance_state')
return d
db.Model.to_dict = to_dict


db.Model.to_dict = to_dict


class Call(db.Model):
__tablename__ = 'calls'
Expand All @@ -28,18 +30,18 @@ class Call(db.Model):
member_id = Column(String(10)) # congress member sunlight identifier
# user attributes
user_id = Column(String(64)) # hashed phone number
zipcode = Column(String(5))
zipcode = Column(String(5))
areacode = Column(String(3)) # first 3 digits of phone number
exchange = Column(String(3)) # next 3 digits of phone number
# twilio attributes
call_id = Column(String(40)) # twilio call ID
status = Column(String(25)) # twilio call status
duration = Column(Integer) # twilio call time in seconds





def __init__(self, campaign_id, member_id,
zipcode=None, phone_number=None,
zipcode=None, phone_number=None,
call_id=None, status='unknown', duration=0):
self.timestamp = datetime.now()
self.status = status
Expand All @@ -57,9 +59,10 @@ def __init__(self, campaign_id, member_id,
def __repr__(self):
return '<Call {}-{}-xxxx to {}>'.format(
self.areacode, self.exchange, self.member_id)



def log_call(db, params, campaign, request):
try:
try:
i = int(request.values.get('call_index'))
kwds = dict(
campaign_id=campaign['id'],
Expand All @@ -77,7 +80,6 @@ def log_call(db, params, campaign, request):


def aggregate_stats(cid):
# TODO - replace with pandas.read_sql as soon as db work is completed
zipcodes = db.session.query(Call.zipcode, func.Count(Call.zipcode))\
.filter(Call.campaign_id == cid)\
.group_by(Call.zipcode).all()
Expand All @@ -89,11 +91,13 @@ def aggregate_stats(cid):
reps=dict(tuple(r) for r in reps)
))


def setUp(app):
db.app = app
db.drop_all()
db.create_all()

db.create_all()


def tearDown(app):
db.app = app
db.drop_all()
Expand Down
File renamed without changes.
Loading

0 comments on commit 99c27b6

Please sign in to comment.