From ddac7ef18ff4fa8401ce12ccf1fd845b29a2404a Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 21 Sep 2023 16:09:48 +0200 Subject: [PATCH 1/4] removing imports.py and increasing coverage --- boatmovers/templates/boatmovers_base.html | 1 + rowers/c2stuff.py | 7 +- rowers/garmin_stuff.py | 5 +- rowers/imports.py | 291 ---------------------- rowers/integrations/strava.py | 8 +- rowers/interactiveplots.py | 43 ++-- rowers/rojabo_stuff.py | 3 +- rowers/tests/mocks.py | 138 +++++----- rowers/tests/statements.py | 14 +- rowers/tests/test_analysis.py | 31 +++ rowers/tests/test_imports.py | 8 +- rowers/tests/test_urls.py | 4 + rowers/tests/testdata/testdata.tcx.gz | Bin 4000 -> 4000 bytes rowers/urls.py | 2 + rowers/views/workoutviews.py | 28 ++- 15 files changed, 177 insertions(+), 406 deletions(-) delete mode 100644 rowers/imports.py diff --git a/boatmovers/templates/boatmovers_base.html b/boatmovers/templates/boatmovers_base.html index cf0666fc..915d729f 100644 --- a/boatmovers/templates/boatmovers_base.html +++ b/boatmovers/templates/boatmovers_base.html @@ -23,6 +23,7 @@
Ranglijst F.A.Q. + History
{% if WARNING_MESSAGE != '' %}

diff --git a/rowers/c2stuff.py b/rowers/c2stuff.py index 0f933238..4fa77a44 100644 --- a/rowers/c2stuff.py +++ b/rowers/c2stuff.py @@ -7,14 +7,13 @@ from rowers.utils import myqueue # (There is still some stuff defined directly in views.py. Need to # move that here.) -from rowers.imports import * import datetime from requests import Request, Session import rowers.mytypes as mytypes from rowers.mytypes import otwtypes from rowers.rower_rules import is_workout_user, ispromember from iso8601 import ParseError - +import pandas as pd import numpy import json from scipy import optimize @@ -22,6 +21,7 @@ from json.decoder import JSONDecodeError from pytz.exceptions import UnknownTimeZoneError from rowers.utils import dologging +import numpy as np from rowsandall_app.settings import ( C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET, @@ -83,6 +83,8 @@ def getagegrouprecord(age, sex='male', weightcategory='hwt', p0 = [700, 120, 700, 10, 100, 100] + p1, success = optimize.leastsq(errfunc, p0[:], + args=(ages, powers)) try: p1, success = optimize.leastsq(errfunc, p0[:], args=(ages, powers)) @@ -99,5 +101,6 @@ def getagegrouprecord(age, sex='male', weightcategory='hwt', else: power = 0 + return power diff --git a/rowers/garmin_stuff.py b/rowers/garmin_stuff.py index 33575778..0def169e 100644 --- a/rowers/garmin_stuff.py +++ b/rowers/garmin_stuff.py @@ -4,7 +4,6 @@ from rowers.utils import custom_exception_handler, NoTokenError from rowingdata import rowingdata from django.core.exceptions import PermissionDenied from rowers.utils import myqueue -from rowers.imports import * import datetime import requests @@ -20,7 +19,9 @@ from iso8601 import ParseError from rowers.plannedsessions import ps_dict_get_description import pandas as pd - +import arrow +from rowers import dataprep +import pytz import numpy import json diff --git a/rowers/imports.py b/rowers/imports.py deleted file mode 100644 index 38cac687..00000000 --- a/rowers/imports.py +++ /dev/null @@ -1,291 +0,0 @@ - -# All the functionality to connect to SportTracks - -# Python -import oauth2 as oauth -import cgi -import pytz -import requests -import requests.auth -import json -from django.utils import timezone -from datetime import datetime -from datetime import timedelta -import arrow -import numpy as np -from dateutil import parser -import time -from time import strftime - -import rowers.dataprep as dataprep -import math -from math import sin, cos, atan2, sqrt -import os -import sys -import urllib -import iso8601 -from uuid import uuid4 - - -# Django -from django.http import HttpResponseRedirect, HttpResponse, JsonResponse -from django.conf import settings -from django.contrib.auth import authenticate, login, logout -from django.contrib.auth.models import User -from django.contrib.auth.decorators import login_required - -# Project -# from .models import Profile -from rowingdata import rowingdata, make_cumvalues -import pandas as pd -from rowers.models import Rower, Workout, TombStone -import rowers.mytypes as mytypes -from rowsandall_app.settings import ( - C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET, - STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, - STRAVA_CLIENT_SECRET, SPORTTRACKS_CLIENT_SECRET, - SPORTTRACKS_CLIENT_ID, SPORTTRACKS_REDIRECT_URI -) - -from rowers.utils import ( - NoTokenError, custom_exception_handler, ewmovingaverage, - geo_distance, uniqify -) - - -# Splits SportTracks data which is one long sequence of -# [t,[lat,lon],t2,[lat2,lon2] ...] -# to [t,t2,t3, ...], [[lat,long],[lat2,long2],... -def splitstdata(lijst): - t = [] - latlong = [] - while len(lijst) >= 2: - t.append(lijst[0]) - latlong.append(lijst[1]) - lijst = lijst[2:] - - return [np.array(t), np.array(latlong)] - -# covered in integrations -def imports_open(user, oauth_data): - r = Rower.objects.get(user=user) - token = getattr(r, oauth_data['tokenname']) - - try: - tokenexpirydate = getattr(r, oauth_data['expirydatename']) - except (TypeError, AttributeError, KeyError): # pragma: no cover - tokenexpirydate = None - - if (token == '') or (token is None): - raise NoTokenError("User has no token") - else: - tokenname = oauth_data['tokenname'] - refreshtokenname = oauth_data['refreshtokenname'] - expirydatename = oauth_data['expirydatename'] - if tokenexpirydate and timezone.now()+timedelta(seconds=60) > tokenexpirydate: - token = imports_token_refresh( - user, - tokenname, - refreshtokenname, - expirydatename, - oauth_data, - ) - elif tokenexpirydate is None and expirydatename is not None and 'strava' in expirydatename: # pragma: no cover - token = imports_token_refresh( - user, - tokenname, - refreshtokenname, - expirydatename, - oauth_data, - ) - - return token - -# covered in integrations -# Refresh token using refresh token -def imports_do_refresh_token(refreshtoken, oauth_data, access_token=''): - # client_auth = requests.auth.HTTPBasicAuth( - # oauth_data['client_id'], - # oauth_data['client_secret'] - # ) - - post_data = {"grant_type": "refresh_token", - "client_secret": oauth_data['client_secret'], - "client_id": oauth_data['client_id'], - "refresh_token": refreshtoken, - } - headers = {'user-agent': 'sanderroosendaal', - 'Accept': 'application/json', - 'Content-Type': oauth_data['content_type']} - - # for Strava - if 'grant_type' in oauth_data: - if oauth_data['grant_type']: - post_data['grant_type'] = oauth_data['grant_type'] - - if oauth_data['bearer_auth']: - headers['authorization'] = 'Bearer %s' % access_token - - baseurl = oauth_data['base_url'] - - if 'json' in oauth_data['content_type']: - try: - response = requests.post(baseurl, - data=json.dumps(post_data), - headers=headers, verify=False) - except: # pragma: no cover - raise NoTokenError("Failed to get token") - else: - try: - response = requests.post(baseurl, - data=post_data, - headers=headers, verify=False, - ) - except: # pragma: no cover - raise NoTokenError("Failed to get token") - - if response.status_code == 200 or response.status_code == 201: - token_json = response.json() - else: # pragma: no cover - raise NoTokenError("User has no token") - - try: - thetoken = token_json['access_token'] - except KeyError: # pragma: no cover - raise NoTokenError("User has no token") - - try: - expires_in = token_json['expires_in'] - except KeyError: - try: - expires_at = arrow.get(token_json['expires_at']).timestamp() - expires_in = expires_at - arrow.now().timestamp() - except KeyError: # pragma: no cover - expires_in = 0 - try: - refresh_token = token_json['refresh_token'] - except KeyError: # pragma: no cover - refresh_token = refreshtoken - try: - expires_in = int(expires_in) - except (TypeError, ValueError): # pragma: no cover - expires_in = 0 - - return [thetoken, expires_in, refresh_token] - -# Exchange ST access code for long-lived ST access token - -# implemented in integrations -def imports_get_token( - code, oauth_data -): - - redirect_uri = oauth_data['redirect_uri'] - client_secret = oauth_data['client_secret'] - client_id = oauth_data['client_id'] - base_uri = oauth_data['base_url'] - - # client_auth = requests.auth.HTTPBasicAuth( - # client_id, client_secret - # ) - - post_data = {"grant_type": "authorization_code", - "code": code, - "redirect_uri": redirect_uri, - "client_secret": client_secret, - "client_id": client_id, - } - - try: - headers = oauth_data['headers'] - except KeyError: - headers = {'Accept': 'application/json', - 'Api-Key': client_id, - 'Content-Type': 'application/json', - 'user-agent': 'sanderroosendaal'} - - if 'grant_type' in oauth_data: - if oauth_data['grant_type']: - post_data['grant_type'] = oauth_data['grant_type'] - if 'strava' in oauth_data['autorization_uri']: - post_data['grant_type'] = "authorization_code" - - if 'json' in oauth_data['content_type']: - response = requests.post( - base_uri, - data=json.dumps(post_data), - headers=headers) - else: # pragma: no cover - response = requests.post( - base_uri, - data=post_data, - headers=headers, verify=False) - - if response.status_code == 200 or response.status_code == 201: - token_json = response.json() - try: - thetoken = token_json['access_token'] - except KeyError: # pragma: no cover - return [0, 0, 0] - try: - refresh_token = token_json['refresh_token'] - except KeyError: # pragma: no cover - refresh_token = '' - try: - expires_in = token_json['expires_in'] - except KeyError: # pragma: no cover - expires_in = 0 - try: - expires_in = int(expires_in) - except (ValueError, TypeError): # pragma: no cover - expires_in = 0 - else: # pragma: no cover - return [0, response.text, 0] - - return [thetoken, expires_in, refresh_token] - -# Make authorization URL including random string - - -def imports_make_authorization_url(oauth_data): # pragma: no cover - # Generate a random string for the state parameter - # Save it for use later to prevent xsrf attacks - - state = str(uuid4()) - - params = {"client_id": oauth_data['client_id'], - "response_type": "code", - "redirect_uri": oauth_data['redirect_uri'], - "scope": oauth_data['scope'], - "state": state} - - url = oauth_data['authorizaton_uri']+urllib.parse.urlencode(params) - - return HttpResponseRedirect(url) - -# This is token refresh. Looks for tokens in our database, then refreshes - - -def imports_token_refresh(user, tokenname, refreshtokenname, expirydatename, oauth_data): - r = Rower.objects.get(user=user) - - refreshtoken = getattr(r, refreshtokenname) - - # for Strava transition - if not refreshtoken: # pragma: no cover - refreshtoken = getattr(r, tokenname) - - res = imports_do_refresh_token(refreshtoken, oauth_data) - access_token = res[0] - expires_in = res[1] - refresh_token = res[2] - expirydatetime = timezone.now()+timedelta(seconds=expires_in) - - setattr(r, tokenname, access_token) - if expirydatename is not None: - setattr(r, expirydatename, expirydatetime) - if refreshtokenname is not None: - setattr(r, refreshtokenname, refresh_token) - - r.save() - return access_token diff --git a/rowers/integrations/strava.py b/rowers/integrations/strava.py index 068ff07d..3e3d17b2 100644 --- a/rowers/integrations/strava.py +++ b/rowers/integrations/strava.py @@ -10,13 +10,17 @@ from rowers.rower_rules import is_workout_user, ispromember from rowers.utils import get_strava_stream from rowers.utils import myqueue, dologging -from rowers.imports import * +#from rowers.imports import * import gzip import time import requests import arrow import datetime +import os +from uuid import uuid4 +from django.utils import timezone +from datetime import timedelta from rowsandall_app.settings import ( STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, STRAVA_CLIENT_SECRET, @@ -154,7 +158,7 @@ class StravaIntegration(SyncIntegration): outF.write(s) try: os.remove(tcxfilename) - except WindowError: # pragma: no cover + except WindowsError: # pragma: no cover pass except FileNotFoundError: return '' diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index e21d2659..86e43d2e 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -718,7 +718,7 @@ def interactive_activitychart2(workouts, startdate, enddate, stack='type', toolb try: rowers.append(rowers[0]) - except IndexError: + except IndexError: # pragma: no cover try: rowers.append(str(workouts[0].user)) except IndexError: @@ -1278,7 +1278,7 @@ def interactive_forcecurve(theworkouts, workstrokesonly=True, plottype='scatter' plot.yaxis.axis_label = "Force (N)" try: plot.title.text = theworkouts[0].name - except ValueError: + except ValueError: # pragma: no cover plot.title.text = "" plot.title.text_font_size = "1.0em" @@ -1507,10 +1507,10 @@ def weightfromrecord(row,metricchoice): vv = row[metricchoice] if vv > 0: return vv - if metricchoice == 'rscore': + if metricchoice == 'rscore': # pragma: no cover return rscore_approx(row) - return 0 + return 0 # pragma: no cover def getfatigues( @@ -1538,7 +1538,7 @@ def getfatigues( try: df2 = df.loc[date.date()] - if type(df2) == pd.Series: + if type(df2) == pd.Series: # pragma: no cover weight += weightfromrecord(df2,metricchoice) else: for index, row in df2.iterrows(): @@ -1772,6 +1772,7 @@ def performance_chart(user, startdate=None, enddate=None, kfitness=42, kfatigue= # make fast dict for dates / workouts records = [] + for w in workouts: dd = { 'date':w.date, @@ -1785,7 +1786,7 @@ def performance_chart(user, startdate=None, enddate=None, kfitness=42, kfatigue= records.append(dd) df = pd.DataFrame.from_records(records) - if df.empty: + if df.empty: # pragma: no cover return ['', 'No Data', 0, 0, 0, outids] df.set_index('date', inplace=True) @@ -1895,7 +1896,7 @@ def performance_chart(user, startdate=None, enddate=None, kfitness=42, kfatigue= rightaxlabel = 'Freshness' if dofatigue: # pragma: no cover yaxlabel = 'Fitness/Fatigue' - else: + else: # pragma: no cover yaxlabel = 'Fitness' if modelchoice == 'banister': # pragma: no cover @@ -1917,7 +1918,7 @@ def performance_chart(user, startdate=None, enddate=None, kfitness=42, kfatigue= if dofatigue: # pragma: no cover y1rangemax = df.loc[:, ['fitness', 'fatigue']].max().max()*1.02 - else: + else: # pragma: no cover y1rangemax = df.loc[:, ['fitness']].max().max()*1.02 if doform: # pragma: no cover @@ -2676,7 +2677,7 @@ def leaflet_chart_compare(course, workoutids, labeldict={}, startenddict={}): pass try: df = pd.concat(data, axis=0) - except ValueError: + except ValueError: # pragma: no cover df = pd.DataFrame() latmean, lonmean, coordinates = course_coord_center(course) @@ -2735,7 +2736,7 @@ def leaflet_chart_compare(course, workoutids, labeldict={}, startenddict={}): try: lat = df['lat'] lon = df['lon'] - except KeyError: + except KeyError: # pragma: no cover return [0, "invalid coordinate data"] if lat.empty or lon.empty: # pragma: no cover return [0, "invalid coordinate data"] @@ -4052,7 +4053,7 @@ def interactive_windchart(id=0, promember=0): legend_label="Tail (+)/Head (-) Wind (m/s)", color='black') try: plot.title.text = row.name - except ValueError: + except ValueError: # pragma: no cover plot.title.text = "" # plot.title.text_font_size="1.0em" plot.title.text_font = "1.0em" @@ -4122,7 +4123,7 @@ def interactive_streamchart(id=0, promember=0): plot.line(dist, vstream, legend_label="River Stream Velocity (m/s)") try: plot.title.text = row.name - except ValueError: + except ValueError: # pragma: no cover plot.title.text = "" plot.title.text_font_size = "1.0em" plot.xaxis.axis_label = "Distance (m)" @@ -4134,7 +4135,7 @@ def interactive_streamchart(id=0, promember=0): return [script, div] -def forcecurve_multi_interactive_chart(selected): +def forcecurve_multi_interactive_chart(selected): # pragma: no cover df_plot = pd.DataFrame() ids = [analysis.id for analysis in selected] @@ -4248,7 +4249,7 @@ def forcecurve_multi_interactive_chart(selected): return (script, div) -def instroke_multi_interactive_chart(selected, *args, **kwargs): +def instroke_multi_interactive_chart(selected, *args, **kwargs): # pragma: no cover df_plot = pd.DataFrame() ids = [analysis.id for analysis in selected] metrics = list(set([analysis.metric for analysis in selected])) @@ -4351,7 +4352,7 @@ def instroke_multi_interactive_chart(selected, *args, **kwargs): def instroke_interactive_chart(df,metric, workout, spm_min, spm_max, activeminutesmin, activeminutesmax, individual_curves, - name='',notes=''): + name='',notes=''): # pragma: no cover df_pos = (df+abs(df))/2. df_min = -(-df+abs(-df))/2. @@ -4578,7 +4579,7 @@ def interactive_chart(id=0, promember=0, intervaldata={}): plot.line('time', 'pace', source=source, legend_label="Pace", name="pace") try: plot.title.text = row.name - except ValueError: + except ValueError: # pragma: no cover plot.title.text = "" plot.title.text_font_size = "1.0em" #plot.sizing_mode = 'stretch_both' @@ -5776,7 +5777,7 @@ def interactive_flex_chart2(id, r, promember=0, doclean=False, workstrokesonly=False) workstrokesonly = False - except TypeError: + except TypeError: # pragma: no cover workstrokesonly = False try: _ = rowdata[yparam2] @@ -6029,7 +6030,7 @@ def interactive_flex_chart2(id, r, promember=0, legend_label="Constant Power") # trendline - if trendline: + if trendline: # pragma: no cover plot.line('x1', 'ytrend', source=source2, legend_label=yaxlabel+' (trend)') if plottype == 'line': @@ -6040,7 +6041,7 @@ def interactive_flex_chart2(id, r, promember=0, try: plot.title.text = row.name - except ValueError: + except ValueError: # pragma: no cover plot.title.text = "" plot.title.text_font_size = "1.0em" @@ -6340,7 +6341,7 @@ def thumbnails_set(r, id, favorites): try: rowdata.dropna(axis=1, how='all', inplace=True) - except TypeError: + except TypeError: # pragma: no cover return [ {'script': "", 'div': "", @@ -6844,7 +6845,7 @@ def interactive_otw_advanced_pace_chart(id=0, promember=0): try: plot.title.text = row.name - except ValueError: + except ValueError: # pragma: no cover plot.title.text = "" #plot.title.text_font_size = value("1.2em") plot.xaxis.axis_label = "Time" diff --git a/rowers/rojabo_stuff.py b/rowers/rojabo_stuff.py index 7f76195d..09c4313e 100644 --- a/rowers/rojabo_stuff.py +++ b/rowers/rojabo_stuff.py @@ -1,8 +1,9 @@ from rowers.models import Rower, Workout, TombStone from rowers import utils import datetime +from django.utils import timezone +from datetime import timedelta -from rowers.imports import * from rowsandall_app.settings import ( ROJABO_CLIENT_ID, ROJABO_REDIRECT_URI, ROJABO_CLIENT_SECRET, SITE_URL, ROJABO_OAUTH_LOCATION, diff --git a/rowers/tests/mocks.py b/rowers/tests/mocks.py index 49fd34f6..b379eb83 100644 --- a/rowers/tests/mocks.py +++ b/rowers/tests/mocks.py @@ -57,7 +57,7 @@ from redis import StrictRedis redis_connection = StrictRedis() -def mocked_grpc(*args, **kwargs): +def mocked_grpc(*args, **kwargs): # pragma: no cover class insecure_channel: def __init__(*args,**kwargs): pass @@ -84,7 +84,7 @@ def mocked_grpc(*args, **kwargs): def mocked_send_template_email(*args,**kwargs): return 1 -def mocked_myqueue(*args, **kwargs): +def mocked_myqueue(*args, **kwargs): # pragma: no cover class Job: def __init__(self,*args, **kwargs): self.result = 1 @@ -95,7 +95,7 @@ def mocked_myqueue(*args, **kwargs): return Job() -def mock_c2open(*args, **kwargs): +def mock_c2open(*args, **kwargs): # pragma: no cover return('aap') def mocked_session(*args, **kwargs): @@ -124,7 +124,7 @@ def mocked_session(*args, **kwargs): def json(self): return self.json_data - class MockContentResponse: + class MockContentResponse: # pragma: no cover def __init__(self,filename,status_code): with open(filename,'rb') as f: s = f.read() @@ -135,7 +135,7 @@ def mocked_session(*args, **kwargs): return MockEngine() -def mocked_sqlalchemy(*args, **kwargs): +def mocked_sqlalchemy(*args, **kwargs): # pragma: no cover # return object with method class MockEngine: @@ -177,7 +177,7 @@ def mocked_sqlalchemy(*args, **kwargs): from rowers import courses -def mocked_sqlalchemy_courses(*args, **kwargs): +def mocked_sqlalchemy_courses(*args, **kwargs): # pragma: no cover # return object with method cs = courses.kmltocourse('rowers/tests/testdata/thyro.kml') course = cs[0] @@ -251,7 +251,7 @@ class DjangoTestCase(TestCase): #, MockTestCase): #MockTestCase.tearDown(self) # delete_strokedata(1) -def mocked_tcx_parser(*args, **kwargs): +def mocked_tcx_parser(*args, **kwargs): # pragma: no cover df = pd.read_csv('rowers/tests/testdata/fake_strokedata.csv') return rowingdata(df=df) @@ -272,12 +272,12 @@ def mocked_fetchcperg(*args, **kwargs): return df import pandas as pd -def mocked_read_df_sql(id): +def mocked_read_df_sql(id): # pragma: no cover df = pd.read_csv('rowers/tests/testdata/fake_strokedata.csv') return df -def mocked_sendmail(*args,**kwargs): +def mocked_sendmail(*args,**kwargs): # pragma: no cover return HttpResponseRedirect('/rowers/email/') def mocked_get_video_data(*args, **kwargs): @@ -299,7 +299,7 @@ def mocked_getrowdata_db(*args, **kwargs): return df,row -def mocked_getrowdata_uh(*args, **kwargs): +def mocked_getrowdata_uh(*args, **kwargs): # pragma: no cover df = pd.read_csv('rowers/tests/testdata/uhfull.csv') id = kwargs['id'] @@ -308,7 +308,7 @@ def mocked_getrowdata_uh(*args, **kwargs): return df, row -def mocked_getsmallrowdata_uh(*args, **kwargs): +def mocked_getsmallrowdata_uh(*args, **kwargs): # pragma: no cover df = pd.read_csv('rowers/tests/testdata/uhfull.csv') return df @@ -335,33 +335,33 @@ def mocked_getsmallrowdata_db(*args, **kwargs): return df -def mocked_getsmallrowdata_db_updatecp(*args, **kwargs): +def mocked_getsmallrowdata_db_updatecp(*args, **kwargs): # pragma: no cover df = pd.read_csv('rowers/tests/testdata/colsfromdb.csv') return df -def mocked_getsmallrowdata_db_setcp(*args, **kwargs): +def mocked_getsmallrowdata_db_setcp(*args, **kwargs): # pragma: no cover df = pd.read_csv('rowers/tests/testdata/colsfromdb2.csv') return df -def mocked_getsmallrowdata_db_water(*args, **kwargs): +def mocked_getsmallrowdata_db_water(*args, **kwargs): # pragma: no cover df = pd.read_csv('rowers/tests/testdata/colsfromdb3.csv') return df -def mocked_getsmallrowdata_db_wps(*args, **kwargs): +def mocked_getsmallrowdata_db_wps(*args, **kwargs): # pragma: no cover df = pd.read_csv('rowers/tests/testdata/driveenergies.csv') return df -def mocked_getpowerdata_db(*args, **kwargs): +def mocked_getpowerdata_db(*args, **kwargs): # pragma: no cover df = pd.read_csv('rowers/tests/testdata/fake_powerdata.csv') return df -def mock_for_interactive_chart(*args, **kwargs): +def mock_for_interactive_chart(*args, **kwargs): # pragma: no cover df = pd.read_csv('rowers/tests/testdata/interactivechart.csv') return df @@ -371,7 +371,7 @@ def mocked_getempowerdata_db(*args, **kwargs): return df -def mocked_read_df_cols_sql_multistats(ids,columns,convertnewtons=True): +def mocked_read_df_cols_sql_multistats(ids,columns,convertnewtons=True): # pragma: no cover df = pd.read_csv('rowers/tests/testdata/cumstats.csv') extracols = [] @@ -387,13 +387,13 @@ def mock_workout_summaries(*args, **kwargs): df = pd.read_csv('rowers/tests/testdata/workout_summaries.csv') return df -def mocked_read_df_cols_sql_multi(ids, columns, convertnewtons=True): +def mocked_read_df_cols_sql_multi(ids, columns, convertnewtons=True): # pragma: no cover df = pd.read_csv('rowers/tests/testdata/fake_strokedata2.csv') extracols = [] return df, extracols -def mocked_read_df_cols_sql_multiflex(ids, columns, convertnewtons=True): +def mocked_read_df_cols_sql_multiflex(ids, columns, convertnewtons=True): # pragma: no cover df = pd.read_csv('rowers/tests/testdata/multiflexdata.csv') extracols = [] @@ -402,11 +402,11 @@ def mocked_read_df_cols_sql_multiflex(ids, columns, convertnewtons=True): # Mocked Strava def mocked_stravaexport(f2,workoutname,stravatoken,description='', - activity_type='Rowing'): + activity_type='Rowing'): # pragma: no cover print("this is mocked strava export") return 1,'success' -def mocked_fetchcp(*args, **kwargs): +def mocked_fetchcp(*args, **kwargs): # pragma: no cover df = pd.read_csv('rowers/tests/testdata/otwcpresult.csv') delta = df['delta'] cpvalue = df['cpvalue'] @@ -426,14 +426,14 @@ def mocked_fetchcp(*args, **kwargs): return delta, cpvalue, avgpower -def mocked_getcpdata_sql(*args, **kwargs): +def mocked_getcpdata_sql(*args, **kwargs): # pragma: no cover df = pd.read_csv('rowers/tests/testdata/otwcpresult.csv') df['cp'] = df['cpvalue'] return df -def mocked_cpraw(*args, **kwargs): +def mocked_cpraw(*args, **kwargs): # pragma: no cover df = pd.read_csv('rowers/tests/testdata/otwcp_df.csv') return df @@ -465,17 +465,17 @@ class MockStravalibClient(): ## Higher level - unfortunately didn't succeed in mocking the gateway -def get_client_token(*args, **kwargs): +def get_client_token(*args, **kwargs): # pragma: no cover return "aap" def mock_create_customer(*args, **kwargs): return 121 -def mock_make_payment(*args, **kwargs): +def mock_make_payment(*args, **kwargs): # pragma: no cover return 15,'' def mock_update_subscription(*args, **kwargs): - try: + try: # pragma: no cover rower = args[0] data = args[1] planid = data['plan'] @@ -497,7 +497,7 @@ def mock_update_subscription(*args, **kwargs): def mock_create_subscription(*args, **kwargs): return mock_update_subscription(*args, **kwargs) -def mock_cancel_subscription(*args, **kwargs): +def mock_cancel_subscription(*args, **kwargs): # pragma: no cover themessages = [] errormessages = [] try: @@ -521,7 +521,7 @@ def mock_mocktest(*args, **kwargs): ## Gateway stuff (not working) -class gatewayresult(): +class gatewayresult(): # pragma: no cover def __init__(self,*args,**kwargs): self.is_success = kwargs.pop('is_success',True) self.customer_id = 1 @@ -533,22 +533,22 @@ class gatewayresult(): def __unicode__(): return "mockedgatewayresult" -class credit_card(): +class credit_card(): # pragma: no cover def __init__(self, *args, **kwargs): self.subscriptions = [vsubscription()] self.country_of_issuance = 'US' -class paypal_account(): +class paypal_account(): # pragma: no cover def __init__(self, *args, **kwargs): self.subscriptions = [vsubscription(),vsubscription()] -class customercreateresult: +class customercreateresult: # pragma: no cover def __init__(self, *args, **kwargs): self.customer = kwargs.pop('customer',customer()) self.is_success = kwargs.pop('is_success',True) self.customer_id = 1 -class customer(): +class customer(): # pragma: no cover def find(*arg, **kwargs): return self @@ -560,17 +560,17 @@ class customer(): self.paypal_accounts = [paypal_account()] self.id = 1 -class client_token(): +class client_token(): # pragma: no cover def generate(*args, **kwargs): return 'aapnooit' -class plan(): +class plan(): # pragma: no cover def all(*args, **kwargs): return [] -class transaction(): +class transaction(): # pragma: no cover def sale(*args, **kwargs): return gatewayresult(is_success=True) @@ -588,7 +588,7 @@ class transaction(): self.created_at = datetime.datetime.now() self.currency_iso_code = 'EUR' -class vtransaction(): +class vtransaction(): # pragma: no cover def __init__(self,*args, **kwargs): self.amount = 15 self.credit_card_details = credit_card() @@ -600,7 +600,7 @@ class vtransaction(): self.created_at = datetime.datetime.now() self.currency_iso_code = 'EUR' -class vsubscription(): +class vsubscription(): # pragma: no cover def update(*args, **kwargs): return gatewayresult(is_success=True) @@ -615,7 +615,7 @@ class vsubscription(): self.price = 15 self.never_expires = True -class subscription(): +class subscription(): # pragma: no cover def create(*args, **kwargs): return gatewayresult(is_success=True) @@ -634,7 +634,7 @@ class subscription(): self.price = 15 self.never_expires = True -class vpayment_method(): +class vpayment_method(): # pragma: no cover def create(*args, **kwargs): return gatewayresult() @@ -642,25 +642,25 @@ class vpayment_method(): self.token = 'liesjeleerdelotje' -class payment_method(): +class payment_method(): # pragma: no cover def create(*args, **kwargs): return gatewayresult() def __init__(self, *args, **kwargs): self.token = 'liesjeleerdelotje' -class notification(): +class notification(): # pragma: no cover def __init__(self, *args, **kwargs): print('notifucation') self.kind = 'subscription_canceled' -class webhook_notification(): +class webhook_notification(): # pragma: no cover def parse(*args, **kwargs): print(args,kwargs,'parse') return notification() # mock braintree gateway -class MockBraintreeGateway: +class MockBraintreeGateway: # pragma: no cover def __init__(self,*args, **kwargs): self.customer = customer() self.client_token = client_token() @@ -671,13 +671,13 @@ class MockBraintreeGateway: self.webhook_notification = webhook_notification() -def mocked_gateway(*args, **kwargs): +def mocked_gateway(*args, **kwargs): # pragma: no cover return MockBraintreeGateway() # Mocked Rowingdata -class mocked_rowingdata(rowingdata): +class mocked_rowingdata(rowingdata): # pragma: no cover def __init__(self, *args, **kwargs): super(mocked_rowingdata).__init__(*args, **kwargs) @@ -938,19 +938,19 @@ def mocked_requests(*args, **kwargs): - class MockHeaderResponse: + class MockHeaderResponse: # pragma: no cover def __init__(self, header_data, status_code): self.headers = header_data self.status_code = status_code - class MockStreamResponse: + class MockStreamResponse: # pragma: no cover def __init__(self, file_name, status_code): self.raw = open(file_name,'rb') self.status_code = status_code self.ok = True self.file_name = file_name - def __enter__(self): + def __enter__(self): # pragma: no cover return self def __exit__(self, exc_type, exc_value, traceback): @@ -977,7 +977,7 @@ def mocked_requests(*args, **kwargs): class MockSession: class headers: - def __init__(self,*args,**kwargs): + def __init__(self,*args,**kwargs): # pragma: no cover pass def update(self,*args,**kwargs): @@ -1013,7 +1013,7 @@ def mocked_requests(*args, **kwargs): args = [kwargs['url']] if "tofit" in kwargs['url']: args = [kwargs['url']] - if "tojson" in kwargs['url']: + if "tojson" in kwargs['url']: # pragma: no cover args = [kwargs['url']] if not args: @@ -1156,7 +1156,7 @@ def mocked_requests(*args, **kwargs): garmintrainingscheduletester = re.compile(garmintrainingscheduleregex) if garmintester.match(args[0]): - if garmindownloadtester.match(args[0]): + if garmindownloadtester.match(args[0]): # pragma: no cover return MockStreamResponse('rowers/tests/testdata/3x250m.fit',200) if garmintrainingtester.match(args[0]): json_data = { @@ -1222,10 +1222,10 @@ def mocked_requests(*args, **kwargs): 'refresh_token': 'jHJhFzCfOOKB8oyiayubhLAlxaMkG3ruC1E8YxaR' } return MockResponse(json_data,200) - elif tpuploadtester.match(args[0]): + elif tpuploadtester.match(args[0]): # pragma: no cover return MockResponse(tpuploadresponse,200) - if uaapitester.match(args[0]): + if uaapitester.match(args[0]): # pragma: no cover if 'access_token' in args[0]: json_data = { 'access_token': 'TA3n1vrNjuQJWw0TdCDHnjSmrjIPULhTlejMIWqq', @@ -1244,7 +1244,7 @@ def mocked_requests(*args, **kwargs): return MockResponse(uauserjson,200) - if uatester.match(args[0]): + if uatester.match(args[0]): # pragma: no cover if 'access_token' in args[0]: json_data = { 'access_token': 'TA3n1vrNjuQJWw0TdCDHnjSmrjIPULhTlejMIWqq', @@ -1254,7 +1254,7 @@ def mocked_requests(*args, **kwargs): return MockResponse(json_data,200) - if rktester.match(args[0]): + if rktester.match(args[0]): # pragma: no cover if 'token' in args[0]: json_data = { 'access_token': 'TA3n1vrNjuQJWw0TdCDHnjSmrjIPULhTlejMIWqq', @@ -1272,7 +1272,7 @@ def mocked_requests(*args, **kwargs): else: json_data = rkworkoutlistjson return MockResponse(json_data,200) - elif rkusertester.match(args[0]): + elif rkusertester.match(args[0]): # pragma: no cover json_data = { "userID": 1234567890, "profile": "/profile", @@ -1289,10 +1289,10 @@ def mocked_requests(*args, **kwargs): "team": "/team" } return MockResponse(json_data, 200) - elif rkstrokestester.match(args[0]): + elif rkstrokestester.match(args[0]): # pragma: no cover return MockResponse(rkstrokesjson,200) - if sttester.match(args[0]): + if sttester.match(args[0]): # pragma: no cover if 'oauth2/token' in args[0]: json_data = { 'access_token': 'TA3n1vrNjuQJWw0TdCDHnjSmrjIPULhTlejMIWqq', @@ -1327,13 +1327,13 @@ def mocked_requests(*args, **kwargs): if nkstrokestester.match(args[0]): params = kwargs.pop('params',{}) - if 'sessionIds' in params and params['sessionIds'] == '404': + if 'sessionIds' in params and params['sessionIds'] == '404': # pragma: no cover return MockResponse(nkimpellerstrokedata, 200) return MockResponse(nkstrokedata,200) if nkworkoutlisttester.match(args[0]): params = kwargs.pop('params',{}) - if 'after' in params and params['after'] == '1267049972000': + if 'after' in params and params['after'] == '1267049972000': # pragma: no cover return MockResponse(nkimpellerworkoutlist,200) return MockResponse(nkworkoutlist,200) @@ -1357,7 +1357,7 @@ def mocked_requests(*args, **kwargs): query = kwargs['json']['query'] if 'download' in query: return MockResponse(rp3linkready,200) - except KeyError: + except KeyError: # pragma: no cover pass json_data = rp3workoutlist return MockResponse(json_data,200) @@ -1392,13 +1392,13 @@ def mocked_requests(*args, **kwargs): if c2strokestester.match(args[0]): return MockResponse(c2strokedata,200) elif c2importtester.match(args[0]): - if '12' in args[0]: + if '12' in args[0]: # pragma: no cover return MockResponse(c2workoutdata,200) elif '31' in args[0]: return MockResponse(c2timezoneworkoutdata2,200) - elif '32' in args[0]: + elif '32' in args[0]: # pragma: no cover return MockResponse(c2timezoneworkoutdatabad,200) - else: + else: # pragma: no cover return MockResponse(c2timezoneworkoutdata,200) elif c2workoutlisttester.match(args[0]): return MockResponse(c2workoutlist,200) @@ -1417,13 +1417,13 @@ def mocked_requests(*args, **kwargs): } } return MockResponse(json_data,200) - elif 'results' in args[0]: + elif 'results' in args[0]: # pragma: no cover json_data = { 'data': { 'id': 1223, } } - else: + else: # pragma: no cover return MockResponse(c2workoutdata,200) if fakturoidtester.match(args[0]): @@ -1451,7 +1451,7 @@ class MockEmailMessage: def send(self): return 1 -class MockResponse: +class MockResponse: # pragma: no cover def __init__(self, json_data, status_code): self.json_data = json_data self.status_code = status_code @@ -1465,7 +1465,7 @@ class MockResponse: -class MockOAuth1Session: +class MockOAuth1Session: # pragma: no cover def __init__(self,*args, **kwargs): pass diff --git a/rowers/tests/statements.py b/rowers/tests/statements.py index 0979619e..7fa4e1ed 100644 --- a/rowers/tests/statements.py +++ b/rowers/tests/statements.py @@ -26,6 +26,7 @@ pytestmark = pytest.mark.django_db from bs4 import BeautifulSoup import re +import pytz from parameterized import parameterized from django.test import TestCase, Client,override_settings, RequestFactory, TransactionTestCase @@ -34,6 +35,8 @@ from django.core.management import call_command from django.core.files.uploadedfile import SimpleUploadedFile from io import StringIO +import factory +from factory.fuzzy import FuzzyDateTime from django.test.client import RequestFactory from rowers.integrations import * @@ -41,7 +44,7 @@ from rowers.integrations import * from rowers.forms import ( DocumentsForm,CNsummaryForm,RegistrationFormUniqueEmail, ChartParamChoiceForm,WorkoutMultipleCompareForm, - BoxPlotChoiceForm,PerformanceManagerForm, + BoxPlotChoiceForm,PerformanceManagerForm,ForceCurveOptionsForm ) import rowers.plots as plots import rowers.interactiveplots as iplots @@ -94,7 +97,7 @@ from rowers.models import * from rowers.forms import * from rowers.tests.mocks import * import factory -from faker import Factory +from faker import Factory, Faker from uuid import uuid4 faker = Factory.create() @@ -181,13 +184,18 @@ class RaceFactory(factory.DjangoModelFactory): sessionvalue = 1 sessionmode = 'time' + +fake = Faker() +now = datetime.datetime.now(tz=pytz.timezone("America/New_York")) +one_month_ago = now - datetime.timedelta(days=30) + class WorkoutFactory(factory.DjangoModelFactory): class Meta: model = Workout name = factory.LazyAttribute(lambda _: faker.word()) notes = faker.text() - startdatetime = get_random_file(name=faker.word())['startdatetime'] + startdatetime = FuzzyDateTime(start_dt=one_month_ago, force_year=datetime.date.today().year) starttime = get_random_file(name=faker.word())['starttime'] workouttype='water' date=timezone.now().date() diff --git a/rowers/tests/test_analysis.py b/rowers/tests/test_analysis.py index c97eaf45..fe0997e4 100644 --- a/rowers/tests/test_analysis.py +++ b/rowers/tests/test_analysis.py @@ -154,6 +154,37 @@ class ForcecurveTest(TestCase): response = self.c.get(url) self.assertEqual(response.status_code,200) + # saving + form_data = { + 'spm_min': 19, + 'spm_max': 25, + 'dist_min': 0, + 'dist_max': 10000, + 'work_min': 0, + 'work_max': 3444, + 'notes': 'aap', + 'name': 'sdsd', + 'includereststrokes': True, + 'plottype':'none', + '_save_as_new':'Save' + } + + form = ForceCurveOptionsForm(form_data) + result = form.is_valid() + if not result: + print(form.errors) + self.assertTrue(form.is_valid()) + + response = self.c.post(url, form_data) + self.assertEqual(response.status_code,200) + + url = '/rowers/analysis/forcecurveanalysis/' + response = self.c.get(url) + self.assertEqual(response.status_code,200) + + + + class WorkoutCompareTestNew(TestCase): def setUp(self): diff --git a/rowers/tests/test_imports.py b/rowers/tests/test_imports.py index 7f6f5cfa..d3eaa5cd 100644 --- a/rowers/tests/test_imports.py +++ b/rowers/tests/test_imports.py @@ -1332,7 +1332,7 @@ class STObjects(DjangoTestCase): self.assertEqual(response.status_code,200) - @patch('rowers.imports.requests.get', side_effect=mocked_requests) + @patch('rowers.integrations.sporttracks.requests.get', side_effect=mocked_requests) def test_sporttracks_import(self, mock_get): response = self.c.get('/rowers/workout/sporttracksimport/12/',follow=True) @@ -1343,7 +1343,7 @@ class STObjects(DjangoTestCase): self.assertEqual(response.status_code, 200) - @patch('rowers.imports.requests.get', side_effect=mocked_requests) + @patch('rowers.integrations.sporttracks.requests.get', side_effect=mocked_requests) def test_sporttracks_import(self, mock_get): response = self.c.get('/rowers/workout/sporttracksimport/13/',follow=True) @@ -1356,7 +1356,7 @@ class STObjects(DjangoTestCase): self.assertEqual(response.status_code, 200) - @patch('rowers.imports.requests.get', side_effect=mocked_requests) + @patch('rowers.integrations.sporttracks.requests.get', side_effect=mocked_requests) def test_sporttracks_import_all(self, mock_get): response = self.c.get('/rowers/workout/sporttracksimport/?selectallnew=true') @@ -1422,7 +1422,7 @@ class TPObjects(DjangoTestCase): csvfilename=filename ) - @patch('rowers.imports.requests.post', side_effect=mocked_requests) + @patch('rowers.integrations.trainingpeaks.requests.post', side_effect=mocked_requests) def test_tp_callback(self, mock_post): response = self.c.get('/tp_callback?code=dsdoij232s',follow=True) diff --git a/rowers/tests/test_urls.py b/rowers/tests/test_urls.py index 9a15d993..87da9b7d 100644 --- a/rowers/tests/test_urls.py +++ b/rowers/tests/test_urls.py @@ -89,6 +89,10 @@ class URLTests(TestCase): '/rowers/laboratory/', '/rowers/laboratory/user/1/', '/rowers/legal/', + '/rowers/workouts/setrpe/', + '/boatmovers/', + '/boatmovers/faq/', + '/boatmovers/compare/', '/rowers/list-courses/', '/rowers/list-graphs/', '/rowers/list-jobs/', diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index 8d627e8707464ba00b7efd810b3c463e2df0c281..c678f9fa92337beca00a7bdea34526e2342869f2 100644 GIT binary patch delta 456 zcmV;(0XP1jAD|xxABzYGxl9a^2QmUv3UFTtvT*4 zPFHJlbauK(Uv7?GEl<16;`+lEZ|Lss6#Lz~Z#c{Sqki>f)xZDcUB6uC8@zIYcbnB` zXL!rz?BaMq0J!+$$>V3+Q~a=6Z~AV1v`j~BR{Oj2K0Z6@`t7g1(GBeBk9tGq-R9+A z5+C^Yu7A*-oL#Jzo5dUa>;3g?sSGy;1Je}BGs0n~Lz-otmt{f`HW-2r!YXZrXX^XJP?tKIhp4=xhNvx^V= zKJC)mb8+@|)2&~2Cnwvdl|HW4$1l6SOXux&Z>9~XYulrYL7gt`B`{(B;tD~+@ zKmGE>xIFh)CrjV`{Cair^77~Yp5DiF=#_`_Vat>5Kk1U$a#?Wo^Vhqr)@iesPy1~* zTt3<_&VTSc-Spva%adidStbto(}zcQ<-QIcckAOv4-bENn%Qjs+-?6gy~R&opT1b8 yI|KLuVE6yQ;>s7hC%=76hj+H9SL^f~9wd~@i-+lHyhxWmyz@Uk{#A9vfB^tJgaoVr delta 456 zcmV;(0XP1jAD|xxABzYGPwESi2QmW343RrXe`mK@?xmgAn|pU(_5Jz&V)6FvTXWo9 zoUYd9=yMDROH+ba)?>4K? z&hVDa*~RgK0C4fglgH1tr}$yD-t^u2Xqk@MtoC>3eSCJ*_1j;2qZ`=MAN7XJyUmNg z566VLa`}+#Ti`DUQ zy0vBh>g>g{Gy;1Je=nVV36h@EOZe`%|M6h4JK)alOdo$^{(Sjqwfp|y!A0VDcJX1~ zr(Jq`F3#R=y7i0hAcv4s|NQ)9b=39g zr(eDpm*@WKWa+z~U$0JHT>jkO)BBhXz4CBAY\d+)/$', views.workouts_setrpe_view, + name='workouts_setrpe_view'), re_path(r'^list-workouts/team/(?P\d+)/$', views.workouts_view, name='workouts_view'), re_path(r'^(?P\d+)/list-workouts/$', views.workouts_view, diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py index 443030c4..cb36bb40 100644 --- a/rowers/views/workoutviews.py +++ b/rowers/views/workoutviews.py @@ -417,7 +417,7 @@ def workout_forcecurve_view(request, id=0, analysis=0, userid=0, workstrokesonly if r == row.user: mayedit = 1 - if analysis: + if analysis: # pragma: no cover try: forceanalysis = ForceCurveAnalysis.objects.get(id=analysis) dist_min = forceanalysis.dist_min @@ -473,7 +473,7 @@ def workout_forcecurve_view(request, id=0, analysis=0, userid=0, workstrokesonly plottype = form.cleaned_data['plottype'] workstrokesonly = not includereststrokes - if "_save" in request.POST and "new" not in request.POST: + if "_save" in request.POST and "new" not in request.POST: # pragma: no cover if not analysis: forceanalysis = ForceCurveAnalysis( workout = row, @@ -929,7 +929,7 @@ def workout_recalcsummary_view(request, id=0): def workouts_duplicates_select_view(request, userid=0): r = getrequestrower(request, userid=userid) - if request.method == 'POST': + if request.method == 'POST': # pragma: no cover form = WorkoutMultipleCompareForm(request.POST) if form.is_valid(): workouts = form.cleaned_data['workouts'] @@ -1922,16 +1922,17 @@ def plannedsession_compare_view(request, id=0, userid=0): # set RPE for list of workouts @login_required() -def workouts_setrpe_view(request): +def workouts_setrpe_view(request,userid=0): today = timezone.now() startdate = today-timezone.timedelta(days=32) enddate = today+timezone.timedelta(days=1) - r = getrequestrower(request) + r = getrequestrower(request,userid=userid) startdate = datetime.datetime.combine(startdate, datetime.time()) enddate = datetime.datetime.combine(enddate, datetime.time(23, 59, 59)) - + startdate = pytz.utc.localize(startdate) + enddate = pytz.utc.localize(enddate) workouts = Workout.objects.filter( user=r,rpe=0, @@ -1950,9 +1951,14 @@ def workouts_setrpe_view(request): ) - if request.method == 'POST': + if request.method == 'POST': # pragma: no cover dateform = DateRangeForm(request.POST) - rpe_formset = WorkoutsRPEFormSet(request.POST) + rpe_formset = WorkoutsRPEFormSet(request.POST, + queryset=Workout.objects.filter(user=r,rpe=0,duplicate=False, + startdatetime__gte=startdate, + startdatetime__lte=enddate + ) +) if dateform.is_valid(): # pragma: no cover startdate = dateform.cleaned_data['startdate'] enddate = dateform.cleaned_data['enddate'] @@ -2209,7 +2215,7 @@ def workouts_view(request, message='', successmessage='', '%Y-%m-%d')+'/'+enddate.strftime('%Y-%m-%d') norpecount = len([w for w in workouts if w.rpe==0 and not w.duplicate]) - if norpecount and r.get_rpe_warnings: + if norpecount and r.get_rpe_warnings: # pragma: no cover messages.info(request,'You have workouts with no RPE value set. \ Click here to update them. \ You can switch off this warning in settings.') @@ -3138,7 +3144,7 @@ def instroke_chart(request, id=0, metric=''): # pragma: no cover " If you are already a Pro user, please log in to access this functionality", redirect_field_name=None) @permission_required('workout.change_workout', fn=get_workout_by_opaqueid, raise_exception=True) -def instroke_data(request, metric='', spm_min=15, spm_max=45, activeminutesmin=0, activeminutesmax=0, id=0, ): +def instroke_data(request, metric='', spm_min=15, spm_max=45, activeminutesmin=0, activeminutesmax=0, id=0, ): # pragma: no cover r = getrequestrower(request, userid=0) w = get_workoutuser(id, request) rowdata = rrdata(csvfile=w.csvfilename) @@ -3255,7 +3261,7 @@ def instroke_chart_interactive(request, id=0, analysis=0, userid=0): 'maxminutes': maxminutes, }) - if analysis: + if analysis: # pragma: no cover try: instroke_analysis = InStrokeAnalysis.objects.get(id=analysis) if instroke_analysis.rower != r: From 53900d533f0907103e7a745c86575cdcfc43f5f2 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 21 Sep 2023 16:34:59 +0200 Subject: [PATCH 2/4] removing debug line --- rowers/c2stuff.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/rowers/c2stuff.py b/rowers/c2stuff.py index 4fa77a44..98738451 100644 --- a/rowers/c2stuff.py +++ b/rowers/c2stuff.py @@ -81,10 +81,6 @@ def getagegrouprecord(age, sex='male', weightcategory='hwt', def errfunc(pars, x, y): return fitfunc(pars, x)-y - p0 = [700, 120, 700, 10, 100, 100] - - p1, success = optimize.leastsq(errfunc, p0[:], - args=(ages, powers)) try: p1, success = optimize.leastsq(errfunc, p0[:], args=(ages, powers)) From 813a9779c1df4f5b2e5c498052ca13c8b9c7a5f1 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 21 Sep 2023 16:50:59 +0200 Subject: [PATCH 3/4] fix --- rowers/c2stuff.py | 3 +++ rowers/tests/testdata/testdata.tcx.gz | Bin 4000 -> 3999 bytes 2 files changed, 3 insertions(+) diff --git a/rowers/c2stuff.py b/rowers/c2stuff.py index 98738451..18f43903 100644 --- a/rowers/c2stuff.py +++ b/rowers/c2stuff.py @@ -81,6 +81,9 @@ def getagegrouprecord(age, sex='male', weightcategory='hwt', def errfunc(pars, x, y): return fitfunc(pars, x)-y + p0 = [700, 120, 700, 10, 100, 100] + + try: p1, success = optimize.leastsq(errfunc, p0[:], args=(ages, powers)) diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index c678f9fa92337beca00a7bdea34526e2342869f2..4d6987e9fb15f5647d6b01cec3b5f15a5adefda8 100644 GIT binary patch delta 3912 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 Xi-+lHyhxWmyz@Uk{#A7Z#DD<+6+_@e From 62edae10ec41e7837f55abf9803747f6b4e2a774 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 22 Sep 2023 17:12:29 +0200 Subject: [PATCH 4/4] adding more logging --- rowers/dataprep.py | 16 ++++++++++++++-- rowers/tests/testdata/testdata.tcx.gz | Bin 3999 -> 4001 bytes 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/rowers/dataprep.py b/rowers/dataprep.py index b7c6b6f5..8a85b83a 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -1735,11 +1735,21 @@ def workout_trimp(w, reset=False): def workout_rscore(w, reset=False): + dologging('metrics.log','Workout_rscore for {w} {id}, {reset}'.format( + w = w, + id = w.id, + reset = reset + )) if w.rscore > -1 and not reset: + dologging('metrics.log','Returning {rscore}, {normp}'.format( + rscore = w.rscore, + normp = w.normp, + )) return w.rscore, w.normp if get_existing_job(w): - return 0, 0 + dologging('metrics.log','Got existing job, returning whatever we have') + return w.rscore, w.normp r = w.user ftp = float(r.ftp) @@ -1751,6 +1761,8 @@ def workout_rscore(w, reset=False): r.hrftp = int(hrftp) r.save() + dologging('metrics.log','Queueing an asynchronous task') + _ = myqueue( queuehigh, handle_calctrimp, @@ -1762,7 +1774,7 @@ def workout_rscore(w, reset=False): r.max, r.rest) - return 0, 0 + return w.rscore, w.normp def workout_normv(w, pp=4.0): diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index 4d6987e9fb15f5647d6b01cec3b5f15a5adefda8..d6dbed7351bc3287920a9fb6bc4200bac276b4cd 100644 GIT binary patch literal 4001 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 H#DD<+%;`Rj literal 3999 zcmV;Q4`A>giwFotRt#kV|8!+@bYx+4VJ>uIcmVC4TW=Ic7J%RR6&4T4!-|@7sZ$rn zI3keMA^{r(l+D|!ViX@kz0nQq>5qCt z=H2GyUmy7Qu7B8_oL#Jzo5dUa>;3=i`lH?c%gamQw_R!w&`l5B{l>e8clP!UK0JW* zGxEnz8{Y3dKR>(Z@8A7r|7v-0clTxb`ti#{0(AF4?h(8|-nY0<&O}EX;co!@00)b- z)5ravbmx1|`|hHDu{vGezx!9mUS232`r@qbPV!;Tmq%yomzzUjf3Wy!>!$-At#)tF z(`EXxn{=P~UcT7)Y44Bt4iUq_;_}m{9Udf6>yDTISf;!Gwdds{rTw%i)$`%A zh`Z(nuDQgOuOF|suWJAPaQjv)6WPlxS0CidWqQDeg!wV<{=P!-Vs(6+Zf)7WI(vEe zv|GRKP7=k>wqD)4JNoZucfZ|oezscoS2yP`(mU`C@8Wj1&yW1#l@McxUz6_}XX;Ijt|*F4|t4ws|X=BvxT_~rL+o(ZlU!@oRZI_JN-zE5Hk4;H(>zP#ol zUG&SJcl|Bq7vVa*TIviYNxN7Db!3Cnz@8s;Mcm0%#63Yoa6!SHlHOF1d$>7ws!QIj zRK(pGVpNUtX^7y0+~aJxJ6jR=nB?7x*Km&|<(*+R+cdn!v$21@Az8>xZ#s{<-dG}&uBHSw* z=PR<~giQn+HQa-gwBP3>@1Y{@&Nv{@=yg$w>htrFcZ7-*JBfx_=cYv{OP+5*p6Vh` z)lBiU56P4hohgSXC4Dz+=Ognh6_K|lCZ%2_^CTt|Brh`}A1flC%sD|0s_={}YR9|z zkhiWL@(9LRB$do3c7iUDCDkAR;I-L^-oi91hTWF*G zDCC2QBvuoiLmx`|?`}oDjrN1&35_H0s?k1_B|o1C9ZE%39a$rQsM7FaENRcriM$OJ zkxzO$6RMK0AeM~MS(?#kj}?)RCLnmLO1o?1XF#6GxQJ0jJ}2iEBtIj+eNCf1i(v;| zGtZMPOWw|id_@r(4h8_cXvhncjLzHJ`0cq_bwlKn{91>qG%S)>(uX%I@)h~;EE`6& znkX&#q8av${7B@T0jne*U@|ga+dN-Ul-360)2pbm=OY;3G`y+L^MWRcyvm+ufT9tb z8IATe$-G#To-L4uJmO?@`xd1gChv?F44Nn{6Bf1S=S03Ddp=ueY*q6-GnzON^4RA2 z>&bJ@y=6escyiKy5_eZg}fMG2r8KuDjKnAk3yaeTNKqiAGoN7=bOxrLf#pSC@Ptcwy5{E$^1Cvx4E~f7HGC? zMdhu?WAot+k-(}8}D>cS{q}@oSZq4ucyX_ zbMCG4h?>z}5GEt@&7L179}NYFK}9|`-G2HJ8=>4SMHQtLhNjz3U*^S{kaT;M%-gbA z*Rj>ojzZoWVO_*Vq%8U7!yAFTO|T7mtIE0-*T~ zbMxd3l6N`lTGa{B5f;_$=S04y(LNeL44U}7V=7uj?OS|)4DvbmHek^7y>*s~=HAYU zd`0#=Swjw4HO~u8Cij+G{rn(#Z_pyCMtes^?fDszcT^Gi+&O_*)8F2CE;`T8iF`%6 zebUxAqH5JL7q~@fXFy&IfT+5hyT~m{JN?m~y%EuMLiEtc-;{hsy1fO%UR0hOkG7W~~oRK|WQGDJ8W06slljGCd zVkbhrHXmN7hrD2}rB$tI;iYK({G4RICJj&6cp=pI@Vs*+{r0n(=PR=3ll*cdRgI1J zu2odtkbFf^T8RdjKsDM2HyPc&d2a{E%RZp-N*|t&MOPu8mCQ>;&x z$-JwGyf+908mlf~n2gLf^26k#acQvEjP`*-QG0$)qkX7{Jd&{%qbBP*aBMPvL-MgA z@?sz;M|)dzrNUWxaw;0_(Hk$K>Xs5*(bY-k(Dz})V7m0*N=kU8Y`&s;UaixQh!7AR1tcgTV#1u zp$~2{QePQ*cJ-i7cA5YZUgQqA>< zWy@{mgq|xxAG3xZby8n;)xw5;9Q3&ps$*4V7N!}iY4|fhFL}UMg=`Upibm__q~=v* z+;K4EF{m_rbj_Q0Q{yhwgFcPj;BtRF4SHX+5ALlpGNBoFgXpaRAl6BJu#-WL&A1yx z?-P1m6OEkBRTMop^rN7UCR(LShrksz?x=b5#z3Ex^@zH%8>H;`Z&E*b^kSUXqNlR0 z(&i-fP!alIL`c;D8!1_YzM-GCOULKVnAVA?j5|sSf=r}+4z^|K2hm%@Ue`V+FN`sn z&PmO#A4cyD1qr$V-$T*DdTeUm80awxu(IF|Dd1!};$ln797N9sovv#ZQY^Y~>8u8P zstCO^U`dye%fORaKkS^nWkwiOJ^9pH{J@XG<>#Y*SVoqb2kS1oM9=btFLD%+fId=nl}o1HUb!RQt!fK z27GJ@T7&4Fv8Y?i5ZT9~jnk;v^`oGVh9X*>h9Am?Z@78$#z1c=_s`OK^Ek$)=1qUV zw_-q)g;y3~GV%J_lCdia+S;VCA;qXdPt@l6>7y4gQMqt2TBf3ryXM0m2R)nUkaXxR zPiA?u=ExmJzipZ5Rcf9U+gv)+cjRtcCZgBP^-d;}pw-ec2hfLY%S6yEd4twVGw!Bt z*N5CP(TbqTk!wS%zPTy-P!al&J80>;YS>sde8X+P9|Jw2VHT&7dV#Xb)$@$hJg5l0 z-L_Uxsd-YkjnbUz>up8oy&YW1vUM4ONs<&t*$6papG5 zK`*(X3hAWYO3}z2G&OG&^ue%T&`G_xqNy59%^L+hCF72itrfg;&5=8Gmrl$r6G6MC zt@nO1b3L`7?I8MGWX+%o-}vC0T|Wc#(MYoHbW$JvWbArsYTgifCL`AB!Z!gzv+HMo zUUK0T=+I+P^m(bEHsFtdJ{Vxs^}`Q2d4D44sd@8;(C3w`0fP=b$H|~?>(>vXXA^{U zk-PMwl+^HZCyIuC1oX~$wo&EM2@XrTbY_KKDncLgnzo>mdXGg1eBVki#y~G*oW-EZ z(g~qxrR}ZIx0<_Q^pdM@f+{07#G;k9w;J$UFP&lZA+Kqxi`+#hy4cyQH2hk!bOg35 zSp#~NBR8T<#-#(TZ{8sK?Ml|Vb?zdS4cbDR>qkNFbM=i9J^RV5ZrSGgLG*bgYerIe z^P=@dt8b|F%^L+hZx=CF8Fx`)TitTv-n@+7<-#i^dKZgIeQT6D4*Hl2uXJ{O4A7dV zP28K8&~I0=Rz~h(#MT*m2I#FxGrej76ar{Ha&L;>kA?oj){h$w7EikK%MZ`a`sL>E zyOaKhpHG&j%XPo^{dUv$dtJW=@BE*kI|8>PEAZWM|HHvzcfg(9nLhr;{Q2_JYWMxY z!;8f6?Bc_|&${&XT%5h#bnBPh$;tL;Bc*#g*6O z!TI^g>Zt3}PrrOIF3