From eec03965c837bf972da6a3456af087e6d0b9628b Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 16 Apr 2024 15:47:03 +0200 Subject: [PATCH] fixing some tests --- rowers/interactiveplots.py | 244 +++----------------------- rowers/tests/mocks.py | 6 + rowers/tests/statements.py | 1 + rowers/tests/test_unit_tests.py | 4 +- rowers/tests/testdata/testdata.tcx.gz | Bin 3999 -> 3999 bytes 5 files changed, 29 insertions(+), 226 deletions(-) diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 8b1d78e4..d5a2842a 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -1731,35 +1731,35 @@ def interactive_flexchart_stacked(id, r, xparam='time', try: tseconds = rowdata['time'] - except KeyError: # pragma: no cover + except (KeyError, ColumnNotFoundError): # pragma: no cover return '', 'No time data - cannot make flex plot' try: rowdata = rowdata.with_columns(x1=pl.col(xparam)) - except KeyError: # pragma: no cover + except (KeyError, ColumnNotFoundError): # pragma: no cover rowdata = rowdata.with_columns(x1=pl.lit(0)) try: rowdata = rowdata.with_columns(y1=pl.col(yparam1)) - except KeyError: # pragma: no cover + except (KeyError, ColumnNotFoundError): # pragma: no cover rowdata = rowdata.with_columns(y1=pl.col("time")) rowdata = rowdata.with_columns((pl.col("y1")).alias(yparam1)) try: rowdata = rowdata.with_columns(y2=pl.col(yparam2)) - except KeyError: # pragma: no cover + except (KeyError, ColumnNotFoundError): # pragma: no cover rowdata = rowdata.with_columns(y2=pl.col("time")) rowdata = rowdata.with_columns((pl.col("y2")).alias(yparam2)) try: rowdata = rowdata.with_columns(y1=pl.col(yparam3)) - except KeyError: # pragma: no cover + except (KeyError, ColumnNotFoundError): # pragma: no cover rowdata = rowdata.with_columns(y3=pl.col("time")) rowdata = rowdata.with_columns((pl.col("y3")).alias(yparam3)) try: rowdata = rowdata.with_columns(y4=pl.col(yparam1)) - except KeyError: # pragma: no cover + except (KeyError, ColumnNotFoundError): # pragma: no cover rowdata = rowdata.with_columns(y4=pl.col("time")) rowdata = rowdata.with_columns((pl.col("y4")).alias(yparam4)) @@ -1800,8 +1800,8 @@ def interactive_flex_chart2(id, r, promember=0, columns = [name for name, d in metrics.rowingmetrics] columns_basic = [name for name, d in metrics.rowingmetrics if d['group'] == 'basic'] - columns = columns + ['spm', 'driveenergy', 'distance'] - columns_basic = columns_basic + ['spm', 'driveenergy', 'distance'] + columns = columns + ['spm', 'driveenergy', 'distance','workoutstate'] + columns_basic = columns_basic + ['spm', 'driveenergy', 'distance','workoutstate'] datadf = pd.DataFrame() if promember: @@ -1838,24 +1838,24 @@ def interactive_flex_chart2(id, r, promember=0, workstrokesonly = False try: _ = rowdata[yparam2] - except (KeyError, TypeError): # pragma: no cover + except (KeyError, TypeError, ColumnNotFoundError): # pragma: no cover yparam2 = 'None' try: _ = rowdata[yparam1] - except (TypeError, KeyError): # pragma: no cover + except (TypeError, KeyError, ColumnNotFoundError): # pragma: no cover yparam1 = 'None' # test if we have drive energy try: _ = rowdata['driveenergy'].mean() - except (KeyError, TypeError): + except (KeyError, TypeError, ColumnNotFoundError): rowdata = rowdata.with_columns(driveenergy=pl.lit(500)) # test if we have power try: _ = rowdata['power'].mean() - except (KeyError, TypeError): + except (KeyError, TypeError, ColumnNotFoundError): rowdata = rowdata.with_columns(power=pl.lit(50)) # replace nans @@ -1870,22 +1870,22 @@ def interactive_flex_chart2(id, r, promember=0, if workstrokesonly: # pragma: no cover try: rowdata = rowdata.filter(~pl.col("workoutstate").is_in(workoutstatesrest)) - except KeyError: + except (KeyError, ColumnNotFoundError): pass try: tseconds = rowdata['time'] - except KeyError: # pragma: no cover + except (KeyError, ColumnNotFoundError): # pragma: no cover return '', 'No time data - cannot make flex plot', workstrokesonly try: rowdata = rowdata.with_columns(x1 = pl.col(xparam)) - except KeyError: # pragma: no cover + except (KeyError, ColumnNotFoundError): # pragma: no cover rowdata = rowdata.with_columns(x1 = pl.col("time")) try: rowdata = rowdata.with_columns(y1 = pl.col(yparam1)) - except KeyError: # pragma: no cover + except (KeyError, ColumnNotFoundError): # pragma: no cover rowdata = rowdata.with_columns(y1 = pl.col("time")) rowdata = rowdata.with_columns(yparam1 = pl.col("y1")) @@ -1893,7 +1893,7 @@ def interactive_flex_chart2(id, r, promember=0, if yparam2 != 'None': try: rowdata = rowdata.with_columns(y2 = pl.col(yparam2)) - except KeyError: # pragma: no cover + except (KeyError, ColumnNotFoundError): # pragma: no cover rowdata = rowdata.with_columns(y2 = pl.col("time")) rowdata = rowdata.with_columns(yparam2 = pl.col("y2")) @@ -1916,18 +1916,18 @@ def interactive_flex_chart2(id, r, promember=0, try: rowdata = rowdata.with_columns((pl.lit(axlabels[xparam])).alias("xname")) - except KeyError: # pragma: no cover + except (KeyError, ColumnNotFoundError): # pragma: no cover rowdata = rowdata.with_columns((pl.lit(xparam)).alias("xname")) try: rowdata = rowdata.with_columns((pl.lit(axlabels[yparam1])).alias("yname1")) - except KeyError: # pragma: no cover + except (KeyError, ColumnNotFoundError): # pragma: no cover rowdata = rowdata.with_columns((pl.lit(yparam1)).alias("yname1")) if yparam2 != 'None': try: rowdata = rowdata.with_columns((pl.lit(axlabels[yparam2])).alias("yname2")) - except KeyError: # pragma: no cover + except (KeyError, ColumnNotFoundError): # pragma: no cover rowdata = rowdata.with_columns((pl.lit(yparam2)).alias("yname2")) else: # pragma: no cover @@ -2185,210 +2185,6 @@ def interactive_multiple_compare_chart(ids, xparam, yparam, plottype='line', script, div = get_chart("/compare", chart_data) return script, div, message, errormessage - if xparam != 'distance' and xparam != 'time' and xparam != 'cumdist': # pragma: no cover - xaxmax = yaxmaxima[xparam] - xaxmin = yaxminima[xparam] - elif xparam == 'time' and not startenddict: - xaxmax = tseconds.max() - xaxmin = tseconds.min() - elif xparam == 'time' and startenddict: # pragma: no cover - deltas = [pair[1]-pair[0] for key, pair in startenddict.items()] - xaxmin = 0 - xaxmax = pd.Series(deltas).max()*1000. - if xaxmax == 0: - xaxmax = tseconds.max() - else: - xaxmax = datadf['distance'].max() - xaxmin = datadf['distance'].min() - - if yparam == 'distance': # pragma: no cover - yaxmin = datadf['distance'].min() - yaxmax = datadf['distance'].max() - elif yparam == 'cumdist': # pragma: no cover - yaxmin = datadf['cumdist'].min() - yaxmax = datadf['cumdist'].max() - else: - yaxmin = yaxminima[yparam] - yaxmax = yaxmaxima[yparam] - - x_axis_type = 'linear' - y_axis_type = 'linear' - - # Add hover to this comma-separated string and see what changes - if (promember == 1): - TOOLS = 'save,pan,box_zoom,wheel_zoom,reset,tap,crosshair' - else: # pragma: no cover - TOOLS = 'pan,box_zoom,wheel_zoom,reset,tap,crosshair' - - if yparam == 'pace': - y_axis_type = 'datetime' - yaxmax = 90.*1e3 - yaxmin = 150.*1e3 - - if xparam == 'time': - x_axis_type = 'datetime' - - plot = figure(x_axis_type=x_axis_type, y_axis_type=y_axis_type, - tools=TOOLS, - toolbar_location="above", - width=920, height=500, - toolbar_sticky=False) - - # add watermark - watermarkurl = "/static/img/logo7.png" - watermarkrange = Range1d(start=0, end=1) - watermarkalpha = 0.6 - watermarkw = 184 - watermarkh = 35 - plot.extra_y_ranges = {"watermark": watermarkrange} - plot.extra_x_ranges = {"watermark": watermarkrange} - #plot.sizing_mode = 'stretch_both' - - plot.image_url([watermarkurl], 0.05, 0.9, - watermarkw, watermarkh, - global_alpha=watermarkalpha, - w_units='screen', - h_units='screen', - anchor='top_left', - dilate=True, - x_range_name="watermark", - y_range_name="watermark", - ) - - colors = itertools.cycle(palette) - - cntr = 0 - l1 = [] - - try: - items = itertools.izip(ids, colors) - except AttributeError: - items = zip(ids, colors) - - for id, color in items: - group = datadf[datadf['workoutid'] == int(id)].copy() - try: - startsecond, endsecond = startenddict[id] - except KeyError: - startsecond = 0 - endsecond = 0 - - group.sort_values(by='time', ascending=True, inplace=True) - - if endsecond > 0: - group['time'] = group['time'] - 1.e3*startsecond - mask = group['time'] < 0 - group.mask(mask, inplace=True) - mask = group['time'] > 1.e3*(endsecond-startsecond) - group.mask(mask, inplace=True) - - if xparam == 'cumdist': - group['cumdist'] = group['cumdist'] - group['cumdist'].min() - res = make_cumvalues(group[xparam]) - group[xparam] = res[0] - elif xparam == 'distance': - group['distance'] = group['distance'] - group['distance'].min() - - try: - group['x'] = group[xparam] - except KeyError: # pragma: no cover - group['x'] = group['time'] - errormessage = xparam+' has no values. Plot invalid' - try: - group['y'] = group[yparam] - except KeyError: - group['y'] = 0.0*group['x'] - - ymean = group['y'].mean() - f = group['time'].diff().mean() - if f != 0 and not np.isnan(f): - windowsize = 2 * (int(20000./(f))) + 1 - else: - windowsize = 1 - - if windowsize > 3 and windowsize < len(group['y']): - try: - group['y'] = savgol_filter(group['y'], windowsize, 3) - except ValueError: # pragma: no cover - pass - - ylabel = Label(x=100, y=60+nrworkouts*20-20*cntr, - x_units='screen', y_units='screen', - text=axlabels[yparam] + - ": {ymean:6.2f}".format(ymean=ymean), - background_fill_alpha=.7, - background_fill_color='white', - text_color=color, - ) - if yparam != 'time' and yparam != 'pace': - plot.add_layout(ylabel) - - source = ColumnDataSource( - group - ) - - TIPS = OrderedDict([ - ('time', '@ftime'), - ('pace', '@fpace'), - ('hr', '@hr'), - ('spm', '@spm{1.1}'), - ('distance', '@distance{5}'), - ]) - - hover = plot.select(type=HoverTool) - hover.tooltips = TIPS - - if labeldict: - try: - legend_label = labeldict[id] - except KeyError: # pragma: no cover - legend_label = str(id) - else: # pragma: no cover - legend_label = str(id) - - if plottype == 'line': - l1.append(plot.line('x', 'y', source=source, color=color, - legend_label=legend_label, line_width=2)) - else: - l1.append(plot.scatter('x', 'y', source=source, color=color, legend_label=legend_label, - fill_alpha=0.4, line_color=None)) - - plot.add_tools(HoverTool(renderers=[l1[cntr]], tooltips=TIPS)) - cntr += 1 - - plot.legend.location = 'top_right' - plot.xaxis.axis_label = axlabels[xparam] - plot.yaxis.axis_label = axlabels[yparam] - - if (xparam != 'time') and (xparam != 'distance') and (xparam != 'cumdist'): # pragma: no cover - xrange1 = Range1d(start=yaxminima[xparam], end=yaxmaxima[xparam]) - plot.x_range = xrange1 - - yrange1 = Range1d(start=yaxmin, end=yaxmax) - plot.y_range = yrange1 - - if xparam == 'time': - xrange1 = Range1d(start=xaxmin, end=xaxmax) - plot.x_range = xrange1 - plot.xaxis[0].formatter = DatetimeTickFormatter( - hours=["%H"], - minutes=["%M"], - seconds=["%S"], - days=["0"], - months=[""], - years=[""] - ) - - if yparam == 'pace': - plot.yaxis[0].formatter = DatetimeTickFormatter( - seconds=["%S"], - minutes=["%M"] - ) - - script, div = components(plot) - - return [script, div, message, errormessage] - def get_zones_report(rower, startdate, enddate, trainingzones='hr', date_agg='week', yaxis='time'): diff --git a/rowers/tests/mocks.py b/rowers/tests/mocks.py index b379eb83..80a12eb1 100644 --- a/rowers/tests/mocks.py +++ b/rowers/tests/mocks.py @@ -38,6 +38,7 @@ from nose.tools import assert_true from mock import Mock, patch #from minimocktest import MockTestCase import pandas as pd +import polars as pl import arrow from django.http import HttpResponseRedirect @@ -335,6 +336,11 @@ def mocked_getsmallrowdata_db(*args, **kwargs): return df +def mocked_getsmallrowdata_pl(*args, **kwargs): + df = pl.read_csv('rowers/tests/testdata/colsfromdb.csv') + + return df + def mocked_getsmallrowdata_db_updatecp(*args, **kwargs): # pragma: no cover df = pd.read_csv('rowers/tests/testdata/colsfromdb.csv') diff --git a/rowers/tests/statements.py b/rowers/tests/statements.py index 3b88c2e2..c809c60e 100644 --- a/rowers/tests/statements.py +++ b/rowers/tests/statements.py @@ -65,6 +65,7 @@ from nose.tools import assert_true from mock import Mock, patch #from minimocktest import MockTestCase import pandas as pd +import polars as pl import rowers.c2stuff as c2stuff import rowers.rojabo_stuff as rojabo_stuff diff --git a/rowers/tests/test_unit_tests.py b/rowers/tests/test_unit_tests.py index 650362d8..92903b5e 100644 --- a/rowers/tests/test_unit_tests.py +++ b/rowers/tests/test_unit_tests.py @@ -735,9 +735,9 @@ class InteractivePlotTests(TestCase): self.assertFalse(len(div)==0) @patch('rowers.dataprep.create_engine') - @patch('rowers.dataprep.getsmallrowdata_db', side_effect=mocked_getsmallrowdata_db) + @patch('rowers.dataprep.getsmallrowdata_pl', side_effect=mocked_getsmallrowdata_pl) def test_interactive_flexchart_stacked(self, mocked_sqlalchemy, - mocked_getsmallrowdata_db): + mocked_getsmallrowdata_pl): workout = Workout.objects.filter(user=self.r,workouttype__in=mytypes.rowtypes)[0] id = workout.id diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index 62c65b68b6e7bcbae639a5433d0bde8f0bbe803e..2f3f0980afdc1d7efcd86f0360156b49a73609c6 100644 GIT binary patch delta 3883 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 delta 3883 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