From 59ab7aaae4232ff686d67c5d7c3e77cbf4ac174d Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 21 May 2024 08:51:50 +0200 Subject: [PATCH] fixes --- rowers/dataroutines.py | 37 +++++++++++++++----------- rowers/interactiveplots.py | 32 +++++++++++++++------- rowers/templates/panel_empower.html | 22 +++++++++++++++ rowers/tests/test_cpchart.py | 3 +++ rowers/tests/test_races.py | 3 +++ rowers/tests/test_settings.py | 3 +++ rowers/tests/test_uploads.py | 3 +++ rowers/tests/testdata/testdata.tcx.gz | Bin 4000 -> 4000 bytes rowers/utils.py | 1 + rowers/views/workoutviews.py | 13 +++++++++ 10 files changed, 91 insertions(+), 26 deletions(-) create mode 100644 rowers/templates/panel_empower.html diff --git a/rowers/dataroutines.py b/rowers/dataroutines.py index 8ad8d38a..a3e06805 100644 --- a/rowers/dataroutines.py +++ b/rowers/dataroutines.py @@ -1554,19 +1554,22 @@ def read_data(columns, ids=[], doclean=True, workstrokesonly=True, debug=False, _ = dataplep(rowdata.df, id=id, bands=True, otwpower=True, barchart=True, polars=True) - df = pl.scan_parquet(f) - if startenddict: - try: - startsecond, endsecond = startenddict[id] - df = df.filter(pl.col("time") >= 1.0e3*startsecond, - pl.col("time") <= 1.0e3*endsecond) - df = df.with_columns(time = pl.col("time")-1.0e3*startsecond) - if 'cumdist' in columns: - df = df.collect() - df = df.with_columns(cumdist = pl.col("cumdist")-df[0, "cumdist"]).lazy() - except KeyError: - pass - data.append(df) + try: + df = pl.scan_parquet(f) + if startenddict: + try: + startsecond, endsecond = startenddict[id] + df = df.filter(pl.col("time") >= 1.0e3*startsecond, + pl.col("time") <= 1.0e3*endsecond) + df = df.with_columns(time = pl.col("time")-1.0e3*startsecond) + if 'cumdist' in columns: + df = df.collect() + df = df.with_columns(cumdist = pl.col("cumdist")-df[0, "cumdist"]).lazy() + except KeyError: + pass + data.append(df) + except ComputeError: + pass try: data = pl.collect_all(data) @@ -2169,14 +2172,16 @@ def dataplep(rowdatadf, id=0, inboard=0.88, forceunit='lbs', bands=True, barchar df = df.with_columns((pl.col(" AverageDriveForce (lbs)")/pl.col(" PeakDriveForce (lbs)")).alias("forceratio")) else: df = df.with_columns((pl.lit(0)).alias("forceratio")) - f = df['TimeStamp (sec)'].diff().mean() + try: + f = df['TimeStamp (sec)'].diff().mean() + except TypeError: + f = 0 + windowsize = 1 if f != 0 and not np.isinf(f): try: windowsize = 2 * (int(10. / (f))) + 1 except ValueError: windowsize = 1 - else: - windowsize = 1 if windowsize <= 3: windowsize = 5 diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 20989940..4ca7676b 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -22,7 +22,7 @@ import polars as pl import pytz from rowers.rower_rules import ispromember -from polars.exceptions import ColumnNotFoundError +from polars.exceptions import ColumnNotFoundError, ComputeError from scipy.interpolate import griddata from scipy.signal import savgol_filter @@ -758,16 +758,28 @@ def performance_chart(user, startdate=None, enddate=None, kfitness=42, kfatigue= df = df.filter(pl.col("date") > startdate) - df2 = pl.DataFrame({ - "testpower" :df['testpower'], - "testduration":df['testduration'].apply( - lambda x: totaltime_sec_to_string(x, shorten=True)), - "fitness":df['fitness'], - "fatigue":df['fatigue'], - "form":df['form'], - "impulse":df['impulse'], - "date": df['date'].dt.strftime('%Y-%m-%d'), + try: + df2 = pl.DataFrame({ + "testpower" :df['testpower'], + "testduration":df['testduration'].apply( + lambda x: totaltime_sec_to_string(x, shorten=True)), + "fitness":df['fitness'], + "fatigue":df['fatigue'], + "form":df['form'], + "impulse":df['impulse'], + "date": df['date'].dt.strftime('%Y-%m-%d'), }) + except ComputeError: + df2 = pl.DataFrame({ + "testpower" :df['testpower'], + "fitness":df['fitness'], + "fatigue":df['fatigue'], + "form":df['form'], + "impulse":df['impulse'], + "date": df['date'].dt.strftime('%Y-%m-%d'), + }) + df2 = df2.with_columns((pl.lit("--")).alias("testduration")) + df2 = df2.fill_nan(0) diff --git a/rowers/templates/panel_empower.html b/rowers/templates/panel_empower.html new file mode 100644 index 00000000..2cd66d26 --- /dev/null +++ b/rowers/templates/panel_empower.html @@ -0,0 +1,22 @@ +{% load rowerfilters %} +{% load tz %} +
    +
  • + + + {% localtime on %} + + {% endlocaltime %} + + + + + + + + + +
    Seat Number:{{ workout.seatnumber}}
    Empower side:{{ workout.empowerside }}
    Boat name:{{ workout.boatname }}
    Oar length:{{ workout.oarlength }}
    Oar inboard:{{ workout.inboard }}
    +
  • +
+ diff --git a/rowers/tests/test_cpchart.py b/rowers/tests/test_cpchart.py index 6f46c0cf..3c6fd404 100644 --- a/rowers/tests/test_cpchart.py +++ b/rowers/tests/test_cpchart.py @@ -95,6 +95,9 @@ class CPChartTest(TestCase): 'distance': 500, 'workouttype': 'rower', 'boattype': '1x', + 'boatname': 'CatchUp', + 'empowerside': 'starboard', + 'seatnumber': 1, 'weightcategory': 'hwt', 'adaptiveclass': 'None', 'notes': faker.text(), diff --git a/rowers/tests/test_races.py b/rowers/tests/test_races.py index ebf0ac06..e783db2e 100644 --- a/rowers/tests/test_races.py +++ b/rowers/tests/test_races.py @@ -1121,6 +1121,9 @@ class IndoorChallengesTest(TestCase): 'adaptiveclass': 'None', 'notes': faker.text(), 'rankingpiece': True, + 'empowerside': 'port', + 'boatname': 'Dolfijn', + 'seatnumber': 1, 'duplicate': False, 'avghr': '160', 'avgpwr': 0, diff --git a/rowers/tests/test_settings.py b/rowers/tests/test_settings.py index 86dc231d..504139fa 100644 --- a/rowers/tests/test_settings.py +++ b/rowers/tests/test_settings.py @@ -34,6 +34,9 @@ class DataTest(TestCase): 'workouttype':'water', 'rpe':1, 'boattype':'1x', + 'boatname': 'BOAT1', + 'empowerside': 'port', + 'seatnumber': 4, 'private':False, } form = WorkoutForm(data=form_data) diff --git a/rowers/tests/test_uploads.py b/rowers/tests/test_uploads.py index b27a198a..f93418e8 100644 --- a/rowers/tests/test_uploads.py +++ b/rowers/tests/test_uploads.py @@ -101,6 +101,9 @@ class ViewTest(TestCase): 'rpe':4, 'dragfactor':'112', 'raceid':0, + 'seatnumber': 1, + 'boatname': '', + 'empowerside': 'port', 'landingpage':'workout_edit_view', 'private':True, 'notes':'noot mies', diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index e5817eac44f785015da56eb72ff0e7cb57406732..8d223ed87f75f5c8b7bde0924f4418860cff8c81 100644 GIT binary patch literal 4000 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 GfB^u;^-ECz diff --git a/rowers/utils.py b/rowers/utils.py index 7f96a94a..49443593 100644 --- a/rowers/utils.py +++ b/rowers/utils.py @@ -64,6 +64,7 @@ workflowmiddlepanel = ( ('panel_summary.html', 'Summary'), ('panel_map.html', 'Map'), ('panel_comments.html', 'Basic Info and Links'), + ('panel_empower.html', 'Empower Oarlock Info'), ('panel_notes.html', 'Workout Notes'), ('panel_shortcomment.html', 'Comment Link'), ('panel_middlesocial.html', 'Social Media Share Buttons'), diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py index 4151a620..8e74ee2f 100644 --- a/rowers/views/workoutviews.py +++ b/rowers/views/workoutviews.py @@ -680,6 +680,9 @@ def addmanual_view(request, raceid=0): privacy = form.cleaned_data.get('privacy', 'visible') rankingpiece = form.cleaned_data.get('rankingpiece', False) duplicate = form.cleaned_data.get('duplicate', False) + seatnumber = form.cleaned_data.get('seatnumber', 1) + boatname = form.cleaned_data.get('boatname', '') + empowerside = form.cleaned_data.get('empowerside','port') if private: # pragma: no cover privacy = 'private' @@ -723,6 +726,9 @@ def addmanual_view(request, raceid=0): w.rpe = rpe w.workouttype = workouttype w.boattype = boattype + w.boatname = boatname + w.empowerside = empowerside + w.seatnumber = seatnumber w.distance = distance w.duration = duration w.save() @@ -4385,6 +4391,10 @@ def workout_edit_view(request, id=0, message="", successmessage=""): 'rankingpiece', Workout.objects.get(id=row.id).rankingpiece) duplicate = form.cleaned_data.get( 'duplicate', Workout.objects.get(id=row.id).duplicate) + seatnumber = form.cleaned_data.get('seatnumber', 1) + boatname = form.cleaned_data.get('boatname', '') + empowerside = form.cleaned_data.get('empowerside','port') + if private: privacy = 'private' @@ -4438,6 +4448,9 @@ def workout_edit_view(request, id=0, message="", successmessage=""): row.rankingpiece = rankingpiece row.timezone = thetimezone row.plannedsession = ps + row.boatname = boatname + row.empowerside = empowerside + row.seatnumber = seatnumber dragchanged = False if newdragfactor != row.dragfactor: # pragma: no cover