Private
Public Access
1
0

Merge branch 'develop' into feature/stravaapi

This commit is contained in:
2024-12-11 21:45:08 +01:00
37 changed files with 5113 additions and 71 deletions
+19 -15
View File
@@ -1,9 +1,10 @@
from rowers.models import Rower, PaidPlan, CoachingGroup from rowers.models import Rower, PaidPlan, CoachingGroup, iDokladToken
from rowers.utils import ProcessorCustomerError from rowers.utils import ProcessorCustomerError
from rowsandall_app.settings import ( from rowsandall_app.settings import (
BRAINTREE_MERCHANT_ID, BRAINTREE_PUBLIC_KEY, BRAINTREE_PRIVATE_KEY, BRAINTREE_MERCHANT_ID, BRAINTREE_PUBLIC_KEY, BRAINTREE_PRIVATE_KEY,
BRAINTREE_SANDBOX_MERCHANT_ID, BRAINTREE_SANDBOX_PUBLIC_KEY, BRAINTREE_SANDBOX_MERCHANT_ID, BRAINTREE_SANDBOX_PUBLIC_KEY,
BRAINTREE_SANDBOX_PRIVATE_KEY, BRAINTREE_MERCHANT_ACCOUNT_ID BRAINTREE_SANDBOX_PRIVATE_KEY, BRAINTREE_MERCHANT_ACCOUNT_ID,
IDOKLAD_CLIENT_ID, IDOKLAD_CLIENT_SECRET, IDOKLAD_REDIRECT_URI,
) )
import pandas as pd import pandas as pd
from rowers.utils import dologging from rowers.utils import dologging
@@ -16,7 +17,7 @@ from rowers.tasks import (
# handle_send_email_transaction_notification, # handle_send_email_transaction_notification,
) )
from rowers.utils import myqueue from rowers.utils import myqueue
import rowers.fakturoid as fakturoid import rowers.idoklad as idoklad
from braintree.exceptions.invalid_signature_error import InvalidSignatureError from braintree.exceptions.invalid_signature_error import InvalidSignatureError
from braintree.exceptions.not_found_error import NotFoundError from braintree.exceptions.not_found_error import NotFoundError
import time import time
@@ -53,6 +54,7 @@ else:
) )
def process_webhook(notification): def process_webhook(notification):
if not settings.TESTING: # pragma: no cover if not settings.TESTING: # pragma: no cover
dologging('braintreewebhooks.log','{kind}'.format(kind=notification.kind)) dologging('braintreewebhooks.log','{kind}'.format(kind=notification.kind))
@@ -112,18 +114,18 @@ def send_invoice(subscription):
else: else:
r = rs[0] r = rs[0]
dologging('braintreewebhooks.log','Rower '+str(r)+'\n') dologging('braintreewebhooks.log','Rower '+str(r)+'\n')
fakturoid_contact_id = fakturoid.get_contacts(r) idoklad_contact_id = idoklad.get_contacts(r)
dologging('braintreewebhooks.log','Fakturoid Contact ID '+str(fakturoid_contact_id)+'\n') dologging('braintreewebhooks.log','Idoklad Contact ID '+str(idoklad_contact_id)+'\n')
if not fakturoid_contact_id: # pragma: no cover if not idoklad_contact_id: # pragma: no cover
fakturoid_contact_id = fakturoid.create_contact(r) idoklad_contact_id = idoklad.create_contact(r)
dologging('braintreewebhooks.log','Created Fakturoid Contact ID ' + dologging('braintreewebhooks.log','Created Idoklad Contact ID ' +
str(fakturoid_contact_id)+'\n') str(idoklad_contact_id)+'\n')
transactions = subscription.transactions transactions = subscription.transactions
if transactions: if transactions:
amount = transactions[0].amount amount = transactions[0].amount
dologging('braintreewebhooks.log','Transaction amount '+str(amount)+'\n') dologging('braintreewebhooks.log','Transaction amount '+str(amount)+'\n')
id = fakturoid.create_invoice(r, amount, subscription_id, dosend=True, id = idoklad.create_invoice(r, amount, subscription_id, dosend=True,
contact_id=fakturoid_contact_id) contact_id=idoklad_contact_id)
return id return id
return 0 # pragma: no cover return 0 # pragma: no cover
@@ -210,10 +212,12 @@ def make_payment(rower, data):
l=rower.user.last_name, l=rower.user.last_name,
) )
fakturoid_contact_id = fakturoid.get_contacts(rower) idoklad_contact_id = idoklad.get_contacts(rower)
if not fakturoid_contact_id: if not idoklad_contact_id:
fakturoid_contact_id = fakturoid.create_contact(rower) idoklad_contact_id = idoklad.create_contact(rower)
_ = fakturoid.create_invoice(rower, amount, transaction.id, dosend=True, contact_id=fakturoid_contact_id,
_ = idoklad.create_invoice(rower, amount, transaction.id, dosend=True,
contact_id=idoklad_contact_id,
name=additional_text) name=additional_text)
_ = myqueue(queuehigh, handle_send_email_transaction, _ = myqueue(queuehigh, handle_send_email_transaction,
+7
View File
@@ -1578,6 +1578,13 @@ def new_workout_from_file(r, f2,
# Get workout type from fit & tcx # Get workout type from fit & tcx
if (fileformat == 'fit'): # pragma: no cover if (fileformat == 'fit'): # pragma: no cover
workouttype = get_workouttype_from_fit(f2, workouttype=workouttype) workouttype = get_workouttype_from_fit(f2, workouttype=workouttype)
new_title = get_title_from_fit(f2)
if new_title:
title = new_title
new_notes = get_notes_from_fit(f2)
if new_notes:
notes = new_notes
# if (fileformat == 'tcx'): # if (fileformat == 'tcx'):
# workouttype_from_tcx = get_workouttype_from_tcx(f2,workouttype=workouttype) # workouttype_from_tcx = get_workouttype_from_tcx(f2,workouttype=workouttype)
# if workouttype != 'rower' and workouttype_from_tcx not in mytypes.otwtypes: # if workouttype != 'rower' and workouttype_from_tcx not in mytypes.otwtypes:
+51 -7
View File
@@ -1289,10 +1289,10 @@ def parsenonpainsled(fileformat, f2, summary, startdatetime='', empowerfirmware=
# handle FIT # handle FIT
if (fileformat == 'fit'): # pragma: no cover if (fileformat == 'fit'): # pragma: no cover
try: try:
s = fitsummarydata(f2) s = FitSummaryData(f2)
s.setsummary() s.setsummary()
summary = s.summarytext summary = s.summarytext
except: except Exception as e:
pass pass
hasrecognized = True hasrecognized = True
@@ -1350,6 +1350,39 @@ def handle_nonpainsled(f2, fileformat, summary='', startdatetime='', empowerfirm
# Create new workout from file and store it in the database # Create new workout from file and store it in the database
# This routine should be used everywhere in views.py # This routine should be used everywhere in views.py
def get_notes_from_fit(filename):
try:
fitfile = FitFile(filename, check_crc=False)
except FitHeaderError: # pragma: no cover
return ''
records = fitfile.messages
notes = ''
for record in records:
if record.name == 'session':
try:
notes = ' '.join(record.get_values()['description'].split())
except KeyError:
pass
return notes
def get_title_from_fit(filename):
try:
fitfile = FitFile(filename, check_crc=False)
except FitHeaderError: # pragma: no cover
return ''
records = fitfile.messages
title = ''
for record in records:
if record.name == 'workout':
try:
title = ' '.join(record.get_values()['wkt_name'].split())
except KeyError:
pass
return title
def get_workouttype_from_fit(filename, workouttype='water'): def get_workouttype_from_fit(filename, workouttype='water'):
try: try:
@@ -1359,16 +1392,27 @@ def get_workouttype_from_fit(filename, workouttype='water'):
records = fitfile.messages records = fitfile.messages
fittype = 'rowing' fittype = 'rowing'
subsporttype = ''
for record in records: for record in records:
if record.name in ['sport', 'lap']: if record.name in ['sport', 'lap','session']:
try: try:
fittype = record.get_values()['sport'].lower() fittype = record.get_values()['sport'].lower()
try:
subsporttype = record.get_values()['sub_sport'].lower()
except KeyError:
subsporttype = ''
except (KeyError, AttributeError): # pragma: no cover except (KeyError, AttributeError): # pragma: no cover
return 'water' pass
if subsporttype:
try:
workouttype = mytypes.fitmappinginv[subsporttype]
except KeyError:
pass
else:
try: try:
workouttype = mytypes.fitmappinginv[fittype] workouttype = mytypes.fitmappinginv[fittype]
except KeyError: # pragma: no cover except KeyError:
return workouttype pass
return workouttype return workouttype
@@ -1604,7 +1648,7 @@ def read_data(columns, ids=[], doclean=True, workstrokesonly=True, debug=False,
datadf = pl.concat(data) datadf = pl.concat(data)
existing_columns = [col for col in columns if col in datadf.columns] existing_columns = [col for col in columns if col in datadf.columns]
datadf = datadf.select(existing_columns) datadf = datadf.select(existing_columns)
except (ShapeError, SchemaError): except (ShapeError, SchemaError, ColumnNotFoundError):
data = [ data = [
df.select(columns) df.select(columns)
for df in data] for df in data]
+3
View File
@@ -554,6 +554,9 @@ class UploadOptionsForm(forms.Form):
upload_to_TrainingPeaks = forms.BooleanField(initial=False, upload_to_TrainingPeaks = forms.BooleanField(initial=False,
required=False, required=False,
label='Export to TrainingPeaks') label='Export to TrainingPeaks')
upload_to_Intervals = forms.BooleanField(initial=False,
required=False,
label='Export to Intervals')
# do_physics = forms.BooleanField(initial=False,required=False,label='Power Estimate (OTW)') # do_physics = forms.BooleanField(initial=False,required=False,label='Power Estimate (OTW)')
makeprivate = forms.BooleanField(initial=False, required=False, makeprivate = forms.BooleanField(initial=False, required=False,
label='Make Workout Private') label='Make Workout Private')
+204
View File
@@ -0,0 +1,204 @@
import requests
import json, yaml
from requests.auth import HTTPBasicAuth
import urllib.parse
from rowers.utils import dologging
import datetime
from django.utils import timezone
from rowsandall_app.settings import (
IDOKLAD_CLIENT_ID, IDOKLAD_CLIENT_SECRET,
)
contacts_url = 'https://api.idoklad.cz/v3/Contacts'
invoice_url = 'https://api.idoklad.cz/v3/IssuedInvoices'
email_url = 'https://api.idoklad.cz/v3/Mails/IssuedInvoice/Send'
from rowers.models import iDokladToken
#idoklad_countries = json.loads(open('rowers/idoklad_countries.json').read())["Data"]["Items"]
with open('rowers/idoklad_countries.yaml') as f:
idoklad_countries = yaml.load(f, Loader=yaml.FullLoader)["Data"]["Items"]
def get_country_id(code):
for c in idoklad_countries:
if c['Code'] == code:
return c['Id']
return 1
def idoklad_token():
try:
token = iDokladToken.objects.get(id=1)
except iDokladToken.DoesNotExist:
return None
if token.updated_at + datetime.timedelta(seconds=token.expires_in) < timezone.now():
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
}
data = {
'grant_type': 'refresh_token',
'client_id': IDOKLAD_CLIENT_ID,
'client_secret': IDOKLAD_CLIENT_SECRET,
'scope': 'eet offline_access',
'refresh_token': token.refresh_token,
}
response = requests.post('https://identity.idoklad.cz/server/connect/token', headers=headers, data=data)
if response.status_code == 200:
token = response.json()
token['updated_at'] = timezone.now()
token = iDokladToken.objects.filter(id=1).update(**token)
return iDokladToken.objects.get(id=1)
else:
return None
return token
def get_contacts(rower):
token = idoklad_token()
if token is None:
return None
headers = {
'Authorization': 'Bearer {t}'.format(t=token.access_token),
}
url = contacts_url+'?filter=(Email~eq~'+urllib.parse.quote(rower.user.email)+')'
dologging('idoklad.log','Searching Contact url: '+str(url))
res = requests.get(url, headers=headers)
dologging('idoklad.log','Searching Contact Status code '+str(res.status_code)+'\n')
if res.status_code != 200: # pragma: no cover
return None
data = res.json()['Data']['Items']
if len(data) >= 1:
r = data[0]
return r['Id']
return None # pragma
def create_contact(rower):
token = idoklad_token()
if token is None:
return None
data = {
'City': rower.city,
'CompanyName': rower.user.first_name+" "+rower.user.last_name,
'CountryId': get_country_id(rower.country.code),
'Email': rower.user.email,
'Firstname': rower.user.first_name,
'PostalCode': rower.postal_code,
'Street': rower.street_address,
'Surname': rower.user.last_name,
'DeliveryAddresses': []
}
if rower.country.numeric is None:
data['CountryId'] = 1
dologging('idoklad.log','Creating idoklad contact for '+str(rower.user.email)+'\n')
headers = {
'Authorization': 'Bearer {t}'.format(t=token.access_token),
"Content-Type": "application/json",
"Accept": "application/json",
}
res = requests.post(contacts_url, json=data, headers=headers)
if res.status_code not in [200, 201]:
return 0
id = res.json()['Data']['Id']
return id
def create_invoice(rower, amount, braintreeid, dosend=True, contact_id=None, name=None):
t = idoklad_token()
if t is None:
return None
if not contact_id:
contact_id = get_contacts(rower)
if not name:
name = 'Rowsandall Subscription '+str(braintreeid)
if not contact_id:
return 0
token = idoklad_token()
if token is None:
return 0
dologging('idoklad.log','Creating idoklad invoice for '+str(rower.user.email)+'\n')
headers = {
'Authorization': 'Bearer {t}'.format(t=token.access_token),
}
res = requests.get(invoice_url+'/Default', headers=headers)
post_data = res.json()['Data']
post_data['DateOfPayment'] = timezone.now().strftime('%Y-%m-%d')
post_data['Description'] = name
post_data['Items'][0]['Name'] = name
post_data['Items'][0]['UnitPrice'] = amount
post_data['PartnerId'] = contact_id
post_data['ItemsTextPrefix'] = 'We invoice you for '+str(name)+' in the amount of '+str(amount)+' EUR.'
post_data['ItemsTextSuffix'] = 'This invoice was already paid. Please do not pay it again.'
post_data['Items'][0]['VatRate'] = 0.0
post_data['Items'][0]['VatRateType'] = 2
post_data['Items'][0]['VatCodeId'] = 3
post_data['CurrencyId'] = 2
post_data['ReportLanguage'] = 3
post_data.pop('ExchangeRate', None)
res = requests.post(invoice_url, json=post_data, headers=headers)
dologging('idoklad.log','Invoice Created - status code '+str(res.status_code)+'\n')
if res.status_code not in [200, 201]:
dologging('idoklad.log','Invoice Created - reason '+str(res.reason)+'\n')
return 0
id = res.json()['Data']['Id']
if dosend:
data = {
'AttachmentIds': [],
'DocumentId': id,
'EmailBody': 'Dear customer, we are sending you the invoice for your subscription. Please do not hesitate to contact us if you have any questions. Best regards, Rowsandall Team',
'EmailSubject': 'Rowsandall Subscription Invoice',
'Method': 1,
'ReportLanguage': 3,
'SendToSelf': True,
'SendToPartner': True,
'SendToAccountant': False,
}
headers = {
'Authorization': 'Bearer {access_token}'.format(access_token=token.access_token),
'Content-Type': 'application/json',
'Accept': 'application/json',
}
res = requests.post(email_url, json=data, headers=headers)
dologging('idoklad.log','Invoice Sent - status code '+str(res.status_code)+'\n')
if res.status_code not in [200, 201]:
dologging('idoklad.log','Invoice Sent - reason '+str(res.text)+'\n')
return id
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2
View File
@@ -5,6 +5,7 @@ from .sporttracks import SportTracksIntegration
from .rp3 import RP3Integration from .rp3 import RP3Integration
from .trainingpeaks import TPIntegration from .trainingpeaks import TPIntegration
from .polar import PolarIntegration from .polar import PolarIntegration
from .intervals import IntervalsIntegration
importsources = { importsources = {
'c2': C2Integration, 'c2': C2Integration,
@@ -15,5 +16,6 @@ importsources = {
'tp':TPIntegration, 'tp':TPIntegration,
'rp3':RP3Integration, 'rp3':RP3Integration,
'polar': PolarIntegration, 'polar': PolarIntegration,
'intervals': IntervalsIntegration,
} }
+1 -1
View File
@@ -63,7 +63,7 @@ class C2Integration(SyncIntegration):
'client_id': C2_CLIENT_ID, 'client_id': C2_CLIENT_ID,
'client_secret': C2_CLIENT_SECRET, 'client_secret': C2_CLIENT_SECRET,
'redirect_uri': C2_REDIRECT_URI, 'redirect_uri': C2_REDIRECT_URI,
'autorization_uri': "https://log.concept2.com/oauth/authorize", 'authorization_uri': "https://log.concept2.com/oauth/authorize",
'content_type': 'application/x-www-form-urlencoded', 'content_type': 'application/x-www-form-urlencoded',
'tokenname': 'c2token', 'tokenname': 'c2token',
'refreshtokenname': 'c2refreshtoken', 'refreshtokenname': 'c2refreshtoken',
+1 -1
View File
@@ -109,7 +109,7 @@ class SyncIntegration(metaclass=ABCMeta):
if 'grant_type' in self.oauth_data: if 'grant_type' in self.oauth_data:
if self.oauth_data['grant_type']: if self.oauth_data['grant_type']:
post_data['grant_type'] = self.oauth_data['grant_type'] post_data['grant_type'] = self.oauth_data['grant_type']
if 'strava' in self.oauth_data['autorization_uri']: if 'strava' in self.oauth_data['authorization_uri']:
post_data['grant_type'] = "authorization_code" post_data['grant_type'] = "authorization_code"
if 'json' in self.oauth_data['content_type']: if 'json' in self.oauth_data['content_type']:
+301
View File
@@ -0,0 +1,301 @@
from .integrations import SyncIntegration, NoTokenError, create_or_update_syncrecord, get_known_ids
from rowers.models import Rower, User, Workout, TombStone
from rowingdata import rowingdata
from rowers import mytypes
from rowers.rower_rules import is_workout_user, ispromember
from rowers.utils import myqueue, dologging, custom_exception_handler
from rowers.tasks import handle_intervals_getworkout
import urllib
import gzip
import requests
import arrow
import datetime
import os
from uuid import uuid4
from django.utils import timezone
from datetime import timedelta
from rowsandall_app.settings import (
INTERVALS_CLIENT_ID, INTERVALS_REDIRECT_URI, INTERVALS_CLIENT_SECRET, SITE_URL
)
import django_rq
queue = django_rq.get_queue('default', default_timeout=3600)
queuelow = django_rq.get_queue('low', default_timeout=3600)
queuehigh = django_rq.get_queue('high', default_timeout=3600)
def seconds_to_duration(seconds):
hours = seconds // 3600
minutes = (seconds % 3600) // 60
remaining_seconds = seconds % 60
# Format as "H:MM:SS" or "MM:SS" if no hours
if hours > 0:
return f"{int(hours)}:{int(minutes):02}:{int(remaining_seconds):02}"
else:
return f"{int(minutes)}:{int(remaining_seconds):02}"
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
intervals_authorize_url = 'https://intervals.icu/oauth/authorize?'
intervals_token_url = 'https://intervals.icu/api/oauth/token'
class IntervalsIntegration(SyncIntegration):
def __init__(self, *args, **kwargs):
super(IntervalsIntegration, self).__init__(*args, **kwargs)
self.oauth_data = {
'client_id': INTERVALS_CLIENT_ID,
'client_secret': INTERVALS_CLIENT_SECRET,
'redirect_uri': INTERVALS_REDIRECT_URI,
'authorization_uri': intervals_authorize_url,
'content_type': 'application/json',
'tokenname': 'intervals_token',
'expirydatename': 'intervals_exp',
'refreshtokenname': 'intervals_r',
'bearer_auth': True,
'base_url': 'https://intervals.icu/api/v1/',
'grant_type': 'refresh_token',
'headers': headers,
'scope': 'ACTIVITY:WRITE, LIBRARY:READ',
}
def get_token(self, code, *args, **kwargs):
post_data = {
'client_id': str(self.oauth_data['client_id']),
'client_secret': self.oauth_data['client_secret'],
'code': code,
}
response = requests.post(
intervals_token_url,
data=post_data,
)
if response.status_code not in [200, 201]:
dologging('intervals.icu.log',response.text)
return [0,"Failed to get token. ",0]
token_json = response.json()
access_token = token_json['access_token']
athlete = token_json['athlete']
return [access_token, athlete, '']
def get_name(self):
return 'Intervals'
def get_shortname(self):
return 'intervals'
def open(self, *args, **kwargs):
# dologging('intervals.icu.log', "Getting token for user {id}".format(id=self.rower.id))
token = super(IntervalsIntegration, self).open(*args, **kwargs)
return token
def createworkoutdata(self, w, *args, **kwargs) -> str:
dozip = kwargs.get('dozip', True)
filename = w.csvfilename
try:
row = rowingdata(csvfile=filename)
except IOError: # pragma: no cover
data = dataprep.read_df_sql(w.id)
try:
datalength = len(data)
except AttributeError:
datalength = 0
if datalength == 0:
data.rename(columns=columndict, inplace=True)
_ = data.to_csv(w.csvfilename+'.gz', index_label='index', compression='gzip')
try:
row = rowingdata(csvfile=filename)
except IOError: # pragma: no cover
return '' # pragma: no cover
else:
return ''
tcxfilename = w.csvfilename[:-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, sport=mytypes.intervalsmapping[w.workouttype])
if dozip:
gzfilename = tcxfilename + '.gz'
try:
with open(tcxfilename, 'rb') as inF:
s = inF.read()
with gzip.GzipFile(gzfilename, 'wb') as outF:
outF.write(s)
try:
os.remove(tcxfilename)
except WindowsError: # pragma: no cover
pass
except FileNotFoundError:
return ''
return gzfilename
return tcxfilename
def workout_export(self, workout, *args, **kwargs) -> str:
token = self.open()
dologging('intervals.icu.log', "Exporting workout {id}".format(id=workout.id))
filename = self.createworkoutdata(workout)
if not filename:
return 0
params = {
'name': workout.name,
'description': workout.notes,
}
authorizationstring = str('Bearer ' + token)
# headers with authorization string and content type multipart/form-data
headers = {
'Authorization': authorizationstring,
}
url = "https://intervals.icu/api/v1/athlete/{athleteid}/activities".format(athleteid=0)
with open(filename, 'rb') as f:
files = {'file': f}
response = requests.post(url, params=params, headers=headers, files=files)
if response.status_code not in [200, 201]:
dologging('intervals.icu.log', response.reason)
return 0
id = response.json()['id']
# set workout type to workouttype
url = "https://intervals.icu/api/v1/activity/{activityid}".format(activityid=id)
thetype = mytypes.intervalsmapping[workout.workouttype]
response = requests.put(url, headers=headers, json={'type': thetype})
if response.status_code not in [200, 201]:
return 0
workout.uploadedtointervals = id
workout.save()
os.remove(filename)
dologging('intervals.icu.log', "Exported workout {id}".format(id=workout.id))
return id
def get_workout_list(self, *args, **kwargs) -> int:
url = self.oauth_data['base_url'] + 'athlete/0/activities?'
startdate = timezone.now() - timedelta(days=30)
enddate = timezone.now() + timedelta(days=1)
startdatestring = kwargs.get("startdate","")
enddatestring = kwargs.get("enddate","")
try:
startdate = arrow.get(startdatestring).datetime
except:
pass
try:
enddate = arrow.get(enddatestring).datetime
except:
pass
url += 'oldest=' + startdate.strftime('%Y-%m-%d') + '&newest=' + enddate.strftime('%Y-%m-%d')
headers = {
'accept': '*/*',
'authorization': 'Bearer ' + self.open(),
}
response = requests.get(url, headers=headers)
if response.status_code != 200:
dologging('intervals.icu.log', response.text)
return []
data = response.json()
known_interval_ids = get_known_ids(self.rower, 'intervalsid')
workouts = []
for item in data:
i = item['id']
r = item['type']
d = item['distance']
ttot = seconds_to_duration(item['moving_time'])
s = item['start_date']
s2 = ''
c = item['name']
if i in known_interval_ids:
nnn = ''
else:
nnn = 'NEW'
keys = ['id','distance','duration','starttime',
'rowtype','source','name','new']
values = [i, d, ttot, s, r, s2, c, nnn]
ress = dict(zip(keys, values))
workouts.append(ress)
return workouts
def get_workout(self, id, *args, **kwargs) -> int:
_ = self.open()
r = self.rower
record = create_or_update_syncrecord(r, None, intervalsid=id)
_ = myqueue(queuehigh,
handle_intervals_getworkout,
self.rower,
self.rower.intervals_token,
id)
return 1
def get_workouts(self, *args, **kwargs):
startdate = timezone.now() - timedelta(days=7)
enddate = timezone.now() + timedelta(days=1)
startdatestring = kwargs.get(startdate,"")
enddatestring = kwargs.get(enddate,"")
try:
startdate = arrow.get(startdatestring).datetime
except:
pass
try:
enddate = arrow.get(enddatestring).datetime
except:
pass
count = 0
workouts = self.get_workout_list(startdate=startdate, enddate=enddate)
for workout in workouts:
if workout['new'] == 'NEW':
self.get_workout(workout['id'])
count +=1
return count
def make_authorization_url(self, *args, **kwargs):
return super(IntervalsIntegration, self).make_authorization_url(*args, **kwargs)
def token_refresh(self, *args, **kwargs):
return super(IntervalsIntegration, self).token_refresh(*args, **kwargs)
+1 -1
View File
@@ -35,7 +35,7 @@ class NKIntegration(SyncIntegration):
'client_id': NK_CLIENT_ID, 'client_id': NK_CLIENT_ID,
'client_secret': NK_CLIENT_SECRET, 'client_secret': NK_CLIENT_SECRET,
'redirect_uri': NK_REDIRECT_URI, 'redirect_uri': NK_REDIRECT_URI,
'autorization_uri': NK_OAUTH_LOCATION+"/oauth/authorize", 'authorization_uri': NK_OAUTH_LOCATION+"/oauth/authorize",
'content_type': 'application/json', 'content_type': 'application/json',
'tokenname': 'nktoken', 'tokenname': 'nktoken',
'refreshtokenname': 'nkrefreshtoken', 'refreshtokenname': 'nkrefreshtoken',
+1 -1
View File
@@ -30,7 +30,7 @@ class RP3Integration(SyncIntegration):
'client_id': RP3_CLIENT_ID, 'client_id': RP3_CLIENT_ID,
'client_secret': RP3_CLIENT_SECRET, 'client_secret': RP3_CLIENT_SECRET,
'redirect_uri': RP3_REDIRECT_URI, 'redirect_uri': RP3_REDIRECT_URI,
'autorization_uri': "https://rp3rowing-app.com/oauth/authorize?", 'authorization_uri': "https://rp3rowing-app.com/oauth/authorize?",
'content_type': 'application/x-www-form-urlencoded', 'content_type': 'application/x-www-form-urlencoded',
# 'content_type': 'application/json', # 'content_type': 'application/json',
'tokenname': 'rp3token', 'tokenname': 'rp3token',
+1 -1
View File
@@ -89,7 +89,7 @@ class StravaIntegration(SyncIntegration):
'client_id': STRAVA_CLIENT_ID, 'client_id': STRAVA_CLIENT_ID,
'client_secret': STRAVA_CLIENT_SECRET, 'client_secret': STRAVA_CLIENT_SECRET,
'redirect_uri': STRAVA_REDIRECT_URI, 'redirect_uri': STRAVA_REDIRECT_URI,
'autorization_uri': "https://www.strava.com/oauth/authorize", 'authorization_uri': "https://www.strava.com/oauth/authorize",
'content_type': 'application/json', 'content_type': 'application/json',
'tokenname': 'stravatoken', 'tokenname': 'stravatoken',
'refreshtokenname': 'stravarefreshtoken', 'refreshtokenname': 'stravarefreshtoken',
+2 -2
View File
@@ -41,7 +41,7 @@ class TPIntegration(SyncIntegration):
'client_id': TP_CLIENT_ID, 'client_id': TP_CLIENT_ID,
'client_secret': TP_CLIENT_SECRET, 'client_secret': TP_CLIENT_SECRET,
'redirect_uri': TP_REDIRECT_URI, 'redirect_uri': TP_REDIRECT_URI,
'autorization_uri': "https://oauth.trainingpeaks.com/oauth/authorize?", 'authorization_uri': "https://oauth.trainingpeaks.com/oauth/authorize?",
'content_type': 'application/x-www-form-urlencoded', 'content_type': 'application/x-www-form-urlencoded',
'tokenname': 'tptoken', 'tokenname': 'tptoken',
'refreshtokenname': 'tprefreshtoken', 'refreshtokenname': 'tprefreshtoken',
@@ -66,7 +66,7 @@ class TPIntegration(SyncIntegration):
except TypeError: except TypeError:
newnotes = 'from '+w.workoutsource+' via rowsandall.com' newnotes = 'from '+w.workoutsource+' via rowsandall.com'
row.exporttotcx(tcxfilename, notes=newnotes) row.exporttotcx(tcxfilename, notes=newnotes, sport=tpmapping(w.workouttype))
return tcxfilename return tcxfilename
+4 -2
View File
@@ -24,7 +24,8 @@ class Command(BaseCommand):
record.sporttracksid = w.uploadedtosporttracks record.sporttracksid = w.uploadedtosporttracks
if w.uploadedtoc2: if w.uploadedtoc2:
record.c2id = w.uploadedtoc2 record.c2id = w.uploadedtoc2
if w.uploadedtointervals:
record.intervalsid = w.uploadedtointervals
try: try:
record.save() record.save()
except IntegrityError: except IntegrityError:
@@ -52,7 +53,8 @@ class Command(BaseCommand):
record.sporttracksid = w.uploadedtosporttracks record.sporttracksid = w.uploadedtosporttracks
if w.uploadedtoc2: if w.uploadedtoc2:
record.c2id = w.uploadedtoc2 record.c2id = w.uploadedtoc2
if w.uploadedtointervals:
record.intervalsid = w.uploadedtointervals
try: try:
record.save() record.save()
except IntegrityError: except IntegrityError:
@@ -117,5 +117,16 @@ class Command(BaseCommand):
lines = traceback.format_exception(exc_type, exc_value, exc_traceback) lines = traceback.format_exception(exc_type, exc_value, exc_traceback)
dologging('processemail.log', ''.join('!! ' + line for line in lines)) dologging('processemail.log', ''.join('!! ' + line for line in lines))
rowers = Rower.objects.filter(intervals_auto_import=True)
for r in rowers:
try:
if user_is_not_basic(r.user) or user_is_coachee(r.user):
intervals_integration = IntervalsIntegration(r.user)
_ = intervals_integration.get_workouts()
except:
exc_type, exc_value, exc_traceback = sys.exc_info()
lines = traceback.format_exception(exc_type, exc_value, exc_traceback)
dologging('processemail.log', ''.join('!! ' + line for line in lines))
self.stdout.write(self.style.SUCCESS( self.stdout.write(self.style.SUCCESS(
'Successfully processed email attachments')) 'Successfully processed email attachments'))
+28 -3
View File
@@ -1172,6 +1172,8 @@ class Rower(models.Model):
default='', max_length=200, blank=True, null=True) default='', max_length=200, blank=True, null=True)
c2_auto_export = models.BooleanField(default=False) c2_auto_export = models.BooleanField(default=False)
c2_auto_import = models.BooleanField(default=False) c2_auto_import = models.BooleanField(default=False)
intervals_auto_export = models.BooleanField(default=False)
intervals_auto_import = models.BooleanField(default=False)
sporttrackstoken = models.CharField( sporttrackstoken = models.CharField(
default='', max_length=200, blank=True, null=True) default='', max_length=200, blank=True, null=True)
sporttrackstokenexpirydate = models.DateTimeField(blank=True, null=True) sporttrackstokenexpirydate = models.DateTimeField(blank=True, null=True)
@@ -1240,6 +1242,10 @@ class Rower(models.Model):
strava_auto_import = models.BooleanField(default=False) strava_auto_import = models.BooleanField(default=False)
strava_auto_delete = models.BooleanField(default=False) strava_auto_delete = models.BooleanField(default=False)
intervals_token = models.CharField(
default='', max_length=200, blank=True, null=True)
intervals_owner_id = models.CharField(default='', max_length=200,blank=True, null=True)
privacychoices = ( privacychoices = (
('visible', 'Visible'), ('visible', 'Visible'),
('hidden', 'Hidden'), ('hidden', 'Hidden'),
@@ -3708,6 +3714,7 @@ class Workout(models.Model):
uploadedtogarmin = models.BigIntegerField(default=0) uploadedtogarmin = models.BigIntegerField(default=0)
uploadedtorp3 = models.BigIntegerField(default=0) uploadedtorp3 = models.BigIntegerField(default=0)
uploadedtonk = models.BigIntegerField(default=0) uploadedtonk = models.BigIntegerField(default=0)
uploadedtointervals = models.CharField(default=None,null=True, max_length=100)
forceunit = models.CharField(default='lbs', forceunit = models.CharField(default='lbs',
choices=( choices=(
('lbs', 'lbs'), ('lbs', 'lbs'),
@@ -3842,6 +3849,7 @@ class TombStone(models.Model):
uploadedtosporttracks = models.BigIntegerField(default=0) uploadedtosporttracks = models.BigIntegerField(default=0)
uploadedtotp = models.BigIntegerField(default=0) uploadedtotp = models.BigIntegerField(default=0)
uploadedtonk = models.BigIntegerField(default=0) uploadedtonk = models.BigIntegerField(default=0)
uploadedtointervals = models.CharField(default=None,null=True, max_length=100)
@receiver(models.signals.pre_delete, sender=Workout) @receiver(models.signals.pre_delete, sender=Workout)
def create_tombstone_on_delete(sender, instance, **kwargs): def create_tombstone_on_delete(sender, instance, **kwargs):
@@ -3850,7 +3858,8 @@ def create_tombstone_on_delete(sender, instance, **kwargs):
uploadedtoc2=instance.uploadedtoc2, uploadedtoc2=instance.uploadedtoc2,
uploadedtostrava=instance.uploadedtostrava, uploadedtostrava=instance.uploadedtostrava,
uploadedtotp=instance.uploadedtotp, uploadedtotp=instance.uploadedtotp,
uploadedtonk=instance.uploadedtonk uploadedtonk=instance.uploadedtonk,
uploadedtointervals=instance.uploadedtointervals,
) )
t.save() t.save()
@@ -3866,6 +3875,7 @@ class SyncRecord(models.Model):
c2id = models.BigIntegerField(unique=True,null=True,default=None) c2id = models.BigIntegerField(unique=True,null=True,default=None)
tpid = models.BigIntegerField(unique=True,null=True,default=None) tpid = models.BigIntegerField(unique=True,null=True,default=None)
rp3id = models.BigIntegerField(unique=True,null=True,default=None) rp3id = models.BigIntegerField(unique=True,null=True,default=None)
intervalsid = models.CharField(unique=True, null=True, default=None, max_length=100)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.workout: if self.workout:
@@ -3881,7 +3891,7 @@ class SyncRecord(models.Model):
str2 = '' str2 = ''
for field in ['stravaid', 'sporttracksid', 'nkid', 'c2id', 'tpid']: for field in ['stravaid', 'sporttracksid', 'nkid', 'c2id', 'tpid', 'intervalsid']:
value = getattr(self, field, None) value = getattr(self, field, None)
if value is not None: if value is not None:
str2 += '{w}: {v},'.format( str2 += '{w}: {v},'.format(
@@ -4567,7 +4577,9 @@ class RowerExportForm(ModelForm):
'strava_auto_import', 'strava_auto_import',
'strava_auto_delete', 'strava_auto_delete',
'trainingpeaks_auto_export', 'trainingpeaks_auto_export',
'rp3_auto_import' 'rp3_auto_import',
'intervals_auto_import',
'intervals_auto_export',
] ]
# Simple form to set rower's Functional Threshold Power # Simple form to set rower's Functional Threshold Power
@@ -5396,3 +5408,16 @@ class ForceCurveAnalysis(models.Model):
date = self.date) date = self.date)
return s return s
class iDokladToken(models.Model):
access_token = models.CharField(max_length=512)
refresh_token = models.CharField(max_length=512)
id_token = models.CharField(max_length=512)
token_type = models.CharField(max_length=512)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
expires_in = models.IntegerField() # Store token expiry duration in seconds
def __str__(self):
return f"iDoklad Token updated at {self.updated_at}, expires at {self.updated_at+datetime.timedelta(seconds=self.expires_in)}"
+72
View File
@@ -148,6 +148,7 @@ garminmapping = {key: value for key, value in Reverse(garmincollection)}
fitcollection = ( fitcollection = (
('water', 'rowing'), ('water', 'rowing'),
('rower', 'rowing'), ('rower', 'rowing'),
('rower', 'indoor_rowing'),
('skierg', 'cross_country_skiing'), ('skierg', 'cross_country_skiing'),
('bike', 'cycling'), ('bike', 'cycling'),
('bikeerg', 'cycling'), ('bikeerg', 'cycling'),
@@ -180,6 +181,74 @@ fitcollection = (
fitmapping = {key: value for key, value in Reverse(fitcollection)} fitmapping = {key: value for key, value in Reverse(fitcollection)}
tcxcollection = (
('water', 'Rowing'),
('rower', 'Rowing'),
('skierg', 'CrossCountrySkiing'),
('bike', 'Biking'),
('bikeerg', 'Biking'),
('dynamic', 'Rowing'),
('slides', 'Rowing'),
('paddle', 'Other'),
('snow', 'CrossCountrySkiing'),
('coastal', 'Rowing'),
('c-boat', 'Rowing'),
('churchboat', 'Rowing'),
('Ride', 'Biking'),
('Run', 'Running'),
('NordicSki', 'CrossCountrySkiing'),
('Swim', 'Swimming'),
('Hike', 'Hiking'),
('Walk', 'Walking'),
('Canoeing', 'Other'),
('Crossfit', 'Other'),
('StandUpPaddling', 'Other'),
('IceSkate', 'Other'),
('WeightTraining', 'Other'),
('InlineSkate', 'Other'),
('Kayaking', 'Other'),
('Workout', 'Other'),
('Yoga', 'Other'),
('other', 'Other'),
)
tcxmapping = {key: value for key, value in Reverse(tcxcollection)}
tcxmappinginv = {value: key for key, value in Reverse(tcxcollection) if value is not None}
intervalscollection = (
('water', 'Rowing'),
('rower', 'VirtualRow'),
('skierg', 'NordicSki'),
('bike', 'Ride'),
('bikeerg', 'VirtualRide'),
('dynamic', 'Rowing'),
('slides', 'Rowing'),
('paddle', 'StandUpPaddling'),
('snow', 'NordicSki'),
('coastal', 'Rowing'),
('c-boat', 'Rowing'),
('churchboat', 'Rowing'),
('Ride', 'Ride'),
('Run', 'Run'),
('NordicSki', 'NordicSki'),
('Swim', 'Swim'),
('Hike', 'Hike'),
('Walk', 'Walk'),
('Canoeing', 'Canoeing'),
('Crossfit', 'Crossfit'),
('StandUpPaddling', 'StandUpPaddling'),
('IceSkate', 'IceSkate'),
('WeightTraining', 'WeightTraining'),
('InlineSkate', 'InlineSkate'),
('Kayaking', 'Kayaking'),
('Workout', 'Workout'),
('Yoga', 'Yoga'),
('other', 'Other'),
)
intervalsmapping = {key: value for key, value in Reverse(intervalscollection)}
stcollection = ( stcollection = (
('water', 'Rowing'), ('water', 'Rowing'),
('rower', 'Rowing'), ('rower', 'Rowing'),
@@ -332,6 +401,9 @@ garminmappinginv = {value: key for key, value in Reverse(
fitmappinginv = {value: key for key, value in Reverse( fitmappinginv = {value: key for key, value in Reverse(
fitcollection) if value is not None} fitcollection) if value is not None}
intervalsmappinginv = {value: key for key, value in Reverse(
intervalscollection) if value is not None}
otwtypes = ( otwtypes = (
'water', 'water',
'coastal', 'coastal',
+69
View File
@@ -24,6 +24,7 @@ from rowers.courseutils import (
InvalidTrajectoryError InvalidTrajectoryError
) )
from rowers.emails import send_template_email from rowers.emails import send_template_email
from rowers.mytypes import intervalsmappinginv
from rowers.nkimportutils import ( from rowers.nkimportutils import (
get_nk_summary, get_nk_allstats, get_nk_intervalstats, getdict, strokeDataToDf, get_nk_summary, get_nk_allstats, get_nk_intervalstats, getdict, strokeDataToDf,
add_workout_from_data add_workout_from_data
@@ -59,6 +60,8 @@ import rowingdata
from rowingdata import make_cumvalues, make_cumvalues_array from rowingdata import make_cumvalues, make_cumvalues_array
from uuid import uuid4 from uuid import uuid4
from rowingdata import rowingdata as rdata from rowingdata import rowingdata as rdata
from rowingdata import FITParser as FP
from rowingdata.otherparsers import FitSummaryData
from datetime import timedelta from datetime import timedelta
@@ -3485,6 +3488,72 @@ def handle_nk_async_workout(alldata, userid, nktoken, nkid, delaysec, defaulttim
return workoutid return workoutid
@app.task
def handle_intervals_getworkout(rower, intervalstoken, workoutid, debug=False, **kwargs):
authorizationstring = str('Bearer '+intervalstoken)
headers = {
'authorization': authorizationstring,
}
url = "https://intervals.icu/api/v1/activity/{}".format(workoutid)
response = requests.get(url, headers=headers)
if response.status_code != 200:
return 0
data = response.json()
try:
title = data['name']
except KeyError:
title = 'Intervals workout'
try:
workouttype = intervalsmappinginv[data['type']]
except KeyError:
workouttype = 'water'
url = "https://intervals.icu/api/v1/activity/{workoutid}/fit-file".format(workoutid=workoutid)
response = requests.get(url, headers=headers)
if response.status_code != 200:
return 0
try:
fit_data = response.content
fit_filename = 'media/'+f'{uuid4().hex[:16]}.fit'
with open(fit_filename, 'wb') as fit_file:
fit_file.write(fit_data)
except Exception as e:
return 0
try:
row = FP(fit_filename)
rowdata = rowingdata.rowingdata(df=row.df)
rowsummary = FitSummaryData(fit_filename)
duration = totaltime_sec_to_string(rowdata.duration)
distance = rowdata.df[" Horizontal (meters)"].iloc[-1]
except Exception as e:
return 0
uploadoptions = {
'secret': UPLOAD_SERVICE_SECRET,
'user': rower.user.id,
'boattype': '1x',
'workouttype': workouttype,
'file': fit_filename,
'intervalsid': workoutid,
'title': title,
'rpe': 0,
'notes': '',
'offline': False,
}
url = UPLOAD_SERVICE_URL
handle_request_post(url, uploadoptions)
return 1
@app.task @app.task
def handle_c2_getworkout(userid, c2token, c2id, defaulttimezone, debug=False, **kwargs): def handle_c2_getworkout(userid, c2token, c2id, defaulttimezone, debug=False, **kwargs):
+14
View File
@@ -231,6 +231,20 @@
</a> </a>
{% endif %} {% endif %}
</li> </li>
<li id="export-intervals">
{% if workout.uploadedtointervals and workout.uploadedtointervals != '0' %}
<a href="https://intervals.icu/activities/{{ workout.uploadedtointervals }}">
Intervals.icu <i class="fas fa-check"></i>
</a>
{% elif user.rower.intervals_token == None or user.rower.intervals_token == '' %}
<a href="/rowers/me/intervalsauthorize">
Connect to Intervals.icu
</a>
{% else %}
<a href="/rowers/workout/{{ workout.id|encode }}/intervalsuploadw/">
Intervals.icu
</a>
{% endif %}
<li id="export-csv"> <li id="export-csv">
<a href="/rowers/workout/{{ workout.id|encode }}/emailcsv/"> <a href="/rowers/workout/{{ workout.id|encode }}/emailcsv/">
CSV CSV
+1
View File
@@ -57,6 +57,7 @@
<li id="sporttracks"><a href="/rowers/workout/sporttracksimport/">SportTracks</a></li> <li id="sporttracks"><a href="/rowers/workout/sporttracksimport/">SportTracks</a></li>
<li id="polar"><a href="/rowers/workout/polarimport/">Polar</a></li> <li id="polar"><a href="/rowers/workout/polarimport/">Polar</a></li>
<li id="rp3"><a href="/rowers/workout/rp3import/">RP3</a></li> <li id="rp3"><a href="/rowers/workout/rp3import/">RP3</a></li>
<li id="intervals"><a href="/rowers/workout/intervalsimport/">Intervals.icu</a></li>
</ul> </ul>
</li> </li>
</ul> <!-- cd-accordion-menu --> </ul> <!-- cd-accordion-menu -->
+7 -2
View File
@@ -32,10 +32,13 @@
Strava, Strava,
{% endif %} {% endif %}
{% if rower.rp3token is not None and rower.rp3token != '' %} {% if rower.rp3token is not None and rower.rp3token != '' %}
RP3 RP3,
{% endif %} {% endif %}
{% if rower.rojabo_token is not None and rower.rojabo_token != '' %} {% if rower.rojabo_token is not None and rower.rojabo_token != '' %}
Rojabo Rojabo,
{% endif %}
{% if rower.intervals_token is not None and rower.intervals_token != '' %}
Intervals.icu
{% endif %} {% endif %}
</p> </p>
@@ -132,6 +135,8 @@
alt="connect with RP3" width="130"></a></p> alt="connect with RP3" width="130"></a></p>
<p><a href="/rowers/me/rojaboauthorize"><img src="/static/img/rojabo.png" <p><a href="/rowers/me/rojaboauthorize"><img src="/static/img/rojabo.png"
alt="connect with Rojabo" width="130"></a></p> alt="connect with Rojabo" width="130"></a></p>
<p><a href="/rowers/me/intervalsauthorize"><img src="/static/img/intervals_icu.png"
alt="connect with intervals.icu" height="30"></a></p>
{% endblock %} {% endblock %}
+49
View File
@@ -57,6 +57,14 @@ from rowers.dataprep import delete_strokedata
from redis import StrictRedis from redis import StrictRedis
redis_connection = StrictRedis() redis_connection = StrictRedis()
def mocked_idoklad_token(*args, **kwargs): # pragma: no cover
class MockToken:
def __init__(self, *args,**kwargs):
self.access_token = "aap"
return MockToken()
def mocked_grpc(*args, **kwargs): # pragma: no cover def mocked_grpc(*args, **kwargs): # pragma: no cover
class insecure_channel: class insecure_channel:
@@ -773,6 +781,9 @@ def mocked_requests(*args, **kwargs):
with open('rowers/tests/testdata/rp3_list.json','r') as infile: with open('rowers/tests/testdata/rp3_list.json','r') as infile:
rp3workoutlist = json.load(infile) rp3workoutlist = json.load(infile)
with open('rowers/tests/testdata/idoklad_default.json','r') as infile:
idokladdefault = json.load(infile)
rp3linkready = {'data': {'download': {'id': 591621, 'status': 'ready', 'link': 'https://rp3rowing-app.com/api/workouts/591621/download?type=csv'}}} rp3linkready = {'data': {'download': {'id': 591621, 'status': 'ready', 'link': 'https://rp3rowing-app.com/api/workouts/591621/download?type=csv'}}}
with open('rowers/tests/testdata/example-session-strokes-with-impeller-data.json','r') as infile: with open('rowers/tests/testdata/example-session-strokes-with-impeller-data.json','r') as infile:
@@ -1117,6 +1128,7 @@ def mocked_requests(*args, **kwargs):
rp3tester = re.compile(r'.*?rp3rowing-app\.com') rp3tester = re.compile(r'.*?rp3rowing-app\.com')
garmintester = re.compile(r'.*?garmin\.com') garmintester = re.compile(r'.*?garmin\.com')
fakturoidtester = re.compile(r'.*?fakturoid\.cz') fakturoidtester = re.compile(r'.*?fakturoid\.cz')
idokladtester = re.compile(r'.*?idoklad\.cz')
polarlistregex = r'.*?polaraccesslink\.com\/.*\/(\d+)$' polarlistregex = r'.*?polaraccesslink\.com\/.*\/(\d+)$'
polarlisttester = re.compile(polarlistregex) polarlisttester = re.compile(polarlistregex)
@@ -1487,6 +1499,43 @@ def mocked_requests(*args, **kwargs):
else: # pragma: no cover else: # pragma: no cover
return MockResponse(c2workoutdata,200) return MockResponse(c2workoutdata,200)
if idokladtester.match(args[0]):
if 'Invoices' in args[0]:
if 'Default' in args[0]:
response_data = idokladdefault
return MockResponse(response_data,200)
response = {
'Data': {
'Id': 1,
}
}
return MockResponse(response,200)
if 'Contacts' in args[0]:
response = {
'Data': {
'Items': [
{
'Id': 1,
'url':'aap',
}
]
}
}
return MockResponse(response,200)
response = [
{
'Id':1,
'url':'aap',
}
]
return MockResponse(response, 200)
if fakturoidtester.match(args[0]): if fakturoidtester.match(args[0]):
if 'invoices' in args[0]: if 'invoices' in args[0]:
response = { response = {
+4 -3
View File
@@ -74,11 +74,12 @@ class BraintreeUnits(TestCase):
self.p2 = PaidPlan.objects.create(price=25,paymentprocessor='braintree') self.p2 = PaidPlan.objects.create(price=25,paymentprocessor='braintree')
@patch('rowers.fakturoid.requests.get',side_effect=mocked_requests) @patch('rowers.idoklad.idoklad_token', side_effect=mocked_idoklad_token)
@patch('rowers.fakturoid.requests.post',side_effect=mocked_requests) @patch('rowers.idoklad.requests.get',side_effect=mocked_requests)
@patch('rowers.idoklad.requests.post',side_effect=mocked_requests)
@patch('rowers.braintreestuff.gateway', side_effect=MockBraintreeGateway) @patch('rowers.braintreestuff.gateway', side_effect=MockBraintreeGateway)
@patch('rowers.braintreestuff.myqueue') @patch('rowers.braintreestuff.myqueue')
def test_process_webhook(self,mock_get,mockpost,mocked_gateway,mocked_myqueue): def test_process_webhook(self,mock_token, mock_get,mockpost,mocked_gateway,mocked_myqueue):
n = notification() n = notification()
res = process_webhook(n) res = process_webhook(n)
self.assertEqual(res,1) self.assertEqual(res,1)
+1 -1
View File
@@ -405,7 +405,7 @@ description: ""
@patch('rowers.views.braintreestuff.gateway', side_effect=MockBraintreeGateway) @patch('rowers.views.braintreestuff.gateway', side_effect=MockBraintreeGateway)
@patch('rowers.fakturoid.create_invoice',side_effect=mocked_invoiceid) @patch('rowers.idoklad.create_invoice',side_effect=mocked_invoiceid)
@patch('rowers.utils.myqueue') @patch('rowers.utils.myqueue')
def test_purchase_trainingplan_view(self, mocked_gateway,mocked_invoiceid, mocked_myqueue): def test_purchase_trainingplan_view(self, mocked_gateway,mocked_invoiceid, mocked_myqueue):
u = UserFactory() u = UserFactory()
+53 -10
View File
@@ -130,7 +130,10 @@ def make_plot(r, w, f1, f2, plottype, title, imagename='', plotnr=0):
def do_sync(w, options, quick=False): def do_sync(w, options, quick=False):
do_strava_export = w.user.strava_auto_export do_strava_export = False
if w.user.strava_auto_export is True:
do_strava_export = True
else:
try: try:
do_strava_export = options['upload_to_Strava'] or do_strava_export do_strava_export = options['upload_to_Strava'] or do_strava_export
except KeyError: except KeyError:
@@ -150,6 +153,27 @@ def do_sync(w, options, quick=False):
except KeyError: except KeyError:
pass pass
do_icu_export = False
if w.user.intervals_auto_export is True:
do_icu_export = True
else:
try:
do_icu_export = options['upload_to_Intervals']
except KeyError:
pass
#dologging("uploads.log", "do_icu_export: {do_icu_export}".format(do_icu_export=do_icu_export))
try:
if options['intervalsid'] != 0 and options['intervalsid'] != '': # pragma: no cover
w.uploadedtointervals = options['intervalsid']
# upload_to_icu = False
do_icu_export = False
w.save()
record = create_or_update_syncrecord(w.user, w, intervalsid=options['intervalsid'])
except KeyError:
pass
try: try:
if options['nkid'] != 0 and options['nkid'] != '': # pragma: no cover if options['nkid'] != 0 and options['nkid'] != '': # pragma: no cover
w.uploadedtonk = options['nkid'] w.uploadedtonk = options['nkid']
@@ -181,7 +205,10 @@ def do_sync(w, options, quick=False):
except KeyError: except KeyError:
pass pass
do_c2_export = w.user.c2_auto_export do_c2_export = False
if w.user.c2_auto_export is True:
do_c2_export = True
else:
try: try:
do_c2_export = options['upload_to_C2'] or do_c2_export do_c2_export = options['upload_to_C2'] or do_c2_export
except KeyError: except KeyError:
@@ -236,14 +263,30 @@ def do_sync(w, options, quick=False):
except NoTokenError: # pragma: no cover except NoTokenError: # pragma: no cover
id = 0 id = 0
message = "Please connect to Strava first" message = "Please connect to Strava first"
except: except Exception as e:
e = sys.exc_info()[0] dologging('stravalog.log', e)
t = time.localtime()
timestamp = time.strftime('%b-%d-%Y_%H%M', t) if do_icu_export:
with open('stravalog.log', 'a') as f: intervals_integration = IntervalsIntegration(w.user.user)
f.write('\n') try:
f.write(timestamp) id = intervals_integration.workout_export(w)
f.write(str(e)) dologging(
'intervals.icu.log',
'exporting workout {id} as {type}'.format(
id=w.id,
type=w.workouttype,
)
)
except NoTokenError:
id = 0
dologging('intervals.icu.log','NoTokenError')
message = "Please connect to Intervals.icu first"
except Exception as e:
dologging(
'intervals.icu.log',
e
)
do_st_export = w.user.sporttracks_auto_export do_st_export = w.user.sporttracks_auto_export
+1
View File
@@ -752,6 +752,7 @@ urlpatterns = [
views.rower_prefs_view, name='rower_prefs_view'), views.rower_prefs_view, name='rower_prefs_view'),
re_path(r'^me/prefs/user/(?P<userid>\d+)/$', re_path(r'^me/prefs/user/(?P<userid>\d+)/$',
views.rower_simpleprefs_view, name='rower_simpleprefs_view'), views.rower_simpleprefs_view, name='rower_simpleprefs_view'),
re_path(r'^me/idokladauthorize/$', views.rower_idoklad_authorize, name='rower_idoklad_authorize'),
re_path(r'^me/rojaboauthorize/$', views.rower_rojabo_authorize, re_path(r'^me/rojaboauthorize/$', views.rower_rojabo_authorize,
name='rower_rojabo_authorize'), name='rower_rojabo_authorize'),
re_path(r'^me/polarauthorize/$', views.rower_polar_authorize, re_path(r'^me/polarauthorize/$', views.rower_polar_authorize,
+25 -4
View File
@@ -575,19 +575,40 @@ def strokedata_fit(request):
return JsonResponse({ return JsonResponse({
"status": "error", "status": "error",
"message": f"An error occurred while saving the FIT file: {str(e)}" "message": f"An error occurred while saving the FIT file: {str(e)}"
}, status=500) }, status=400)
try: try:
# Parse the FIT file # Parse the FIT file
try:
row = FP(fit_filename) row = FP(fit_filename)
except ValueError as e:
return JsonResponse({
"status": "error",
"message": f"An error occurred while parsing the FIT file: {str(e)}"
}, status=422)
rowdata = rowingdata(df=row.df) rowdata = rowingdata(df=row.df)
duration = totaltime_sec_to_string(rowdata.duration) duration = totaltime_sec_to_string(rowdata.duration)
title = "ActiveSpeed water" distance = rowdata.df[" Horizontal (meters)"].iloc[-1]
title = ""
try:
startdatetime = rowdata.rowdatetime
startdate = startdatetime.date()
partofday = part_of_day(startdatetime.hour)
title = '{partofday} water'.format(partofday=partofday)
except Exception as e:
dologging('apilog.log','FIT error to get time')
dologging('apilog.log',e)
_ = myqueue(queuehigh, handle_sendemail_unrecognized, fit_filename, "fit parser")
return HttpResponse(status=422)
w = Workout.objects.create(user=request.user.rower, w = Workout.objects.create(user=request.user.rower,
duration=duration, duration=duration,
name=title,) distance=distance,
name=title,
date=startdate,
workouttype='water',)
uploadoptions = { uploadoptions = {
'secret': UPLOAD_SERVICE_SECRET, 'secret': UPLOAD_SERVICE_SECRET,
@@ -598,7 +619,7 @@ def strokedata_fit(request):
'title': title, 'title': title,
'rpe': 0, 'rpe': 0,
'notes': '', 'notes': '',
'workoutid': w.id, 'id': w.id,
'offline': False, 'offline': False,
} }
+35
View File
@@ -24,6 +24,7 @@ importauthorizeviews = {
'nk': 'rower_integration_authorize', 'nk': 'rower_integration_authorize',
'rp3': 'rower_integration_authorize', 'rp3': 'rower_integration_authorize',
'garmin': 'rower_garmin_authorize', 'garmin': 'rower_garmin_authorize',
'intervals': 'rower_integration_authorize',
} }
@@ -173,6 +174,37 @@ def rower_process_twittercallback(request): # pragma: no cover
# Process Polar Callback # Process Polar Callback
@login_required()
def rower_process_intervalscallback(request):
integration = importsources['intervals'](request.user)
r = getrower(request.user)
try:
code = request.GET['code']
res = integration.get_token(code)
except MultiValueDictKeyError:
message = "The resource owner or authorization server denied the request"
messages.error(request, message)
url = reverse('rower_exportsettings_view')
return HttpResponseRedirect(url)
access_token = res[0]
athlete = res[1]
if access_token == 0:
message = res[1]
message += 'Connection to intervals.icu failed.'
messages.error(request, message)
url = reverse('rower_exportsettings_view')
return HttpResponseRedirect(url)
r.intervals_token = access_token
r.intervals_owner_id = athlete['id']
r.save()
successmessage = "Tokens stored. Good to go. Please check your import/export settings"
messages.info(request, successmessage)
url = reverse('rower_exportsettings_view')
return HttpResponseRedirect(url)
@login_required() @login_required()
def rower_process_polarcallback(request): def rower_process_polarcallback(request):
@@ -439,7 +471,10 @@ def workout_import_view(request, source='c2'):
try: try:
tdict = dict(request.POST.lists()) tdict = dict(request.POST.lists())
ids = tdict['workoutid'] ids = tdict['workoutid']
try:
nkids = [int(id) for id in ids] nkids = [int(id) for id in ids]
except ValueError:
nkids = ids
for nkid in nkids: for nkid in nkids:
try: try:
_ = integration.get_workout(nkid, startdate=startdate, enddate=enddate) _ = integration.get_workout(nkid, startdate=startdate, enddate=enddate)
+66
View File
@@ -8,6 +8,72 @@ from django.core.mail import EmailMessage
from rowers import credits from rowers import credits
@login_required()
def rower_idoklad_authorize(request):
state=str(uuid4())
params = {
"client_id":IDOKLAD_CLIENT_ID,
"response_type": "code",
"redirect_uri": IDOKLAD_REDIRECT_URI,
"scope": "idoklad_api offline_access",
}
url = "https://identity.idoklad.cz/server/connect/authorize?"+urllib.parse.urlencode(params)
return HttpResponseRedirect(url)
@login_required()
def process_idokladcallback(request):
dologging('idoklad.log',' /rowers/idokladcallback/')
try:
code = request.GET['code']
except KeyError:
error = request.GET['error']
messages.error(request,error)
return HttpResponseRedirect(reverse('workouts_view'))
post_data = {
'grant_type': "authorization_code",
'client_id': IDOKLAD_CLIENT_ID,
'client_secret': IDOKLAD_CLIENT_SECRET,
'scope': 'idoklad_api offline_access',
'code': code,
'redirect_uri': IDOKLAD_REDIRECT_URI,
}
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
}
base_url = 'https://identity.idoklad.cz/server/connect/token'
response = requests.post(base_url, data=post_data, headers=headers)
if response.status_code == 200:
result = response.json()
try:
t = iDokladToken.objects.get(id=1)
t.acces_token = result['access_token'],
t.refresh_token = result['refresh_token']
t.expires_in = result['expires_in']
t.id_token = result['id_token']
t.save()
except iDokladToken.DoesNotExist:
t = iDokladToken(
access_token = result['access_token'],
refresh_token = result['refresh_token'],
expires_in = result['expires_in'],
)
t.save()
messages.info(request,"Token refreshed and stored")
else:
messages.error(request,"Error")
url = reverse('rower_exportsettings_view')
return HttpResponseRedirect(url)
@csrf_exempt @csrf_exempt
def braintree_webhook_view(request): def braintree_webhook_view(request):
+2 -1
View File
@@ -181,7 +181,7 @@ from rowers.models import ( RowerPowerForm, RowerHRZonesForm, SimpleRowerPowerFo
IndoorVirtualRaceForm, PlannedSessionCommentForm, Alert, IndoorVirtualRaceForm, PlannedSessionCommentForm, Alert,
Condition, StaticChartRowerForm, FollowerForm, Condition, StaticChartRowerForm, FollowerForm,
VirtualRaceAthleteForm, InstantPlanForm, DataRowerForm, VirtualRaceAthleteForm, InstantPlanForm, DataRowerForm,
StepEditorForm, ) StepEditorForm, iDokladToken )
from rowers.models import ( from rowers.models import (
FavoriteForm, BaseFavoriteFormSet, SiteAnnouncement, BasePlannedSessionFormSet, FavoriteForm, BaseFavoriteFormSet, SiteAnnouncement, BasePlannedSessionFormSet,
get_course_timezone, BaseConditionFormSet, get_course_timezone, BaseConditionFormSet,
@@ -228,6 +228,7 @@ from rowsandall_app.settings import (
RECAPTCHA_SITE_KEY, RECAPTCHA_SITE_SECRET, RECAPTCHA_SITE_KEY, RECAPTCHA_SITE_SECRET,
NK_REDIRECT_URI, NK_CLIENT_ID, NK_CLIENT_SECRET, NK_REDIRECT_URI, NK_CLIENT_ID, NK_CLIENT_SECRET,
ROJABO_REDIRECT_URI, ROJABO_CLIENT_ID, ROJABO_CLIENT_SECRET, ROJABO_REDIRECT_URI, ROJABO_CLIENT_ID, ROJABO_CLIENT_SECRET,
IDOKLAD_REDIRECT_URI, IDOKLAD_CLIENT_ID, IDOKLAD_CLIENT_SECRET,
) )
from django.contrib import messages from django.contrib import messages
+11
View File
@@ -5249,6 +5249,7 @@ def workout_upload_view(request,
upload_to_strava = uploadoptions.get('upload_to_Strava', False) upload_to_strava = uploadoptions.get('upload_to_Strava', False)
upload_to_st = uploadoptions.get('upload_to_SportTracks', False) upload_to_st = uploadoptions.get('upload_to_SportTracks', False)
upload_to_tp = uploadoptions.get('upload_to_TrainingPeaks', False) upload_to_tp = uploadoptions.get('upload_to_TrainingPeaks', False)
upload_to_intervals = uploadoptions.get('upload_to_Intervals', False)
response = {} response = {}
if request.method == 'POST': if request.method == 'POST':
@@ -5298,6 +5299,7 @@ def workout_upload_view(request,
upload_to_strava = optionsform.cleaned_data['upload_to_Strava'] upload_to_strava = optionsform.cleaned_data['upload_to_Strava']
upload_to_st = optionsform.cleaned_data['upload_to_SportTracks'] upload_to_st = optionsform.cleaned_data['upload_to_SportTracks']
upload_to_tp = optionsform.cleaned_data['upload_to_TrainingPeaks'] upload_to_tp = optionsform.cleaned_data['upload_to_TrainingPeaks']
upload_to_intervals = optionsform.cleaned_data['upload_to_Intervals']
makeprivate = optionsform.cleaned_data['makeprivate'] makeprivate = optionsform.cleaned_data['makeprivate']
landingpage = optionsform.cleaned_data['landingpage'] landingpage = optionsform.cleaned_data['landingpage']
raceid = optionsform.cleaned_data['raceid'] raceid = optionsform.cleaned_data['raceid']
@@ -5315,6 +5317,7 @@ def workout_upload_view(request,
'upload_to_Strava': upload_to_strava, 'upload_to_Strava': upload_to_strava,
'upload_to_SportTracks': upload_to_st, 'upload_to_SportTracks': upload_to_st,
'upload_to_TrainingPeaks': upload_to_tp, 'upload_to_TrainingPeaks': upload_to_tp,
'upload_to_Intervals': upload_to_intervals,
'landingpage': landingpage, 'landingpage': landingpage,
'boattype': boattype, 'boattype': boattype,
'rpe': rpe, 'rpe': rpe,
@@ -5449,6 +5452,14 @@ def workout_upload_view(request,
message = "Please connect to TrainingPeaks first" message = "Please connect to TrainingPeaks first"
messages.error(request, message) messages.error(request, message)
if (upload_to_intervals):
intervals_integration = IntervalsIntegration(request.user)
try:
id = intervals_integration.workout_export(w)
except NoTokenError:
message = "Please connect to Intervals.icu first"
messages.error(request, message)
if int(registrationid) < 0: # pragma: no cover if int(registrationid) < 0: # pragma: no cover
race = VirtualRace.objects.get(id=-int(registrationid)) race = VirtualRace.objects.get(id=-int(registrationid))
if race.sessiontype == 'race': if race.sessiontype == 'race':
+29 -1
View File
@@ -97,6 +97,8 @@ AUTHENTICATION_BACKENDS = (
#'rules.permissions.ObjectPermissionBackend', #'rules.permissions.ObjectPermissionBackend',
) )
CSRF_TRUSTED_ORIGINS = ['https://rowsandall.com', 'https://www.rowsandall.com', 'http://localhost', 'https://dunav.ngrok.io']
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.common.BrokenLinkEmailsMiddleware', 'django.middleware.common.BrokenLinkEmailsMiddleware',
@@ -294,6 +296,21 @@ C2_CLIENT_SECRET = CFG['c2_client_secret']
C2_REDIRECT_URI = CFG['c2_callback'] C2_REDIRECT_URI = CFG['c2_callback']
# C2_REDIRECT_URI = "http://localhost:8000/call_back" # C2_REDIRECT_URI = "http://localhost:8000/call_back"
# Intervals.icu
try:
INTERVALS_CLIENT_ID = CFG['intervals_client_id']
except KeyError:
INTERVALS_CLIENT_ID = '0'
try:
INTERVALS_CLIENT_SECRET = CFG['intervals_client_secret']
except KeyError:
INTERVALS_CLIENT_SECRET = 'aa'
try:
INTERVALS_REDIRECT_URI = CFG['intervals_callback']
except KeyError:
INTERVALS_REDIRECT_URI = 'http://localhost:8000/intervals_icu_callback'
# Strava # Strava
STRAVA_CLIENT_ID = CFG['strava_client_id'] STRAVA_CLIENT_ID = CFG['strava_client_id']
@@ -461,7 +478,8 @@ OAUTH2_PROVIDER = {
"https", "https",
"rowingcoachexport", "rowingcoachexport",
"com.performancephones.crewnerd", "com.performancephones.crewnerd",
"pocketcox"], "pocketcox",
"app"],
'ACCESS_TOKEN_MODEL': 'oauth2_provider.AccessToken', 'ACCESS_TOKEN_MODEL': 'oauth2_provider.AccessToken',
'APPLICATION_MODEL': 'oauth2_provider.Application', 'APPLICATION_MODEL': 'oauth2_provider.Application',
'REFRESH_TOKEN_MODEL': 'oauth2_provider.RefreshToken', 'REFRESH_TOKEN_MODEL': 'oauth2_provider.RefreshToken',
@@ -598,6 +616,16 @@ try:
except KeyError: # pragma: no cover except KeyError: # pragma: no cover
FAKTUROID_SLUG = '' FAKTUROID_SLUG = ''
try:
IDOKLAD_CLIENT_ID = CFG['idoklad_client_id']
IDOKLAD_CLIENT_SECRET = CFG['idoklad_client_secret']
IDOKLAD_REDIRECT_URI = CFG['idoklad_redirect_uri']
except KeyError: # pragma: no cover
IDOKLAD_CLIENT_ID = ''
IDOKLAD_CLIENT_SECRET = ''
IDOKLAD_REDIRECT_URI = ''
# ID obfuscation # ID obfuscation
try: try:
OPAQUE_SECRET_KEY = CFG['opaque_secret_key'] OPAQUE_SECRET_KEY = CFG['opaque_secret_key']
+2
View File
@@ -93,6 +93,8 @@ urlpatterns += [
re_path(r'^tp\_callback', rowersviews.rower_process_tpcallback), re_path(r'^tp\_callback', rowersviews.rower_process_tpcallback),
re_path(r'^rp3\_callback', rowersviews.rower_process_rp3callback), re_path(r'^rp3\_callback', rowersviews.rower_process_rp3callback),
re_path(r'^twitter\_callback', rowersviews.rower_process_twittercallback), re_path(r'^twitter\_callback', rowersviews.rower_process_twittercallback),
re_path(r'^idoklad\_callback', rowersviews.process_idokladcallback),
re_path(r'^intervals\_icu\_callback', rowersviews.rower_process_intervalscallback),
re_path(r'^i18n/', include('django.conf.urls.i18n')), re_path(r'^i18n/', include('django.conf.urls.i18n')),
re_path(r'^tz_detect/', include('tz_detect.urls')), re_path(r'^tz_detect/', include('tz_detect.urls')),
re_path(r'^logo/', logoview), re_path(r'^logo/', logoview),
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB