diff --git a/rowers/mailprocessing.py b/rowers/mailprocessing.py index f27ca372..ffedcf99 100644 --- a/rowers/mailprocessing.py +++ b/rowers/mailprocessing.py @@ -91,6 +91,9 @@ def make_new_workout_from_email(rower, datafile, name, cntr=0,testing=False): path='media/')[6:] fileformat = fileformat[2] + if testing: + print 'Fileformat = ',fileformat + if fileformat == 'unknown': # extension = datafilename[-4:].lower() # fcopy = "media/"+datafilename[:-4]+"_copy"+extension diff --git a/rowers/management/commands/processemail.py b/rowers/management/commands/processemail.py index bcb27787..c8c3f2f9 100644 --- a/rowers/management/commands/processemail.py +++ b/rowers/management/commands/processemail.py @@ -20,6 +20,7 @@ from rowingdata import rowingdata as rrdata import rowers.uploads as uploads from rowers.mailprocessing import make_new_workout_from_email, send_confirm +import rowers.polarstuff as polarstuff workoutmailbox = Mailbox.objects.get(name='workouts') failedmailbox = Mailbox.objects.get(name='Failed') @@ -47,6 +48,8 @@ def processattachment(rower, fileobj, title, uploadoptions,testing=False): filename = fileobj.name except AttributeError: filename = fileobj[6:] + if testing: + print 'Attribute Error', filename # test if file exists and is not empty @@ -54,12 +57,20 @@ def processattachment(rower, fileobj, title, uploadoptions,testing=False): with open('media/'+filename,'r') as fop: line = fop.readline() except IOError: + if testing: + print 'IOError',filename,'media/'+filename return 0 + if testing: + print 'Creating workout from email' + workoutid = [ make_new_workout_from_email(rower, filename, title,testing=testing) ] + if testing: + print 'Workout id = {workoutid}'.format(workoutid=workoutid) + if workoutid[0]: link = settings.SITE_URL+reverse( rower.defaultlandingpage, @@ -137,6 +148,9 @@ class Command(BaseCommand): """Run the Email processing command """ def handle(self, *args, **options): + polar_available = polarstuff.get_polar_notifications() + res = polarstuff.get_all_new_workouts(polar_available) + messages = Message.objects.filter(mailbox_id = workoutmailbox.id) message_ids = [m.id for m in messages] attachments = MessageAttachment.objects.filter( @@ -185,6 +199,9 @@ class Command(BaseCommand): ) else: # move attachment and make workout + if testing: + print name + print attachment.document workoutid = processattachment( rower, attachment.document, name, uploadoptions, testing=testing diff --git a/rowers/models.py b/rowers/models.py index 945bbd39..5649595a 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -642,18 +642,29 @@ class Rower(models.Model): c2token = models.CharField(default='',max_length=200,blank=True,null=True) tokenexpirydate = models.DateTimeField(blank=True,null=True) c2refreshtoken = models.CharField(default='',max_length=200,blank=True,null=True) + c2_auto_export = models.BooleanField(default=False) sporttrackstoken = models.CharField(default='',max_length=200,blank=True,null=True) sporttrackstokenexpirydate = models.DateTimeField(blank=True,null=True) sporttracksrefreshtoken = models.CharField(default='',max_length=200, blank=True,null=True) + sporttracks_auto_export = models.BooleanField(default=False) underarmourtoken = models.CharField(default='',max_length=200,blank=True,null=True) underarmourtokenexpirydate = models.DateTimeField(blank=True,null=True) underarmourrefreshtoken = models.CharField(default='',max_length=200, blank=True,null=True) + mapmyfitness_auto_export = models.BooleanField(default=False) tptoken = models.CharField(default='',max_length=1000,blank=True,null=True) tptokenexpirydate = models.DateTimeField(blank=True,null=True) tprefreshtoken = models.CharField(default='',max_length=1000, blank=True,null=True) + trainingpeaks_auto_export = models.BooleanField(default=False) + + polartoken = models.CharField(default='',max_length=1000,blank=True,null=True) + polartokenexpirydate = models.DateTimeField(blank=True,null=True) + polarrefreshtoken = models.CharField(default='',max_length=1000, + blank=True,null=True) + polaruserid = models.IntegerField(default=0) + polar_auto_import = models.BooleanField(default=False) stravatoken = models.CharField(default='',max_length=200,blank=True,null=True) stravaexportas = models.CharField(default="Rowing", @@ -661,8 +672,10 @@ class Rower(models.Model): choices=stravatypes, verbose_name="Export Workouts to Strava as") + strava_auto_export = models.BooleanField(default=False) runkeepertoken = models.CharField(default='',max_length=200, blank=True,null=True) + runkeeper_auto_export = models.BooleanField(default=False) # Plan plans = ( @@ -1990,6 +2003,22 @@ class RowerPowerZonesForm(ModelForm): return cleaned_data +# Form to set rower's Auto Import and Export settings +class RowerImportExportForm(ModelForm): + class Meta: + model = Rower + fields = [ + 'polar_auto_import', + 'c2_auto_export', + 'mapmyfitness_auto_export', + 'runkeeper_auto_export', + 'sporttracks_auto_export', + 'strava_auto_export', + 'trainingpeaks_auto_export', + ] + + + # Form to set rower's Email and Weight category class AccountRowerForm(ModelForm): class Meta: diff --git a/rowers/polarstuff.py b/rowers/polarstuff.py new file mode 100644 index 00000000..5bf1a535 --- /dev/null +++ b/rowers/polarstuff.py @@ -0,0 +1,351 @@ +# All the functionality needed to connect to Strava + +# 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 +from math import sin,cos,atan2,sqrt +import os,sys +import gzip +import base64 +import yaml +from uuid import uuid4 + +# 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 rowers.models import checkworkoutuser +import dataprep +from dataprep import columndict + +from io import StringIO + +import stravalib +from stravalib.exc import ActivityUploadFailed,TimeoutExceeded + +from django_mailbox.models import Message,Mailbox,MessageAttachment + +from rowsandall_app.settings import ( + POLAR_CLIENT_ID, POLAR_REDIRECT_URI, POLAR_CLIENT_SECRET, + ) + +#baseurl = 'https://polaraccesslink.com/v3-example' +baseurl = 'https://polaraccesslink.com/v3' + +# 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 + +# Custom error class - to raise a NoTokenError +class PolarNoTokenError(Exception): + def __init__(self,value): + self.value=value + + def __str__(self): + return repr(self.value) + + +# Exchange access code for long-lived access token +def get_token(code): + + post_data = {"grant_type": "authorization_code", + "code": code, + "redirect_uri": POLAR_REDIRECT_URI, + } + + auth_string = '{id}:{secret}'.format( + id= POLAR_CLIENT_ID, + secret=POLAR_CLIENT_SECRET + ) + + headers = { 'Authorization': 'Basic %s' % base64.b64encode(auth_string) } + + response = requests.post("https://polarremote.com/v2/oauth2/token", + data=post_data, + headers=headers) + + + try: + token_json = response.json() + thetoken = token_json['access_token'] + expires_in = token_json['expires_in'] + user_id = token_json['x_user_id'] + except KeyError: + thetoken = 0 + expires_in = 0 + + return [thetoken,expires_in,user_id] + +# Make authorization URL including random string +def make_authorization_url(): + # Generate a random string for the state parameter + # Save it for use later to prevent xsrf attacks + state = str(uuid4()) + + params = {"client_id": POLAR_CLIENT_ID, + "response_type": "code", + "redirect_uri": POLAR_REDIRECT_URI, + "scope":"write"} + import urllib + url = "https://flow.polar.com/oauth2/authorization" +urllib.urlencode(params) + + + return HttpResponseRedirect(url) + +def get_polar_notifications(): + url = baseurl+'/notifications' + state = str(uuid4()) + auth_string = '{id}:{secret}'.format( + id= POLAR_CLIENT_ID, + secret=POLAR_CLIENT_SECRET + ) + + headers = { 'Authorization': 'Basic %s' % base64.b64encode(auth_string) } + + response = requests.get(url, headers=headers) + + available_data = [] + + if response.status_code == 200: + available_data = response.json()['available-user-data'] + + return available_data + +def get_all_new_workouts(available_data,testing=False): + for record in available_data: + if testing: + print record + if record['data-type'] == 'EXERCISE': + try: + r = Rower.objects.get(polaruserid=record['user-id']) + u = r.user + if r.polar_auto_import: + exercise_list = get_polar_workouts(u) + if testing: + print exercise_list + except Rower.DoesNotExist: + pass + + return 1 + + +def get_polar_workouts(user): + r = Rower.objects.get(user=user) + + exercise_list = [] + + if (r.polartoken == '') or (r.polartoken is None): + s = "Token doesn't exist. Need to authorize" + return custom_exception_handler(401,s) + elif (timezone.now()>r.polartokenexpirydate): + s = "Token expired. Needs to refresh" + return custom_exception_handler(401,s) + else: + authorizationstring = str('Bearer ' + r.polartoken) + headers = {'Authorization':authorizationstring, + 'Accept': 'application/json'} + + headers2 = { + 'Authorization':authorizationstring, + } + + url = baseurl+'/users/{userid}/exercise-transactions'.format( + userid = r.polaruserid + ) + + + response = requests.post(url, headers=headers) + + + if response.status_code == 201: + + workoutsbox = Mailbox.objects.filter(name='workouts')[0] + uploadoptions = { + 'makeprivate':False, + } + + bodyyaml = yaml.safe_dump( + uploadoptions, + default_flow_style=False + ) + + transactionid = response.json()['transaction-id'] + url = baseurl+'/users/{userid}/exercise-transactions/{transactionid}'.format( + transactionid = transactionid, + userid = r.polaruserid + ) + + response = requests.get(url, headers=headers) + if response.status_code == 200: + exerciseurls = response.json()['exercises'] + for exerciseurl in exerciseurls: + response = requests.get(exerciseurl,headers=headers) + if response.status_code == 200: + exercise_dict = response.json() + tcxuri = exerciseurl+'/tcx' + response = requests.get(tcxuri,headers=headers2) + if response.status_code == 200: + filename = 'media/mailbox_attachments/{code}_{id}.tcx'.format( + id = exercise_dict['id'], + code = uuid4().hex[:16] + ) + + with open(filename,'wb') as fop: + fop.write(response.content) + + msg = Message(mailbox=workoutsbox, + from_header=user.email, + subject = 'Import from Polar Flow', + body=bodyyaml) + + msg.save() + + a = MessageAttachment(message=msg,document=filename[6:]) + a.save() + + exercise_dict['filename'] = filename + else: + exercise_dict['filename'] = '' + + exercise_list.append(exercise_dict) + + # commit transaction + requests.put(url, headers=headers) + + return exercise_list + +def get_polar_user_info(user,physical=False): + r = Rower.objects.get(user=user) + if (r.polartoken == '') or (r.polartoken is None): + s = "Token doesn't exist. Need to authorize" + return custom_exception_handler(401,s) + elif (timezone.now()>r.polartokenexpirydate): + s = "Token expired. Needs to refresh" + return custom_exception_handler(401,s) + else: + authorizationstring = str('Bearer ' + r.polartoken) + headers = { + 'Authorization':authorizationstring, + 'Accept': 'application/json' + } + + + params = { + 'user-id': r.polaruserid + } + + if not physical: + url = baseurl+'/users/{userid}'.format( + userid = r.polaruserid + ) + else: + url = 'https://www.polaraccesslink.com/v3/users/{userid}/physical-information-transactions/'.format( + userid = r.polaruserid + ) + + + if physical: + response = requests.post(url, headers=headers) + else: + response = requests.get(url, headers=headers) + + return response + + +def get_polar_workout(user,id,transactionid): + + r = Rower.objects.get(user=user) + if (r.polartoken == '') or (r.polartoken is None): + s = "Token doesn't exist. Need to authorize" + return custom_exception_handler(401,s) + elif (timezone.now()>r.polartokenexpirydate): + s = "Token expired. Needs to refresh" + return custom_exception_handler(401,s) + else: + authorizationstring = str('Bearer ' + r.polartoken) + headers = { + 'Authorization':authorizationstring, + 'Accept': 'application/json' + } + + + url = baseurl+'/users/{userid}/exercise-transactions'.format( + userid = r.polaruserid + ) + + + response = requests.post(url, headers=headers) + + if response.status_code == 201: + transactionid = response.json()['transaction-id'] + url = baseurl+'/users/{userid}/exercise-transactions/{transactionid}'.format( + transactionid = transactionid, + userid = r.polaruserid + ) + + response = requests.get(url, headers=headers) + if response.status_code == 200: + exerciseurls = response.json()['exercises'] + for exerciseurl in exerciseurls: + response = requests.gedt(exerciseurl,headers=headers) + if response.status_code == 200: + exercise_dict = response.json() + thisid = exercise_dict['id'] + if thisid == id: + url = baseurl+'/users/{userid}/exercise-transactions/{transactionid}/exercises/{exerciseid}/tcx'.format( + userid = r.polaruserid, + transactionid = transactionid, + exerciseid = id + ) + + response = requests.get(url,headers = headers2) + + if response.status_code == 200: + print response.text + + result = response.text + # commit transaction + response = requests.put(url,headers=headers) + else: + result = None + + return result + + return None + + + + diff --git a/rowers/templates/email.html b/rowers/templates/email.html index 02f23929..562f8615 100644 --- a/rowers/templates/email.html +++ b/rowers/templates/email.html @@ -92,7 +92,7 @@ When the site is down, this is the appropriate channel to look for apologies, up 602 00 Brno
Czech Republic
IČ: 070 48 572
- DIČ: CZ 070 48 572
+ DIČ: CZ 070 48 572 (Nejsme plátce DPH)
Datová schránka: 7897syr
Email: info@rowsandall.com
The company is registered in the business register at the diff --git a/rowers/templates/imports.html b/rowers/templates/imports.html index d6e5a6af..857802a2 100644 --- a/rowers/templates/imports.html +++ b/rowers/templates/imports.html @@ -1,9 +1,9 @@ {% extends "base.html" %} - {% block title %}Contact Us{% endblock title %} + {% block title %}Import Workouts{% endblock title %} {% block content %}
-

Import Workouts

+

Import Workouts

@@ -53,14 +53,25 @@

Import workouts from MapMyFitness/UnderArmour

+
+
+

+ Polar logo +

+
+
+

Import workouts from Polar Flow.

Note: No workout selection possible. Automatically imports all new workouts

+
+
-

Connect

+

Connect

Click one of the below logos to connect to the service of your choice. - You only need to do this once. After that, the site will have access until you + You only need to do this once. After that, the site will have + access until you revoke the authorization for the "rowingdata" app.

@@ -81,10 +92,39 @@

connect with RunKeeper

-
+

connect with Under Armour

+
+

connect with Polar

+
+ +

Auto Import/Export Settings

+ +

Use the form below to set your auto import/export settings. + As we implement auto import/export for various partner sites, this form will be expanded.

+ +

Auto Import = rowsandall.com will regularly poll the partner site for new + workouts and automatically import new workouts +

+ +

Auto Export = New workouts uploaded to rowsandall.com will be automatically synchronized + to the partner site

+ +
+
+ + {{ form.as_table }} +
+ {% csrf_token %} +
+ +
+
+
+
diff --git a/rowers/templates/polar_list_import.html b/rowers/templates/polar_list_import.html new file mode 100644 index 00000000..3e61acf2 --- /dev/null +++ b/rowers/templates/polar_list_import.html @@ -0,0 +1,49 @@ +{% extends "base.html" %} +{% load staticfiles %} +{% load rowerfilters %} + +{% block title %}Workouts{% endblock %} + +{% block content %} +

New Workouts Imported From Polar Flow

+ +

Due to a limitation in Polar Flow's API, we can only access new workouts. We + have imported the following workouts and are now processing them. You will + receive email when the workouts are ready.

+ +{% if workouts %} +
+ + + + + + + + + + + + {% for workout in workouts %} + + + + + + + + + {% endfor %} + +
Imported Date Duration Distance Type
+ {% if workout|lookup:'filename' == '' %} + NO + {% else %} + YES + {% endif %} + {{ workout|lookup:'starttime' }}{{ workout|lookup:'duration' }} {{ workout|lookup:'distance' }} m{{ workout|lookup:'type' }}
+
+{% else %} +

No new workouts found

+{% endif %} +{% endblock %} diff --git a/rowers/uploads.py b/rowers/uploads.py index a5a4fc61..a667e200 100644 --- a/rowers/uploads.py +++ b/rowers/uploads.py @@ -374,14 +374,14 @@ def make_private(w,options): return 1 def do_sync(w,options): - if 'upload_to_C2' in options and options['upload_to_C2']: + if ('upload_to_C2' in options and options['upload_to_C2']) or w.user.c2_auto_export: try: message,id = c2stuff.workout_c2_upload(w.user.user,w) except c2stuff.C2NoTokenError: id = 0 message = "Something went wrong with the Concept2 sync" - if 'upload_to_Strava' in options and options['upload_to_Strava']: + if ('upload_to_Strava' in options and options['upload_to_Strava']) or w.user.strava_auto_export: try: message,id = stravastuff.workout_strava_upload( w.user.user,w @@ -390,8 +390,8 @@ def do_sync(w,options): id = 0 message = "Please connect to Strava first" - - if 'upload_to_SportTracks' in options and options['upload_to_SportTracks']: + + if ('upload_to_SportTracks' in options and options['upload_to_SportTracks']) or w.user.sporttracks_auto_export: try: message,id = sporttracksstuff.workout_sporttracks_upload( w.user.user,w @@ -401,7 +401,7 @@ def do_sync(w,options): id = 0 - if 'upload_to_RunKeeper' in options and options['upload_to_RunKeeper']: + if ('upload_to_RunKeeper' in options and options['upload_to_RunKeeper']) or w.user.runkeeper_auto_export: try: message,id = runkeeperstuff.workout_runkeeper_upload( w.user.user,w @@ -410,7 +410,7 @@ def do_sync(w,options): message = "Please connect to Runkeeper first" id = 0 - if 'upload_to_MapMyFitness' in options and options['upload_to_MapMyFitness']: + if ('upload_to_MapMyFitness' in options and options['upload_to_MapMyFitness']) or w.user.mapmyfitness_auto_export: try: message,id = underarmourstuff.workout_ua_upload( w.user.user,w @@ -420,7 +420,7 @@ def do_sync(w,options): id = 0 - if 'upload_to_TrainingPeaks' in options and options['upload_to_TrainingPeaks']: + if ('upload_to_TrainingPeaks' in options and options['upload_to_TrainingPeaks']) or w.user.trainingpeaks_auto_export: try: message,id = tpstuff.workout_tp_upload( w.user.user,w diff --git a/rowers/urls.py b/rowers/urls.py index de5d498d..b8d22ef1 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -120,7 +120,8 @@ urlpatterns = [ url(r'^404/$', TemplateView.as_view(template_name='404.html'),name='404'), url(r'^400/$', TemplateView.as_view(template_name='400.html'),name='400'), url(r'^403/$', TemplateView.as_view(template_name='403.html'),name='403'), - url(r'^imports/$', TemplateView.as_view(template_name='imports.html'), name='imports'), +# url(r'^imports/$', TemplateView.as_view(template_name='imports.html'), name='imports'), + url(r'^imports/$', views.imports_view), url(r'^exportallworkouts/?$',views.workouts_summaries_email_view), url(r'^update_empower$',views.rower_update_empower_view), url(r'^agegroupcp/(?P\d+)$',views.agegroupcpview), @@ -328,6 +329,7 @@ urlpatterns = [ url(r'^workout/sporttracksimport/$',views.workout_sporttracksimport_view), url(r'^workout/sporttracksimport/(?P\d+)/$',views.workout_getsporttracksworkout_view), url(r'^workout/sporttracksimport/all/$',views.workout_getsporttracksworkout_all), + url(r'^workout/polarimport/$',views.workout_polarimport_view), url(r'^workout/runkeeperimport/$',views.workout_runkeeperimport_view), url(r'^workout/runkeeperimport/(?P\d+)/$',views.workout_getrunkeeperworkout_view), url(r'^workout/underarmourimport/$',views.workout_underarmourimport_view), @@ -379,6 +381,7 @@ urlpatterns = [ url(r'^rower/edit/(?P\d+)$',views.rower_edit_view), url(r'^me/edit/(.+.*)/$',views.rower_edit_view), url(r'^me/c2authorize/$',views.rower_c2_authorize), + url(r'^me/polarauthorize/$',views.rower_polar_authorize), url(r'^me/revokeapp/(?P\d+)$',views.rower_revokeapp_view), url(r'^me/stravaauthorize/$',views.rower_strava_authorize), url(r'^me/sporttracksauthorize/$',views.rower_sporttracks_authorize), diff --git a/rowers/views.py b/rowers/views.py index 78f6c367..1c41a8ba 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -21,6 +21,9 @@ from django.views.decorators.csrf import csrf_exempt from matplotlib.backends.backend_agg import FigureCanvas import gc from pyparsing import ParseException +from uuid import uuid4 + +import isodate from django.shortcuts import render from django.http import ( @@ -69,7 +72,7 @@ from rowers.models import ( WorkoutComment,WorkoutCommentForm,RowerExportForm, CalcAgePerformance,PowerTimeFitnessMetric,PlannedSessionForm, PlannedSessionFormSmall,GeoCourseEditForm,VirtualRace, - VirtualRaceForm,VirtualRaceResultForm, + VirtualRaceForm,VirtualRaceResultForm,RowerImportExportForm ) from rowers.models import ( FavoriteForm,BaseFavoriteFormSet,SiteAnnouncement,BasePlannedSessionFormSet @@ -93,6 +96,8 @@ from sporttracksstuff import SportTracksNoTokenError,sporttracks_open from tpstuff import TPNoTokenError,tp_open from iso8601 import ParseError import stravastuff +import polarstuff +from polarstuff import PolarNoTokenError from stravastuff import StravaNoTokenError import sporttracksstuff import underarmourstuff @@ -104,6 +109,7 @@ from ownapistuff import TEST_CLIENT_ID, TEST_CLIENT_SECRET, TEST_REDIRECT_URI from rowsandall_app.settings import ( C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET, STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, STRAVA_CLIENT_SECRET, + POLAR_CLIENT_ID, POLAR_REDIRECT_URI, POLAR_CLIENT_SECRET, SPORTTRACKS_CLIENT_ID, SPORTTRACKS_REDIRECT_URI, SPORTTRACKS_CLIENT_SECRET, UNDERARMOUR_CLIENT_ID, UNDERARMOUR_REDIRECT_URI, @@ -1228,7 +1234,10 @@ def add_workout_from_strokedata(user,importid,data,strokedata, # Create CSV file name and save data to CSV file - csvfilename ='media/Import_'+str(importid)+'.csv' + csvfilename ='media/{code}_{importid}.csv'.format( + importid=importid, + code = uuid4().hex[:16] + ) res = df.to_csv(csvfilename+'.gz',index_label='index', compression='gzip') @@ -1418,7 +1427,11 @@ def add_workout_from_runkeeperdata(user,importid,data): timestr = strftime("%Y%m%d-%H%M%S") - csvfilename ='media/Import_'+str(importid)+'.csv' +# csvfilename ='media/Import_'+str(importid)+'.csv' + csvfilename ='media/{code}_{importid}.csv'.format( + importid=importid, + code = uuid4().hex[:16] + ) res = df.to_csv(csvfilename+'.gz',index_label='index', compression='gzip') @@ -1584,7 +1597,11 @@ def add_workout_from_stdata(user,importid,data): timestr = strftime("%Y%m%d-%H%M%S") - csvfilename ='media/Import_'+str(importid)+'.csv' +# csvfilename ='media/Import_'+str(importid)+'.csv' + csvfilename ='media/{code}_{importid}.csv'.format( + importid=importid, + code = uuid4().hex[:16] + ) res = df.to_csv(csvfilename+'.gz',index_label='index', compression='gzip') @@ -1751,7 +1768,11 @@ def add_workout_from_underarmourdata(user,importid,data): timestr = strftime("%Y%m%d-%H%M%S") - csvfilename ='media/Import_'+str(importid)+'.csv' +# csvfilename ='media/Import_'+str(importid)+'.csv' + csvfilename ='media/{code}_{importid}.csv'.format( + importid=importid, + code = uuid4().hex[:16] + ) res = df.to_csv(csvfilename+'.gz',index_label='index', compression='gzip') @@ -2358,7 +2379,7 @@ def workout_sporttracks_upload_view(request,id=0): def rower_c2_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()) scope = "user:read,results:write" params = {"client_id": C2_CLIENT_ID, @@ -2373,7 +2394,7 @@ def rower_c2_authorize(request): def rower_strava_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": STRAVA_CLIENT_ID, @@ -2385,12 +2406,30 @@ def rower_strava_authorize(request): return HttpResponseRedirect(url) +# Polar Authorization +@login_required() +def rower_polar_authorize(request): + + state = str(uuid4()) + + params = {"client_id": POLAR_CLIENT_ID, + "response_type": "code", + "redirect_uri": POLAR_REDIRECT_URI, + "state": state, +# "scope":"accesslink.read_all" + } + url = "https://flow.polar.com/oauth2/authorization?" +urllib.urlencode(params) + + return HttpResponseRedirect(url) + + + # Runkeeper authorization @login_required() def rower_runkeeper_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": RUNKEEPER_CLIENT_ID, @@ -2408,7 +2447,7 @@ def rower_runkeeper_authorize(request): def rower_sporttracks_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": SPORTTRACKS_CLIENT_ID, @@ -2426,7 +2465,7 @@ def rower_sporttracks_authorize(request): def rower_underarmour_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()) redirect_uri = UNDERARMOUR_REDIRECT_URI @@ -2443,7 +2482,7 @@ def rower_underarmour_authorize(request): 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", @@ -2588,12 +2627,31 @@ def rower_process_callback(request): # The imports page @login_required() -def imports_view(request,successmessage="",message=""): - messages.info(request,successmessage) - messages.error(request,message) +def imports_view(request): + + r = getrower(request.user) + + if request.method == 'POST': + form = RowerImportExportForm(request.POST) + if form.is_valid(): + cd = form.cleaned_data + + for attr, value in cd.items(): + setattr(r,attr,value) + + r.save() + + # polar_auto_import = cd['polar_auto_import'] + # r.polar_auto_import = polar_auto_import + # r.save() + else: + form = RowerImportExportForm(instance=r) + + return render(request,"imports.html", { 'teams':get_my_teams(request.user), + 'form':form, }) # Just for testing purposes @@ -2608,6 +2666,36 @@ def test_reverse_view(request): def rower_process_twittercallback(request): return "dummy" +# Process Polar Callback +@login_required() +def rower_process_polarcallback(request): + try: + code = request.GET['code'] + except MultiValueDictKeyError: + try: + message = request.GET['error'] + except MultiValueDictKeyError: + message = "access error" + + messages.error(request,message) + return imports_view(request) + + access_token, expires_in, user_id = polarstuff.get_token(code) + + expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in) + + r = getrower(request.user) + r.polartoken = access_token + r.polartokenexpirydate = expirydatetime + r.polaruserid = user_id + + r.save() + + successmessage = "Tokens stored. Good to go" + messages.info(request,successmessage) + return imports_view(request) + + # Process Strava Callback @login_required() def rower_process_stravacallback(request): @@ -9591,6 +9679,48 @@ def workout_underarmourimport_view(request,message=""): return HttpResponse(res) +# the page where you select which Polar workout to Import +@login_required() +def workout_polarimport_view(request): + exercises = polarstuff.get_polar_workouts(request.user) + workouts = [] + + try: + a = exercises.status_code + if a == 401: + messages.error(request,'Not authorized. You need to connect to Polar first') + url = reverse(imports_view) + return HttpResponseRedirect(url) + except: + pass + + for exercise in exercises: + try: + d = exercise['distance'] + except KeyError: + d = 0 + + i = exercise['id'] + transactionid = exercise['transaction-id'] + starttime = exercise['start-time'] + rowtype = exercise['sport'] + durationstring = exercise['duration'] + duration = isodate.parse_duration(durationstring) + keys = ['id','distance','duration','starttime','type','transactionid'] + values = [i,d,duration,starttime,rowtype,transactionid] + res = dict(zip(keys,values)) + workouts.append(res) + + + return render(request, 'polar_list_import.html', + { + 'workouts':workouts, + 'teams':get_my_teams(request.user), + }) + + + + # The page where you select which SportTracks workout to import @login_required() def workout_sporttracksimport_view(request,message=""): @@ -9767,6 +9897,7 @@ def workout_c2import_view(request,page=1,message=""): 'page':page, }) + # Import a workout from Strava @login_required() def workout_getstravaworkout_view(request,stravaid): @@ -10342,7 +10473,7 @@ def workout_upload_view(request, request.session['async_tasks'] = [(jobid,'make_plot')] # upload to C2 - if (upload_to_c2): + if (upload_to_c2) or (w.user.c2_auto_export): try: message,id = c2stuff.workout_c2_upload(request.user,w) except C2NoTokenError: @@ -10353,7 +10484,7 @@ def workout_upload_view(request, else: messages.error(request,message) - if (upload_to_strava): + if (upload_to_strava) or (w.user.strava_auto_export): try: message,id = stravastuff.workout_strava_upload( request.user,w @@ -10366,7 +10497,7 @@ def workout_upload_view(request, else: messages.error(request,message) - if (upload_to_st): + if (upload_to_st) or (w.user.sporttracks_auto_export): try: message,id = sporttracksstuff.workout_sporttracks_upload( request.user,w @@ -10379,7 +10510,7 @@ def workout_upload_view(request, else: messages.error(request,message) - if (upload_to_rk): + if (upload_to_rk) or (w.user.runkeeper_auto_export): try: message,id = runkeeperstuff.workout_runkeeper_upload( request.user,w @@ -10394,7 +10525,7 @@ def workout_upload_view(request, messages.error(request,message) - if (upload_to_ua): + if (upload_to_ua) or (w.user.mapmyfitness_auto_export): try: message,id = underarmourstuff.workout_ua_upload( request.user,w @@ -10409,7 +10540,7 @@ def workout_upload_view(request, messages.error(request,message) - if (upload_to_tp): + if (upload_to_tp) or (w.user.trainingpeaks_auto_export): try: message,id = tpstuff.workout_tp_upload( request.user,w diff --git a/rowsandall_app/settings.py b/rowsandall_app/settings.py index 9bfc9d1e..661ae2ad 100644 --- a/rowsandall_app/settings.py +++ b/rowsandall_app/settings.py @@ -254,6 +254,11 @@ RUNKEEPER_CLIENT_ID = CFG['runkeeper_client_id'] RUNKEEPER_CLIENT_SECRET = CFG['runkeeper_client_secret'] RUNKEEPER_REDIRECT_URI = CFG['runkeeper_callback'] +# Polar Flow + +POLAR_CLIENT_ID = CFG['polarflow_client_id'] +POLAR_CLIENT_SECRET = CFG['polarflow_client_secret'] +POLAR_REDIRECT_URI = CFG['polarflow_callback'] # Under Armour diff --git a/rowsandall_app/urls.py b/rowsandall_app/urls.py index 64e2a3dc..5fdc8511 100644 --- a/rowsandall_app/urls.py +++ b/rowsandall_app/urls.py @@ -71,6 +71,7 @@ urlpatterns += [ url(r'^stravacall\_back',rowersviews.rower_process_stravacallback), url(r'^sporttracks\_callback',rowersviews.rower_process_sporttrackscallback), url(r'^underarmour\_callback',rowersviews.rower_process_underarmourcallback), + url(r'^polarflowcallback',rowersviews.rower_process_polarcallback), url(r'^runkeeper\_callback',rowersviews.rower_process_runkeepercallback), url(r'^tp\_callback',rowersviews.rower_process_tpcallback), url(r'^twitter\_callback',rowersviews.rower_process_twittercallback), diff --git a/static/img/Polar_API_logo_3-horizontal.png b/static/img/Polar_API_logo_3-horizontal.png new file mode 100644 index 00000000..7f6dc054 Binary files /dev/null and b/static/img/Polar_API_logo_3-horizontal.png differ diff --git a/static/img/Polar_connectwith_btn_white.png b/static/img/Polar_connectwith_btn_white.png new file mode 100644 index 00000000..1f63c5a7 Binary files /dev/null and b/static/img/Polar_connectwith_btn_white.png differ