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 from MapMyFitness/UnderArmour
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.
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
+ +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 %} +| 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' }} | +
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