Private
Public Access
1
0

successfully authorizes, obtained and refreshes token

This commit is contained in:
Sander Roosendaal
2017-04-18 18:07:58 +02:00
parent ee162437b0
commit 256d0af28c
6 changed files with 417 additions and 9 deletions

View File

@@ -199,10 +199,17 @@ class Rower(models.Model):
underarmourtokenexpirydate = models.DateTimeField(blank=True,null=True) underarmourtokenexpirydate = models.DateTimeField(blank=True,null=True)
underarmourrefreshtoken = models.CharField(default='',max_length=200, underarmourrefreshtoken = models.CharField(default='',max_length=200,
blank=True,null=True) blank=True,null=True)
tptoken = models.CharField(default='',max_length=200,blank=True,null=True)
tptokenexpirydate = models.DateTimeField(blank=True,null=True)
tprefreshtoken = models.CharField(default='',max_length=200,
blank=True,null=True)
stravatoken = models.CharField(default='',max_length=200,blank=True,null=True) stravatoken = models.CharField(default='',max_length=200,blank=True,null=True)
runkeepertoken = models.CharField(default='',max_length=200, runkeepertoken = models.CharField(default='',max_length=200,
blank=True,null=True) blank=True,null=True)
# runkeepertokenexpirydate = models.DateTimeField(blank=True,null=True) # runkeepertokenexpirydate = models.DateTimeField(blank=True,null=True)
# runkeeperrefreshtoken = models.CharField(default='',max_length=200, # runkeeperrefreshtoken = models.CharField(default='',max_length=200,
# blank=True,null=True) # blank=True,null=True)
@@ -365,6 +372,7 @@ class Workout(models.Model):
uploadedtostrava = models.IntegerField(default=0) uploadedtostrava = models.IntegerField(default=0)
uploadedtosporttracks = models.IntegerField(default=0) uploadedtosporttracks = models.IntegerField(default=0)
uploadedtounderarmour = models.IntegerField(default=0) uploadedtounderarmour = models.IntegerField(default=0)
uploadedtotp = models.IntegerField(default=0)
uploadedtorunkeeper = models.IntegerField(default=0) uploadedtorunkeeper = models.IntegerField(default=0)
# empower stuff # empower stuff

250
rowers/tpstuff.py Normal file
View File

@@ -0,0 +1,250 @@
# All the functionality needed to connect to Runkeeper
# Python
import oauth2 as oauth
import cgi
import requests
import requests.auth
import json
from django.utils import timezone
from datetime import datetime
import numpy as np
from dateutil import parser
import time
import math
import gzip
from math import sin,cos,atan2,sqrt
import os,sys
import urllib
import base64
from io import BytesIO
# Django
from django.shortcuts import render_to_response
from django.http import HttpResponseRedirect, HttpResponse,JsonResponse
from django.conf import settings
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required
# Project
# from .models import Profile
from rowingdata import rowingdata
import pandas as pd
from rowers.models import Rower,Workout
from rowsandall_app.settings import (
C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET,
STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, STRAVA_CLIENT_SECRET,
TP_CLIENT_ID, TP_CLIENT_SECRET,
TP_REDIRECT_URI,TP_CLIENT_KEY,
)
tpapilocation = "https://api.sandbox.trainingpeaks.com"
# Custom error class - to raise a NoTokenError
class TPNoTokenError(Exception):
def __init__(self,value):
self.value=value
def __str__(self):
return repr(self.value)
# Exponentially weighted moving average
# Used for data smoothing of the jagged data obtained by Strava
# See bitbucket issue 72
def ewmovingaverage(interval,window_size):
# Experimental code using Exponential Weighted moving average
try:
intervaldf = pd.DataFrame({'v':interval})
idf_ewma1 = intervaldf.ewm(span=window_size)
idf_ewma2 = intervaldf[::-1].ewm(span=window_size)
i_ewma1 = idf_ewma1.mean().ix[:,'v']
i_ewma2 = idf_ewma2.mean().ix[:,'v']
interval2 = np.vstack((i_ewma1,i_ewma2[::-1]))
interval2 = np.mean( interval2, axis=0) # average
except ValueError:
interval2 = interval
return interval2
from utils import geo_distance
# Custom exception handler, returns a 401 HTTP message
# with exception details in the json data
def custom_exception_handler(exc,message):
response = {
"errors": [
{
"code": str(exc),
"detail": message,
}
]
}
res = HttpResponse(message)
res.status_code = 401
res.json = json.dumps(response)
return res
# Refresh ST token using refresh token
def do_refresh_token(refreshtoken,access_token):
client_auth = requests.auth.HTTPBasicAuth(TP_CLIENT_KEY, TP_CLIENT_SECRET)
post_data = {"grant_type": "refresh_token",
"client_secret": TP_CLIENT_SECRET,
"client_id":TP_CLIENT_KEY,
"refresh_token": refreshtoken,
}
headers = {'user-agent': 'sanderroosendaal',
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
'authorization': 'Bearer %s' % access_token}
url = "https://oauth.sandbox.trainingpeaks.com/oauth/token"
response = requests.post(url,
data=post_data,
headers=headers)
token_json = response.json()
thetoken = token_json['access_token']
expires_in = token_json['expires_in']
try:
refresh_token = token_json['refresh_token']
except KeyError:
refresh_token = refreshtoken
return [thetoken,expires_in,refresh_token]
# Exchange access code for long-lived access token
def get_token(code):
client_auth = requests.auth.HTTPBasicAuth(TP_CLIENT_KEY, TP_CLIENT_SECRET)
post_data = {
"client_id":TP_CLIENT_KEY,
"grant_type": "authorization_code",
"code": code,
"redirect_uri":TP_REDIRECT_URI,
"client_secret": TP_CLIENT_SECRET,
}
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
}
response = requests.post("https://oauth.sandbox.trainingpeaks.com/oauth/token",
data=post_data)
print "Reason"
print response.reason
print response.content
try:
token_json = response.json()
thetoken = token_json['access_token']
expires_in = token_json['expires_in']
refresh_token = token_json['refresh_token']
except KeyError:
thetoken = 0
return thetoken,expires_in,refresh_token
# Make authorization URL including random string
def make_authorization_url(request):
# Generate a random string for the state parameter
# Save it for use later to prevent xsrf attacks
from uuid import uuid4
state = str(uuid4())
params = {"client_id": TP_CLIENT_KEY,
"response_type": "code",
"redirect_uri": TP_REDIRECT_URI,
"scope": "write",
}
url = "https://oauth.sandbox.trainingpeaks.com/oauth/authorize?" +urllib.urlencode(params)
return HttpResponseRedirect(url)
def getidfromresponse(response):
t = json.loads(response.text)
links = t["_links"]
id = links["self"][0]["id"]
return int(id)
def createtpworkoutdata(w):
filename = w.csvfilename
try:
row = rowingdata(filename)
tcxfilename = filename[:-4]+'.tcx'
row.exporttotcx(tcxfilename,notes=w.notes)
# with file(tcxfilename,'rb') as inF:
# s = inF.read()
# with gzip.GzipFile(tcxfilename+'.gz','wb') as outF:
# outF.write(s)
return tcxfilename
except:
tcxfilename = 0
return tcxfilename
def tp_check(access_token):
headers = {
"Content-Type": "application/json",
'Accept': 'application/json',
'authorization': 'Bearer %s' % access_token
}
resp = requests.post(tpapilocation+"/v1/info/version",
headers=headers)
return resp
def uploadactivity(access_token,filename,description=''):
data_gz = BytesIO()
with file(filename,'rb') as inF:
s = inF.read()
with gzip.GzipFile(fileobj=data_gz,mode="w") as gzf:
gzf.write(s)
headers = {
"Content-Type": "application/json",
'Accept': 'application/json',
'authorization': 'Bearer %s' % access_token
}
data = {
"UploadClient": "rowsandall",
"Filename": filename,
"SetWorkoutPublic": True,
"Comment": description,
"Data": base64.b64encode(data_gz.getvalue()).decode("ascii")
}
resp = requests.post(tpapilocation+"/v1/file",
data = json.dumps(data),
headers=headers)
if resp.status_code != 200:
print resp.status_code
print resp.reason
print ""
print headers
print ""
return 0
else:
return resp.json()[0]["Id"]
return 0

View File

@@ -238,6 +238,7 @@ urlpatterns = [
url(r'^workout/(\d+)/sporttracksuploadw/$',views.workout_sporttracks_upload_view), url(r'^workout/(\d+)/sporttracksuploadw/$',views.workout_sporttracks_upload_view),
url(r'^workout/(\d+)/runkeeperuploadw/$',views.workout_runkeeper_upload_view), url(r'^workout/(\d+)/runkeeperuploadw/$',views.workout_runkeeper_upload_view),
url(r'^workout/(\d+)/underarmouruploadw/$',views.workout_underarmour_upload_view), url(r'^workout/(\d+)/underarmouruploadw/$',views.workout_underarmour_upload_view),
url(r'^workout/(\d+)/tpuploadw/$',views.workout_tp_upload_view),
url(r'^multi-compare$',views.multi_compare_view), url(r'^multi-compare$',views.multi_compare_view),
url(r'^me/teams/c/(?P<message>\w+.*)/s/(?P<successmessage>\w+.*)$',views.rower_teams_view), url(r'^me/teams/c/(?P<message>\w+.*)/s/(?P<successmessage>\w+.*)$',views.rower_teams_view),
url(r'^me/teams/s/(?P<successmessage>\w+.*)$',views.rower_teams_view), url(r'^me/teams/s/(?P<successmessage>\w+.*)$',views.rower_teams_view),
@@ -273,9 +274,11 @@ urlpatterns = [
url(r'^me/stravaauthorize/$',views.rower_strava_authorize), url(r'^me/stravaauthorize/$',views.rower_strava_authorize),
url(r'^me/sporttracksauthorize/$',views.rower_sporttracks_authorize), url(r'^me/sporttracksauthorize/$',views.rower_sporttracks_authorize),
url(r'^me/underarmourauthorize/$',views.rower_underarmour_authorize), url(r'^me/underarmourauthorize/$',views.rower_underarmour_authorize),
url(r'^me/tpauthorize/$',views.rower_tp_authorize),
url(r'^me/runkeeperauthorize/$',views.rower_runkeeper_authorize), url(r'^me/runkeeperauthorize/$',views.rower_runkeeper_authorize),
url(r'^me/sporttracksrefresh/$',views.rower_sporttracks_token_refresh), url(r'^me/sporttracksrefresh/$',views.rower_sporttracks_token_refresh),
url(r'^me/underarmourrefresh/$',views.rower_underarmour_token_refresh), url(r'^me/underarmourrefresh/$',views.rower_underarmour_token_refresh),
url(r'^me/tprefresh/$',views.rower_tp_token_refresh),
url(r'^me/c2refresh/$',views.rower_c2_token_refresh), url(r'^me/c2refresh/$',views.rower_c2_token_refresh),
url(r'^me/favoritecharts/$',views.rower_favoritecharts_view), url(r'^me/favoritecharts/$',views.rower_favoritecharts_view),
url(r'^email/send/$', views.sendmail), url(r'^email/send/$', views.sendmail),

View File

@@ -3,6 +3,7 @@ import timestring
import zipfile import zipfile
import operator import operator
import warnings import warnings
import urllib
from numbers import Number from numbers import Number
from django.views.generic.base import TemplateView from django.views.generic.base import TemplateView
from django.db.models import Q from django.db.models import Q
@@ -53,10 +54,12 @@ import c2stuff
from c2stuff import C2NoTokenError from c2stuff import C2NoTokenError
from runkeeperstuff import RunKeeperNoTokenError from runkeeperstuff import RunKeeperNoTokenError
from sporttracksstuff import SportTracksNoTokenError from sporttracksstuff import SportTracksNoTokenError
from tpstuff import TPNoTokenError
from iso8601 import ParseError from iso8601 import ParseError
import stravastuff import stravastuff
import sporttracksstuff import sporttracksstuff
import underarmourstuff import underarmourstuff
import tpstuff
import runkeeperstuff import runkeeperstuff
import ownapistuff import ownapistuff
from ownapistuff import TEST_CLIENT_ID, TEST_CLIENT_SECRET, TEST_REDIRECT_URI from ownapistuff import TEST_CLIENT_ID, TEST_CLIENT_SECRET, TEST_REDIRECT_URI
@@ -68,6 +71,7 @@ from rowsandall_app.settings import (
UNDERARMOUR_CLIENT_ID, UNDERARMOUR_REDIRECT_URI, UNDERARMOUR_CLIENT_ID, UNDERARMOUR_REDIRECT_URI,
UNDERARMOUR_CLIENT_SECRET,UNDERARMOUR_CLIENT_KEY, UNDERARMOUR_CLIENT_SECRET,UNDERARMOUR_CLIENT_KEY,
RUNKEEPER_CLIENT_ID,RUNKEEPER_REDIRECT_URI,RUNKEEPER_CLIENT_SECRET, RUNKEEPER_CLIENT_ID,RUNKEEPER_REDIRECT_URI,RUNKEEPER_CLIENT_SECRET,
TP_CLIENT_ID,TP_REDIRECT_URI,TP_CLIENT_KEY,TP_CLIENT_SECRET,
) )
import requests import requests
@@ -1075,6 +1079,20 @@ def underarmour_open(user):
return thetoken return thetoken
# Checks if user has UnderArmour token, renews them if they are expired
def tp_open(user):
r = Rower.objects.get(user=user)
if (r.tptoken == '') or (r.tptoken is None):
s = "Token doesn't exist. Need to authorize"
raise TPNoTokenError("User has no token")
else:
if (timezone.now()>r.tptokenexpirydate):
thetoken = tpstuff.rower_tp_token_refresh(user)
else:
thetoken = r.tptoken
return thetoken
# Checks if user has SportTracks token, renews them if they are expired # Checks if user has SportTracks token, renews them if they are expired
def runkeeper_open(user): def runkeeper_open(user):
r = Rower.objects.get(user=user) r = Rower.objects.get(user=user)
@@ -1190,6 +1208,73 @@ def workout_csvemail_view(request,id=0):
return response return response
# Send workout to TP
@login_required()
def workout_tp_upload_view(request,id=0):
message = ""
r = Rower.objects.get(user=request.user)
res = -1
try:
thetoken = tp_open(r.user)
except TPNoTokenError:
return HttpResponseRedirect("/rowers/me/tpauthorize/")
# ready to upload. Hurray
try:
w = Workout.objects.get(id=id)
r = w.user
except Workout.DoesNotExist:
raise Http404("Workout doesn't exist")
if (checkworkoutuser(request.user,w)):
tcxfile = tpstuff.createtpworkoutdata(w)
if tcxfile:
res = tpstuff.uploadactivity(r.tptoken,tcxfile,
description=w.notes)
if res == 0:
message = "Upload to TrainingPeaks failed"
w.uploadedtotp = -1
w.save()
try:
os.remove(tcxfile)
except WindowsError:
pass
url = reverse(workout_export_view,
kwargs = {
'id':str(w.id),
'message':message,
})
else: # res != 0
w.uploadedtotp = res
w.save()
os.remove(tcxfile)
url = reverse(workout_export_view,
kwargs = {
'id':str(w.id),
'successmessage':'Uploaded to TP',
})
else: # no tcxfile
message = "Upload to TrainingPeaks failed"
w.uploadedtotp = -1
w.save()
url = reverse(workout_export_view,
kwargs = {
'id':str(w.id),
'message':message,
})
else: # not allowed to upload
message = "You are not allowed to export this workout to TP"
url = reverse(workout_export_view,
kwargs = {
'id':str(w.id),
'message':message,
})
return HttpResponseRedirect(url)
# Send workout to Strava # Send workout to Strava
# abundance of error logging here because there were/are some bugs # abundance of error logging here because there were/are some bugs
@login_required() @login_required()
@@ -1319,7 +1404,6 @@ def workout_c2_upload_view(request,id=0):
headers = {'Authorization': authorizationstring, headers = {'Authorization': authorizationstring,
'user-agent': 'sanderroosendaal', 'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'} 'Content-Type': 'application/json'}
import urllib
try: try:
url = "https://log.concept2.com/api/users/%s/results" % (c2userid) url = "https://log.concept2.com/api/users/%s/results" % (c2userid)
response = requests.post(url,headers=headers,data=json.dumps(data)) response = requests.post(url,headers=headers,data=json.dumps(data))
@@ -1404,7 +1488,6 @@ def workout_runkeeper_upload_view(request,id=0):
'Content-Type': 'application/vnd.com.runkeeper.NewFitnessActivity+json', 'Content-Type': 'application/vnd.com.runkeeper.NewFitnessActivity+json',
'Content-Length':'nnn'} 'Content-Length':'nnn'}
import urllib
url = "https://api.runkeeper.com/fitnessActivities" url = "https://api.runkeeper.com/fitnessActivities"
response = requests.post(url,headers=headers,data=json.dumps(data)) response = requests.post(url,headers=headers,data=json.dumps(data))
@@ -1470,7 +1553,6 @@ def workout_underarmour_upload_view(request,id=0):
'Content-Type': 'application/json', 'Content-Type': 'application/json',
} }
import urllib
url = "https://api.ua.com/v7.1/workout/" url = "https://api.ua.com/v7.1/workout/"
response = requests.post(url,headers=headers,data=json.dumps(data)) response = requests.post(url,headers=headers,data=json.dumps(data))
@@ -1533,7 +1615,6 @@ def workout_sporttracks_upload_view(request,id=0):
'user-agent': 'sanderroosendaal', 'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'} 'Content-Type': 'application/json'}
import urllib
url = "https://api.sporttracks.mobi/api/v2/fitnessActivities.json" url = "https://api.sporttracks.mobi/api/v2/fitnessActivities.json"
response = requests.post(url,headers=headers,data=json.dumps(data)) response = requests.post(url,headers=headers,data=json.dumps(data))
@@ -1575,7 +1656,6 @@ def rower_c2_authorize(request):
params = {"client_id": C2_CLIENT_ID, params = {"client_id": C2_CLIENT_ID,
"response_type": "code", "response_type": "code",
"redirect_uri": C2_REDIRECT_URI} "redirect_uri": C2_REDIRECT_URI}
import urllib
url = "http://log.concept2.com/oauth/authorize?"+ urllib.urlencode(params) url = "http://log.concept2.com/oauth/authorize?"+ urllib.urlencode(params)
url += "&scope="+scope url += "&scope="+scope
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@@ -1593,7 +1673,6 @@ def rower_strava_authorize(request):
"redirect_uri": STRAVA_REDIRECT_URI, "redirect_uri": STRAVA_REDIRECT_URI,
"scope": "write"} "scope": "write"}
import urllib
url = "https://www.strava.com/oauth/authorize?"+ urllib.urlencode(params) url = "https://www.strava.com/oauth/authorize?"+ urllib.urlencode(params)
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@@ -1611,7 +1690,6 @@ def rower_runkeeper_authorize(request):
"state": state, "state": state,
"redirect_uri": RUNKEEPER_REDIRECT_URI} "redirect_uri": RUNKEEPER_REDIRECT_URI}
import urllib
url = "https://runkeeper.com/apps/authorize?"+ urllib.urlencode(params) url = "https://runkeeper.com/apps/authorize?"+ urllib.urlencode(params)
@@ -1630,7 +1708,6 @@ def rower_sporttracks_authorize(request):
"state": state, "state": state,
"redirect_uri": SPORTTRACKS_REDIRECT_URI} "redirect_uri": SPORTTRACKS_REDIRECT_URI}
import urllib
url = "https://api.sporttracks.mobi/oauth2/authorize?"+ urllib.urlencode(params) url = "https://api.sporttracks.mobi/oauth2/authorize?"+ urllib.urlencode(params)
@@ -1653,6 +1730,23 @@ def rower_underarmour_authorize(request):
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
# Underarmour Authorization
@login_required()
def rower_tp_authorize(request):
# Generate a random string for the state parameter
# Save it for use later to prevent xsrf attacks
from uuid import uuid4
state = str(uuid4())
params = {"client_id": TP_CLIENT_KEY,
"response_type": "code",
"redirect_uri": TP_REDIRECT_URI,
"scope": "write",
}
url = "https://oauth.sandbox.trainingpeaks.com/oauth/authorize/?" +urllib.urlencode(params)
return HttpResponseRedirect(url)
# Concept2 token refresh. URL for manual refresh. Not visible to users # Concept2 token refresh. URL for manual refresh. Not visible to users
@login_required() @login_required()
def rower_c2_token_refresh(request): def rower_c2_token_refresh(request):
@@ -1702,6 +1796,30 @@ def rower_underarmour_token_refresh(request):
return imports_view(request,successmessage=successmessage) return imports_view(request,successmessage=successmessage)
# TrainingPeaks token refresh. URL for manual refresh. Not visible to users
@login_required()
def rower_tp_token_refresh(request):
r = Rower.objects.get(user=request.user)
res = tpstuff.do_refresh_token(
r.tprefreshtoken,
r.tptoken
)
access_token = res[0]
expires_in = res[1]
refresh_token = res[2]
expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in)
r = Rower.objects.get(user=request.user)
r.tptoken = access_token
r.tptokenexpirydate = expirydatetime
r.tprefreshtoken = refresh_token
r.save()
successmessage = "Tokens refreshed. Good to go"
return imports_view(request,successmessage=successmessage)
# SportTracks token refresh. URL for manual refresh. Not visible to users # SportTracks token refresh. URL for manual refresh. Not visible to users
@login_required() @login_required()
def rower_sporttracks_token_refresh(request): def rower_sporttracks_token_refresh(request):
@@ -1863,6 +1981,28 @@ def rower_process_underarmourcallback(request):
successmessage = "Tokens stored. Good to go" successmessage = "Tokens stored. Good to go"
return imports_view(request,successmessage=successmessage) return imports_view(request,successmessage=successmessage)
# Process TrainingPeaks callback
@login_required()
def rower_process_tpcallback(request):
code = request.GET['code']
res = tpstuff.get_token(code)
access_token = res[0]
expires_in = res[1]
refresh_token = res[2]
expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in)
r = Rower.objects.get(user=request.user)
r.tptoken = access_token
r.tptokenexpirydate = expirydatetime
r.tprefreshtoken = refresh_token
r.save()
successmessage = "Tokens stored. Good to go"
return imports_view(request,successmessage=successmessage)
# Process Own API callback - for API testing purposes # Process Own API callback - for API testing purposes
@login_required() @login_required()
def rower_process_testcallback(request): def rower_process_testcallback(request):
@@ -5720,7 +5860,7 @@ def workout_upload_view(request,message="",
headers = {'Authorization': authorizationstring, headers = {'Authorization': authorizationstring,
'user-agent': 'sanderroosendaal', 'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'} 'Content-Type': 'application/json'}
import urllib
url = "https://log.concept2.com/api/users/%s/results" % (c2userid) url = "https://log.concept2.com/api/users/%s/results" % (c2userid)
response = requests.post(url,headers=headers,data=json.dumps(data)) response = requests.post(url,headers=headers,data=json.dumps(data))

View File

@@ -242,6 +242,12 @@ UNDERARMOUR_CLIENT_KEY = CFG['underarmour_client_key']
UNDERARMOUR_REDIRECT_URI = "http://rowsandall.com/underarmour_callback" UNDERARMOUR_REDIRECT_URI = "http://rowsandall.com/underarmour_callback"
#UNDERARMOUR_REDIRECT_URI = "http://localhost:8000/underarmour_callback" #UNDERARMOUR_REDIRECT_URI = "http://localhost:8000/underarmour_callback"
# TrainingPeaks
TP_CLIENT_ID = CFG["tp_client_id"]
TP_CLIENT_SECRET = CFG["tp_client_secret"]
TP_REDIRECT_URI = "http://localhost:8000/tp_callback"
TP_CLIENT_KEY = TP_CLIENT_ID
# RQ stuff # RQ stuff
RQ_QUEUES = { RQ_QUEUES = {

View File

@@ -57,6 +57,7 @@ urlpatterns += [
url(r'^sporttracks\_callback',rowersviews.rower_process_sporttrackscallback), url(r'^sporttracks\_callback',rowersviews.rower_process_sporttrackscallback),
url(r'^underarmour\_callback',rowersviews.rower_process_underarmourcallback), url(r'^underarmour\_callback',rowersviews.rower_process_underarmourcallback),
url(r'^runkeeper\_callback',rowersviews.rower_process_runkeepercallback), url(r'^runkeeper\_callback',rowersviews.rower_process_runkeepercallback),
url(r'^tp\_callback',rowersviews.rower_process_tpcallback),
url(r'^twitter\_callback',rowersviews.rower_process_twittercallback), url(r'^twitter\_callback',rowersviews.rower_process_twittercallback),
url(r'^i18n/', include('django.conf.urls.i18n')), url(r'^i18n/', include('django.conf.urls.i18n')),
] ]