Private
Public Access
1
0

Merge branch 'develop' into feature/idoklad

This commit is contained in:
2024-12-19 16:24:11 +01:00
44 changed files with 1867 additions and 217 deletions
+28 -3
View File
@@ -217,6 +217,9 @@ def workout_goldmedalstandard(workout, reset=False):
def check_marker(workout): def check_marker(workout):
r = workout.user r = workout.user
if workout.workoutsource == 'strava':
return None
gmstandard, gmseconds = workout_goldmedalstandard(workout) gmstandard, gmseconds = workout_goldmedalstandard(workout)
if gmseconds < 60: if gmseconds < 60:
return None return None
@@ -369,8 +372,20 @@ def workout_summary_to_df(
return df return df
def resample(id, r, parent, overwrite='copy'): def resample(id, r, parent, overwrite=False):
data, row = getrowdata_db(id=id) data, row = getrowdata_db(id=id)
rowdata = rrdata(csvfile=parent.csvfilename).df
# drop all columns except ' latitude' and ' longitude' and 'TimeStamp (sec)' from rowdata
allowedcolumns = [' latitude', ' longitude', 'TimeStamp (sec)']
rowdata = rowdata.filter(allowedcolumns)
rowdata.rename(columns={'TimeStamp (sec)': 'time'}, inplace=True)
rowdata['time'] = (rowdata['time']-rowdata.loc[0,'time'])*1000.
rowdata.set_index('time', inplace=True)
data.set_index('time', inplace=True)
rowdata_interpolated = rowdata.reindex(data.index.union(rowdata.index)).interpolate('index')
data = data.merge(rowdata_interpolated, left_index=True, right_index=True, how='left')
data = data.reset_index()
messages = [] messages = []
# resample # resample
@@ -393,7 +408,7 @@ def resample(id, r, parent, overwrite='copy'):
data['pace'] = data['pace'] / 1000. data['pace'] = data['pace'] / 1000.
data['time'] = data['time'] / 1000. data['time'] = data['time'] / 1000.
if overwrite == 'overwrite': if overwrite == True:
# remove CP data # remove CP data
try: try:
cpfile = 'media/cpdata_{id}.parquet.gz'.format(id=parent.id) cpfile = 'media/cpdata_{id}.parquet.gz'.format(id=parent.id)
@@ -1304,8 +1319,11 @@ def save_workout_database(f2, r, dosmooth=True, workouttype='rower',
if makeprivate: # pragma: no cover if makeprivate: # pragma: no cover
privacy = 'hidden' privacy = 'hidden'
else: elif workoutsource != 'strava':
privacy = 'visible' privacy = 'visible'
else:
privacy = 'hidden'
# checking for inf values # checking for inf values
@@ -1572,6 +1590,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:
+68 -15
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
try: if subsporttype:
workouttype = mytypes.fitmappinginv[fittype] try:
except KeyError: # pragma: no cover workouttype = mytypes.fitmappinginv[subsporttype]
return workouttype except KeyError:
pass
else:
try:
workouttype = mytypes.fitmappinginv[fittype]
except KeyError:
pass
return workouttype return workouttype
@@ -1605,9 +1649,13 @@ def read_data(columns, ids=[], doclean=True, workstrokesonly=True, debug=False,
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):
data = [ try:
df.select(columns) data = [
for df in data] df.select(columns)
for df in data]
except ColumnNotFoundError:
existing_columns = [col for col in columns if col in df.columns]
df = df.select(existing_columns)
# float columns # float columns
floatcolumns = [] floatcolumns = []
@@ -1642,14 +1690,19 @@ def read_data(columns, ids=[], doclean=True, workstrokesonly=True, debug=False,
] ]
except ComputeError: except ComputeError:
pass pass
except ColumnNotFoundError:
pass
try: try:
datadf = pl.concat(data) datadf = pl.concat(data)
except SchemaError: except SchemaError:
data = [ try:
df.with_columns(cs.integer().cast(pl.Float64)) for df in data data = [
] df.with_columns(cs.integer().cast(pl.Float64)) for df in data
datadf = pl.concat(data) ]
datadf = pl.concat(data)
except ShapeError:
return pl.DataFrame()
+17 -6
View File
@@ -67,13 +67,12 @@ class FlexibleDecimalField(forms.DecimalField):
class ResampleForm(forms.Form): class ResampleForm(forms.Form):
resamplechoices = ( # add resamplechoice field, the result is a True or False boolean, labels are "overwrite" and "create copy"
('overwrite', 'Overwrite Workout'),
('copy', 'Create a Duplicate Workout')
)
resamplechoice = forms.ChoiceField( resamplechoice = forms.ChoiceField(
initial='copy', choices=resamplechoices, label='Copy behavior') required=True,
choices=((True, 'overwrite'), (False, 'create copy')),
label='Resample choice',
widget=forms.RadioSelect)
class TrainingZonesForm(forms.Form): class TrainingZonesForm(forms.Form):
@@ -554,6 +553,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')
@@ -579,6 +581,11 @@ class UploadOptionsForm(forms.Form):
races = VirtualRace.objects.filter( races = VirtualRace.objects.filter(
registration_closure__gt=timezone.now()) registration_closure__gt=timezone.now())
# set upload_to_X based on r.X_auto_export
for field in ['C2', 'Strava', 'SportTracks', 'TrainingPeaks', 'Intervals']:
if getattr(r, field.lower()+'_auto_export') and r.rowerplan in ['pro', 'plan','coach']:
self.fields['upload_to_'+field].initial = True
registrations = IndoorVirtualRaceResult.objects.filter( registrations = IndoorVirtualRaceResult.objects.filter(
race__in=races, race__in=races,
userid=r.id) userid=r.id)
@@ -662,6 +669,10 @@ class TeamUploadOptionsForm(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 TrainingPeaks')
# 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')
+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']:
+319
View File
@@ -0,0 +1,319 @@
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
import rowers.dataprep as dataprep
from rowers.opaque import encoder
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)
# resample if wanted by user, not tested
if w.user.intervals_resample_to_1s:
datadf, id, msgs = dataprep.resample(
w.id, w.user, w, overwrite=False
)
w_resampled = Workout.objects.get(id=id)
filename = w_resampled.csvfilename
else:
w_resampled = None
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'
if w.user.intervals_resample_to_1s and w_resampled:
w_resampled.delete()
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,
'external_id': encoder.encode_hex(workout.id),
}
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:
try:
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)
except KeyError:
dologging('intervals.icu.log', item)
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',
+2 -2
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',
@@ -214,7 +214,7 @@ class StravaIntegration(SyncIntegration):
def get_workout(self, id, *args, **kwargs) -> int: def get_workout(self, id, *args, **kwargs) -> int:
try: try:
_ = self.open() _ = self.open()
except NoTokenError("Strava error"): except NoTokenError:
return 0 return 0
record = create_or_update_syncrecord(self.rower, None, stravaid=id) record = create_or_update_syncrecord(self.rower, None, stravaid=id)
+6 -2
View File
@@ -15,6 +15,7 @@ from rowingdata import rowingdata
from rowers.rower_rules import is_workout_user from rowers.rower_rules import is_workout_user
import time import time
from django_rq import job from django_rq import job
from rowers.mytypes import tpmapping
from rowers.tasks import check_tp_workout_id, handle_workout_tp_upload from rowers.tasks import check_tp_workout_id, handle_workout_tp_upload
@@ -41,7 +42,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 +67,10 @@ 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) try:
row.exporttotcx(tcxfilename, notes=newnotes, sport=tpmapping[w.workouttype])
except KeyError:
row.exporttotcx(tcxfilename, notes=newnotes, sport='other')
return tcxfilename return tcxfilename
+1 -1
View File
@@ -550,7 +550,7 @@ def goldmedalscorechart(user, startdate=None, enddate=None):
workouts = Workout.objects.filter(user=user.rower, date__gte=startdate, workouts = Workout.objects.filter(user=user.rower, date__gte=startdate,
date__lte=enddate, date__lte=enddate,
workouttype__in=mytypes.rowtypes, workouttype__in=mytypes.rowtypes,
duplicate=False).order_by('date') duplicate=False).order_by('date').exclude(workoutsource='strava')
markerworkouts = workouts.filter(rankingpiece=True) markerworkouts = workouts.filter(rankingpiece=True)
outids = [w.id for w in markerworkouts] outids = [w.id for w in markerworkouts]
+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'))
@@ -0,0 +1,38 @@
#!/srv/venv/bin/python
import sys
import os
from django.core.management.base import BaseCommand, CommandError
from django.conf import settings
import time
from rowers.models import (
Workout, User, Rower, WorkoutForm,
RowerForm, GraphImage, AdvancedWorkoutForm)
from django.core.files.base import ContentFile
from rowsandall_app.settings import BASE_DIR
from rowers.dataprep import *
# If you find a solution that does not need the two paths, please comment!
sys.path.append('$path_to_root_of_project$')
sys.path.append('$path_to_root_of_project$/$project_name$')
os.environ['DJANGO_SETTINGS_MODULE'] = '$project_name$.settings'
class Command(BaseCommand):
def handle(self, *args, **options):
# find all Workout instances with uploadedtostrava not 0 or None, workoutsource not 'strava'
workouts = Workout.objects.filter(uploadedtostrava__gt=0)
# report the number of workouts found to the console
self.stdout.write(self.style.SUCCESS('Found {} Strava workouts.'.format(workouts.count())))
# set workout.privacy to hidden and workout.workoutsource to 'strava, report percentage complete to console'
for workout in workouts:
workout.privacy = 'hidden'
workout.workoutsource = 'strava'
workout.save()
self.stdout.write(self.style.SUCCESS('Set workout {} private.'.format(workout.id)))
self.stdout.write(self.style.SUCCESS('Successfully set all Strava data private.'))
+132 -7
View File
@@ -21,7 +21,7 @@ from django.forms import ModelForm
from django.dispatch import receiver from django.dispatch import receiver
from django.forms.widgets import SplitDateTimeWidget, SelectDateWidget from django.forms.widgets import SplitDateTimeWidget, SelectDateWidget
from django.forms.formsets import BaseFormSet from django.forms.formsets import BaseFormSet
from django.db.models.signals import post_save
from django.contrib.admin.widgets import AdminDateWidget, AdminTimeWidget, AdminSplitDateTime from django.contrib.admin.widgets import AdminDateWidget, AdminTimeWidget, AdminSplitDateTime
import os import os
@@ -1172,6 +1172,9 @@ 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)
intervals_resample_to_1s = models.BooleanField(default=False, verbose_name='Resample to 1s on export')
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)
@@ -1238,7 +1241,11 @@ class Rower(models.Model):
strava_auto_export = models.BooleanField(default=False) strava_auto_export = models.BooleanField(default=False)
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=True)
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'),
@@ -1248,6 +1255,8 @@ class Rower(models.Model):
getemailnotifications = models.BooleanField(default=False, getemailnotifications = models.BooleanField(default=False,
verbose_name='Receive email notifications') verbose_name='Receive email notifications')
imports_are_private = models.BooleanField(default=False, verbose_name='Make imports private by default')
# Friends/Team # Friends/Team
friends = models.ManyToManyField("self", blank=True) friends = models.ManyToManyField("self", blank=True)
mycoachgroup = models.ForeignKey( mycoachgroup = models.ForeignKey(
@@ -1434,9 +1443,26 @@ parchoicesy1 = list(sorted(favchartlabelsy1.items(), key=lambda x: x[1]))
parchoicesy2 = list(sorted(favchartlabelsy2.items(), key=lambda x: x[1])) parchoicesy2 = list(sorted(favchartlabelsy2.items(), key=lambda x: x[1]))
parchoicesx = list(sorted(favchartlabelsx.items(), key=lambda x: x[1])) parchoicesx = list(sorted(favchartlabelsx.items(), key=lambda x: x[1]))
# special filter for workouts to exclude strava workouts by default
class WorkoutQuerySet(models.QuerySet):
def filter(self, *args, exclude_strava=True, **kwargs):
queryset = super().filter(*args, **kwargs)
if exclude_strava:
queryset = queryset.exclude(workoutsource='strava')
return queryset
def get(self, *args, **kwargs):
queryset = self
return super().get(*args, **kwargs)
class WorkoutManager(models.Manager):
def get_queryset(self):
return WorkoutQuerySet(self.model, using=self._db)
# Saving a chart as a favorite chart # Saving a chart as a favorite chart
class FavoriteChart(models.Model): class FavoriteChart(models.Model):
workouttypechoices = [ workouttypechoices = [
('ote', 'Erg/SkiErg'), ('ote', 'Erg/SkiErg'),
@@ -3691,6 +3717,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'),
@@ -3715,6 +3742,9 @@ class Workout(models.Model):
default=False, verbose_name='Duplicate Workout') default=False, verbose_name='Duplicate Workout')
impeller = models.BooleanField(default=False, verbose_name='Impeller') impeller = models.BooleanField(default=False, verbose_name='Impeller')
# attach the WorkoutManager
#objects = WorkoutManager()
def url(self): def url(self):
str = '/rowers/workout/{id}/'.format( str = '/rowers/workout/{id}/'.format(
id=encoder.encode_hex(self.id) id=encoder.encode_hex(self.id)
@@ -3752,6 +3782,15 @@ class Workout(models.Model):
super(Workout, self).save(*args, **kwargs) super(Workout, self).save(*args, **kwargs)
@classmethod
def post_create(cls, sender, instance, created, *args, **kwargs):
if created:
user = instance.user
if user.imports_are_private:
instance.privacy = 'hidden'
instance.save()
def __str__(self): def __str__(self):
try: try:
@@ -3810,6 +3849,8 @@ class Workout(models.Model):
return stri return stri
post_save.connect(Workout.post_create, sender=Workout)
class WorkoutRPEForm(ModelForm): class WorkoutRPEForm(ModelForm):
class Meta: class Meta:
model = Workout model = Workout
@@ -3822,6 +3863,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):
@@ -3830,7 +3872,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()
@@ -3846,6 +3889,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:
@@ -3861,7 +3905,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(
@@ -4547,9 +4591,90 @@ 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',
'intervals_resample_to_1s',
'imports_are_private'
] ]
class RowerPrivateImportForm(ModelForm):
class Meta:
model = Rower
fields = [
'imports_are_private'
]
class RowerExportFormStrava(ModelForm):
class Meta:
model = Rower
fields = [
'stravaexportas',
'strava_auto_export',
'strava_auto_import',
'strava_auto_delete',
]
class RowerExportFormIntervals(ModelForm):
class Meta:
model = Rower
fields = [
'intervals_auto_import',
'intervals_auto_export',
'intervals_resample_to_1s',
]
class RowerExportFormGarmin(ModelForm):
class Meta:
model = Rower
fields = [
'garminactivity',
]
class RowerExportFormPolar(ModelForm):
class Meta:
model = Rower
fields = [
'polar_auto_import',
]
class RowerExportFormConcept2(ModelForm):
class Meta:
model = Rower
fields = [
'c2_auto_export',
'c2_auto_import',
]
class RowerExportFormSportTracks(ModelForm):
class Meta:
model = Rower
fields = [
'sporttracks_auto_export',
]
class RowerExportFormTrainingPeaks(ModelForm):
class Meta:
model = Rower
fields = [
'trainingpeaks_auto_export',
]
class RowerExportFormRP3(ModelForm):
class Meta:
model = Rower
fields = [
'rp3_auto_import',
]
class RowerExportFormNK(ModelForm):
class Meta:
model = Rower
fields = [
'nk_auto_import'
]
# Simple form to set rower's Functional Threshold Power # Simple form to set rower's Functional Threshold Power
class SimpleRowerPowerForm(ModelForm): class SimpleRowerPowerForm(ModelForm):
otwftp = forms.IntegerField(initial=0,required=True, label='FTP on water') otwftp = forms.IntegerField(initial=0,required=True, label='FTP on water')
+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',
+41
View File
@@ -1597,13 +1597,28 @@ def add_workout_fastestrace(ws, race, r, recordid=0, doregister=False):
enddatetime enddatetime
) )
# from ws, remove any w where w.workoutsource = 'strava'. For each removal add an error "strava workout not permitted" to the errors list and if there are no workouts left, return 0, comments, errors, 0
ws2 = []
for w in ws:
if w.workoutsource != 'strava':
ws2.append(w)
else:
errors.append('Strava workouts are not permitted')
ws = ws2
if len(ws) == 0:
return result, comments, errors, 0
ids = [w.id for w in ws] ids = [w.id for w in ws]
ids = list(set(ids)) ids = list(set(ids))
if len(ids) > 1 and race.sessiontype in ['test', 'coursetest', 'race', 'indoorrace', 'fastest_time', 'fastest_distance']: # pragma: no cover if len(ids) > 1 and race.sessiontype in ['test', 'coursetest', 'race', 'indoorrace', 'fastest_time', 'fastest_distance']: # pragma: no cover
errors.append('For tests, you can only attach one workout') errors.append('For tests, you can only attach one workout')
return result, comments, errors, 0 return result, comments, errors, 0
if r.birthdate: if r.birthdate:
age = calculate_age(r.birthdate) age = calculate_age(r.birthdate)
else: # pragma: no cover else: # pragma: no cover
@@ -1759,6 +1774,19 @@ def add_workout_indoorrace(ws, race, r, recordid=0, doregister=False):
enddatetime enddatetime
) )
# from ws, remove any w where w.workoutsource = 'strava'. For each removal add an error "strava workout not permitted" to the errors list and if there are no workouts left, return 0, comments, errors, 0
ws2 = []
for w in ws:
if w.workoutsource != 'strava':
ws2.append(w)
else:
errors.append('Strava workouts are not permitted')
ws = ws2
if len(ws) == 0:
return result, comments, errors, 0
# check if all sessions have same date # check if all sessions have same date
dates = [w.date for w in ws] dates = [w.date for w in ws]
if (not all(d == dates[0] for d in dates)) and race.sessiontype not in ['challenge', 'cycletarget']: # pragma: no cover if (not all(d == dates[0] for d in dates)) and race.sessiontype not in ['challenge', 'cycletarget']: # pragma: no cover
@@ -1906,6 +1934,19 @@ def add_workout_race(ws, race, r, splitsecond=0, recordid=0, doregister=False):
enddatetime enddatetime
) )
# from ws, remove any w where w.workoutsource = 'strava'. For each removal add an error "strava workout not permitted" to the errors list and if there are no workouts left, return 0, comments, errors, 0
ws2 = []
for w in ws:
if w.workoutsource != 'strava':
ws2.append(w)
else:
errors.append('Strava workouts are not permitted')
ws = ws2
if len(ws) == 0:
return result, comments, errors, 0
# check if all sessions have same date # check if all sessions have same date
dates = [w.date for w in ws] dates = [w.date for w in ws]
if (not all(d == dates[0] for d in dates)) and race.sessiontype not in ['challenge', 'cycletarget']: # pragma: no cover if (not all(d == dates[0] for d in dates)) and race.sessiontype not in ['challenge', 'cycletarget']: # pragma: no cover
+16 -1
View File
@@ -451,6 +451,11 @@ def is_workout_user(user, workout):
except AttributeError: # pragma: no cover except AttributeError: # pragma: no cover
return False return False
if workout.privacy == 'hidden':
return user == workout.user.user
if workout.workoutsource == 'strava':
return user == workout.user.user
if workout.user == r: if workout.user == r:
return True return True
@@ -458,6 +463,9 @@ def is_workout_user(user, workout):
# check if user is in same team as owner of workout # check if user is in same team as owner of workout
@rules.predicate
def workout_is_strava(workout):
return workout.workoutsource == 'strava'
@rules.predicate @rules.predicate
def is_workout_team(user, workout): def is_workout_team(user, workout):
@@ -469,6 +477,11 @@ def is_workout_team(user, workout):
except AttributeError: # pragma: no cover except AttributeError: # pragma: no cover
return False return False
if workout.privacy == 'hidden':
return user == workout.user.user
if workout.workoutsource == 'strava':
return user == workout.user.user
if workout.user == r: if workout.user == r:
return True return True
@@ -479,7 +492,9 @@ def is_workout_team(user, workout):
@rules.predicate @rules.predicate
def can_view_workout(user, workout): def can_view_workout(user, workout):
if workout.privacy != 'private': if workout.workoutsource == 'strava':
return user == workout.user.user
if workout.privacy not in ('hidden', 'private'):
return True return True
if user.is_anonymous: # pragma: no cover if user.is_anonymous: # pragma: no cover
return False return False
+73 -2
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):
@@ -3626,7 +3695,8 @@ def handle_c2_async_workout(alldata, userid, c2token, c2id, delaysec,
code=uuid4().hex[:16], c2id=c2id) code=uuid4().hex[:16], c2id=c2id)
startdatetime, starttime, workoutdate, duration, starttimeunix, timezone = utils.get_startdatetime_from_c2data( startdatetime, starttime, workoutdate, duration, starttimeunix, timezone = utils.get_startdatetime_from_c2data(
data) data
)
s = 'Time zone {timezone}, startdatetime {startdatetime}, duration {duration}'.format( s = 'Time zone {timezone}, startdatetime {startdatetime}, duration {duration}'.format(
timezone=timezone, startdatetime=startdatetime, timezone=timezone, startdatetime=startdatetime,
@@ -3686,6 +3756,7 @@ def handle_c2_async_workout(alldata, userid, c2token, c2id, delaysec,
strokelength = np.zeros(nr_rows) strokelength = np.zeros(nr_rows)
dist2 = 0.1*strokedata.loc[:, 'd'] dist2 = 0.1*strokedata.loc[:, 'd']
cumdist, intervals = make_cumvalues(dist2)
try: try:
spm = strokedata.loc[:, 'spm'] spm = strokedata.loc[:, 'spm']
@@ -3727,7 +3798,7 @@ def handle_c2_async_workout(alldata, userid, c2token, c2id, delaysec,
' lapIdx': lapidx, ' lapIdx': lapidx,
' WorkoutState': 4, ' WorkoutState': 4,
' ElapsedTime (sec)': seconds, ' ElapsedTime (sec)': seconds,
'cum_dist': dist2 'cum_dist': cumdist
}) })
df.sort_values(by='TimeStamp (sec)', ascending=True) df.sort_values(by='TimeStamp (sec)', ascending=True)
+9 -10
View File
@@ -8,7 +8,7 @@
<ul class="main-content"> <ul class="main-content">
<li class="grid_4"> <li class="grid_4">
<p>On this page, a work in progress, I will collect useful information <p>On this page, I will collect useful information
for developers of rowing data apps and hardware.</p> for developers of rowing data apps and hardware.</p>
<p>I presume you have an app (smartphone app, dedicated hardware, web site) <p>I presume you have an app (smartphone app, dedicated hardware, web site)
@@ -61,11 +61,11 @@
</ul></p> </ul></p>
<h2>Using the REST API</h2> <h2>Using the REST API</h2>
<p>We are building a REST API which will allow you to post and <p>We have a REST API which will allow you to post and
receive stroke receive stroke
data from the site directly.</p> data from the site directly.</p>
<p>The REST API is a work in progress. We are open to improvement <p>We are open to improvement
suggestions (provided they don't break existing apps). Please send suggestions (provided they don't break existing apps). Please send
email to <a href="mailto:info@rowsandall.com">info@rowsandall.com</a> email to <a href="mailto:info@rowsandall.com">info@rowsandall.com</a>
with questions and/or suggestions. We with questions and/or suggestions. We
@@ -84,7 +84,6 @@
<li>Disadvantages <li>Disadvantages
<p><ul class="contentli"> <p><ul class="contentli">
<li>The API is not stable and not fully tested yet.</li>
<li>You need to register your app with us. We can revoke your <li>You need to register your app with us. We can revoke your
permissions if you misuse them.</li> permissions if you misuse them.</li>
<li>The user user must grant permissions to your app.</li> <li>The user user must grant permissions to your app.</li>
@@ -114,7 +113,7 @@
<p>We have disabled the self service app link for security reasons. <p>We have disabled the self service app link for security reasons.
We will replace it with a secure self service app link soon. If you If you
need to register an app, please send email to info@rowsandall.com</p> need to register an app, please send email to info@rowsandall.com</p>
<h3>Authentication</h3> <h3>Authentication</h3>
@@ -728,11 +727,11 @@
<li><b>peakdriveforce</b>: Peak handle force (lbs)</li> <li><b>peakdriveforce</b>: Peak handle force (lbs)</li>
<li><b>lapidx</b>: Lap identifier</li> <li><b>lapidx</b>: Lap identifier</li>
<li><b>hr</b>: Heart rate (beats per minute)</li> <li><b>hr</b>: Heart rate (beats per minute)</li>
<li><b>wash</b>: Wash as defined per Empower oarlock (degrees)</li> <li><b>wash</b>: Wash as defined for your smart power measuring oarlock (degrees)</li>
<li><b>catch</b>: Catch angle per Empower oarlock (degrees)</li> <li><b>catch</b>: Catch angle for your smart power measuring oarlock (degrees)</li>
<li><b>finish</b>: Finish angle per Empower oarlock (degrees)</li> <li><b>finish</b>: Finish angle for your smart power measuring oarlock (degrees)</li>
<li><b>peakforceangle</b>: Peak Force Angle per Empower oarlock (degrees)</li> <li><b>peakforceangle</b>: Peak Force Angle for your smart power measuring oarlock (degrees)</li>
<li><b>slip</b>: Slip as defined per Empower oarlock (degrees)</li> <li><b>slip</b>: Slip as defined for your smart power measuring oarlock (degrees)</li>
</ul> </ul>
</p> </p>
+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 -->
+6 -2
View File
@@ -11,12 +11,16 @@
<th>Distance:</th><td>{{ workout.distance }}m</td> <th>Distance:</th><td>{{ workout.distance }}m</td>
</tr><tr> </tr><tr>
<th>Duration:</th><td>{{ workout.duration |durationprint:"%H:%M:%S.%f" }}</td> <th>Duration:</th><td>{{ workout.duration |durationprint:"%H:%M:%S.%f" }}</td>
</tr><tr> </tr>
{% if workout.privacy != 'hidden' %}
<tr>
<th>Public link to this workout</th> <th>Public link to this workout</th>
<td> <td>
<a href="/rowers/workout/{{ workout.id|encode }}">https://rowsandall.com/rowers/workout/{{ workout.id|encode }}</a> <a href="/rowers/workout/{{ workout.id|encode }}">https://rowsandall.com/rowers/workout/{{ workout.id|encode }}</a>
</td> </td>
</tr><tr> </tr>
{% endif %}
<tr>
<th>Comments</th> <th>Comments</th>
<td> <td>
<a href="/rowers/workout/{{ workout.id|encode }}/comment">Comment ({{ aantalcomments }})</a> <a href="/rowers/workout/{{ workout.id|encode }}/comment">Comment ({{ aantalcomments }})</a>
+150 -65
View File
@@ -7,8 +7,10 @@
{% block main %} {% block main %}
<h1>Import and Export Settings for {{ rower.user.first_name }} {{ rower.user.last_name }}</h1> <h1>Import and Export Settings for {{ rower.user.first_name }} {{ rower.user.last_name }}</h1>
<form enctype="multipart/form-data" action="" method="post">
{% csrf_token %}
<ul class="main-content"> <ul class="main-content">
<li class="grid_2"> <li class="grid_4">
<p>You are currently connected to: <p>You are currently connected to:
{% if rower.c2token is not None and rower.c2token != '' %} {% if rower.c2token is not None and rower.c2token != '' %}
Concept2 Logbook, Concept2 Logbook,
@@ -32,50 +34,158 @@
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>
<p>
{% if form.errors %} Click on the icons to establish the connection or to renew the authorization.
<p style="color: red;"> </p>
Please correct the error{{ form.errors|pluralize }} below. <p>
</p> By default, imported workouts are set to have a public URL. However, new workouts can be set to
{% endif %} private by default with the following setting:
<p> <table>
<form enctype="multipart/form-data" action="" method="post"> {{ forms.imports_are_private.as_table }}
<table> <input type="submit" value="Save">
{{ form.as_table }} </table>
</table> </p>
{% csrf_token %} </li>
<input type="submit" value="Save"> <li class="grid_4">
</form> <h2>API Key</h2>
</p> <p>{{ apikey }}</p>
{% if rower.garmintoken and rower.garmintoken != '' %} <p>
<p> <a href="/rowers/me/regenerateapikey/">Regenerate</a>
<em>You are connected to Garmin.</em> Switching off Garmin Connect sync is on the </p>
<a href="https://connect.garmin.com/modern/settings/accountInformation">Account settings</a> <p>This API key can be used to access the Rowsandall API. It is used by some third party applications to access your data. Keep it secret.</p>
page. Look for the "Rowsandall" app. </li>
</p>
{% endif %} {% if form.errors %}
<p> <li class="rounder">
Garmin Connnect has no manual sync, so connecting your account to your Garmin account will <p style="color: red;">
automatically auto-sync workouts from Garmin to Rowsandall (but not in the other direction). If you Please correct the error{{ form.errors|pluralize }} below.
want to export our structured workout sessions to your Garmin device, you have to set the "Garmin Activity" </p>
to a activity type that is supported by your watch. Not all watches support "Custom" activities, so </li>
you may have to set your activity to Run or Ride while rowing. {% endif %}
</p> <li class="rounder">
<p> <h2>NK</h2>
Strava Auto Import also imports activity changes on Strava to Rowsandall, except when you delete <table>
a workout on Strava. If you want Deletions to propagate to Rowsandall, tick the Strava Auto Delete {{ forms.nk.as_table }}
check box. <input type="submit" value="Save">
</p> </table>
<p><a href="/rowers/me/nkauthorize/"><img src="/static/img/NKLiNKLogbook.png" alt="connect with NK Logbook" width="120"></a></p>
</li>
<li class="rounder">
<h2>Concept2</h2>
<table>
{{ forms.c2.as_table }}
<input type="submit" value="Save">
</table>
<p><a href="/rowers/me/c2authorize/"><img src="/static/img/blueC2logo.png" alt="connect with Concept2" width="120"></a></p>
</li>
<li class="rounder">
<h2>RP3</h2>
<table>
{{ forms.rp3.as_table }}
<input type="submit" value="Save">
</table>
<p><a href="/rowers/me/rp3authorize"><img src="/static/img/logo-rp3-full-black.png"
alt="connect with RP3" width="130"></a></p>
</li>
<li class="rounder">
<h2>Rojabo</h2>
<p><a href="/rowers/me/rojaboauthorize"><img src="/static/img/rojabo.png"
alt="connect with Rojabo" width="130"></a></p>
</li>
<li class="rounder">
<h2>Intervals.icu</h2>
<table>
{{ forms.intervals.as_table }}
<input type="submit" value="Save">
</table>
<p><a href="/rowers/me/intervalsauthorize"><img src="/static/img/intervals_logo_with_name.png"
alt="connect with intervals.icu"></a></p>
</li>
<li class="rounder">
<h2>SportTracks</h2>
<table>
{{ forms.sporttracks.as_table }}
<input type="submit" value="Save">
</table>
<p><a href="/rowers/me/sporttracksauthorize/"><img src="/static/img/sporttracks-button.png" alt="connect with SportTracks" width="120"></a></p>
</li>
<li class="rounder">
<h2>TrainingPeaks</h2>
<table>
{{ forms.trainingpeaks.as_table }}
<input type="submit" value="Save">
</table>
<p><a href="/rowers/me/tpauthorize/"><img src="/static/img/TP_logo_horz_2_color.png"
alt="connect with Polar" width="130"></a></p>
</li>
<li class="rounder">
<h2>Polar</h2>
<table>
{{ forms.polar.as_table }}
<input type="submit" value="Save">
</table>
<p><a href="/rowers/me/polarauthorize/"><img src="/static/img/Polar_connectwith_btn_white.png"
alt="connect with Polar" width="130"></a></p>
</li>
<li class="rounder">
<h2>Garmin Connect</h2>
<table>
{{ forms.garmin.as_table }}
<input type="submit" value="Save">
</table>
<p><a href="/rowers/me/garminauthorize"><img src="/static/img/garmin_badge_130.png"
alt="connect with Garmin" width="130"></a></p>
<p>
Garmin Connnect has no manual sync, so connecting your account to your Garmin account will
automatically auto-sync workouts from Garmin to Rowsandall (but not in the other direction). If you
want to export our structured workout sessions to your Garmin device, you have to set the "Garmin Activity"
to a activity type that is supported by your watch. Not all watches support "Custom" activities, so
you may have to set your activity to Run or Ride while rowing.
</p>
{% if rower.garmintoken and rower.garmintoken != '' %}
<p>
<em>You are connected to Garmin.</em> Switching off Garmin Connect sync is on the
<a href="https://connect.garmin.com/modern/settings/accountInformation">Account settings</a>
page. Look for the "Rowsandall" app.
</p>
{% endif %}
</li>
<li class="rounder">
<h2>Strava</h2>
<p><em>Warning: API restrictions!</em></p>
<p><input type="submit" value="Save"></p>
{{ forms.strava.as_p }}
<p><a href="/rowers/me/stravaauthorize/"><img src="/static/img/ConnectWithStrava.png" alt="connect with strava" width="120"></a></p>
<p>
Strava Auto Import also imports activity changes on Strava to Rowsandall, except when you delete
a workout on Strava. If you want Deletions to propagate to Rowsandall, tick the Strava Auto Delete
check box.
</p>
{% if rower.stravatoken and rower.stravatoken != '' %}
<p>
<em>You are connected to Strava.</em> Workouts imported from Strava will not be synced
to other platforms and the data will only be visible to you, not your team members or coaches.
We have to respect the terms and conditions of the Strava API, which do not allow us to sync
data to other platforms or to share the data with others.
</p>
{% endif %}
</li> </li>
<li class="grid_2">
{% if grants %} {% if grants %}
<li class="rounder">
<h2>Applications</h2> <h2>Applications</h2>
<p>
These applications have access to your Rowsandall data.
</p>
<table width="100%"> <table width="100%">
<thead> <thead>
<tr> <tr>
@@ -96,36 +206,11 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<li>
{% endif %} {% endif %}
<h2>API Key</h2>
<p>{{ apikey }}</p>
<p>
<a href="/rowers/me/regenerateapikey/">Regenerate</a>
</p>
This API key can be used to access the Rowsandall API. It is used by some third party applications to access your data. Keep it secret.
</li>
</ul> </ul>
<p>Click on one of the icons below to connect to the service of your </form>
choice or to renew the authorization.</p>
<p><a href="/rowers/me/stravaauthorize/"><img src="/static/img/ConnectWithStrava.png" alt="connect with strava" width="120"></a></p>
<p><a href="/rowers/me/c2authorize/"><img src="/static/img/blueC2logo.png" alt="connect with Concept2" width="120"></a></p>
<p><a href="/rowers/me/nkauthorize/"><img src="/static/img/NKLiNKLogbook.png" alt="connect with NK Logbook" width="120"></a></p>
<p><a href="/rowers/me/sporttracksauthorize/"><img src="/static/img/sporttracks-button.png" alt="connect with SportTracks" width="120"></a></p>
<p><a href="/rowers/me/polarauthorize/"><img src="/static/img/Polar_connectwith_btn_white.png"
alt="connect with Polar" width="130"></a></p>
<p><a href="/rowers/me/tpauthorize/"><img src="/static/img/TP_logo_horz_2_color.png"
alt="connect with Polar" width="130"></a></p>
<p><a href="/rowers/me/garminauthorize"><img src="/static/img/garmin_badge_130.png"
alt="connect with Garmin" width="130"></a></p>
<p><a href="/rowers/me/rp3authorize"><img src="/static/img/logo-rp3-full-black.png"
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>
{% if user.is_staff %}
<p><a href="/rowers/me/idokladauthorize/">iDoklad authorize</a></p>
{% endif %}
{% endblock %} {% endblock %}
+1
View File
@@ -35,6 +35,7 @@
<form enctype="multipart/form-data" action="" method="post"> <form enctype="multipart/form-data" action="" method="post">
<table> <table>
{{ userform.as_table }} {{ userform.as_table }}
{{ privateform.as_table }}
{{ accountform.as_table }} {{ accountform.as_table }}
<tr> <tr>
<th>&nbsp;</th><td></td> <th>&nbsp;</th><td></td>
+17 -14
View File
@@ -150,25 +150,28 @@
<th>Duration:</th><td>{{ workout.duration |durationprint:"%H:%M:%S.%f" }}</td> <th>Duration:</th><td>{{ workout.duration |durationprint:"%H:%M:%S.%f" }}</td>
</tr><tr> </tr><tr>
<th>Source:</th><td>{{ workout.workoutsource }}</td> <th>Source:</th><td>{{ workout.workoutsource }}</td>
</tr><tr> </tr>
{% if workout.privacy != 'hidden' %}
<tr>
<th>Public link to this workout:</th> <th>Public link to this workout:</th>
<td> <td>
<a href="/rowers/workout/{{ workout.id|encode }}/">https://rowsandall.com/rowers/workout/{{ workout.id|encode }}/</a> <a href="/rowers/workout/{{ workout.id|encode }}/">https://rowsandall.com/rowers/workout/{{ workout.id|encode }}/</a>
</td> </td>
</tr> </tr>
{% for course in courses %} {% endif %}
<tr> {% for course in courses %}
<th> <tr>
Timed Course: <th>
</th> Timed Course:
<td> </th>
<a href="/rowers/courses/{{ course.id }}"/>{{ course }}</a> <td>
</td> <a href="/rowers/courses/{{ course.id }}"/>{{ course }}</a>
</tr> </td>
{% endfor %} </tr>
</table> {% endfor %}
</li> </table>
<li class="grid_2"> </li>
<li class="grid_2">
{% if form.errors %} {% if form.errors %}
<p style="color: red;"> <p style="color: red;">
Please correct the error{{ form.errors|pluralize }} below. Please correct the error{{ form.errors|pluralize }} below.
+443 -3
View File
@@ -22,12 +22,454 @@ from rowers.opaque import encoder
from rest_framework.test import APIRequestFactory, force_authenticate from rest_framework.test import APIRequestFactory, force_authenticate
UPLOAD_SERVICE_URL = '/rowers/workout/api/upload/'
UPLOAD_SERVICE_SECRET = "FoYezZWLSyfAVimumpHEeYsJjsNCerxV"
import json import json
# import BeautifulSoup
from bs4 import BeautifulSoup
from rowers.ownapistuff import * from rowers.ownapistuff import *
from rowers.views.apiviews import * from rowers.views.apiviews import *
from rowers.models import APIKey from rowers.models import APIKey
from rowers.teams import add_member, add_coach
from rowers.views.analysisviews import histodata
class TeamFactory(factory.DjangoModelFactory):
class Meta:
model = Team
name = factory.LazyAttribute(lambda _: faker.word())
notes = faker.text()
private = 'open'
viewing = 'allmembers'
class StravaPrivacy(TestCase):
def setUp(self):
self.u = UserFactory()
self.u2 = UserFactory()
self.u3 = UserFactory()
self.r = Rower.objects.create(user=self.u,
birthdate=faker.profile()['birthdate'],
gdproptin=True, ftpset=True,surveydone=True,
gdproptindate=timezone.now(),
rowerplan='coach',subscription_id=1)
self.r.stravatoken = '12'
self.r.stravarefreshtoken = '123'
self.r.stravatokenexpirydate = arrow.get(datetime.datetime.now()-datetime.timedelta(days=1)).datetime
self.r.strava_owner_id = 4
self.r.save()
self.c = Client()
self.factory = RequestFactory()
self.password = faker.word()
self.u.set_password(self.password)
self.u.save()
self.factory = APIRequestFactory()
self.r2 = Rower.objects.create(user=self.u2,
birthdate=faker.profile()['birthdate'],
gdproptin=True, ftpset=True,surveydone=True,
gdproptindate=timezone.now(),
rowerplan='coach',clubsize=3)
self.r3 = Rower.objects.create(user=self.u3,
birthdate=faker.profile()['birthdate'],
gdproptin=True, ftpset=True,surveydone=True,
gdproptindate=timezone.now(),
rowerplan='basic')
self.c = Client()
self.password2 = faker.word()
self.u2.set_password(self.password2)
self.u2.save()
self.password3 = faker.word()
self.u3.set_password(self.password3)
self.u3.save()
self.team = TeamFactory(manager=self.u2)
# all are team members
add_member(self.team.id, self.r)
add_member(self.team.id, self.r2)
add_member(self.team.id, self.r3)
self.user_workouts = WorkoutFactory.create_batch(5, user=self.r)
for w in self.user_workouts:
if w.id <= 2:
w.workoutsource = 'strava'
w.privacy = 'hidden'
elif w.id == 3: # user can change privacy but cannot change workoutsource
w.workoutsource = 'strava'
w.privacy = 'visible'
else:
w.workoutsource = 'concept2'
w.privacy = 'visible'
w.team.add(self.team)
w.csvfilename = get_random_file(filename='rowers/tests/testdata/thyro.csv')['filename']
w.save()
# r2 coaches r
add_coach(self.r2, self.r)
self.factory = APIRequestFactory()
def tearDown(self):
for workout in self.user_workouts:
try:
os.remove(workout.csvfilename)
except (OSError, FileNotFoundError, IOError):
pass
# Test if workout with workoutsource strava and privacy hidden can be seen by coach
def test_privacy_coach(self):
login = self.c.login(username=self.u2.username, password=self.password2)
self.assertTrue(login)
w = self.user_workouts[0]
url = reverse('workout_view',kwargs={'id':encoder.encode_hex(w.id)})
response = self.c.get(url)
self.assertEqual(response.status_code,403)
# Same test as above but for 'workout_edit_view'
def test_privacy_coach_edit(self):
login = self.c.login(username=self.u2.username, password=self.password2)
self.assertTrue(login)
w = self.user_workouts[0]
url = reverse('workout_edit_view',kwargs={'id':encoder.encode_hex(w.id)})
response = self.c.get(url)
self.assertEqual(response.status_code,403)
# Test if workout with workoutsource strava and privacy hidden can be seen by team member
def test_privacy_member(self):
login = self.c.login(username=self.u3.username, password=self.password3)
self.assertTrue(login)
w = self.user_workouts[0]
url = reverse('workout_view',kwargs={'id':encoder.encode_hex(w.id)})
response = self.c.get(url)
self.assertEqual(response.status_code,403)
# Same test as above but for 'workout_edit_view'
def test_privacy_member_edit(self):
login = self.c.login(username=self.u3.username, password=self.password3)
self.assertTrue(login)
w = self.user_workouts[0]
url = reverse('workout_edit_view',kwargs={'id':encoder.encode_hex(w.id)})
response = self.c.get(url)
self.assertEqual(response.status_code,403)
# same test as above but with user r and the response code should be 200
def test_privacy_owner(self):
login = self.c.login(username=self.u.username, password=self.password)
self.assertTrue(login)
w = self.user_workouts[0]
url = reverse('workout_view',kwargs={'id':encoder.encode_hex(w.id)})
response = self.c.get(url)
self.assertEqual(response.status_code,200)
# same test as above but for 'workout_edit_view'
def test_privacy_owner_edit(self):
login = self.c.login(username=self.u.username, password=self.password)
self.assertTrue(login)
w = self.user_workouts[0]
url = reverse('workout_edit_view',kwargs={'id':encoder.encode_hex(w.id)})
response = self.c.get(url)
self.assertEqual(response.status_code,200)
# test if list_workouts returns all workouts for user r
def test_list_workouts(self):
login = self.c.login(username=self.u.username, password=self.password)
self.assertTrue(login)
url = reverse('workouts_view')
response = self.c.get(url)
self.assertEqual(response.status_code,200)
# the response.content is html, so we need to parse it
soup = BeautifulSoup(response.content, 'html.parser')
# the workouts look like <a href="/rowers/workout/{id}/...">...</a> and there should be 5 unique ids
# the id is a hex string
workouts = set([a['href'].split('/')[3] for a in soup.find_all('a') if a['href'].startswith('/rowers/workout/')])
# throw out "c2import", "nkimport", "stravaimport", "concept2import", "sporttracksimport" from the set
workouts = set([w for w in workouts if w not in [
'upload', 'addmanual', 'c2import', 'polarimport', 'rp3import', 'nkimport', 'stravaimport', 'concept2import', 'sporttracksimport',
'intervalsimport']])
self.assertEqual(len(workouts),5)
# same test as above but list_workouts with team id = self.team.id
def test_list_workouts_team(self):
login = self.c.login(username=self.u.username, password=self.password)
self.assertTrue(login)
url = reverse('workouts_view',kwargs={'teamid':self.team.id})
response = self.c.get(url)
self.assertEqual(response.status_code,200)
# the response.content is html, so we need to parse it
soup = BeautifulSoup(response.content, 'html.parser')
# the workouts look like <a href="/rowers/workout/{id}/...">...</a> and there should be 5 unique ids
# the id is a hex string
workouts = set([a['href'].split('/')[3] for a in soup.find_all('a') if a['href'].startswith('/rowers/workout/')])
# throw out "c2import", "nkimport", "stravaimport", "concept2import", "sporttracksimport" from the set
workouts = set([w for w in workouts if w not in [
'upload', 'addmanual', 'c2import', 'polarimport', 'rp3import', 'nkimport', 'stravaimport', 'concept2import', 'sporttracksimport',
'intervalsimport']])
self.assertEqual(len(workouts),2)
# same test as the previous one but with self.r2 and the number of workouts found should 0
def test_list_workouts_team_coach(self):
login = self.c.login(username=self.u2.username, password=self.password2)
self.assertTrue(login)
url = reverse('workouts_view',kwargs={'teamid':self.team.id})
response = self.c.get(url)
self.assertEqual(response.status_code,200)
# the response.content is html, so we need to parse it
soup = BeautifulSoup(response.content, 'html.parser')
# the workouts look like <a href="/rowers/workout/{id}/...">...</a> and there should be 5 unique ids
# the id is a hex string
workouts = set([a['href'].split('/')[3] for a in soup.find_all('a') if a['href'].startswith('/rowers/workout/')])
# throw out "c2import", "nkimport", "stravaimport", "concept2import", "sporttracksimport" from the set
workouts = set([w for w in workouts if w not in [
'upload', 'addmanual', 'c2import', 'polarimport', 'rp3import', 'nkimport', 'stravaimport', 'concept2import', 'sporttracksimport',
'intervalsimport']])
self.assertEqual(len(workouts),2)
# same test as above but with without the teamid kwarg but with a rowerid=self.r.id
def test_list_workouts_team_coach2(self):
login = self.c.login(username=self.u2.username, password=self.password2)
self.assertTrue(login)
url = reverse('workouts_view',kwargs={'rowerid':self.r.id})
response = self.c.get(url)
self.assertEqual(response.status_code,200)
# the response.content is html, so we need to parse it
soup = BeautifulSoup(response.content, 'html.parser')
# the workouts look like <a href="/rowers/workout/{id}/...">...</a> and there should be 5 unique ids
# the id is a hex string
workouts = set([a['href'].split('/')[3] for a in soup.find_all('a') if a['href'].startswith('/rowers/workout/')])
# throw out "c2import", "nkimport", "stravaimport", "concept2import", "sporttracksimport" from the set
workouts = set([w for w in workouts if w not in [
'upload', 'addmanual', 'c2import', 'polarimport', 'rp3import', 'nkimport', 'stravaimport', 'concept2import', 'sporttracksimport',
'intervalsimport']])
self.assertEqual(len(workouts),2)
# same test as the previous one but with self.r3 and the number of workouts found should 0
def test_list_workouts_team_member(self):
login = self.c.login(username=self.u3.username, password=self.password3)
self.assertTrue(login)
url = reverse('workouts_view',kwargs={'teamid':self.team.id})
response = self.c.get(url)
self.assertEqual(response.status_code,200)
# the response.content is html, so we need to parse it
soup = BeautifulSoup(response.content, 'html.parser')
# the workouts look like <a href="/rowers/workout/{id}/...">...</a> and there should be 5 unique ids
# the id is a hex string
workouts = set([a['href'].split('/')[3] for a in soup.find_all('a') if a['href'].startswith('/rowers/workout/')])
# throw out "c2import", "nkimport", "stravaimport", "concept2import", "sporttracksimport" from the set
workouts = set([w for w in workouts if w not in [
'upload', 'addmanual', 'c2import', 'polarimport', 'rp3import', 'nkimport', 'stravaimport', 'concept2import', 'sporttracksimport',
'intervalsimport']])
self.assertEqual(len(workouts),2)
# now test strava import and test if the created workout has workoutsource strava and privacy hidden
@patch('rowers.utils.requests.get', side_effect=mocked_requests)
@patch('rowers.integrations.strava.requests.post', side_effect=mocked_requests)
@patch('rowers.dataprep.read_data')
def test_stravaimport(self, mock_get, mock_post, mocked_read_data):
login = self.c.login(username=self.u.username, password=self.password)
self.assertTrue(login)
# remove all self.workouts
Workout.objects.filter(user=self.r).delete()
# create a workout using dataprep.new_workout_from_file with workoutsource = strava
result = get_random_file(filename='rowers/tests/testdata/thyro.csv')
workout_id, message, filename = dataprep.new_workout_from_file(self.r, result['filename'],
workoutsource='strava', makeprivate=True)
# check if the workout was created
ws = Workout.objects.filter(user=self.r)
self.assertEqual(len(ws),1)
w = ws[0]
self.assertEqual(w.workoutsource,'strava')
self.assertEqual(w.privacy,'hidden')
# same as test above but makeprivate = False
@patch('rowers.utils.requests.get', side_effect=mocked_requests)
@patch('rowers.integrations.strava.requests.post', side_effect=mocked_requests)
@patch('rowers.dataprep.read_data')
def test_stravaimport_public(self, mock_get, mock_post, mocked_read_data):
login = self.c.login(username=self.u.username, password=self.password)
self.assertTrue(login)
# remove all self.workouts
Workout.objects.filter(user=self.r).delete()
# create a workout using dataprep.new_workout_from_file with workoutsource = strava
result = get_random_file(filename='rowers/tests/testdata/thyro.csv')
workout_id, message, filename = dataprep.new_workout_from_file(self.r, result['filename'],
workoutsource='strava', makeprivate=False)
# check if the workout was created
ws = Workout.objects.filter(user=self.r)
self.assertEqual(len(ws),1)
w = ws[0]
self.assertEqual(w.workoutsource,'strava')
self.assertEqual(w.privacy,'hidden')
# test ownapi with stravaid = '122'
def test_ownapi(self):
# remove all self.workouts
Workout.objects.filter(user=self.r).delete()
result = get_random_file(filename='rowers/tests/testdata/thyro.csv')
uploadoptions = {
'workouttype': 'water',
'boattype': '1x',
'notes': 'A test file upload',
'stravaid': '122',
'secret': UPLOAD_SERVICE_SECRET,
'user': self.u.id,
'file': result['filename'],
}
url = reverse('workout_upload_api')
response = self.c.post(url, uploadoptions)
self.assertEqual(response.status_code,200)
# check if the workout was created
ws = Workout.objects.filter(user=self.r)
self.assertEqual(len(ws),1)
w = ws[0]
self.assertEqual(w.workoutsource,'strava')
self.assertEqual(w.privacy,'hidden')
# test some analysis, should only use the workouts with workoutsource != strava
#@patch('rowers.dataprep.read_data', side_effect=mocked_read_data)
#def test_workouts_analysis(self, mocked_read_data):
def test_workouts_analysis(self):
login = self.c.login(username=self.u.username, password=self.password)
self.assertTrue(login)
url = '/rowers/history/'
response = self.c.get(url)
self.assertEqual(response.status_code,200)
url = '/rowers/history/data/'
response = self.c.get(url)
self.assertEqual(response.status_code,200)
# response.json() has a key "script" with a javascript script
# check if this is correct
self.assertTrue('script' in response.json())
# now check histogram
startdate = (self.user_workouts[0].startdatetime-datetime.timedelta(days=3)).date()
enddate = (self.user_workouts[0].startdatetime+datetime.timedelta(days=3)).date()
# make sure the dates are not naive
try:
startdate = pytz.utc.localize(startdate)
except (ValueError, AttributeError):
pass
try:
enddate = pytz.utc.localize(enddate)
except (ValueError, AttributeError):
pass
form_data = {
'function':'histo',
'xparam':'hr',
'plotfield':'spm',
'yparam':'pace',
'groupby':'spm',
'palette':'monochrome_blue',
'xaxis':'time',
'yaxis1':'power',
'yaxis2':'hr',
'startdate':startdate,
'enddate':enddate,
'plottype':'scatter',
'spmmin':15,
'spmmax':55,
'workmin':0,
'workmax':1500,
'includereststrokes':False,
'modality':'all',
'waterboattype':['1x','2x','4x'],
'userid':self.u.id,
'workouts':[w.id for w in Workout.objects.filter(user=self.r)],
}
form = AnalysisChoiceForm(form_data)
optionsform = AnalysisOptionsForm(form_data)
dateform = DateRangeForm(form_data)
result = form.is_valid()
if not result:
print(form.errors)
self.assertTrue(form.is_valid())
self.assertTrue(optionsform.is_valid())
self.assertTrue(dateform.is_valid())
response = self.c.post('/rowers/user-analysis-select/',form_data)
self.assertEqual(response.status_code,200)
# count number of workouts by counting the number of occurences of '<label for="id_workouts_xx">' in response.content where xx is a number
# print all lines of response.content that contain '<label for="id_workouts_'
#print([line for line in response.content.decode('utf-8').split('\n') if '<label for="id_workouts_' in line])
#print(form_data['workouts'])
#self.assertEqual(response.content.count(b'<label for="id_workouts_'),2) <-- if we forbid the user to use strava workouts
self.assertEqual(response.content.count(b'<label for="id_workouts_'),5)
# get data from histodata function
ws = Workout.objects.filter(user=self.r)
script, div = histodata(ws,form_data)
# script has a line starting with 'data = [ ... ]'
# we need to get that line
data = [line for line in script.split('\n') if line.startswith('data = [')][0]
# the line should be a list of float values
self.assertTrue(data.startswith('data = ['))
self.assertTrue(data.endswith(']'))
# count the number of commas between the brackets
#self.assertEqual(data.count(','),2062) <-- if we forbid the user to use strava workouts
self.assertEqual(data.count(','),5155)
class OwnApi(TestCase): class OwnApi(TestCase):
def setUp(self): def setUp(self):
self.u = UserFactory() self.u = UserFactory()
@@ -36,9 +478,7 @@ class OwnApi(TestCase):
birthdate=faker.profile()['birthdate'], birthdate=faker.profile()['birthdate'],
gdproptin=True, ftpset=True,surveydone=True, gdproptin=True, ftpset=True,surveydone=True,
gdproptindate=timezone.now(), gdproptindate=timezone.now(),
rowerplan='coach',subscription_id=1) rowerplan='pro',subscription_id=1)
self.c = Client() self.c = Client()
self.user_workouts = WorkoutFactory.create_batch(5, user=self.r) self.user_workouts = WorkoutFactory.create_batch(5, user=self.r)
self.factory = RequestFactory() self.factory = RequestFactory()
+88
View File
@@ -106,10 +106,26 @@ class ChallengesTest(TestCase):
workouttype = 'water', workouttype = 'water',
) )
self.wthyro.startdatetime = arrow.get(nu).datetime self.wthyro.startdatetime = arrow.get(nu).datetime
self.wthyro.date = nu.date() self.wthyro.date = nu.date()
self.wthyro.save() self.wthyro.save()
result = get_random_file(filename='rowers/tests/testdata/thyro.csv')
self.w_strava = WorkoutFactory(user=self.r,
csvfilename=result['filename'],
starttime=result['starttime'],
startdatetime=result['startdatetime'],
duration=result['duration'],
distance=result['totaldist'],
workouttype = 'water',
workoutsource = 'strava',
privacy = 'hidden',
)
self.w_strava.startdatetime = arrow.get(nu).datetime
self.w_strava.date = nu.date()
self.w_strava.save()
result = get_random_file(filename='rowers/tests/testdata/thyro.csv') result = get_random_file(filename='rowers/tests/testdata/thyro.csv')
self.wthyro2 = WorkoutFactory(user=self.r2, self.wthyro2 = WorkoutFactory(user=self.r2,
csvfilename=result['filename'], csvfilename=result['filename'],
@@ -591,6 +607,78 @@ class ChallengesTest(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# repeat previous test for self.w_strava, but the response status of virtualevent_submit_result_view should be 403 and len(records) should be 0
@patch('django.contrib.gis.geoip2.GeoIP2.city', side_effect=mocked_requests)
def test_fastestrace_view_strava(self, mock_get):
login = self.c.login(username=self.u.username, password=self.password)
self.assertTrue(login)
race = self.FastestRace
if self.r.birthdate:
age = calculate_age(self.r.birthdate)
else:
age = 25
# look at event
url = reverse('virtualevent_view',kwargs={'id':race.id})
response = self.c.get(url)
self.assertEqual(response.status_code,200)
# register
url = reverse('virtualevent_register_view',kwargs={'id':race.id})
response = self.c.get(url)
self.assertEqual(response.status_code,200)
form_data = {
'teamname': 'ApeTeam',
'boatclass': 'water',
'boattype': '1x',
'weightcategory': 'hwt',
'adaptiveclass': 'None',
'age': age,
'mix': False,
'acceptsocialmedia': True,
}
form = VirtualRaceResultForm(form_data)
self.assertTrue(form.is_valid())
response = self.c.post(url,form_data,follow=True)
expected_url = reverse('virtualevent_view',kwargs={'id':race.id})
self.assertRedirects(response, expected_url=expected_url,
status_code=302,target_status_code=200)
self.assertEqual(response.status_code, 200)
# submit workout
url = reverse('virtualevent_submit_result_view',kwargs={'id':race.id,'workoutid':self.w_strava.id})
response = self.c.get(url)
self.assertEqual(response.status_code, 200)
# response.content should have a form with only one instance of <label for="id_workouts_0">
self.assertEqual(response.content.count(b'<label for="id_workouts_0">'),1)
records = IndoorVirtualRaceResult.objects.filter(userid=self.u.id)
self.assertEqual(len(records),1)
record = records[0]
form_data = {
'workouts':[self.w_strava.id],
'record':record.id,
}
response = self.c.post(url,form_data,follow=True)
self.assertEqual(response.status_code, 200)
# in response.content, there should be a p with class errormessage and the text "Error in form"
self.assertTrue(b'Error in form' in response.content)
@patch('django.contrib.gis.geoip2.GeoIP2.city', side_effect=mocked_requests) @patch('django.contrib.gis.geoip2.GeoIP2.city', side_effect=mocked_requests)
def test_virtualevents_view(self, mock_get): def test_virtualevents_view(self, mock_get):
Binary file not shown.
+81 -24
View File
@@ -130,19 +130,49 @@ 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
try: if w.user.strava_auto_export is True:
do_strava_export = options['upload_to_Strava'] or do_strava_export do_strava_export = True
except KeyError: else:
pass try:
do_strava_export = options['upload_to_Strava'] or do_strava_export
except KeyError:
pass
try: try:
if options['stravaid'] != 0 and options['stravaid'] != '': # pragma: no cover if options['stravaid'] != 0 and options['stravaid'] != '': # pragma: no cover
w.uploadedtostrava = options['stravaid'] w.uploadedtostrava = options['stravaid']
# upload_to_strava = False # upload_to_strava = False
do_strava_export = False do_strava_export = False
w.workoutsource = 'strava'
w.privacy = 'hidden'
w.save() w.save()
record = create_or_update_syncrecord(w.user, w, stravaid=options['stravaid']) record = create_or_update_syncrecord(w.user, w, stravaid=options['stravaid'])
# strava, we shall not sync to other sites -> return
return 1
except KeyError:
pass
do_icu_export = False
if w.user.intervals_auto_export is True:
do_icu_export = True
if w.workoutsource == 'strava':
do_icu_export = False
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: except KeyError:
pass pass
@@ -177,11 +207,16 @@ 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
try: if w.user.c2_auto_export is True:
do_c2_export = options['upload_to_C2'] or do_c2_export do_c2_export = True
except KeyError: if w.workoutsource == 'strava':
pass do_c2_export = False
else:
try:
do_c2_export = options['upload_to_C2'] or do_c2_export
except KeyError:
pass
try: try:
if options['c2id'] != 0 and options['c2id'] != '': # pragma: no cover if options['c2id'] != 0 and options['c2id'] != '': # pragma: no cover
@@ -218,28 +253,27 @@ def do_sync(w, options, quick=False):
dologging('c2_log.log','Error C2') dologging('c2_log.log','Error C2')
pass pass
if do_strava_export: # pragma: no cover if do_icu_export:
strava_integration = StravaIntegration(w.user.user) intervals_integration = IntervalsIntegration(w.user.user)
try: try:
id = strava_integration.workout_export(w) id = intervals_integration.workout_export(w)
dologging( dologging(
'strava_export_log.log', 'intervals.icu.log',
'exporting workout {id} as {type}'.format( 'exporting workout {id} as {type}'.format(
id=w.id, id=w.id,
type=w.workouttype, type=w.workouttype,
) )
) )
except NoTokenError: # pragma: no cover except NoTokenError:
id = 0 id = 0
message = "Please connect to Strava first" dologging('intervals.icu.log','NoTokenError')
except: message = "Please connect to Intervals.icu first"
e = sys.exc_info()[0] except Exception as e:
t = time.localtime() dologging(
timestamp = time.strftime('%b-%d-%Y_%H%M', t) 'intervals.icu.log',
with open('stravalog.log', 'a') as f: e
f.write('\n') )
f.write(timestamp)
f.write(str(e))
do_st_export = w.user.sporttracks_auto_export do_st_export = w.user.sporttracks_auto_export
@@ -252,6 +286,8 @@ def do_sync(w, options, quick=False):
try: # pragma: no cover try: # pragma: no cover
upload_to_st = options['upload_to_SportTracks'] or do_st_export upload_to_st = options['upload_to_SportTracks'] or do_st_export
do_st_export = upload_to_st do_st_export = upload_to_st
if w.workoutsource == 'strava':
do_st_export = False
except KeyError: except KeyError:
upload_to_st = False upload_to_st = False
@@ -274,6 +310,8 @@ def do_sync(w, options, quick=False):
do_tp_export = w.user.trainingpeaks_auto_export do_tp_export = w.user.trainingpeaks_auto_export
try: try:
upload_to_tp = options['upload_to_TrainingPeaks'] or do_tp_export upload_to_tp = options['upload_to_TrainingPeaks'] or do_tp_export
if w.workoutsource == 'strava':
do_tp_export = False
do_tp_export = upload_to_tp do_tp_export = upload_to_tp
except KeyError: except KeyError:
upload_to_st = False upload_to_st = False
@@ -291,4 +329,23 @@ def do_sync(w, options, quick=False):
dologging('tp_export.log','No Token Error') dologging('tp_export.log','No Token Error')
return 0 return 0
# we do Strava last.
if do_strava_export: # pragma: no cover
strava_integration = StravaIntegration(w.user.user)
try:
id = strava_integration.workout_export(w)
dologging(
'strava_export_log.log',
'exporting workout {id} as {type}'.format(
id=w.id,
type=w.workouttype,
)
)
except NoTokenError: # pragma: no cover
id = 0
message = "Please connect to Strava first"
except Exception as e:
dologging('stravalog.log', e)
return 1 return 1
+3 -1
View File
@@ -80,7 +80,8 @@ class WorkoutViewSet(viewsets.ModelViewSet):
def get_queryset(self): # pragma: no cover def get_queryset(self): # pragma: no cover
try: try:
r = Rower.objects.get(user=self.request.user) r = Rower.objects.get(user=self.request.user)
return Workout.objects.filter(user=r).order_by("-date", "-starttime") #return Workout.objects.filter(user=r).order_by("-date", "-starttime")
return Workout.objects.filter(user=r).exclude(workoutsource='strava').order_by("-date", "-starttime")
except TypeError: except TypeError:
return [] return []
@@ -662,6 +663,7 @@ urlpatterns = [
re_path(r'^me/messages/$', views.user_messages, name='user_messages'), re_path(r'^me/messages/$', views.user_messages, name='user_messages'),
re_path(r'^me/messages/delete/$', views.user_messages_delete_all, name='user_messages_delete_all'), re_path(r'^me/messages/delete/$', views.user_messages_delete_all, name='user_messages_delete_all'),
re_path(r'^me/messages/(?P<id>\d+)/markread/$', views.user_message_markread, name='user_message_markread'), re_path(r'^me/messages/(?P<id>\d+)/markread/$', views.user_message_markread, name='user_message_markread'),
re_path(r'^me/messages/(?P<id>\d+)/delete/$', views.user_message_delete, name='user_message_delete'),
re_path(r'^me/messages/user/(?P<userid>\d+)/$', views.user_messages, name='user_messages'), re_path(r'^me/messages/user/(?P<userid>\d+)/$', views.user_messages, name='user_messages'),
re_path(r'^me/delete/$', views.remove_user, name='remove_user'), re_path(r'^me/delete/$', views.remove_user, name='remove_user'),
re_path(r'^survey/$', views.survey, name='survey'), re_path(r'^survey/$', views.survey, name='survey'),
+50 -8
View File
@@ -48,6 +48,9 @@ def analysis_new(request,
firstworkout = get_workout(id) firstworkout = get_workout(id)
if not is_workout_team(request.user, firstworkout): # pragma: no cover if not is_workout_team(request.user, firstworkout): # pragma: no cover
raise PermissionDenied("You are not allowed to use this workout") raise PermissionDenied("You are not allowed to use this workout")
#if workout_is_strava(firstworkout):
# messages.error(request, "You cannot use Strava workouts for analysis")
# raise PermissionDenied("You cannot use Strava workouts for analysis")
firstworkoutquery = Workout.objects.filter(id=encoder.decode_hex(id)) firstworkoutquery = Workout.objects.filter(id=encoder.decode_hex(id))
try: try:
@@ -199,14 +202,14 @@ def analysis_new(request,
startdatetime__lte=enddate, startdatetime__lte=enddate,
workouttype__in=modalities, workouttype__in=modalities,
rankingpiece__in=rankingtypes, rankingpiece__in=rankingtypes,
) )#.exclude(workoutsource='strava')
elif theteam is not None and theteam.viewing == 'coachonly': # pragma: no cover elif theteam is not None and theteam.viewing == 'coachonly': # pragma: no cover
workouts = Workout.objects.filter(team=theteam, user=r, workouts = Workout.objects.filter(team=theteam, user=r,
startdatetime__gte=startdate, startdatetime__gte=startdate,
startdatetime__lte=enddate, startdatetime__lte=enddate,
workouttype__in=modalities, workouttype__in=modalities,
rankingpiece__in=rankingtypes, rankingpiece__in=rankingtypes,
) )#.exclude(workoutsource='strava')
elif thesession is not None: elif thesession is not None:
workouts = get_workouts_session(r, thesession) workouts = get_workouts_session(r, thesession)
else: else:
@@ -218,6 +221,7 @@ def analysis_new(request,
) )
if firstworkout: if firstworkout:
workouts = firstworkoutquery | workouts workouts = firstworkoutquery | workouts
workouts = workouts.order_by( workouts = workouts.order_by(
"-date", "-starttime" "-date", "-starttime"
).exclude(boattype__in=negtypes) ).exclude(boattype__in=negtypes)
@@ -253,7 +257,7 @@ def analysis_new(request,
else: else:
selectedworkouts = Workout.objects.filter(id__in=ids) selectedworkouts = Workout.objects.filter(id__in=ids)
form.fields["workouts"].queryset = workouts | selectedworkouts form.fields["workouts"].queryset = (workouts | selectedworkouts)#.exclude(workoutsource='strava')
optionsform = AnalysisOptionsForm(initial={ optionsform = AnalysisOptionsForm(initial={
'modality': modality, 'modality': modality,
@@ -363,6 +367,10 @@ def trendflexdata(workouts, options, userid=0):
savedata = options.get('savedata',False) savedata = options.get('savedata',False)
#try:
# workouts = workouts.exclude(workoutsource='strava')
#except AttributeError: # pragma: no cover
# workouts = [w for w in workouts if w.workoutsource != 'strava']
fieldlist, fielddict = dataprep.getstatsfields() fieldlist, fielddict = dataprep.getstatsfields()
fieldlist = [xparam, yparam, groupby, fieldlist = [xparam, yparam, groupby,
@@ -566,6 +574,11 @@ def flexalldata(workouts, options):
trendline = options['trendline'] trendline = options['trendline']
promember = True promember = True
#try:
# workouts = workouts.exclude(workoutsource='strava')
#except AttributeError: # pragma: no cover
# workouts = [w for w in workouts if w.workoutsource != 'strava']
workstrokesonly = not includereststrokes workstrokesonly = not includereststrokes
userid = options['userid'] userid = options['userid']
@@ -612,6 +625,12 @@ def histodata(workouts, options):
workmax = options['workmax'] workmax = options['workmax']
userid = options['userid'] userid = options['userid']
#try:
# workouts = workouts.exclude(workoutsource='strava')
#except AttributeError: # pragma: no cover
# workouts = [w for w in workouts if w.workoutsource != 'strava']
if userid == 0: # pragma: no cover if userid == 0: # pragma: no cover
extratitle = '' extratitle = ''
else: else:
@@ -646,6 +665,7 @@ def cpdata(workouts, options):
u = User.objects.get(id=userid) u = User.objects.get(id=userid)
r = u.rower r = u.rower
delta, cpvalue, avgpower, workoutnames, urls = dataprep.fetchcp_new( delta, cpvalue, avgpower, workoutnames, urls = dataprep.fetchcp_new(
r, workouts) r, workouts)
@@ -798,6 +818,11 @@ def cpdata(workouts, options):
def statsdata(workouts, options): def statsdata(workouts, options):
#try:
# workouts = workouts.exclude(workoutsource='strava')
#except AttributeError: # pragma: no cover
# workouts = [w for w in workouts if w.workoutsource != 'strava']
includereststrokes = options['includereststrokes'] includereststrokes = options['includereststrokes']
ids = options['ids'] ids = options['ids']
@@ -872,6 +897,11 @@ def statsdata(workouts, options):
def comparisondata(workouts, options): def comparisondata(workouts, options):
#try:
# workouts = workouts.exclude(workoutsource='strava')
#except AttributeError: # pragma: no cover
# workouts = [w for w in workouts if w.workoutsource != 'strava']
includereststrokes = options['includereststrokes'] includereststrokes = options['includereststrokes']
xparam = options['xaxis'] xparam = options['xaxis']
yparam1 = options['yaxis1'] yparam1 = options['yaxis1']
@@ -915,6 +945,10 @@ def comparisondata(workouts, options):
def boxplotdata(workouts, options): def boxplotdata(workouts, options):
#try:
# workouts = workouts.exclude(workoutsource='strava')
#except AttributeError:
# workouts = [w for w in workouts if w.workoutsource != 'strava']
includereststrokes = options['includereststrokes'] includereststrokes = options['includereststrokes']
spmmin = options['spmmin'] spmmin = options['spmmin']
@@ -1020,11 +1054,15 @@ def analysis_view_data(request, userid=0):
for id in ids: for id in ids:
try: try:
workouts.append(Workout.objects.get(id=id)) w = Workout.objects.get(id=id)
#if w.workoutsource != 'strava':
# workouts.append(w)
workouts.append(w)
except Workout.DoesNotExist: # pragma: no cover except Workout.DoesNotExist: # pragma: no cover
pass pass
if function == 'boxplot': if function == 'boxplot':
script, div = boxplotdata(workouts, options) script, div = boxplotdata(workouts, options)
elif function == 'trendflex': # pragma: no cover elif function == 'trendflex': # pragma: no cover
@@ -1069,7 +1107,7 @@ def create_marker_workouts_view(request, userid=0,
workouts = Workout.objects.filter(user=theuser.rower, date__gte=startdate, workouts = Workout.objects.filter(user=theuser.rower, date__gte=startdate,
date__lte=enddate, date__lte=enddate,
workouttype__in=mytypes.rowtypes, workouttype__in=mytypes.rowtypes,
duplicate=False).order_by('date') duplicate=False).order_by('date')#.exclude(workoutsource='strava')
for workout in workouts: for workout in workouts:
_ = dataprep.check_marker(workout) _ = dataprep.check_marker(workout)
@@ -1113,7 +1151,7 @@ def goldmedalscores_view(request, userid=0,
theuser, startdate=startdate, enddate=enddate, theuser, startdate=startdate, enddate=enddate,
) )
bestworkouts = Workout.objects.filter(id__in=ids).order_by('-date') bestworkouts = Workout.objects.filter(id__in=ids).order_by('-date')#.exclude(workoutsource='strava')
breadcrumbs = [ breadcrumbs = [
{ {
@@ -1311,7 +1349,7 @@ def performancemanager_view(request, userid=0, mode='rower',
user = therower, date__gte=startdate-datetime.timedelta(days=90), user = therower, date__gte=startdate-datetime.timedelta(days=90),
date__lte=enddate, date__lte=enddate,
duplicate=False, duplicate=False,
rankingpiece=True, workouttype__in=mytypes.rowtypes).order_by('date') rankingpiece=True, workouttype__in=mytypes.rowtypes).order_by('date')#.exclude(workoutsource='strava')
ids = [w.id for w in markerworkouts] ids = [w.id for w in markerworkouts]
form = PerformanceManagerForm(initial={ form = PerformanceManagerForm(initial={
@@ -1323,7 +1361,7 @@ def performancemanager_view(request, userid=0, mode='rower',
ids = pd.Series(ids, dtype='int').dropna().values ids = pd.Series(ids, dtype='int').dropna().values
bestworkouts = Workout.objects.filter(id__in=ids).order_by('-date') bestworkouts = Workout.objects.filter(id__in=ids).order_by('-date')#.exclude(workoutsource='strava')
breadcrumbs = [ breadcrumbs = [
{ {
@@ -2276,6 +2314,8 @@ def history_view_data(request, userid=0):
ddf = ddf.with_columns(pl.col("time").diff().clip(lower_bound=0).alias("deltat")) ddf = ddf.with_columns(pl.col("time").diff().clip(lower_bound=0).alias("deltat"))
except KeyError: # pragma: no cover except KeyError: # pragma: no cover
pass pass
except ColumnNotFoundError:
pass
ddf = dataprep.clean_df_stats_pl(ddf, workstrokesonly=False, ddf = dataprep.clean_df_stats_pl(ddf, workstrokesonly=False,
ignoreadvanced=True) ignoreadvanced=True)
@@ -2288,6 +2328,8 @@ def history_view_data(request, userid=0):
ddict['hrmax'] = int(ddf['hr'].max()) ddict['hrmax'] = int(ddf['hr'].max())
except (KeyError, ValueError, AttributeError, ColumnNotFoundError): # pragma: no cover except (KeyError, ValueError, AttributeError, ColumnNotFoundError): # pragma: no cover
ddict['hrmax'] = 0 ddict['hrmax'] = 0
except ColumnNotFoundError:
ddict['hrmax'] = 0
ddict['powermean'] = int(wavg(ddf, 'power', 'deltat')) ddict['powermean'] = int(wavg(ddf, 'power', 'deltat'))
try: try:
+26 -5
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
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) 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,
} }
+36 -1
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']
nkids = [int(id) for id in ids] try:
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)
+9 -3
View File
@@ -3397,12 +3397,12 @@ def virtualevent_submit_result_view(request, id=0, workoutid=0):
startdatetime__gte=startdatetime, startdatetime__gte=startdatetime,
startdatetime__lte=enddatetime, startdatetime__lte=enddatetime,
distance__gte=race.approximate_distance, distance__gte=race.approximate_distance,
).order_by("-date", "-startdatetime", "id") ).order_by("-date", "-startdatetime", "id").exclude(workoutsource='strava')
if not ws: # pragma: no cover if not ws: # pragma: no cover
messages.info( messages.info(
request, request,
'You have no workouts executed during the race window. Please upload a result or enter it manually.' 'You have no eligible workouts executed during the race window. Please upload a result or enter it manually.'
) )
url = reverse('virtualevent_view', url = reverse('virtualevent_view',
@@ -3436,6 +3436,7 @@ def virtualevent_submit_result_view(request, id=0, workoutid=0):
splitsecond = 0 splitsecond = 0
recordid = w_form.cleaned_data['record'] recordid = w_form.cleaned_data['record']
else: else:
messages.error(request,"Error in form")
selectedworkout = None selectedworkout = None
if selectedworkout is not None: if selectedworkout is not None:
@@ -3518,7 +3519,12 @@ def virtualevent_submit_result_view(request, id=0, workoutid=0):
else: else:
if workoutid: if workoutid:
workoutdata['initial'] = encoder.decode_hex(workoutid) try:
w = Workout.objects.get(id=workoutid)
if w.workoutsource != 'strava':
workoutdata['initial'] = encoder.decode_hex(workoutid)
except Workout.DoesNotExist:
pass
w_form = WorkoutRaceSelectForm(workoutdata, entries) w_form = WorkoutRaceSelectForm(workoutdata, entries)
breadcrumbs = [ breadcrumbs = [
+10 -2
View File
@@ -28,6 +28,7 @@ from rest_framework.response import Response
from rq.job import Job from rq.job import Job
from rules.contrib.views import permission_required, objectgetter from rules.contrib.views import permission_required, objectgetter
from django.core.cache import cache from django.core.cache import cache
from django.db import models
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from rq.registry import StartedJobRegistry from rq.registry import StartedJobRegistry
from rq.exceptions import NoSuchJobError from rq.exceptions import NoSuchJobError
@@ -81,7 +82,8 @@ from rowers.rower_rules import (
can_add_workout_member, can_plan_user, is_paid_coach, can_add_workout_member, can_plan_user, is_paid_coach,
can_start_trial, can_start_plantrial, can_start_coachtrial, can_start_trial, can_start_plantrial, can_start_coachtrial,
can_plan, is_workout_team, can_plan, is_workout_team,
is_promember,user_is_basic, is_coachtrial, is_coach is_promember,user_is_basic, is_coachtrial, is_coach,
workout_is_strava
) )
from django.shortcuts import render from django.shortcuts import render
@@ -179,7 +181,13 @@ 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, iDokladToken ) StepEditorForm, iDokladToken,
RowerExportFormStrava, RowerExportFormPolar,
RowerExportFormSportTracks, RowerExportFormTrainingPeaks,
RowerExportFormConcept2, RowerExportFormGarmin,
RowerExportFormIntervals, RowerExportFormRP3,
RowerExportFormNK, RowerPrivateImportForm,
)
from rowers.models import ( from rowers.models import (
FavoriteForm, BaseFavoriteFormSet, SiteAnnouncement, BasePlannedSessionFormSet, FavoriteForm, BaseFavoriteFormSet, SiteAnnouncement, BasePlannedSessionFormSet,
get_course_timezone, BaseConditionFormSet, get_course_timezone, BaseConditionFormSet,
+37 -4
View File
@@ -280,7 +280,6 @@ def user_message_delete(request,id=0): # pragma: no cover
url = reverse('user_messages') url = reverse('user_messages')
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
if msg.receiver == request.user.rower: if msg.receiver == request.user.rower:
msg.delete() msg.delete()
messages.info(request,'Deleted message {id}'.format(id=id)) messages.info(request,'Deleted message {id}'.format(id=id))
@@ -458,19 +457,45 @@ def rower_exportsettings_view(request, userid=0):
'polar_auto_import': 'polartoken', 'polar_auto_import': 'polartoken',
'c2_auto_export': 'c2token', 'c2_auto_export': 'c2token',
'c2_auto_import': 'c2token', 'c2_auto_import': 'c2token',
'runkeeper_auto_export': 'runkeepertoken',
'sporttracks_auto_export': 'sporttrackstoken', 'sporttracks_auto_export': 'sporttrackstoken',
'strava_auto_export': 'stravatoken', 'strava_auto_export': 'stravatoken',
'strava_auto_import': 'stravatoken', 'strava_auto_import': 'stravatoken',
'strava_auto_delete': 'stravatoken', 'strava_auto_delete': 'stravatoken',
'trainingpeaks_auto_export': 'tptoken', 'trainingpeaks_auto_export': 'tptoken',
'rp3_auto_import': 'rp3token', 'rp3_auto_import': 'rp3token',
'nk_auto_import': 'nktoken' 'nk_auto_import': 'nktoken',
'intervals_auto_export': 'intervals_token',
'intervals_resample_to_1s': 'intervals_token',
} }
r = getrequestrowercoachee(request, userid=userid) r = getrequestrowercoachee(request, userid=userid)
forms = {
'polar': RowerExportFormPolar(instance=r),
'c2': RowerExportFormConcept2(instance=r),
'sporttracks': RowerExportFormSportTracks(instance=r),
'strava': RowerExportFormStrava(instance=r),
'trainingpeaks': RowerExportFormTrainingPeaks(instance=r),
'rp3': RowerExportFormRP3(instance=r),
'intervals': RowerExportFormIntervals(instance=r),
'nk': RowerExportFormNK(instance=r),
'garmin': RowerExportFormGarmin(instance=r),
'imports_are_private': RowerPrivateImportForm(instance=r)
}
if request.method == 'POST': if request.method == 'POST':
form = RowerExportForm(request.POST) form = RowerExportForm(request.POST)
forms = {
'polar': RowerExportFormPolar(request.POST, instance=r),
'c2': RowerExportFormConcept2(request.POST, instance=r),
'sporttracks': RowerExportFormSportTracks(request.POST, instance=r),
'strava': RowerExportFormStrava(request.POST, instance=r),
'trainingpeaks': RowerExportFormTrainingPeaks(request.POST, instance=r),
'rp3': RowerExportFormRP3(request.POST, instance=r),
'intervals': RowerExportFormIntervals(request.POST, instance=r),
'nk': RowerExportFormNK(request.POST, instance=r),
'garmin': RowerExportFormGarmin(request.POST, instance=r),
'imports_are_private': RowerPrivateImportForm(request.POST, instance=r),
}
if form.is_valid(): if form.is_valid():
cd = form.cleaned_data cd = form.cleaned_data
if r.rowerplan == 'basic': # pragma: no cover if r.rowerplan == 'basic': # pragma: no cover
@@ -529,6 +554,7 @@ def rower_exportsettings_view(request, userid=0):
return render(request, 'rower_exportsettings.html', return render(request, 'rower_exportsettings.html',
{'form': form, {'form': form,
'forms': forms,
'rower': r, 'rower': r,
'breadcrumbs': breadcrumbs, 'breadcrumbs': breadcrumbs,
'grants': grants, 'grants': grants,
@@ -569,11 +595,13 @@ def rower_edit_view(request, rowerid=0, userid=0, message=""):
if request.method == 'POST': if request.method == 'POST':
accountform = AccountRowerForm(request.POST, instance=r) accountform = AccountRowerForm(request.POST, instance=r)
userform = UserForm(request.POST, instance=r.user) userform = UserForm(request.POST, instance=r.user)
privateform = RowerPrivateImportForm(request.POST, instance=r)
if accountform.is_valid() and userform.is_valid(): if accountform.is_valid() and userform.is_valid() and privateform.is_valid():
# process # process
cd = accountform.cleaned_data cd = accountform.cleaned_data
ucd = userform.cleaned_data ucd = userform.cleaned_data
pcd = privateform.cleaned_data
first_name = ucd['first_name'] first_name = ucd['first_name']
last_name = ucd['last_name'] last_name = ucd['last_name']
@@ -609,6 +637,7 @@ def rower_edit_view(request, rowerid=0, userid=0, message=""):
resetbounce = True resetbounce = True
emailalternatives = cd['emailalternatives'] emailalternatives = cd['emailalternatives']
imports_are_private = pcd['imports_are_private']
u.save() u.save()
r.defaulttimezone = defaulttimezone r.defaulttimezone = defaulttimezone
@@ -620,6 +649,7 @@ def rower_edit_view(request, rowerid=0, userid=0, message=""):
r.defaultlandingpage = defaultlandingpage r.defaultlandingpage = defaultlandingpage
r.showfavoritechartnotes = showfavoritechartnotes r.showfavoritechartnotes = showfavoritechartnotes
r.share_course_results = share_course_results r.share_course_results = share_course_results
r.imports_are_private = imports_are_private
r.sex = sex r.sex = sex
r.birthdate = birthdate r.birthdate = birthdate
r.autojoin = autojoin r.autojoin = autojoin
@@ -634,11 +664,13 @@ def rower_edit_view(request, rowerid=0, userid=0, message=""):
accountform = AccountRowerForm(instance=r) accountform = AccountRowerForm(instance=r)
userform = UserForm(instance=u) userform = UserForm(instance=u)
privateform = RowerPrivateImportForm(instance=r)
successmessage = 'Account Information changed' successmessage = 'Account Information changed'
messages.info(request, successmessage) messages.info(request, successmessage)
else: else:
accountform = AccountRowerForm(instance=r) accountform = AccountRowerForm(instance=r)
userform = UserForm(instance=r.user) userform = UserForm(instance=r.user)
privateform = RowerPrivateImportForm(instance=r)
grants = AccessToken.objects.filter(user=request.user) grants = AccessToken.objects.filter(user=request.user)
try: try:
@@ -654,6 +686,7 @@ def rower_edit_view(request, rowerid=0, userid=0, message=""):
'grants': grants, 'grants': grants,
'userform': userform, 'userform': userform,
'accountform': accountform, 'accountform': accountform,
'privateform': privateform,
'rower': r, 'rower': r,
'apikey': apikey.key, 'apikey': apikey.key,
}) })
+29 -24
View File
@@ -687,7 +687,7 @@ def addmanual_view(request, raceid=0):
empowerside = form.cleaned_data.get('empowerside','port') empowerside = form.cleaned_data.get('empowerside','port')
if private: # pragma: no cover if private: # pragma: no cover
privacy = 'private' privacy = 'hidden'
else: else:
privacy = 'visible' privacy = 'visible'
@@ -2204,25 +2204,25 @@ def workouts_view(request, message='', successmessage='',
team=theteam, team=theteam,
startdatetime__gte=startdate, startdatetime__gte=startdate,
startdatetime__lte=enddate, startdatetime__lte=enddate,
privacy='visible').order_by("-date", "-starttime") privacy='visible').order_by("-date", "-starttime").exclude(workoutsource='strava')
g_workouts = Workout.objects.filter( g_workouts = Workout.objects.filter(
team=theteam, team=theteam,
startdatetime__gte=activity_startdate, startdatetime__gte=activity_startdate,
startdatetime__lte=activity_enddate, startdatetime__lte=activity_enddate,
duplicate=False, duplicate=False,
privacy='visible').order_by("-date", "-starttime") privacy='visible').order_by("-date", "-starttime").exclude(workoutsource='strava')
elif theteam.viewing == 'coachonly': # pragma: no cover elif theteam.viewing == 'coachonly': # pragma: no cover
workouts = Workout.objects.filter( workouts = Workout.objects.filter(
team=theteam, user=r, team=theteam, user=r,
startdatetime__gte=startdate, startdatetime__gte=startdate,
startdatetime__lte=enddate, startdatetime__lte=enddate,
privacy='visible').order_by("-startdatetime") privacy='visible').order_by("-startdatetime").exclude(workoutsource='strava')
g_workouts = Workout.objects.filter( g_workouts = Workout.objects.filter(
team=theteam, user=r, team=theteam, user=r,
startdatetime__gte=activity_startdate, startdatetime__gte=activity_startdate,
startdatetime__lte=activity_enddate, startdatetime__lte=activity_enddate,
duplicate=False, duplicate=False,
privacy='visible').order_by("-startdatetime") privacy='visible').order_by("-startdatetime").exclude(workoutsource='strava')
elif request.user != r.user: elif request.user != r.user:
theteam = None theteam = None
@@ -2230,13 +2230,13 @@ def workouts_view(request, message='', successmessage='',
user=r, user=r,
startdatetime__gte=startdate, startdatetime__gte=startdate,
startdatetime__lte=enddate, startdatetime__lte=enddate,
privacy='visible').order_by("-date", "-starttime") privacy='visible').order_by("-date", "-starttime").exclude(workoutsource='strava')
g_workouts = Workout.objects.filter( g_workouts = Workout.objects.filter(
user=r, user=r,
startdatetime__gte=activity_startdate, startdatetime__gte=activity_startdate,
startdatetime__lte=activity_enddate, startdatetime__lte=activity_enddate,
duplicate=False, duplicate=False,
privacy='visible').order_by("-startdatetime") privacy='visible').order_by("-startdatetime").exclude(workoutsource='strava')
else: else:
theteam = None theteam = None
workouts = Workout.objects.filter( workouts = Workout.objects.filter(
@@ -2252,7 +2252,7 @@ def workouts_view(request, message='', successmessage='',
if g_workouts.count() == 0: if g_workouts.count() == 0:
g_workouts = Workout.objects.filter( g_workouts = Workout.objects.filter(
user=r, user=r,
startdatetime__gte=timezone.now()-timedelta(days=15)).order_by("-startdatetime") startdatetime__gte=timezone.now()-timedelta(days=15)).order_by("-startdatetime").exclude(workoutsource='strava')
g_enddate = timezone.now() g_enddate = timezone.now()
g_startdate = (timezone.now()-timedelta(days=15)) g_startdate = (timezone.now()-timedelta(days=15))
@@ -2266,7 +2266,8 @@ def workouts_view(request, message='', successmessage='',
reduce(operator.and_, reduce(operator.and_,
(Q(name__icontains=q) for q in query_list)) | (Q(name__icontains=q) for q in query_list)) |
reduce(operator.and_, reduce(operator.and_,
(Q(notes__icontains=q) for q in query_list)) (Q(notes__icontains=q) for q in query_list)),
exclude_strava=False,
) )
searchform = SearchForm(initial={'q': query}) searchform = SearchForm(initial={'q': query})
else: else:
@@ -4269,7 +4270,7 @@ def workout_flexchart_stacked_view(request, *args, **kwargs):
def workout_unsubscribe_view(request, id=0): def workout_unsubscribe_view(request, id=0):
w = get_workout(id) w = get_workout(id)
if w.privacy == 'private' and w.user.user != request.user: # pragma: no cover if w.privacy == 'hidden' and w.user.user != request.user: # pragma: no cover
return HttpResponseForbidden("Permission error") return HttpResponseForbidden("Permission error")
comments = WorkoutComment.objects.filter(workout=w, comments = WorkoutComment.objects.filter(workout=w,
@@ -4299,7 +4300,7 @@ def workout_unsubscribe_view(request, id=0):
def workout_comment_view(request, id=0): def workout_comment_view(request, id=0):
w = get_workout(id) w = get_workout(id)
if w.privacy == 'private' and w.user.user != request.user: # pragma: no cover if w.privacy == 'hidden' and w.user.user != request.user: # pragma: no cover
return HttpResponseForbidden("Permission error") return HttpResponseForbidden("Permission error")
comments = WorkoutComment.objects.filter(workout=w).order_by("created") comments = WorkoutComment.objects.filter(workout=w).order_by("created")
@@ -4489,7 +4490,7 @@ def workout_edit_view(request, id=0, message="", successmessage=""):
if private: if private:
privacy = 'private' privacy = 'hidden'
else: # pragma: no cover else: # pragma: no cover
privacy = 'visible' privacy = 'visible'
@@ -4699,6 +4700,7 @@ def workout_map_view(request, id=0):
u = w.user.user u = w.user.user
r = getrower(u) r = getrower(u)
rowdata = rdata(csvfile=f1) rowdata = rdata(csvfile=f1)
hascoordinates = 1 hascoordinates = 1
if rowdata != 0: if rowdata != 0:
try: try:
@@ -4933,7 +4935,7 @@ def workout_upload_api(request):
# only allow local host # only allow local host
hostt = request.get_host().split(':') hostt = request.get_host().split(':')
if hostt[0] not in ['localhost', '127.0.0.1', 'dev.rowsandall.com', 'rowsandall.com']: if hostt[0] not in ['localhost', '127.0.0.1', 'dev.rowsandall.com', 'rowsandall.com','testserver']:
message = {'status': 'false', message = {'status': 'false',
'message': 'permission denied for host '+hostt[0]} 'message': 'permission denied for host '+hostt[0]}
return JSONResponse(status=403, data=message) return JSONResponse(status=403, data=message)
@@ -4986,6 +4988,7 @@ def workout_upload_api(request):
boatname = post_data.get('boatName','') boatname = post_data.get('boatName','')
portStarboard = post_data.get('portStarboard', 1) portStarboard = post_data.get('portStarboard', 1)
empowerside = 'port' empowerside = 'port'
stravaid = post_data.get('stravaid','')
if portStarboard == 1: if portStarboard == 1:
empowerside = 'starboard' empowerside = 'starboard'
@@ -5194,6 +5197,8 @@ def workout_upload_view(request,
is_ajax = False is_ajax = False
r = getrower(request.user) r = getrower(request.user)
if r.imports_are_private:
uploadoptions['makeprivate'] = True
if r.rowerplan == 'freecoach': # pragma: no cover if r.rowerplan == 'freecoach': # pragma: no cover
url = reverse('team_workout_upload_view') url = reverse('team_workout_upload_view')
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@@ -5247,6 +5252,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':
@@ -5296,6 +5302,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']
@@ -5313,6 +5320,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,
@@ -5447,6 +5455,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':
@@ -5598,17 +5614,6 @@ def workout_upload_view(request,
return response return response
else: else:
if not is_ajax: if not is_ajax:
if r.c2_auto_export and ispromember(r.user): # pragma: no cover
uploadoptions['upload_to_C2'] = True
if r.strava_auto_export and ispromember(r.user): # pragma: no cover
uploadoptions['upload_to_Strava'] = True
if r.sporttracks_auto_export and ispromember(r.user): # pragma: no cover
uploadoptions['upload_to_SportTracks'] = True
if r.trainingpeaks_auto_export and ispromember(r.user): # pragma: no cover
uploadoptions['upload_to_TrainingPeaks'] = True
form = DocumentsForm(initial=docformoptions) form = DocumentsForm(initial=docformoptions)
optionsform = UploadOptionsForm(initial=uploadoptions, optionsform = UploadOptionsForm(initial=uploadoptions,
+17 -1
View File
@@ -296,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']
@@ -463,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',
+1
View File
@@ -94,6 +94,7 @@ urlpatterns += [
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'^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: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB