Merge branch 'develop' into feature/stravaapi
This commit is contained in:
@@ -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 rowsandall_app.settings import (
|
||||
BRAINTREE_MERCHANT_ID, BRAINTREE_PUBLIC_KEY, BRAINTREE_PRIVATE_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
|
||||
from rowers.utils import dologging
|
||||
@@ -16,7 +17,7 @@ from rowers.tasks import (
|
||||
# handle_send_email_transaction_notification,
|
||||
)
|
||||
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.not_found_error import NotFoundError
|
||||
import time
|
||||
@@ -52,6 +53,7 @@ else:
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
def process_webhook(notification):
|
||||
if not settings.TESTING: # pragma: no cover
|
||||
@@ -112,18 +114,18 @@ def send_invoice(subscription):
|
||||
else:
|
||||
r = rs[0]
|
||||
dologging('braintreewebhooks.log','Rower '+str(r)+'\n')
|
||||
fakturoid_contact_id = fakturoid.get_contacts(r)
|
||||
dologging('braintreewebhooks.log','Fakturoid Contact ID '+str(fakturoid_contact_id)+'\n')
|
||||
if not fakturoid_contact_id: # pragma: no cover
|
||||
fakturoid_contact_id = fakturoid.create_contact(r)
|
||||
dologging('braintreewebhooks.log','Created Fakturoid Contact ID ' +
|
||||
str(fakturoid_contact_id)+'\n')
|
||||
idoklad_contact_id = idoklad.get_contacts(r)
|
||||
dologging('braintreewebhooks.log','Idoklad Contact ID '+str(idoklad_contact_id)+'\n')
|
||||
if not idoklad_contact_id: # pragma: no cover
|
||||
idoklad_contact_id = idoklad.create_contact(r)
|
||||
dologging('braintreewebhooks.log','Created Idoklad Contact ID ' +
|
||||
str(idoklad_contact_id)+'\n')
|
||||
transactions = subscription.transactions
|
||||
if transactions:
|
||||
amount = transactions[0].amount
|
||||
dologging('braintreewebhooks.log','Transaction amount '+str(amount)+'\n')
|
||||
id = fakturoid.create_invoice(r, amount, subscription_id, dosend=True,
|
||||
contact_id=fakturoid_contact_id)
|
||||
id = idoklad.create_invoice(r, amount, subscription_id, dosend=True,
|
||||
contact_id=idoklad_contact_id)
|
||||
return id
|
||||
|
||||
return 0 # pragma: no cover
|
||||
@@ -210,11 +212,13 @@ def make_payment(rower, data):
|
||||
l=rower.user.last_name,
|
||||
)
|
||||
|
||||
fakturoid_contact_id = fakturoid.get_contacts(rower)
|
||||
if not fakturoid_contact_id:
|
||||
fakturoid_contact_id = fakturoid.create_contact(rower)
|
||||
_ = fakturoid.create_invoice(rower, amount, transaction.id, dosend=True, contact_id=fakturoid_contact_id,
|
||||
name=additional_text)
|
||||
idoklad_contact_id = idoklad.get_contacts(rower)
|
||||
if not idoklad_contact_id:
|
||||
idoklad_contact_id = idoklad.create_contact(rower)
|
||||
|
||||
_ = idoklad.create_invoice(rower, amount, transaction.id, dosend=True,
|
||||
contact_id=idoklad_contact_id,
|
||||
name=additional_text)
|
||||
|
||||
_ = myqueue(queuehigh, handle_send_email_transaction,
|
||||
name, rower.user.email, amount)
|
||||
|
||||
@@ -1578,6 +1578,13 @@ def new_workout_from_file(r, f2,
|
||||
# Get workout type from fit & tcx
|
||||
if (fileformat == 'fit'): # pragma: no cover
|
||||
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'):
|
||||
# workouttype_from_tcx = get_workouttype_from_tcx(f2,workouttype=workouttype)
|
||||
# if workouttype != 'rower' and workouttype_from_tcx not in mytypes.otwtypes:
|
||||
|
||||
@@ -1289,10 +1289,10 @@ def parsenonpainsled(fileformat, f2, summary, startdatetime='', empowerfirmware=
|
||||
# handle FIT
|
||||
if (fileformat == 'fit'): # pragma: no cover
|
||||
try:
|
||||
s = fitsummarydata(f2)
|
||||
s = FitSummaryData(f2)
|
||||
s.setsummary()
|
||||
summary = s.summarytext
|
||||
except:
|
||||
except Exception as e:
|
||||
pass
|
||||
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
|
||||
# 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'):
|
||||
try:
|
||||
@@ -1359,16 +1392,27 @@ def get_workouttype_from_fit(filename, workouttype='water'):
|
||||
|
||||
records = fitfile.messages
|
||||
fittype = 'rowing'
|
||||
subsporttype = ''
|
||||
for record in records:
|
||||
if record.name in ['sport', 'lap']:
|
||||
if record.name in ['sport', 'lap','session']:
|
||||
try:
|
||||
fittype = record.get_values()['sport'].lower()
|
||||
try:
|
||||
subsporttype = record.get_values()['sub_sport'].lower()
|
||||
except KeyError:
|
||||
subsporttype = ''
|
||||
except (KeyError, AttributeError): # pragma: no cover
|
||||
return 'water'
|
||||
try:
|
||||
workouttype = mytypes.fitmappinginv[fittype]
|
||||
except KeyError: # pragma: no cover
|
||||
return workouttype
|
||||
pass
|
||||
if subsporttype:
|
||||
try:
|
||||
workouttype = mytypes.fitmappinginv[subsporttype]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
workouttype = mytypes.fitmappinginv[fittype]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return workouttype
|
||||
|
||||
@@ -1604,7 +1648,7 @@ def read_data(columns, ids=[], doclean=True, workstrokesonly=True, debug=False,
|
||||
datadf = pl.concat(data)
|
||||
existing_columns = [col for col in columns if col in datadf.columns]
|
||||
datadf = datadf.select(existing_columns)
|
||||
except (ShapeError, SchemaError):
|
||||
except (ShapeError, SchemaError, ColumnNotFoundError):
|
||||
data = [
|
||||
df.select(columns)
|
||||
for df in data]
|
||||
|
||||
@@ -554,6 +554,9 @@ class UploadOptionsForm(forms.Form):
|
||||
upload_to_TrainingPeaks = forms.BooleanField(initial=False,
|
||||
required=False,
|
||||
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)')
|
||||
makeprivate = forms.BooleanField(initial=False, required=False,
|
||||
label='Make Workout Private')
|
||||
|
||||
204
rowers/idoklad.py
Normal file
204
rowers/idoklad.py
Normal 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
|
||||
|
||||
2212
rowers/idoklad_countries.json
Normal file
2212
rowers/idoklad_countries.json
Normal file
File diff suppressed because it is too large
Load Diff
1808
rowers/idoklad_countries.yaml
Normal file
1808
rowers/idoklad_countries.yaml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ from .sporttracks import SportTracksIntegration
|
||||
from .rp3 import RP3Integration
|
||||
from .trainingpeaks import TPIntegration
|
||||
from .polar import PolarIntegration
|
||||
from .intervals import IntervalsIntegration
|
||||
|
||||
importsources = {
|
||||
'c2': C2Integration,
|
||||
@@ -15,5 +16,6 @@ importsources = {
|
||||
'tp':TPIntegration,
|
||||
'rp3':RP3Integration,
|
||||
'polar': PolarIntegration,
|
||||
'intervals': IntervalsIntegration,
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ class C2Integration(SyncIntegration):
|
||||
'client_id': C2_CLIENT_ID,
|
||||
'client_secret': C2_CLIENT_SECRET,
|
||||
'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',
|
||||
'tokenname': 'c2token',
|
||||
'refreshtokenname': 'c2refreshtoken',
|
||||
|
||||
@@ -109,7 +109,7 @@ class SyncIntegration(metaclass=ABCMeta):
|
||||
if 'grant_type' in self.oauth_data:
|
||||
if 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"
|
||||
|
||||
if 'json' in self.oauth_data['content_type']:
|
||||
|
||||
301
rowers/integrations/intervals.py
Normal file
301
rowers/integrations/intervals.py
Normal 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)
|
||||
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ class NKIntegration(SyncIntegration):
|
||||
'client_id': NK_CLIENT_ID,
|
||||
'client_secret': NK_CLIENT_SECRET,
|
||||
'redirect_uri': NK_REDIRECT_URI,
|
||||
'autorization_uri': NK_OAUTH_LOCATION+"/oauth/authorize",
|
||||
'authorization_uri': NK_OAUTH_LOCATION+"/oauth/authorize",
|
||||
'content_type': 'application/json',
|
||||
'tokenname': 'nktoken',
|
||||
'refreshtokenname': 'nkrefreshtoken',
|
||||
|
||||
@@ -30,7 +30,7 @@ class RP3Integration(SyncIntegration):
|
||||
'client_id': RP3_CLIENT_ID,
|
||||
'client_secret': RP3_CLIENT_SECRET,
|
||||
'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/json',
|
||||
'tokenname': 'rp3token',
|
||||
|
||||
@@ -89,7 +89,7 @@ class StravaIntegration(SyncIntegration):
|
||||
'client_id': STRAVA_CLIENT_ID,
|
||||
'client_secret': STRAVA_CLIENT_SECRET,
|
||||
'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',
|
||||
'tokenname': 'stravatoken',
|
||||
'refreshtokenname': 'stravarefreshtoken',
|
||||
|
||||
@@ -41,7 +41,7 @@ class TPIntegration(SyncIntegration):
|
||||
'client_id': TP_CLIENT_ID,
|
||||
'client_secret': TP_CLIENT_SECRET,
|
||||
'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',
|
||||
'tokenname': 'tptoken',
|
||||
'refreshtokenname': 'tprefreshtoken',
|
||||
@@ -66,7 +66,7 @@ class TPIntegration(SyncIntegration):
|
||||
except TypeError:
|
||||
newnotes = 'from '+w.workoutsource+' via rowsandall.com'
|
||||
|
||||
row.exporttotcx(tcxfilename, notes=newnotes)
|
||||
row.exporttotcx(tcxfilename, notes=newnotes, sport=tpmapping(w.workouttype))
|
||||
|
||||
return tcxfilename
|
||||
|
||||
|
||||
@@ -24,7 +24,8 @@ class Command(BaseCommand):
|
||||
record.sporttracksid = w.uploadedtosporttracks
|
||||
if w.uploadedtoc2:
|
||||
record.c2id = w.uploadedtoc2
|
||||
|
||||
if w.uploadedtointervals:
|
||||
record.intervalsid = w.uploadedtointervals
|
||||
try:
|
||||
record.save()
|
||||
except IntegrityError:
|
||||
@@ -52,7 +53,8 @@ class Command(BaseCommand):
|
||||
record.sporttracksid = w.uploadedtosporttracks
|
||||
if w.uploadedtoc2:
|
||||
record.c2id = w.uploadedtoc2
|
||||
|
||||
if w.uploadedtointervals:
|
||||
record.intervalsid = w.uploadedtointervals
|
||||
try:
|
||||
record.save()
|
||||
except IntegrityError:
|
||||
|
||||
@@ -117,5 +117,16 @@ class Command(BaseCommand):
|
||||
lines = traceback.format_exception(exc_type, exc_value, exc_traceback)
|
||||
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(
|
||||
'Successfully processed email attachments'))
|
||||
|
||||
@@ -372,7 +372,7 @@ def update_records(url=c2url, verbose=True):
|
||||
|
||||
# Create a DataFrame
|
||||
df = pd.DataFrame(rows, columns=headers)
|
||||
except: # pragma: no cover
|
||||
except: # pragma: no cover
|
||||
df = pd.DataFrame()
|
||||
|
||||
if not df.empty:
|
||||
@@ -1172,6 +1172,8 @@ class Rower(models.Model):
|
||||
default='', max_length=200, blank=True, null=True)
|
||||
c2_auto_export = 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(
|
||||
default='', max_length=200, 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_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 = (
|
||||
('visible', 'Visible'),
|
||||
('hidden', 'Hidden'),
|
||||
@@ -3708,6 +3714,7 @@ class Workout(models.Model):
|
||||
uploadedtogarmin = models.BigIntegerField(default=0)
|
||||
uploadedtorp3 = models.BigIntegerField(default=0)
|
||||
uploadedtonk = models.BigIntegerField(default=0)
|
||||
uploadedtointervals = models.CharField(default=None,null=True, max_length=100)
|
||||
forceunit = models.CharField(default='lbs',
|
||||
choices=(
|
||||
('lbs', 'lbs'),
|
||||
@@ -3842,6 +3849,7 @@ class TombStone(models.Model):
|
||||
uploadedtosporttracks = models.BigIntegerField(default=0)
|
||||
uploadedtotp = 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)
|
||||
def create_tombstone_on_delete(sender, instance, **kwargs):
|
||||
@@ -3850,7 +3858,8 @@ def create_tombstone_on_delete(sender, instance, **kwargs):
|
||||
uploadedtoc2=instance.uploadedtoc2,
|
||||
uploadedtostrava=instance.uploadedtostrava,
|
||||
uploadedtotp=instance.uploadedtotp,
|
||||
uploadedtonk=instance.uploadedtonk
|
||||
uploadedtonk=instance.uploadedtonk,
|
||||
uploadedtointervals=instance.uploadedtointervals,
|
||||
)
|
||||
t.save()
|
||||
|
||||
@@ -3866,6 +3875,7 @@ class SyncRecord(models.Model):
|
||||
c2id = 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)
|
||||
intervalsid = models.CharField(unique=True, null=True, default=None, max_length=100)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.workout:
|
||||
@@ -3881,7 +3891,7 @@ class SyncRecord(models.Model):
|
||||
|
||||
str2 = ''
|
||||
|
||||
for field in ['stravaid', 'sporttracksid', 'nkid', 'c2id', 'tpid']:
|
||||
for field in ['stravaid', 'sporttracksid', 'nkid', 'c2id', 'tpid', 'intervalsid']:
|
||||
value = getattr(self, field, None)
|
||||
if value is not None:
|
||||
str2 += '{w}: {v},'.format(
|
||||
@@ -4567,7 +4577,9 @@ class RowerExportForm(ModelForm):
|
||||
'strava_auto_import',
|
||||
'strava_auto_delete',
|
||||
'trainingpeaks_auto_export',
|
||||
'rp3_auto_import'
|
||||
'rp3_auto_import',
|
||||
'intervals_auto_import',
|
||||
'intervals_auto_export',
|
||||
]
|
||||
|
||||
# Simple form to set rower's Functional Threshold Power
|
||||
@@ -5396,3 +5408,16 @@ class ForceCurveAnalysis(models.Model):
|
||||
date = self.date)
|
||||
|
||||
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)}"
|
||||
|
||||
|
||||
@@ -148,6 +148,7 @@ garminmapping = {key: value for key, value in Reverse(garmincollection)}
|
||||
fitcollection = (
|
||||
('water', 'rowing'),
|
||||
('rower', 'rowing'),
|
||||
('rower', 'indoor_rowing'),
|
||||
('skierg', 'cross_country_skiing'),
|
||||
('bike', 'cycling'),
|
||||
('bikeerg', 'cycling'),
|
||||
@@ -180,6 +181,74 @@ 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 = (
|
||||
('water', 'Rowing'),
|
||||
('rower', 'Rowing'),
|
||||
@@ -332,6 +401,9 @@ garminmappinginv = {value: key for key, value in Reverse(
|
||||
fitmappinginv = {value: key for key, value in Reverse(
|
||||
fitcollection) if value is not None}
|
||||
|
||||
intervalsmappinginv = {value: key for key, value in Reverse(
|
||||
intervalscollection) if value is not None}
|
||||
|
||||
otwtypes = (
|
||||
'water',
|
||||
'coastal',
|
||||
|
||||
@@ -24,6 +24,7 @@ from rowers.courseutils import (
|
||||
InvalidTrajectoryError
|
||||
)
|
||||
from rowers.emails import send_template_email
|
||||
from rowers.mytypes import intervalsmappinginv
|
||||
from rowers.nkimportutils import (
|
||||
get_nk_summary, get_nk_allstats, get_nk_intervalstats, getdict, strokeDataToDf,
|
||||
add_workout_from_data
|
||||
@@ -59,6 +60,8 @@ import rowingdata
|
||||
from rowingdata import make_cumvalues, make_cumvalues_array
|
||||
from uuid import uuid4
|
||||
from rowingdata import rowingdata as rdata
|
||||
from rowingdata import FITParser as FP
|
||||
from rowingdata.otherparsers import FitSummaryData
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
@@ -3485,6 +3488,72 @@ def handle_nk_async_workout(alldata, userid, nktoken, nkid, delaysec, defaulttim
|
||||
|
||||
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
|
||||
def handle_c2_getworkout(userid, c2token, c2id, defaulttimezone, debug=False, **kwargs):
|
||||
|
||||
@@ -231,6 +231,20 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
</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">
|
||||
<a href="/rowers/workout/{{ workout.id|encode }}/emailcsv/">
|
||||
CSV
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
<li id="sporttracks"><a href="/rowers/workout/sporttracksimport/">SportTracks</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="intervals"><a href="/rowers/workout/intervalsimport/">Intervals.icu</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul> <!-- cd-accordion-menu -->
|
||||
|
||||
@@ -32,10 +32,13 @@
|
||||
Strava,
|
||||
{% endif %}
|
||||
{% if rower.rp3token is not None and rower.rp3token != '' %}
|
||||
RP3
|
||||
RP3,
|
||||
{% endif %}
|
||||
{% 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 %}
|
||||
</p>
|
||||
|
||||
@@ -132,6 +135,8 @@
|
||||
alt="connect with RP3" width="130"></a></p>
|
||||
<p><a href="/rowers/me/rojaboauthorize"><img src="/static/img/rojabo.png"
|
||||
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 %}
|
||||
|
||||
|
||||
@@ -57,6 +57,14 @@ from rowers.dataprep import delete_strokedata
|
||||
from redis import 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
|
||||
class insecure_channel:
|
||||
@@ -773,6 +781,9 @@ def mocked_requests(*args, **kwargs):
|
||||
with open('rowers/tests/testdata/rp3_list.json','r') as 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'}}}
|
||||
|
||||
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')
|
||||
garmintester = re.compile(r'.*?garmin\.com')
|
||||
fakturoidtester = re.compile(r'.*?fakturoid\.cz')
|
||||
idokladtester = re.compile(r'.*?idoklad\.cz')
|
||||
|
||||
polarlistregex = r'.*?polaraccesslink\.com\/.*\/(\d+)$'
|
||||
polarlisttester = re.compile(polarlistregex)
|
||||
@@ -1487,6 +1499,43 @@ def mocked_requests(*args, **kwargs):
|
||||
else: # pragma: no cover
|
||||
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 'invoices' in args[0]:
|
||||
response = {
|
||||
|
||||
@@ -74,11 +74,12 @@ class BraintreeUnits(TestCase):
|
||||
self.p2 = PaidPlan.objects.create(price=25,paymentprocessor='braintree')
|
||||
|
||||
|
||||
@patch('rowers.fakturoid.requests.get',side_effect=mocked_requests)
|
||||
@patch('rowers.fakturoid.requests.post',side_effect=mocked_requests)
|
||||
@patch('rowers.idoklad.idoklad_token', side_effect=mocked_idoklad_token)
|
||||
@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.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()
|
||||
res = process_webhook(n)
|
||||
self.assertEqual(res,1)
|
||||
|
||||
@@ -405,7 +405,7 @@ description: ""
|
||||
|
||||
|
||||
@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')
|
||||
def test_purchase_trainingplan_view(self, mocked_gateway,mocked_invoiceid, mocked_myqueue):
|
||||
u = UserFactory()
|
||||
|
||||
@@ -130,11 +130,14 @@ def make_plot(r, w, f1, f2, plottype, title, imagename='', plotnr=0):
|
||||
|
||||
|
||||
def do_sync(w, options, quick=False):
|
||||
do_strava_export = w.user.strava_auto_export
|
||||
try:
|
||||
do_strava_export = options['upload_to_Strava'] or do_strava_export
|
||||
except KeyError:
|
||||
pass
|
||||
do_strava_export = False
|
||||
if w.user.strava_auto_export is True:
|
||||
do_strava_export = True
|
||||
else:
|
||||
try:
|
||||
do_strava_export = options['upload_to_Strava'] or do_strava_export
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
if options['stravaid'] != 0 and options['stravaid'] != '': # pragma: no cover
|
||||
@@ -150,6 +153,27 @@ def do_sync(w, options, quick=False):
|
||||
except KeyError:
|
||||
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:
|
||||
if options['nkid'] != 0 and options['nkid'] != '': # pragma: no cover
|
||||
w.uploadedtonk = options['nkid']
|
||||
@@ -181,11 +205,14 @@ def do_sync(w, options, quick=False):
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
do_c2_export = w.user.c2_auto_export
|
||||
try:
|
||||
do_c2_export = options['upload_to_C2'] or do_c2_export
|
||||
except KeyError:
|
||||
pass
|
||||
do_c2_export = False
|
||||
if w.user.c2_auto_export is True:
|
||||
do_c2_export = True
|
||||
else:
|
||||
try:
|
||||
do_c2_export = options['upload_to_C2'] or do_c2_export
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
if options['c2id'] != 0 and options['c2id'] != '': # pragma: no cover
|
||||
@@ -236,14 +263,30 @@ def do_sync(w, options, quick=False):
|
||||
except NoTokenError: # pragma: no cover
|
||||
id = 0
|
||||
message = "Please connect to Strava first"
|
||||
except:
|
||||
e = sys.exc_info()[0]
|
||||
t = time.localtime()
|
||||
timestamp = time.strftime('%b-%d-%Y_%H%M', t)
|
||||
with open('stravalog.log', 'a') as f:
|
||||
f.write('\n')
|
||||
f.write(timestamp)
|
||||
f.write(str(e))
|
||||
except Exception as e:
|
||||
dologging('stravalog.log', e)
|
||||
|
||||
if do_icu_export:
|
||||
intervals_integration = IntervalsIntegration(w.user.user)
|
||||
try:
|
||||
id = intervals_integration.workout_export(w)
|
||||
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
|
||||
|
||||
|
||||
@@ -752,6 +752,7 @@ urlpatterns = [
|
||||
views.rower_prefs_view, name='rower_prefs_view'),
|
||||
re_path(r'^me/prefs/user/(?P<userid>\d+)/$',
|
||||
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,
|
||||
name='rower_rojabo_authorize'),
|
||||
re_path(r'^me/polarauthorize/$', views.rower_polar_authorize,
|
||||
|
||||
@@ -575,19 +575,40 @@ def strokedata_fit(request):
|
||||
return JsonResponse({
|
||||
"status": "error",
|
||||
"message": f"An error occurred while saving the FIT file: {str(e)}"
|
||||
}, status=500)
|
||||
}, status=400)
|
||||
|
||||
try:
|
||||
# Parse the FIT file
|
||||
row = FP(fit_filename)
|
||||
try:
|
||||
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)
|
||||
duration = totaltime_sec_to_string(rowdata.duration)
|
||||
title = "ActiveSpeed water"
|
||||
|
||||
duration = totaltime_sec_to_string(rowdata.duration)
|
||||
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,
|
||||
duration=duration,
|
||||
name=title,)
|
||||
distance=distance,
|
||||
name=title,
|
||||
date=startdate,
|
||||
workouttype='water',)
|
||||
|
||||
uploadoptions = {
|
||||
'secret': UPLOAD_SERVICE_SECRET,
|
||||
@@ -598,7 +619,7 @@ def strokedata_fit(request):
|
||||
'title': title,
|
||||
'rpe': 0,
|
||||
'notes': '',
|
||||
'workoutid': w.id,
|
||||
'id': w.id,
|
||||
'offline': False,
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ importauthorizeviews = {
|
||||
'nk': 'rower_integration_authorize',
|
||||
'rp3': 'rower_integration_authorize',
|
||||
'garmin': 'rower_garmin_authorize',
|
||||
'intervals': 'rower_integration_authorize',
|
||||
}
|
||||
|
||||
|
||||
@@ -173,6 +174,37 @@ def rower_process_twittercallback(request): # pragma: no cover
|
||||
|
||||
# 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()
|
||||
def rower_process_polarcallback(request):
|
||||
@@ -439,7 +471,10 @@ def workout_import_view(request, source='c2'):
|
||||
try:
|
||||
tdict = dict(request.POST.lists())
|
||||
ids = tdict['workoutid']
|
||||
nkids = [int(id) for id in ids]
|
||||
try:
|
||||
nkids = [int(id) for id in ids]
|
||||
except ValueError:
|
||||
nkids = ids
|
||||
for nkid in nkids:
|
||||
try:
|
||||
_ = integration.get_workout(nkid, startdate=startdate, enddate=enddate)
|
||||
|
||||
@@ -8,6 +8,72 @@ from django.core.mail import EmailMessage
|
||||
|
||||
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
|
||||
def braintree_webhook_view(request):
|
||||
|
||||
@@ -181,7 +181,7 @@ from rowers.models import ( RowerPowerForm, RowerHRZonesForm, SimpleRowerPowerFo
|
||||
IndoorVirtualRaceForm, PlannedSessionCommentForm, Alert,
|
||||
Condition, StaticChartRowerForm, FollowerForm,
|
||||
VirtualRaceAthleteForm, InstantPlanForm, DataRowerForm,
|
||||
StepEditorForm, )
|
||||
StepEditorForm, iDokladToken )
|
||||
from rowers.models import (
|
||||
FavoriteForm, BaseFavoriteFormSet, SiteAnnouncement, BasePlannedSessionFormSet,
|
||||
get_course_timezone, BaseConditionFormSet,
|
||||
@@ -228,6 +228,7 @@ from rowsandall_app.settings import (
|
||||
RECAPTCHA_SITE_KEY, RECAPTCHA_SITE_SECRET,
|
||||
NK_REDIRECT_URI, NK_CLIENT_ID, NK_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
|
||||
|
||||
@@ -5249,6 +5249,7 @@ def workout_upload_view(request,
|
||||
upload_to_strava = uploadoptions.get('upload_to_Strava', False)
|
||||
upload_to_st = uploadoptions.get('upload_to_SportTracks', False)
|
||||
upload_to_tp = uploadoptions.get('upload_to_TrainingPeaks', False)
|
||||
upload_to_intervals = uploadoptions.get('upload_to_Intervals', False)
|
||||
|
||||
response = {}
|
||||
if request.method == 'POST':
|
||||
@@ -5298,6 +5299,7 @@ def workout_upload_view(request,
|
||||
upload_to_strava = optionsform.cleaned_data['upload_to_Strava']
|
||||
upload_to_st = optionsform.cleaned_data['upload_to_SportTracks']
|
||||
upload_to_tp = optionsform.cleaned_data['upload_to_TrainingPeaks']
|
||||
upload_to_intervals = optionsform.cleaned_data['upload_to_Intervals']
|
||||
makeprivate = optionsform.cleaned_data['makeprivate']
|
||||
landingpage = optionsform.cleaned_data['landingpage']
|
||||
raceid = optionsform.cleaned_data['raceid']
|
||||
@@ -5315,6 +5317,7 @@ def workout_upload_view(request,
|
||||
'upload_to_Strava': upload_to_strava,
|
||||
'upload_to_SportTracks': upload_to_st,
|
||||
'upload_to_TrainingPeaks': upload_to_tp,
|
||||
'upload_to_Intervals': upload_to_intervals,
|
||||
'landingpage': landingpage,
|
||||
'boattype': boattype,
|
||||
'rpe': rpe,
|
||||
@@ -5449,6 +5452,14 @@ def workout_upload_view(request,
|
||||
message = "Please connect to TrainingPeaks first"
|
||||
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
|
||||
race = VirtualRace.objects.get(id=-int(registrationid))
|
||||
if race.sessiontype == 'race':
|
||||
|
||||
Reference in New Issue
Block a user