From f6d6d60e0d443db94684d0622403989d4c9f35a8 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 26 Jan 2021 19:29:16 +0100 Subject: [PATCH] rp3 in progress --- rowers/models.py | 203 +++++++--------------------------- rowers/rp3stuff.py | 214 ++++++++++++++++++++++++++++++++++++ rowers/views/importviews.py | 27 +++++ rowsandall_app/settings.py | 6 + rowsandall_app/urls.py | 1 + 5 files changed, 287 insertions(+), 164 deletions(-) create mode 100644 rowers/rp3stuff.py diff --git a/rowers/models.py b/rowers/models.py index 745e9386..4bfe1ceb 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -906,14 +906,6 @@ class Rower(models.Model): 'Pwr TR', 'Pwr AN']) - hrzones = PowerZonesField(default=['Rest', - 'UT2', - 'UT1', - 'AT', - 'TR', - 'AN','max']) - - emailalternatives = AlternativeEmails(default=[],null=True,blank=True,verbose_name='Alternative Email addresses (separate with ",")') # Site Settings @@ -944,6 +936,11 @@ class Rower(models.Model): tptokenexpirydate = models.DateTimeField(blank=True,null=True) tprefreshtoken = models.CharField(default='',max_length=1000, blank=True,null=True) + rp3token = models.CharField(default='',max_length=1000,blank=True,null=True) + rp3tokenexpirydate = models.DateTimeField(blank=True,null=True) + rp3refreshtoken = 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) @@ -3662,160 +3659,6 @@ class RowerCPForm(ModelForm): model = Rower fields = ['cprange','kfit','kfatigue'] - -# Form to set rower's Power zones, including test routines -# to enable consistency -class RowerHRZonesForm(ModelForm): - - hrzones = ['Rest','UT2','UT1','AT','TR','AN','Max'] - hrrestname = forms.CharField(initial=hrzones[0]) - hrut2name = forms.CharField(initial=hrzones[1]) - hrut1name = forms.CharField(initial=hrzones[2]) - hratname = forms.CharField(initial=hrzones[3]) - hrtrname = forms.CharField(initial=hrzones[4]) - hranname = forms.CharField(initial=hrzones[5]) - hrmaxname = forms.CharField(initial=hrzones[6]) - - def __init__(self, *args,**kwargs): - super(RowerHRZonesForm, self).__init__(*args, **kwargs) - - if 'instance' in kwargs: - hrzones = kwargs['instance'].hrzones - else: - hrzones = ['Rest','UT2','UT1','AT','TR','AN','Max'] - - self.fields['hrrestname'].initial = hrzones[0] - self.fields['hrut2name'].initial = hrzones[1] - self.fields['hrut1name'].initial = hrzones[2] - self.fields['hratname'].initial = hrzones[3] - self.fields['hrtrname'].initial = hrzones[4] - self.fields['hranname'].initial = hrzones[5] - self.fields['hrmaxname'].initial = hrzones[6] - - class Meta: - model = Rower - fields = ['rest','ut2','ut1','at','tr','an','max'] - - def clean(self): - cleaned_data = super(RowerHRZonesForm, self).clean() - - try: - rest = cleaned_data['rest'] - except KeyError: - raise ValidationError("Value cannot be empty") - except: - rest = int(self.data['rest']) - - try: - ut2 = cleaned_data['ut2'] - except KeyError: - raise ValidationError("Value cannot be empty") - except: - ut2 = int(self.data['ut2']) - try: - ut1 = cleaned_data['ut1'] - except KeyError: - raise ValidationError("Value cannot be empty") - except: - ut1 = int(self.data['ut1']) - try: - at = cleaned_data['at'] - except KeyError: - raise ValidationError("Value cannot be empty") - except: - at = int(self.data['at']) - try: - tr = cleaned_data['tr'] - except KeyError: - raise ValidationError("Value cannot be empty") - except: - tr = int(self.data['tr']) - try: - an = cleaned_data['an'] - except KeyError: - raise ValidationError("Value cannot be empty") - except: - an = int(self.data['an']) - - try: - max = cleaned_data['max'] - except KeyError: - raise ValidationError("Value cannot be empty") - except: - max = int(self.data['max']) - - try: - hrrestname = cleaned_data['hrrestname'] - except: - hrrestname = 'Rest' - cleaned_data['hrut3name'] = 'Rest' - try: - hrut2name = cleaned_data['hrut2name'] - except: - hrut2name = 'UT2' - cleaned_data['hrut2name'] = 'UT2' - try: - hrut1name = cleaned_data['hrut1name'] - except: - hrut1name = 'UT1' - cleaned_data['hrut1name'] = 'UT1' - try: - hratname = cleaned_data['hratname'] - except: - hratname = 'AT' - cleaned_data['hratname'] = 'AT' - try: - hrtrname = cleaned_data['hrtrname'] - except: - hrtrname = 'TR' - cleaned_data['hrtrname'] = 'TR' - try: - hranname = cleaned_data['hranname'] - except: - hranname = 'AN' - cleaned_data['hranname'] = 'AN' - - - if rest >= ut2: - e = "{ut2name} should be higher than {restname}".format( - restname=hrrestname, - ut2name=hrut2name - ) - raise forms.ValidationError(e) - - if ut1 <= ut2: - e = "{ut1name} should be higher than {ut2name}".format( - ut1name = hrut1name, - ut2name= hrut2name, - ) - raise forms.ValidationError(e) - if at <= ut1: - e = "{atname} should be higher than {ut1name}".format( - atname = hratname, - ut1name= hrut1name, - ) - raise forms.ValidationError(e) - if tr <= at: - e = "{trname} should be higher than {atname}".format( - atname = hratname, - trname= hrtrname, - ) - raise forms.ValidationError(e) - if an <= tr: - e = "{anname} should be higher than {trname}".format( - anname = hranname, - trname= hrtrname, - ) - raise forms.ValidationError(e) - - if max <= an: - e = "{anname} should be lower than {maxname}".format( - anname=hranname, - maxname=hrmaxname, - ) - - return cleaned_data - # Form to set rower's Power zones, including test routines # to enable consistency class RowerPowerZonesForm(ModelForm): @@ -3905,12 +3748,44 @@ class RowerPowerZonesForm(ModelForm): trname = cleaned_data['trname'] except: trname = 'TR' - cleaned_data['trname'] = 'TR' + cleaned_data['ut1name'] = 'TR' try: anname = cleaned_data['anname'] except: anname = 'AN' - cleaned_data['anname'] = 'AN' + cleaned_data['ut1name'] = 'AN' + + + try: + ut3name = cleaned_data['ut3name'] + except: + ut2name = 'UT3' + cleaned_data['ut3name'] = 'UT3' + try: + ut2name = cleaned_data['ut2name'] + except: + ut2name = 'UT2' + cleaned_data['ut2name'] = 'UT2' + try: + ut1name = cleaned_data['ut1name'] + except: + ut1name = 'UT1' + cleaned_data['ut1name'] = 'UT1' + try: + atname = cleaned_data['atname'] + except: + atname = 'AT' + cleaned_data['atname'] = 'AT' + try: + trname = cleaned_data['trname'] + except: + trname = 'TR' + cleaned_data['ut1name'] = 'TR' + try: + anname = cleaned_data['anname'] + except: + anname = 'AN' + cleaned_data['ut1name'] = 'AN' if pw_ut1 <= pw_ut2: diff --git a/rowers/rp3stuff.py b/rowers/rp3stuff.py new file mode 100644 index 00000000..233e059d --- /dev/null +++ b/rowers/rp3stuff.py @@ -0,0 +1,214 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from __future__ import unicode_literals, absolute_import +# All the functionality needed to connect to Runkeeper +from rowers.imports import * + +# Python +import gzip + +import base64 +from io import BytesIO + + +from rowsandall_app.settings import ( + C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET, + STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, STRAVA_CLIENT_SECRET, + rp3_CLIENT_ID, rp3_CLIENT_SECRET, + rp3_REDIRECT_URI,rp3_CLIENT_KEY, + RP3_CLIENT_ID, RP3_CLIENT_KEY, RP3_REDIRECT_URI, RP3_CLIENT_SECRET + ) + +tpapilocation = "https://api.trainingpeaks.com" + +from celery import Celery,app +from django_rq import job +import time +#from async_messages import message_user,messages + +oauth_data = { + 'client_id': RP3_CLIENT_ID, + 'client_secret': RP3_CLIENT_SECRET, + 'redirect_uri': RP3_REDIRECT_URI, + 'autorization_uri': "https://rp3rowing-app.com/oauth/authorize?", + 'content_type': 'application/x-www-form-urlencoded', +# 'content_type': 'application/json', + 'tokenname': 'rp3token', + 'refreshtokenname': 'rp3refreshtoken', + 'expirydatename': 'rp3tokenexpirydate', + 'bearer_auth': False, + 'base_url': "https://rp3rowing-app.com/oauth/token", + 'scope':'read,write', + } + +from rowers.rower_rules import is_workout_user + + +# Checks if user has UnderArmour token, renews them if they are expired +def rp3_open(user): + return imports_open(user, oauth_data) + +# Refresh ST token using refresh token +def do_refresh_token(refreshtoken): + return imports_do_refresh_token(refreshtoken, oauth_data) + +# Exchange access code for long-lived access token +def get_token(code): + client_auth = requests.auth.HTTPBasicAuth(rp3_CLIENT_KEY, rp3_CLIENT_SECRET) + post_data = { + "client_id":rp3_CLIENT_KEY, + "grant_type": "authorization_code", + "code": code, + "redirect_uri":rp3_REDIRECT_URI, + "client_secret": rp3_CLIENT_SECRET, + } + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + } + + response = requests.post( + "https://oauth.trainingpeaks.com/oauth/token", + data=post_data,verify=False, + ) + + + 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 + expires_in = 0 + refresh_token = 0 + + return thetoken,expires_in,refresh_token + +# Make authorization URL including random string +def make_authorization_url(request): + return imports_make_authorization_url(oauth_data) + + +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 + row = rowingdata(csvfile=filename) + tcxfilename = filename[:-4]+'.tcx' + try: + newnotes = w.notes+'\n from '+w.workoutsource+' via rowsandall.com' + except TypeError: + newnotes = 'from '+w.workoutsource+' via rowsandall.com' + + row.exporttotcx(tcxfilename,notes=newnotes) + + return tcxfilename + + +def rp3_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,verify=False) + + return resp + +def uploadactivity(access_token,filename,description='', + name='Rowsandall.com workout'): + + data_gz = BytesIO() + with open(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, + "Title":name, + "Type": "rowing", + "Comment": description, + "Data": base64.b64encode(data_gz.getvalue()).decode("ascii") + } + + resp = requests.post(tpapilocation+"/v1/file", + data = json.dumps(data), + headers=headers,verify=False) + + if resp.status_code != 200: + return 0,resp.reason,resp.status_code,headers + else: + return resp.json()[0]["Id"],"ok",200,"" + + return 0,0,0,0 + + +def workout_rp3_upload(user,w): + message = "Uploading to TrainingPeaks" + tpid = 0 + r = w.user + + thetoken = rp3_open(r.user) + + # need some code if token doesn't refresh + + + if (is_workout_user(user,w)): + tcxfile = createtpworkoutdata(w) + if tcxfile: + res,reason,status_code,headers = uploadactivity( + thetoken,tcxfile, + name=w.name + ) + if res == 0: + message = "Upload to TrainingPeaks failed with status code "+str(status_code)+": "+reason + w.tpid = -1 + try: + os.remove(tcxfile) + except WindowsError: + pass + + return message,tpid + + else: # res != 0 + w.uploadedtotp = res + tpid = res + w.save() + os.remove(tcxfile) + return 'Successfully synchronized to TrainingPeaks',tpid + + else: # no tcxfile + message = "Upload to TrainingPeaks failed" + w.uploadedtotp = -1 + tpid = -1 + w.save() + return message,tpid + else: # not allowed to upload + message = "You are not allowed to export this workout to TP" + tpid = 0 + return message,tpid + + return message,tpid diff --git a/rowers/views/importviews.py b/rowers/views/importviews.py index 5056926d..e7fce5cd 100644 --- a/rowers/views/importviews.py +++ b/rowers/views/importviews.py @@ -859,7 +859,34 @@ def rower_process_underarmourcallback(request): url = reverse('rower_exportsettings_view') return HttpResponseRedirect(url) +# Process RP3 callback +@login_required() +def rower_process_rp3callback(request): + try: + code = request.GET['code'] + except MultiValueDictKeyError: + messages.error(request,"There was an error with the callback") + try: + errormessage = request.GET['error'] + messages.error(request,errormessage) + except MultiValueDictKeyError: + pass + url = reverse('rower_exportsettings_view') + return HttpResponseRedirect(url) + 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 = getrower(request.user) + r.tptoken = access_token + r.tptokenexpirydate = expirydatetime + r.tprefreshtoken = refresh_token + + r.save() # Process TrainingPeaks callback @login_required() diff --git a/rowsandall_app/settings.py b/rowsandall_app/settings.py index 1f9f0298..e57f83ec 100644 --- a/rowsandall_app/settings.py +++ b/rowsandall_app/settings.py @@ -321,6 +321,12 @@ TP_CLIENT_SECRET = CFG["tp_client_secret"] TP_REDIRECT_URI = CFG["tp_redirect_uri"] TP_CLIENT_KEY = TP_CLIENT_ID +# RP3 +RP3_CLIENT_ID = CFG["rp3_client_id"] +RP3_CLIENT_SECRET = CFG["rp3_client_secret"] +RP3_REDIRECT_URI = CFG["rp3_redict_uri"] +RP3_CLIENT_KEY = RP3_CLIENT_ID + # Full Site URL SITE_URL = CFG['site_url'] diff --git a/rowsandall_app/urls.py b/rowsandall_app/urls.py index f6d3628e..e7ff383f 100644 --- a/rowsandall_app/urls.py +++ b/rowsandall_app/urls.py @@ -77,6 +77,7 @@ urlpatterns += [ re_path(r'^polarflowcallback',rowersviews.rower_process_polarcallback), re_path(r'^runkeeper\_callback',rowersviews.rower_process_runkeepercallback), re_path(r'^tp\_callback',rowersviews.rower_process_tpcallback), + re_path(r'^rp3\_callback',rowersviews.rower_process_rp3callback), re_path(r'^twitter\_callback',rowersviews.rower_process_twittercallback), re_path(r'^i18n/', include('django.conf.urls.i18n')), re_path(r'^tz_detect/', include('tz_detect.urls')),