Private
Public Access
1
0

Merge branch 'develop' into feature/idoklad

This commit is contained in:
2024-12-26 10:39:04 +01:00
9 changed files with 295 additions and 9 deletions

View File

@@ -1648,7 +1648,7 @@ def read_data(columns, ids=[], doclean=True, workstrokesonly=True, debug=False,
datadf = pl.concat(data) datadf = pl.concat(data)
existing_columns = [col for col in columns if col in datadf.columns] existing_columns = [col for col in columns if col in datadf.columns]
datadf = datadf.select(existing_columns) datadf = datadf.select(existing_columns)
except (ShapeError, SchemaError): except ShapeError:
try: try:
data = [ data = [
df.select(columns) df.select(columns)
@@ -1703,7 +1703,61 @@ def read_data(columns, ids=[], doclean=True, workstrokesonly=True, debug=False,
datadf = pl.concat(data) datadf = pl.concat(data)
except ShapeError: except ShapeError:
return pl.DataFrame() return pl.DataFrame()
except SchemaError:
try:
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
floatcolumns = []
intcolumns = []
stringcolumns = []
for c in columns:
try:
if metricsdicts[c]['numtype'] == 'float':
floatcolumns.append(c)
if metricsdicts[c]['numtype'] == 'integer':
intcolumns.append(c)
except KeyError:
if c[0] == 'f':
stringcolumns.append(c)
else:
intcolumns.append(c)
try:
data = [
df.with_columns(
cs.float().cast(pl.Float64)
).with_columns(
cs.integer().cast(pl.Int64)
).with_columns(
cs.by_name(intcolumns).cast(pl.Int64)
).with_columns(
cs.by_name(floatcolumns).cast(pl.Float64)
).with_columns(
cs.by_name(stringcolumns).cast(pl.String)
)
for df in data
]
except ComputeError:
pass
except ColumnNotFoundError:
pass
try:
datadf = pl.concat(data)
except SchemaError:
try:
data = [
df.with_columns(cs.integer().cast(pl.Float64)) for df in data
]
datadf = pl.concat(data)
except ShapeError:
return pl.DataFrame()

View File

@@ -1,6 +1,7 @@
from .integrations import SyncIntegration, NoTokenError, create_or_update_syncrecord, get_known_ids from .integrations import SyncIntegration, NoTokenError, create_or_update_syncrecord, get_known_ids
from rowers.models import Rower, User, Workout, TombStone from rowers.models import Rower, User, Workout, TombStone, PlannedSession
from rowingdata import rowingdata from rowingdata import rowingdata
from rowers.rower_rules import user_is_not_basic, user_is_coachee
from rowers import mytypes from rowers import mytypes
@@ -49,6 +50,8 @@ headers = {
intervals_authorize_url = 'https://intervals.icu/oauth/authorize?' intervals_authorize_url = 'https://intervals.icu/oauth/authorize?'
intervals_token_url = 'https://intervals.icu/api/oauth/token' intervals_token_url = 'https://intervals.icu/api/oauth/token'
webhookverification = 'JA9Vt6RNH10'
class IntervalsIntegration(SyncIntegration): class IntervalsIntegration(SyncIntegration):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(IntervalsIntegration, self).__init__(*args, **kwargs) super(IntervalsIntegration, self).__init__(*args, **kwargs)
@@ -336,6 +339,28 @@ class IntervalsIntegration(SyncIntegration):
return data return data
def update_plannedsession(self, ps, data, *args, **kwargs):
_ = self.open()
r = self.rower
if data['category'] == 'WORKOUT':
url = self.oauth_data['base_url'] + 'athlete/0/events/' + str(ps.intervals_icu_id) + '/downloadfit'
headers = {
'Authorization': 'Bearer ' + r.intervals_token,
}
response = requests.get(url, headers=headers)
if response.status_code != 200:
dologging('intervals.icu.log', response.text)
else:
filename = 'planned_' + str(ps.intervals_icu_id) + '.fit'
filename2 = 'media/planned_' + str(ps.intervals_icu_id) + '.fit'
with open(filename2, 'wb') as f:
f.write(response.content)
data['fitfile'] = filename
return data
def get_plannedsession(self, id, *args, **kwargs): def get_plannedsession(self, id, *args, **kwargs):
_ = self.open() _ = self.open()
r = self.rower r = self.rower
@@ -396,6 +421,7 @@ class IntervalsIntegration(SyncIntegration):
"name": ps.name, "name": ps.name,
"description": stepstext, "description": stepstext,
"indoor": ps.sessionsport in mytypes.ergtypes, "indoor": ps.sessionsport in mytypes.ergtypes,
'external_id': ps.id,
} }
if ps.sessiontype == 'cycletarget': if ps.sessiontype == 'cycletarget':
@@ -442,3 +468,119 @@ class IntervalsIntegration(SyncIntegration):
ps.save() ps.save()
return 1 return 1
def update_calendar(self, r, event, *args, **kwargs):
try:
records = event["events"]
except KeyError:
records = []
for record in records:
id = record['id']
data = {}
try:
pss = PlannedSession.objects.filter(intervals_icu_id=id)
if pss.count() > 0:
ps = pss[0]
data = self.update_plannedsession(ps, record)
else:
data = self.get_plannedsession(id)
ps = PlannedSession(
manager=r.user,
intervals_icu_id=id,
)
ps.save()
ps.rower.add(r)
except PlannedSession.DoesNotExist:
continue
# got data
if data:
ps.name = data['name']
ps.comment = data['description']
ps.startdate = arrow.get(data['start_date_local']).datetime
ps.enddate = arrow.get(data['end_date_local']).datetime
ps.preferreddate = arrow.get(data['start_date_local']).datetime
ps.sessionsport = mytypes.intervalsmappinginv[data['type']]
ps.sessiontype = 'session'
ps.save()
try:
timetarget = data['time_target']
except KeyError:
timetarget = None
if timetarget is None:
try:
timetarget = data['moving_time']
except KeyError:
timetarget = None
if timetarget is None:
timetarget = 3600
timetarget = int(timetarget)/60.
ps.sessionvalue = timetarget
ps.save()
if data['category'].lower() == 'workout':
ps.fitfile = data['fitfile']
ps.save()
ps.update_steps()
if data['category'].lower() == 'target':
ps.sessiontype = 'cycletarget'
ps.sessionvalue = int(data['time_target'])/60.
ps.enddate = ps.startdate + datetime.timedelta(days=6)
ps.save()
try:
deleted_records = event["deleted_events"]
except KeyError:
deleted_records = []
for record in deleted_records:
id = record['id']
try:
pss = PlannedSession.objects.filter(intervals_icu_id=id)
if r.intervals_delete_plannedsession and pss.count() > 0:
for ps in pss:
ps.delete()
except PlannedSession.DoesNotExist:
continue
return 1
def import_activities(self, event, *args, **kwargs):
if not self.rower.intervals_auto_import:
return 0
if user_is_not_basic(self.rower.user) or user_is_coachee(self.rower.user):
try:
record = event["activity"]
except KeyError:
records = []
try:
id = record['id']
result = self.get_workout(id)
except KeyError:
pass
return 1
return 0
def delete_activities(self, event, *args, **kwargs):
if not self.rower.intervals_auto_delete:
return 0
try:
record = event["activity"]
except KeyError:
records = []
try:
id = record['id']
try:
w = Workout.objects.get(uploadedtointervals=id)
if w.user == self.rower:
w.delete()
except Workout.DoesNotExist:
pass
except KeyError:
pass
return 1

View File

@@ -10,7 +10,7 @@ class Command(BaseCommand):
counter = 0 counter = 0
print('----- Workouts ---------') print('----- Workouts ---------')
for w in ws: for w in ws:
if w.uploadedtostrava or w.uploadedtotp or w.uploadedtonk or w.uploadedtosporttracks or w.uploadedtoc2: if w.uploadedtostrava or w.uploadedtotp or w.uploadedtonk or w.uploadedtosporttracks or w.uploadedtoc2 or w.uploadedtointervals:
record = SyncRecord( record = SyncRecord(
workout = w, workout = w,
) )
@@ -40,7 +40,7 @@ class Command(BaseCommand):
aantal = ts.count() aantal = ts.count()
counter = 0 counter = 0
for w in ts: for w in ts:
if w.uploadedtostrava or w.uploadedtotp or w.uploadedtonk or w.uploadedtosporttracks or w.uploadedtoc2: if w.uploadedtostrava or w.uploadedtotp or w.uploadedtonk or w.uploadedtosporttracks or w.uploadedtoc2 or w.uploadedtointervals:
record = SyncRecord( record = SyncRecord(
) )
if w.uploadedtostrava: if w.uploadedtostrava:

View File

@@ -1176,6 +1176,7 @@ class Rower(models.Model):
intervals_auto_export = models.BooleanField(default=False) intervals_auto_export = models.BooleanField(default=False)
intervals_auto_import = models.BooleanField(default=False) intervals_auto_import = models.BooleanField(default=False)
intervals_delete_plannedsession = models.BooleanField(default=False, verbose_name="Deleting planned session deletes it on intervals.icu") intervals_delete_plannedsession = models.BooleanField(default=False, verbose_name="Deleting planned session deletes it on intervals.icu")
intervals_auto_delete = models.BooleanField(default=True, verbose_name="Deleting an activity on intervals.icu deletes it on both")
intervals_resample_to_1s = models.BooleanField(default=False, verbose_name='Resample to 1s on export') 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)
@@ -3830,6 +3831,36 @@ class Workout(models.Model):
record.boatclass = self.workouttype record.boatclass = self.workouttype
record.save() record.save()
if self.uploadedtostrava:
record = create_or_update_syncrecord(self.user, self,
stravaid=self.uploadedtostrava,
)
if self.uploadedtointervals:
record = create_or_update_syncrecord(self.user, self,
intervalsid= self.uploadedtointervals,
)
if self.uploadedtotp:
record = create_or_update_syncrecord(self.user, self,
tpid= self.uploadedtotp,
)
if self.uploadedtonk:
record = create_or_update_syncrecord(self.user, self,
nkid= self.uploadedtonk,
)
if self.uploadedtosporttracks:
record = create_or_update_syncrecord(self.user, self,
sporttracksid=self.uploadedtosporttracks,
)
if self.uploadedtoc2:
record = create_or_update_syncrecord(self.user, self,
c2id= self.uploadedtoc2,
)
super(Workout, self).save(*args, **kwargs) super(Workout, self).save(*args, **kwargs)
@classmethod @classmethod
@@ -4006,7 +4037,7 @@ def create_or_update_syncrecord(rower, workout, **kwargs):
try: try:
record.save() record.save()
except IntegrityError: except (IntegrityError, ValueError):
pass pass
return record return record
@@ -4646,6 +4677,7 @@ class RowerExportForm(ModelForm):
'intervals_auto_export', 'intervals_auto_export',
'intervals_delete_plannedsession', 'intervals_delete_plannedsession',
'intervals_resample_to_1s', 'intervals_resample_to_1s',
'intervals_auto_delete',
'imports_are_private' 'imports_are_private'
] ]
@@ -4673,6 +4705,7 @@ class RowerExportFormIntervals(ModelForm):
'intervals_auto_import', 'intervals_auto_import',
'intervals_auto_export', 'intervals_auto_export',
'intervals_resample_to_1s', 'intervals_resample_to_1s',
'intervals_auto_delete',
'intervals_delete_plannedsession', 'intervals_delete_plannedsession',
] ]

View File

@@ -1070,7 +1070,7 @@ def get_workouts_session(r, ps):
def create_sessions_from_json(plansteps, rower, startdate, manager, planbyrscore=False, plan=None, def create_sessions_from_json(plansteps, rower, startdate, manager, planbyrscore=False, plan=None,
plan_past_days=False, plan_past_days=False,
asynchronous=False, queue=queue): asynchronous=True, queue=queue):
trainingdays = plansteps['trainingDays'] trainingdays = plansteps['trainingDays']
planstartdate = startdate planstartdate = startdate
if not asynchronous: if not asynchronous:

View File

@@ -397,6 +397,7 @@ def create_sessions_from_json_async(plansteps, rower, startdate, manager, planby
elif preferreddate >= timezone.now().date(): elif preferreddate >= timezone.now().date():
create_session = True create_session = True
if create_session: if create_session:
ps = PlannedSession( ps = PlannedSession(
startdate=preferreddate - startdate=preferreddate -

View File

@@ -633,6 +633,7 @@ urlpatterns = [
name='workout_rojaboimport_view'), name='workout_rojaboimport_view'),
re_path(r'^session/intervalsimport/$', views.plannedsession_intervalsimport_view, re_path(r'^session/intervalsimport/$', views.plannedsession_intervalsimport_view,
name='plannedsession_intervalsimport_view'), name='plannedsession_intervalsimport_view'),
re_path(r'^session/intervals/webhook/$', views.intervals_webhook_view, name='intervals_webhook_view'),
re_path(r'^workout/(?P<source>\w+.*)import/$', re_path(r'^workout/(?P<source>\w+.*)import/$',
views.workout_import_view, name='workout_import_view'), views.workout_import_view, name='workout_import_view'),
re_path(r'^workout/(?P<source>\w+.*)import/(?P<externalid>\d+)/$', re_path(r'^workout/(?P<source>\w+.*)import/(?P<externalid>\d+)/$',

View File

@@ -10,7 +10,9 @@ from rowers.utils import NoTokenError
from rowers.models import PlannedSession from rowers.models import PlannedSession
import rowers.integrations.strava as strava import rowers.integrations.strava as strava
import rowers.integrations.intervals as intervals
from rowers.integrations import importsources from rowers.integrations import importsources
from rowers.integrations import IntervalsIntegration
from rowers.utils import NoTokenError from rowers.utils import NoTokenError
import numpy import numpy
@@ -913,9 +915,62 @@ def workout_rojaboimport_view(request, message="", userid=0): # pragma: no cover
@csrf_exempt
def intervals_webhook_view(request):
if request.method == 'GET':
verificationtoken = request.GET.get('secret')
if verificationtoken != intervals.webhookverification:
return HttpResponse(status=403)
dologging("intervals_webhooks.log","GET request")
dologging("intervals_webhooks.log",request.body)
else:
data = json.loads(request.body)
try:
verificationtoken = data['secret']
except KeyError:
return HttpResponse(status=403)
if verificationtoken != intervals.webhookverification:
return HttpResponse(status=403)
try:
events = data['events']
except KeyError:
# return invalid request if no events
return HttpResponse(status=200)
webhook_type = None
for event in events:
try:
athlete_id = event['athlete_id']
webhook_type = event['type']
r = Rower.objects.get(intervals_owner_id=athlete_id)
except Rower.DoesNotExist:
return HttpResponse(status=200)
except MultipleObjectsReturned:
rs = Rower.objects.filter(intervals_owner_id=athlete_id)
r = rs[0]
except KeyError:
return HttpResponse(status=200)
integration = IntervalsIntegration(r.user)
if webhook_type.lower() == 'calendar_updated':
integration.update_calendar(r, event)
if webhook_type.lower() == 'activity_uploaded':
integration.import_activities(event)
if webhook_type.lower() == 'activity_deleted':
integration.delete_activities(event)
return HttpResponse(status=200)
# for Strava webhook request validation # for Strava webhook request validation
@csrf_exempt @csrf_exempt
def strava_webhook_view(request): def strava_webhook_view(request):
if request.method == 'GET': if request.method == 'GET':

View File

@@ -2777,7 +2777,7 @@ def rower_view_instantplan(request, id='', userid=0):
messages.info(request, 'Your Sessions have been added') messages.info(request, 'Your Sessions have been added')
url = reverse('plannedsessions_view') url = reverse('plannedsessions_view')
timeperiod = startdate.strftime( timeperiod = timezone.now().date().strftime(
'%Y-%m-%d')+'/'+enddate.strftime('%Y-%m-%d') '%Y-%m-%d')+'/'+enddate.strftime('%Y-%m-%d')
url = url+'?when='+timeperiod url = url+'?when='+timeperiod