Skip to content

Commit

Permalink
added access control decorator for cross-origin AJAX requests, added …
Browse files Browse the repository at this point in the history
…FFTF leaderboard hooks to log complete calls (optional and wont affect anything if not passed in)
  • Loading branch information
rubbingalcoholic committed Sep 2, 2014
1 parent 87db5ac commit c208f25
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 4 deletions.
45 changes: 45 additions & 0 deletions access_control_decorator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from datetime import timedelta
from flask import make_response, request, current_app
from functools import update_wrapper


def crossdomain(origin=None, methods=None, headers=None,
max_age=21600, attach_to_all=True,
automatic_options=True):
if methods is not None:
methods = ', '.join(sorted(x.upper() for x in methods))
if headers is not None and not isinstance(headers, basestring):
headers = ', '.join(x.upper() for x in headers)
if not isinstance(origin, basestring):
origin = ', '.join(origin)
if isinstance(max_age, timedelta):
max_age = max_age.total_seconds()

def get_methods():
if methods is not None:
return methods

options_resp = current_app.make_default_options_response()
return options_resp.headers['allow']

def decorator(f):
def wrapped_function(*args, **kwargs):
if automatic_options and request.method == 'OPTIONS':
resp = current_app.make_default_options_response()
else:
resp = make_response(f(*args, **kwargs))
if not attach_to_all and request.method != 'OPTIONS':
return resp

h = resp.headers

h['Access-Control-Allow-Origin'] = origin
h['Access-Control-Allow-Methods'] = get_methods()
h['Access-Control-Max-Age'] = str(max_age)
if headers is not None:
h['Access-Control-Allow-Headers'] = headers
return resp

f.provide_automatic_options = False
return update_wrapper(wrapped_function, f)
return decorator
36 changes: 33 additions & 3 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
from models import db, aggregate_stats, log_call, call_count
from political_data import PoliticalData
from cache_handler import CacheHandler
from fftf_leaderboard import FFTFLeaderboard
from access_control_decorator import crossdomain


app = Flask(__name__)
Expand All @@ -34,6 +36,8 @@
# Optional Redis cache, for caching Google spreadsheet campaign overrides
cache_handler = CacheHandler(app.config['REDIS_URL'])

# FFTF Leaderboard handler. Only used if FFTF Leadboard params are passed in
leaderboard = FFTFLeaderboard(app.debug, app.config['FFTF_LB_ASYNC_POOL_SIZE'])

call_methods = ['GET', 'POST']

Expand Down Expand Up @@ -68,7 +72,13 @@ def parse_params(r):
'userPhone': r.values.get('userPhone'),
'campaignId': r.values.get('campaignId', 'default'),
'zipcode': r.values.get('zipcode', None),
'repIds': r.values.getlist('repIds')
'repIds': r.values.getlist('repIds'),

# optional values for Fight for the Future Leaderboards
# if present, these add extra logging functionality in call_complete
'fftfCampaign': r.values.get('fftfCampaign'),
'fftfReferer': r.values.get('fftfReferer'),
'fftfSession': r.values.get('fftfSession')
}

# lookup campaign by ID
Expand Down Expand Up @@ -120,7 +130,7 @@ def make_calls(params, campaign):
"""
Connect a user to a sequence of congress members.
Required params: campaignId, repIds
Optional params: zipcode,
Optional params: zipcode, fftfCampaign, fftfReferer, fftfSession
"""
resp = twilio.twiml.Response()

Expand All @@ -145,6 +155,7 @@ def _make_calls():


@app.route('/create', methods=call_methods)
@crossdomain(origin='*')
def call_user():
"""
Makes a phone call to a user.
Expand All @@ -154,6 +165,9 @@ def call_user():
Optional Params:
zipcode
repIds
fftfCampaign
fftfReferer
fftfSession
"""
# parse the info needed to make the call
params, campaign = parse_params(request)
Expand Down Expand Up @@ -181,6 +195,7 @@ def call_user():


@app.route('/connection', methods=call_methods)
@crossdomain(origin='*')
def connection():
"""
Call handler to connect a user with their congress person(s).
Expand All @@ -189,6 +204,9 @@ def connection():
Optional Params:
zipcode
repIds (if not present go to incoming_call flow and asked for zipcode)
fftfCampaign
fftfReferer
fftfSession
"""
params, campaign = parse_params(request)

Expand Down Expand Up @@ -216,6 +234,7 @@ def incoming_call():
"""
Handles incoming calls to the twilio numbers.
Required Params: campaignId
Optional Params: fftfCampaign, fftfReferer, fftfSession
Each Twilio phone number needs to be configured to point to:
server.com/incoming_call?campaignId=12345
Expand Down Expand Up @@ -314,13 +333,21 @@ def call_complete():

log_call(params, campaign, request)

# If FFTF Leaderboard params are present, log this call
if params['fftfCampaign'] and params['fftfReferer']:
leaderboard.log_call(params, campaign, request)

resp = twilio.twiml.Response()

i = int(request.values.get('call_index', 0))

if i == len(params['repIds']) - 1:
# thank you for calling message
play_or_say(resp, campaign['msg_final_thanks'])

# If FFTF Leaderboard params are present, log the call completion status
if params['fftfCampaign'] and params['fftfReferer']:
leaderboard.log_complete(params, campaign, request)
else:
# call the next representative
params['call_index'] = i + 1 # increment the call counter
Expand All @@ -344,7 +371,10 @@ def call_complete_status():
'phoneNumber': request.values.get('To', ''),
'callStatus': request.values.get('CallStatus', 'unknown'),
'repIds': params['repIds'],
'campaignId': params['campaignId']
'campaignId': params['campaignId'],
'fftfCampaign': params['fftfCampaign'],
'fftfReferer': params['fftfReferer'],
'fftfSession': params['fftfSession']
})


Expand Down
3 changes: 3 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ class Config(object):
# limit on the amount of time to ring before giving up
TW_TIMEOUT = 40 # seconds

# number of threads to limit asynchronous leaderboard requests
FFTF_LB_ASYNC_POOL_SIZE = 8

SECRET_KEY = 'AOUSBDAONPSOMDASIDUBSDOUABER)*#(R&(&@@#))'


Expand Down
2 changes: 1 addition & 1 deletion data/campaigns.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@
msg_between_thanks: Thanks
msg_final_thanks: https://shutthebackdoor.net/call-tool/msg_final_thanks.mp3

- id: battle-for-the-net
- id: battleforthenet
numbers:
- 650-614-5872
target_house: true
Expand Down
73 changes: 73 additions & 0 deletions fftf_leaderboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import json
import grequests

class FFTFLeaderboard():

debug_mode = False
pool_size = 1

def __init__(self, debug_mode, pool_size):

self.debug_mode = debug_mode

def log_call(self, params, campaign, request):

if params['fftfCampaign'] == None or params['fftfReferer'] == None:
return

i = int(request.values.get('call_index'))

kwds = {
'campaign_id': campaign['id'],
'member_id': params['repIds'][i],
'zipcode': params['zipcode'],
'phone_number': params['userPhone'],
'call_id': request.values.get('CallSid', None),
'status': request.values.get('DialCallStatus', 'unknown'),
'duration': request.values.get('DialCallDuration', 0)
}
data = json.dumps(kwds)

self.post_to_leaderboard(
params['fftfCampaign'],
'call',
data,
params['fftfReferer'],
params['fftfSession'])

def log_complete(self, params, campaign, request):

if params['fftfCampaign'] == None or params['fftfReferer'] == None:
return

self.post_to_leaderboard(
params['fftfCampaign'],
'calls_complete',
'yay',
params['fftfReferer'],
params['fftfSession'])

def post_to_leaderboard(self, fftf_campaign, stat, data, host, session):

debug_mode = self.debug_mode

def finished(res, **kwargs):
if debug_mode:
print "FFTF Leaderboard call complete: %s" % res

data = {
'campaign': fftf_campaign,
'stat': stat,
'data': data,
'host': host,
'session': session
}

if self.debug_mode:
print "FFTF Leaderboard sending: %s" % data

url = 'https://leaderboard.fightforthefuture.org/log'
req = grequests.post(url, data=data, hooks=dict(response=finished))
job = grequests.send(req, grequests.Pool(self.pool_size))

return
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ argparse==1.2.1
blinker==1.3
gevent==1.0
greenlet==0.4.2
grequests==0.2.0
httplib2==0.8
itsdangerous==0.23
mysql-connector-python==1.2.2
Expand Down

0 comments on commit c208f25

Please sign in to comment.