diff --git a/rowers/dataflow.py b/rowers/dataflow.py
index 0f28e85d..542c543e 100644
--- a/rowers/dataflow.py
+++ b/rowers/dataflow.py
@@ -58,7 +58,7 @@ from uuid import uuid4
def getrower(user):
try:
- if user is None or user.is_anonymous:
+ if user is None or user.is_anonymous: # pragma: no cover
return None
except AttributeError: # pragma: no cover
if User.objects.get(id=user).is_anonymous:
@@ -77,12 +77,12 @@ def generate_job_id():
def valid_uploadoptions(uploadoptions):
fstr = uploadoptions.get('file', None)
- if fstr is None:
+ if fstr is None: # pragma: no cover
return False, "Missing file in upload options."
# check if file can be found
if isinstance(fstr, str):
- if not os.path.isfile(fstr):
+ if not os.path.isfile(fstr): # pragma: no cover
return False, f"File not found: {fstr}"
@@ -108,19 +108,19 @@ def is_invalid_file(file_path):
return False, "Image files are not supported for upload."
if fileformat == "json":
return False, "JSON files are not supported for upload."
- if fileformat == "c2log":
+ if fileformat == "c2log": # pragma: no cover
return False, "Concept2 log files are not supported for upload."
- if fileformat == "nostrokes":
+ if fileformat == "nostrokes": # pragma: no cover
return False, "No stroke data found in the file."
if fileformat == "kml":
return False, "KML files are not supported for upload."
- if fileformat == "notgzip":
+ if fileformat == "notgzip": # pragma: no cover
return False, "The gzip file appears to be corrupted."
- if fileformat == "rowprolog":
+ if fileformat == "rowprolog": # pragma: no cover
return False, "RowPro logbook summary files are not supported for upload."
if fileformat == "gpx":
return False, "GPX files are not supported for upload."
- if fileformat == "unknown":
+ if fileformat == "unknown": # pragma: no cover
extension = os.path.splitext(f2)[1]
filename = os.path.splitext(f2)[0]
if extension == '.gz':
@@ -139,14 +139,14 @@ def is_invalid_file(file_path):
def upload_handler(uploadoptions, filename):
valid, message = valid_uploadoptions(uploadoptions)
- if not valid:
+ if not valid: # pragma: no cover
return {
"status": "error",
"job_id": None,
"message": message
}
is_valid, message = is_invalid_file(filename)
- if not is_valid:
+ if not is_valid: # pragma: no cover
os.remove(filename)
return {
"status": "error",
@@ -203,7 +203,7 @@ def unzip_and_process(zip_filepath, uploadoptions, parent_job_id, debug=False, *
def get_rower_from_uploadoptions(uploadoptions):
rowerform = TeamInviteForm(uploadoptions)
- if not rowerform.is_valid():
+ if not rowerform.is_valid(): # pragma: no cover
return None
try:
u = rowerform.cleaned_data['user']
@@ -214,7 +214,7 @@ def get_rower_from_uploadoptions(uploadoptions):
if len(us):
u = us[0]
r = getrower(u)
- else:
+ else: # pragma: no cover
r = None
for rwr in Rower.objects.all():
if rwr.emailalternatives is not None:
@@ -322,7 +322,7 @@ def update_workout_attributes(w, row, file_path, uploadoptions,
empowerside = 'starboard'
stravaid = uploadoptions.get('stravaid',0)
- if stravaid != 0:
+ if stravaid != 0: # pragma: no cover
workoutsource = 'strava'
w.uploadedtostrava = stravaid
@@ -347,10 +347,10 @@ def update_workout_attributes(w, row, file_path, uploadoptions,
totaltime = row.df['TimeStamp (sec)'].max() - row.df['TimeStamp (sec)'].min()
try:
totaltime = totaltime + row.df.loc[:, ' ElapsedTime (sec)'].iloc[0]
- except KeyError:
+ except KeyError: # pragma: no cover
pass
- if np.isnan(totaltime):
+ if np.isnan(totaltime): # pragma: no cover
totaltime = 0
if uploadoptions.get('summary', '') == '':
@@ -358,11 +358,11 @@ def update_workout_attributes(w, row, file_path, uploadoptions,
else:
summary = uploadoptions.get('summary', '')
- if uploadoptions.get('makeprivate', False):
+ if uploadoptions.get('makeprivate', False): # pragma: no cover
privacy = 'hidden'
elif workoutsource != 'strava':
privacy = 'visible'
- else:
+ else: # pragma: no cover
privacy = 'hidden'
# checking for in values
@@ -378,7 +378,7 @@ def update_workout_attributes(w, row, file_path, uploadoptions,
try:
workoutenddatetime = startdatetime+delta
- except AttributeError as e:
+ except AttributeError as e: # pragma: no cover
workoutstartdatetime = pendulum.parse(str(startdatetime))
workoutenddatetime = startdatetime+delta
@@ -386,7 +386,7 @@ def update_workout_attributes(w, row, file_path, uploadoptions,
# check for duplicate start times and duration
duplicate = checkduplicates(
w.user, startdate, startdatetime, workoutenddatetime)
- if duplicate:
+ if duplicate: # pragma: no cover
rankingpiece = False
# test title length
@@ -431,7 +431,7 @@ def update_workout_attributes(w, row, file_path, uploadoptions,
w.save()
# check for registrationid
- if registrationid != 0:
+ if registrationid != 0: # pragma: no cover
races = VirtualRace.objects.filter(
registration_closure__gt=tz.now(),
id=raceid,
@@ -495,11 +495,11 @@ def update_running_wps(r, w, row):
duplicate=False).count()
new_value = (cntr*r.running_wps_erg + row.df['driveenergy'].mean())/(cntr+1.0)
# if new_value is not zero or infinite or -inf, r.running_wps can be set to value
- if not (math.isnan(new_value) or math.isinf(new_value) or new_value == 0):
+ if not (math.isnan(new_value) or math.isinf(new_value) or new_value == 0): # pragma: no cover
r.running_wps_erg = new_value
elif not (math.isnan(r.running_wps_erg) or math.isinf(r.running_wps_erg) or r.running_wps_erg == 0):
pass
- else:
+ else: # pragma: no cover
r.running_wps_erg = 600.
r.save()
@@ -509,13 +509,13 @@ def update_running_wps(r, w, row):
duplicate=False).count()
try:
new_value = (cntr*r.running_wps_erg + row.df['driveenergy'].mean())/(cntr+1.0)
- except TypeError:
+ except TypeError: # pragma: no cover
new_value = r.running_wps
if not (math.isnan(new_value) or math.isinf(new_value) or new_value == 0):
r.running_wps = new_value
elif not (math.isnan(r.running_wps) or math.isinf(r.running_wps) or r.running_wps == 0):
pass
- else:
+ else: # pragma: no cover
r.running_wps = 400.
r.save()
@@ -531,7 +531,7 @@ def process_single_file(file_path, uploadoptions, job_id, debug=False, **kwargs)
f1 = uuid4().hex[:10]+'-'+strftime('%Y%m%d-%H%M%S')+ext
f2 = 'media/'+f1
copyfile(file_path, f2)
- except FileNotFoundError:
+ except FileNotFoundError: # pragma: no cover
return {
"status": "error",
"job_id": job_id,
@@ -540,7 +540,7 @@ def process_single_file(file_path, uploadoptions, job_id, debug=False, **kwargs)
# determine the user
r = get_rower_from_uploadoptions(uploadoptions)
- if r is None:
+ if r is None: # pragma: no cover
os.remove(f2)
return {
"status": "error",
@@ -550,7 +550,7 @@ def process_single_file(file_path, uploadoptions, job_id, debug=False, **kwargs)
try:
fileformat = get_file_type(f2)
- except Exception as e:
+ except Exception as e: # pragma: no cover
os.remove(f2)
return {
"status": "error",
@@ -559,16 +559,17 @@ def process_single_file(file_path, uploadoptions, job_id, debug=False, **kwargs)
}
# Get fileformat from fit & tcx
- if fileformat == 'fit':
+ if "fit" in fileformat:
workouttype = get_workouttype_from_fit(f2)
uploadoptions['workouttype'] = workouttype
new_title = get_title_from_fit(f2)
- if new_title:
+ if new_title: # pragma: no cover
uploadoptions['title'] = new_title
new_notes = get_notes_from_fit(f2)
- if new_notes:
+ if new_notes: # pragma: no cover
uploadoptions['notes'] = new_notes
+
# handle non-Painsled
if fileformat != 'csv':
f2, summary, oarlength, inboard, fileformat, impeller = handle_nonpainsled(
@@ -581,7 +582,7 @@ def process_single_file(file_path, uploadoptions, job_id, debug=False, **kwargs)
uploadoptions['useImpeller'] = impeller
if uploadoptions['workouttype'] != 'strave':
uploadoptions['workoutsource'] = fileformat
- if not f2:
+ if not f2: # pragma: no cover
return {
"status": "error",
"job_id": job_id,
@@ -600,7 +601,7 @@ def process_single_file(file_path, uploadoptions, job_id, debug=False, **kwargs)
powerperc=powerperc, powerzones=r.powerzones)
row = rdata(f2, rower=rr)
- if row.df.empty:
+ if row.df.empty: # pragma: no cover
os.remove(f2)
return {
"status": "error",
@@ -608,7 +609,7 @@ def process_single_file(file_path, uploadoptions, job_id, debug=False, **kwargs)
"message": "No valid data found in the uploaded file."
}
- if row == 0:
+ if row == 0: # pragma: no cover
os.remove(f2)
return {
"status": "error",
@@ -639,7 +640,7 @@ def process_single_file(file_path, uploadoptions, job_id, debug=False, **kwargs)
pass
workoutid = uploadoptions.get('id', None)
- if workoutid is not None:
+ if workoutid is not None: # pragma: no cover
try:
w = Workout.objects.get(id=workoutid)
except Workout.DoesNotExist:
@@ -657,7 +658,7 @@ def process_single_file(file_path, uploadoptions, job_id, debug=False, **kwargs)
if w.privacy == 'visible':
ts = Team.objects.filter(rower=r
)
- for t in ts:
+ for t in ts: # pragma: no cover
w.team.add(t)
# put stroke data in file store through "dataplep"
@@ -686,14 +687,14 @@ def process_single_file(file_path, uploadoptions, job_id, debug=False, **kwargs)
wps_avg = r.median_wps
elif w.workouttype in otetypes:
wps_avg = r.median_wps_erg
- else:
+ else: # pragma: no cover
wps_avg = 0
_ = myqueue(queuehigh, handle_calctrimp, w.id, f2,
r.ftp, r.sex, r.hrftp, r.max, r.rest, wps_avg)
# make plots
- if uploadoptions.get('makeplot', False):
+ if uploadoptions.get('makeplot', False): # pragma: no cover
plottype = uploadoptions.get('plottype', 'timeplot')
res, jobid = uploads.make_plot(r, w, f1, f2, plottype, w.name)
elif r.staticchartonupload != 'None': # pragma: no cover
diff --git a/rowers/tests/test_uploads2.py b/rowers/tests/test_uploads2.py
index 0365bf66..c261c396 100644
--- a/rowers/tests/test_uploads2.py
+++ b/rowers/tests/test_uploads2.py
@@ -7,11 +7,47 @@ from __future__ import unicode_literals
from .statements import *
nu = datetime.datetime.now()
from django.db import transaction
+import shutil
from rowers.views import add_defaultfavorites
from rowers.dataflow import process_single_file, upload_handler, unzip_and_process
from django.core.files.uploadedfile import SimpleUploadedFile
from django.conf import settings
+from rowingdata import get_file_type
+
+file_list = [
+ 'rowers/tests/testdata/testdata.csv',
+ 'rowers/tests/testdata/testdata.csv.gz',
+ 'rowers/tests/testdata/tim.csv',
+ 'rowers/tests/testdata/crewnerddata.tcx',
+ 'rowers/tests/testdata/Speedcoach2example.csv',
+ 'rowers/tests/testdata/Impeller.csv',
+ 'rowers/tests/testdata/speedcoach3test3.csv',
+ 'rowers/tests/testdata/SpeedCoach2Linkv1.27.csv',
+ 'rowers/tests/testdata/SpeedCoach2Link_interval.csv',
+ 'rowers/tests/testdata/NoHR.tcx',
+ 'rowers/tests/testdata/rowinginmotionexample.tcx',
+ 'rowers/tests/testdata/RP_testdata.csv',
+ 'rowers/tests/testdata/mystery.csv',
+ 'rowers/tests/testdata/RP_interval.csv',
+ 'rowers/tests/testdata/3x250m.fit',
+ 'rowers/tests/testdata/painsled_desktop_example.csv',
+ 'rowers/tests/testdata/ergdata_example.csv',
+ 'rowers/tests/testdata/boatcoach_2021-09-09__18-15-53.csv',
+ 'rowers/tests/testdata/colinforce.csv',
+ 'rowers/tests/testdata/PainsledForce.csv',
+ 'rowers/tests/testdata/EmpowerSpeedCoachForce.csv',
+ 'rowers/tests/testdata/boatcoach.csv',
+ 'rowers/tests/testdata/ergstick.csv',
+]
+
+fail_list = [
+ 'rowers/tests/testdata/lofoten.jpg',
+ 'rowers/tests/testdata/c2records.json',
+ 'rowers/tests/testdata/alphen.kml',
+ 'rowers/tests/testdata/testdata.gpx'
+]
+
#@pytest.mark.django_db
@override_settings(TESTING=True)
@@ -31,28 +67,29 @@ class ViewTest(TestCase):
self.nu = datetime.datetime.now()
- file_list = ['rowers/tests/testdata/testdata.csv',
- 'rowers/tests/testdata/tim.csv',
- 'rowers/tests/testdata/crewnerddata.tcx',
- 'rowers/tests/testdata/Speedcoach2example.csv',
- 'rowers/tests/testdata/Impeller.csv',
- 'rowers/tests/testdata/speedcoach3test3.csv',
- 'rowers/tests/testdata/SpeedCoach2Linkv1.27.csv',
- 'rowers/tests/testdata/SpeedCoach2Link_interval.csv',
- 'rowers/tests/testdata/NoHR.tcx',
- 'rowers/tests/testdata/rowinginmotionexample.tcx',
- 'rowers/tests/testdata/RP_testdata.csv',
- 'rowers/tests/testdata/mystery.csv',
- 'rowers/tests/testdata/RP_interval.csv',
- 'rowers/tests/testdata/painsled_desktop_example.csv',
- 'rowers/tests/testdata/ergdata_example.csv',
- 'rowers/tests/testdata/boatcoach_2021-09-09__18-15-53.csv',
- 'rowers/tests/testdata/colinforce.csv',
- 'rowers/tests/testdata/PainsledForce.csv',
- 'rowers/tests/testdata/EmpowerSpeedCoachForce.csv',
- 'rowers/tests/testdata/boatcoach.csv',
- 'rowers/tests/testdata/ergstick.csv',
- ]
+ # copy every file in fail_list to rowers/tests/testdata/backup folder
+ # Zorg ervoor dat de backup-map bestaat
+ backup_dir = 'rowers/tests/testdata/backup'
+ os.makedirs(backup_dir, exist_ok=True)
+
+ # Kopieer elk bestand in fail_list naar de backup-map
+ for file_path in fail_list:
+ if os.path.exists(file_path):
+ shutil.copy(file_path, backup_dir)
+ else:
+ print(f"Bestand niet gevonden: {file_path}")
+
+ def tearDown(self):
+ backup_dir = 'rowers/tests/testdata/backup'
+ for file_path in fail_list:
+ backup_file = os.path.join(backup_dir, os.path.basename(file_path))
+ if os.path.exists(backup_file):
+ shutil.copy(backup_file, os.path.dirname(file_path))
+ else:
+ print(f"Backup-bestand niet gevonden: {backup_file}")
+
+
+
@parameterized.expand(file_list)
@patch('rowers.dataflow.myqueue')
def test_upload_view(self, filename, mocked_myqueue):
@@ -93,12 +130,62 @@ class ViewTest(TestCase):
response = self.c.post('/rowers/workout/upload/', data = form_data,
files = {'file': uploaded_file}, follow=True)
+ self.assertEqual(response.status_code, 200)
+
uploadoptions = form.cleaned_data.copy()
uploadoptions.update(optionsform.cleaned_data)
result = upload_handler(uploadoptions, filename)
self.assertEqual(result["status"], "processing")
+ @parameterized.expand(fail_list)
+ @patch('rowers.dataflow.myqueue')
+ def test_upload_view(self, filename, mocked_myqueue):
+ # simple test to see if upload view works. Submits a DocumentsForm to /rowers/workout/upload/
+ login = self.c.login(username='john',password='koeinsloot')
+ self.assertTrue(login)
+
+ with open(filename, 'rb') as f:
+ file_content = f.read()
+ uploaded_file = SimpleUploadedFile(
+ "testdata.csv",
+ file_content,
+ content_type="text/csv"
+ )
+ form_data = {
+ 'title':'test',
+ 'workouttype':'rower',
+ 'boattype':'1x',
+ 'notes':'aap noot mies',
+ 'make_plot':False,
+ 'rpe':6,
+ 'upload_to_c2':False,
+ 'plottype':'timeplot',
+ 'landingpage':'workout_edit_view',
+ 'raceid':0,
+ 'file': filename,
+ }
+
+
+ request = RequestFactory()
+ request.user = self.u
+ form = DocumentsForm(data = form_data,files={'file': uploaded_file})
+ self.assertTrue(form.is_valid())
+
+ optionsform = UploadOptionsForm(form_data,request=request)
+ self.assertTrue(optionsform.is_valid())
+
+ response = self.c.post('/rowers/workout/upload/', data = form_data,
+ files = {'file': uploaded_file}, follow=True)
+
+ self.assertEqual(response.status_code, 200)
+
+ uploadoptions = form.cleaned_data.copy()
+ uploadoptions.update(optionsform.cleaned_data)
+ result = upload_handler(uploadoptions, filename)
+
+ self.assertEqual(result["status"], "error")
+
@parameterized.expand(file_list)
@patch('rowers.dataflow.myqueue')
def test_process_single_file(self, filename, mocked_myqueue):
@@ -120,6 +207,75 @@ class ViewTest(TestCase):
self.assertEqual(result, True)
os.remove(f2+'.gz')
+ # process a single file without 'user'
+ @patch('rowers.dataflow.myqueue')
+ def test_process_single_file_nouser(self, mocked_myqueue):
+ filename = 'rowers/tests/testdata/testdata.csv'
+ uploadoptions = {
+ 'title':'test',
+ 'workouttype':'rower',
+ 'boattype':'1x',
+ 'notes':'aap noot mies',
+ 'make_plot':False,
+ 'rpe':6,
+ 'upload_to_c2':False,
+ 'plottype':'timeplot',
+ 'landingpage':'workout_edit_view',
+ 'raceid':0,
+ 'useremail': self.u.email,
+ 'file': filename,
+ }
+ result, f2 = process_single_file(filename, uploadoptions, 1)
+ self.assertEqual(result, True)
+ os.remove(f2+'.gz')
+
+ # process a zip file
+ @patch('rowers.dataflow.myqueue')
+ def test_process_single_zipfile(self, mocked_myqueue):
+ filename = 'rowers/tests/testdata/zipfile.zip'
+ uploadoptions = {
+ 'title':'test',
+ 'workouttype':'rower',
+ 'boattype':'1x',
+ 'notes':'aap noot mies',
+ 'make_plot':False,
+ 'rpe':6,
+ 'upload_to_c2':False,
+ 'plottype':'timeplot',
+ 'landingpage':'workout_edit_view',
+ 'raceid':0,
+ 'user': self.u,
+ 'file': filename,
+ }
+ result = process_single_file(filename, uploadoptions, 1)
+
+ self.assertEqual(result["status"], "error")
+
+ result = upload_handler(uploadoptions, filename)
+
+ self.assertEqual(result["status"], "processing")
+
+ # process a single file without 'title'
+ @patch('rowers.dataflow.myqueue')
+ def test_process_single_file_nouser(self, mocked_myqueue):
+ filename = 'rowers/tests/testdata/testdata.csv'
+ uploadoptions = {
+ 'workouttype':'rower',
+ 'boattype':'1x',
+ 'notes':'aap noot mies',
+ 'make_plot':False,
+ 'rpe':6,
+ 'upload_to_c2':False,
+ 'plottype':'timeplot',
+ 'landingpage':'workout_edit_view',
+ 'raceid':0,
+ 'user': self.u,
+ 'file': filename,
+ }
+ result, f2 = process_single_file(filename, uploadoptions, 1)
+ self.assertEqual(result, True)
+ os.remove(f2+'.gz')
+
@patch('rowers.dataflow.myqueue')
def test_process_zip_file(self, mocked_myqueue):
filename = 'rowers/tests/testdata/zipfile.zip'
diff --git a/rowers/tests/testdata/backup/alphen.kml b/rowers/tests/testdata/backup/alphen.kml
new file mode 100644
index 00000000..9ed981cb
--- /dev/null
+++ b/rowers/tests/testdata/backup/alphen.kml
@@ -0,0 +1,178 @@
+
+
+
+ Courses.kml
+
+ Courses
+
+ Alphen - Alphen aan den Rijn
+ 1
+
+ Start
+
+ 1
+
+
+ 4.704149601313898,52.14611068342334,0 4.704648516706039,52.14606840788696,0 4.704642182077736,52.14626893773362,0 4.704151599747837,52.14628828501986,0 4.704149601313898,52.14611068342334,0 4.704149601313898,52.14611068342334,0
+
+
+
+
+
+ Gate 1
+
+ 1
+
+
+ 4.704040567073562,52.14772365703576,0 4.704544185247905,52.14767250842382,0 4.704570221164488,52.14791407188889,0 4.704130359234369,52.14797079566858,0 4.704040567073562,52.14772365703576,0 4.704040567073562,52.14772365703576,0
+
+
+
+
+
+ Gate 2
+
+ 1
+
+
+ 4.707120374629225,52.15459940303027,0 4.707573702026327,52.15460568431943,0 4.70761596147063,52.15486728249238,0 4.707159504658982,52.15489881627455,0 4.707120374629225,52.15459940303027,0 4.707120374629225,52.15459940303027,0
+
+
+
+
+
+ Gate 3
+
+ 1
+
+
+ 4.709028668490356,52.1646474322453,0 4.70984931790314,52.16449178436365,0 4.709978566943311,52.16488586779201,0 4.709244456319242,52.16499245615274,0 4.709028668490356,52.1646474322453,0 4.709028668490356,52.1646474322453,0
+
+
+
+
+
+ Gate 4
+
+ 1
+
+
+ 4.718138359290078,52.17865355742074,0 4.718653235056161,52.17830639665007,0 4.719134204848634,52.17862031168055,0 4.71867160984541,52.17894003397144,0 4.718138359290078,52.17865355742074,0 4.718138359290078,52.17865355742074,0
+
+
+
+
+
+ Gate 5
+
+ 1
+
+
+ 4.727641648412835,52.18284846695732,0 4.728273789904367,52.18251973845241,0 4.728577606945771,52.1827641768111,0 4.7279847617705,52.1830837392454,0 4.727641648412835,52.18284846695732,0 4.727641648412835,52.18284846695732,0
+
+
+
+
+
+ Gate 6
+
+ 1
+
+
+ 4.738716857017891,52.19396028458393,0 4.739294818571407,52.19389560588872,0 4.739411118817641,52.19428660874426,0 4.738864571028594,52.19431307372239,0 4.738716857017891,52.19396028458393,0 4.738716857017891,52.19396028458393,0
+
+
+
+
+
+ Gate 7
+
+ 1
+
+
+ 4.734183821236371,52.20620514880871,0 4.734924962205387,52.20637199686158,0 4.734802543714663,52.20688025274802,0 4.733601274999542,52.20663721340052,0 4.734183821236371,52.20620514880871,0 4.734183821236371,52.20620514880871,0
+
+
+
+
+
+ Gate 8
+
+ 1
+
+
+ 4.738785303605908,52.19457123452171,0 4.739333350356509,52.19459196501802,0 4.739304304831564,52.19482691469288,0 4.73885420703549,52.19479878738656,0 4.738785303605908,52.19457123452171,0 4.738785303605908,52.19457123452171,0
+
+
+
+
+
+ Gate 9
+
+ 1
+
+
+ 4.728292586661338,52.18327969510192,0 4.728884338045631,52.18302182842039,0 4.729083849790216,52.1833152834237,0 4.728606271720666,52.18355598784883,0 4.728292586661338,52.18327969510192,0 4.728292586661338,52.18327969510192,0
+
+
+
+
+
+ Gate 10
+
+ 1
+
+
+ 4.717008631662971,52.17788756203277,0 4.717714777374475,52.17758571819474,0 4.718168595226933,52.17803093936305,0 4.717634575621297,52.17832999894938,0 4.717008631662971,52.17788756203277,0 4.717008631662971,52.17788756203277,0
+
+
+
+
+
+ Gate 11
+
+ 1
+
+
+ 4.708580146922809,52.16405851453961,0 4.709467162927956,52.16392338577828,0 4.709761923185198,52.16427786809471,0 4.708922971852094,52.16448915385681,0 4.708580146922809,52.16405851453961,0 4.708580146922809,52.16405851453961,0
+
+
+
+
+
+ Gate 12
+
+ 1
+
+
+ 4.70716800510311,52.15500418035832,0 4.707671825192278,52.15498496004398,0 4.707743878685751,52.15525628533189,0 4.707149393888881,52.1553218720998,0 4.70716800510311,52.15500418035832,0 4.70716800510311,52.15500418035832,0
+
+
+
+
+
+ Gate 13
+
+ 1
+
+
+ 4.704140681716737,52.14813498986593,0 4.704864196194787,52.1479883822655,0 4.705153909432487,52.14838874308533,0 4.704223464041033,52.14854260247372,0 4.704140681716737,52.14813498986593,0 4.704140681716737,52.14813498986593,0
+
+
+
+
+
+ Finish
+
+ 1
+
+
+ 4.70414987291546,52.1461319705247,0 4.704561170436561,52.14607111930849,0 4.704642182077736,52.14626893773362,0 4.70415735390207,52.14628831020436,0 4.70414987291546,52.1461319705247,0 4.70414987291546,52.1461319705247,0
+
+
+
+
+
+
+
+
diff --git a/rowers/tests/testdata/backup/lofoten.jpg b/rowers/tests/testdata/backup/lofoten.jpg
new file mode 100644
index 00000000..474999ca
Binary files /dev/null and b/rowers/tests/testdata/backup/lofoten.jpg differ
diff --git a/rowers/tests/testdata/backup/testdata.gpx b/rowers/tests/testdata/backup/testdata.gpx
new file mode 100644
index 00000000..462f2508
--- /dev/null
+++ b/rowers/tests/testdata/backup/testdata.gpx
@@ -0,0 +1,574 @@
+Garmin InternationalExport by rowingdata
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz
index 529a7af8..4dbdb99b 100644
Binary files a/rowers/tests/testdata/testdata.tcx.gz and b/rowers/tests/testdata/testdata.tcx.gz differ