Private
Public Access
1
0

Merge branch 'release/v18.9.7'

This commit is contained in:
Sander Roosendaal
2023-01-18 20:30:32 +01:00
11 changed files with 501 additions and 177 deletions
+8 -3
View File
@@ -1214,7 +1214,12 @@ def save_workout_database(f2, r, dosmooth=True, workouttype='rower',
delta = datetime.timedelta(
seconds=totaltime)
try:
workoutenddatetime = workoutstartdatetime+delta
except AttributeError as e:
workoutstartdatetime = pendulum.parse(str(workoutstartdatetime))
workoutenddatetime = workoutstartdatetime+delta
# check for duplicate start times and duration
duplicate = checkduplicates(
@@ -1570,7 +1575,7 @@ def get_existing_job(w):
def workout_trimp(w, reset=False):
r = w.user
if w.trimp > 0 and not reset:
if w.trimp > -1 and not reset:
return w.trimp, w.hrtss
if get_existing_job(w):
@@ -1615,7 +1620,7 @@ def workout_trimp(w, reset=False):
def workout_rscore(w, reset=False):
if w.rscore > 0 and not reset:
if w.rscore > -1 and not reset:
return w.rscore, w.normp
if get_existing_job(w):
@@ -1646,7 +1651,7 @@ def workout_rscore(w, reset=False):
def workout_normv(w, pp=4.0):
if w.normv > 0: # pragma: no cover
if w.normv > -1: # pragma: no cover
return w.normv, w.normw
if get_existing_job(w):
+10
View File
@@ -67,6 +67,7 @@ from rowers.models import (
)
import pytz
from pytz.exceptions import UnknownTimeZoneError
import collections
import pendulum
from rowingdata import rowingdata as rrdata
@@ -915,6 +916,12 @@ parsers = {
def get_startdate_time_zone(r, row, startdatetime=None):
if isinstance(startdatetime, str):
try:
startdatetime = pendulum.parse(startdatetime)
except:
dologging('debuglog.log','Could not parse start date time '+startdatetime)
if startdatetime is not None and startdatetime != '':
try:
timezone_str = pendulum.instance(startdatetime).timezone.name
@@ -969,7 +976,10 @@ def get_startdate_time_zone(r, row, startdatetime=None):
else:
timezone_str = str(startdatetime.tzinfo)
try:
startdatetime = startdatetime.astimezone(pytz.timezone(timezone_str))
except UnknownTimeZoneError:
startdatetime = startdatetime.astimezone(pytz.utc)
startdate = startdatetime.strftime('%Y-%m-%d')
starttime = startdatetime.strftime('%H:%M:%S')
+2 -2
View File
@@ -1524,10 +1524,10 @@ def getfatigues(
for w in ws:
if getattr(w, metricchoice) > 0:
weight += getattr(w, metricchoice)
if getattr(w, metricchoice) <= 0:
if getattr(w, metricchoice) == 0:
if metricchoice == 'rscore' and w.hrtss > 0: # pragma: no cover
weight += w.hrtss
else:
if getattr(w, metricchoice) < 0:
trimp, hrtss = dataprep.workout_trimp(w)
rscore, normp = dataprep.workout_rscore(w)
if w.rpe and w.rpe > 0: # pragma: no cover
+13 -2
View File
@@ -9,13 +9,15 @@ from rowsandall_app.settings import (
RP3_CLIENT_ID, RP3_CLIENT_KEY, RP3_REDIRECT_URI, RP3_CLIENT_SECRET,
UPLOAD_SERVICE_URL, UPLOAD_SERVICE_SECRET
)
from rowers.utils import myqueue
from rowers.utils import myqueue, NoTokenError
# All the functionality needed to connect to Runkeeper
from rowers.imports import *
# Python
import gzip
from datetime import timedelta
import base64
from io import BytesIO
@@ -48,6 +50,11 @@ graphql_url = "https://rp3rowing-app.com/graphql"
# Checks if user has UnderArmour token, renews them if they are expired
def rp3_open(user):
tokenexpirydate = user.rower.rp3tokenexpirydate
if timezone.now()-timedelta(days=120)>tokenexpirydate:
user.rower.rp3tokenexpirydate = timezone.now()-timedelta(days=1)
user.rower.save()
raise NoTokenError
return imports_open(user, oauth_data)
# Refresh ST token using refresh token
@@ -137,8 +144,12 @@ def get_rp3_workouts(rower, do_async=True): # pragma: no cover
w.uploadedtorp3 for w in Workout.objects.filter(user=rower)
])
dologging('rp3_import.log',rp3ids)
newids = [rp3id for rp3id in rp3ids if rp3id not in knownrp3ids]
dologging('rp3_import.log',newids)
for id in newids:
startdatetime = workouts_list.loc[id, 'executed_at']
dologging('rp3_import.log', startdatetime)
@@ -150,7 +161,7 @@ def get_rp3_workouts(rower, do_async=True): # pragma: no cover
auth_token,
id,
startdatetime,
10,
20,
)
return 1
+8
View File
@@ -2967,6 +2967,7 @@ def handle_rp3_async_workout(userid, rp3token, rp3id, startdatetime, max_attempt
headers=headers,
json={'query': get_download_link}
)
dologging('rp3_import.log',response.status_code)
if response.status_code != 200: # pragma: no cover
have_link = True
@@ -2974,6 +2975,7 @@ def handle_rp3_async_workout(userid, rp3token, rp3id, startdatetime, max_attempt
try:
workout_download_details = pd.json_normalize(
response.json()['data']['download'])
dologging('rp3_import.log', response.json())
except: # pragma: no cover
return 0
@@ -2981,8 +2983,12 @@ def handle_rp3_async_workout(userid, rp3token, rp3id, startdatetime, max_attempt
download_url = workout_download_details.iat[0, 2]
have_link = True
dologging('rp3_import.log', download_url)
counter += 1
dologging('rp3_import.log', counter)
if counter > max_attempts: # pragma: no cover
have_link = True
@@ -2994,6 +3000,8 @@ def handle_rp3_async_workout(userid, rp3token, rp3id, startdatetime, max_attempt
filename = 'media/RP3Import_'+str(rp3id)+'.csv'
res = requests.get(download_url, headers=headers)
dologging('rp3_import.log','tasks.py '+str(rp3id))
dologging('rp3_import.log',startdatetime)
if not startdatetime: # pragma: no cover
startdatetime = str(timezone.now())
+84
View File
@@ -658,6 +658,90 @@
fails a test, its values are silently replaced by zeros.</p>
</td>
</tr>
<tr>
<td>Stroke Data (v3)</td>
<td><a href="/rowers/api/v3/workouts/">/rowers/api/v3/workouts/</a>
</td>
<td>POST</td>
<td>
<pre>{
"totalDistance": 100,
"elapsedTime": 29000,
"title": "Test Workout (GO)",
"startdatetime": "2023-01-16 17:54:35.588838+00:00",
"workouttype": "water",
"boattype": "1x",
"notes": "some\nnotes",
"strokes": {
"data": [
{
"time": 3200.0000476837,
"pace": 155068.4885951763,
"hr": 85.7857142857,
"power": 84.6531131591,
"distance": 23,
"spm": 16.380952381
},
{
"time": 6700.0000476837,
"pace" : 144402.6407586741,
"hr": 91.2142857143,
"power": 117.458827834,
"distance": 36,
"spm": 21.1666666667
},
{
"time": 10099.9999046326,
"pace": 138830.8712654931,
"hr": 95.7142857143,
"power": 141.31057207,
"distance": 48,
"spm": 19.8095238095
}
]
}
}
</pre>
With v3, you can post stroke data and workout metadata in one JSON object.
<br>
<p>For v3, mandatory data fields are:</p>
<p>
<ul class="contentli">
<li><b>time</b>: Time (milliseconds since workout start)</li>
<li><b>distance</b>: Distance (meters)</li>
<li><b>pace</b>: Pace (milliseconds per 500m)</li>
<li><b>spm</b> Stroke rate (strokes per minute)</li>
</ul>
</p>
<p>Optional data fiels are:</p>
<p>
<ul class="contentli">
<li><b>power</b>: Power (Watt)</li>
<li><b>latitude</b>: GPS position (latitude)</li>
<li><b>longitude</b>: GPS position (longitude)</li>
<li><b>drivelength</b>: Drive length (meters)</li>
<li><b>dragfactor</b>: Drag factor</li>
<li><b>drivetime</b>: Drive time (ms)</li>
<li><b>strokerecoverytime</b>: Recovery time (ms)</li>
<li><b>averagedriveforce</b>: Average handle force (lbs)</li>
<li><b>peakdriveforce</b>: Peak handle force (lbs)</li>
<li><b>lapidx</b>: Lap identifier</li>
<li><b>hr</b>: Heart rate (beats per minute)</li>
<li><b>wash</b>: Wash as defined per Empower oarlock (degrees)</li>
<li><b>catch</b>: Catch angle per Empower oarlock (degrees)</li>
<li><b>finish</b>: Finish angle per Empower oarlock (degrees)</li>
<li><b>peakforceangle</b>: Peak Force Angle per Empower oarlock (degrees)</li>
<li><b>slip</b>: Slip as defined per Empower oarlock (degrees)</li>
</ul>
</p>
<p>Also for this API version, consistency checks will be done and the stroke data will be
refused if the mandatory data fields don't pass the checks.
All mandatory data fields
must have the same number of records. If an optional data field
fails a test, its values are silently replaced by zeros.</p>
</td>
</tr>
</tbody>
</table>
</p>
+72
View File
@@ -180,3 +180,75 @@ class OwnApi(TestCase):
response = strokedatajson_v2(request,id=w.id)
self.assertEqual(response.status_code,200)
def test_strokedataform_v3(self):
login = self.c.login(username=self.u.username, password=self.password)
self.assertTrue(login)
w = self.user_workouts[1]
url = reverse('strokedatajson_v3')
request = self.factory.get(url)
request.user = self.u
force_authenticate(request, user=self.u)
response = strokedatajson_v3(request)
self.assertEqual(response.status_code,405)
strokedata = {
"data": [
{
"time": 3200.0000476837,
"pace": 155068.4885951763,
"hr": 85.7857142857,
"power": 84.6531131591,
"distance": 23,
"spm": 16.380952381
},
{
"time": 6700.0000476837,
"pace" : 144402.6407586741,
"hr": 91.2142857143,
"power": 117.458827834,
"distance": 36,
"spm": 21.1666666667
},
{
"time": 10099.9999046326,
"pace": 138830.8712654931,
"hr": 95.7142857143,
"power": 141.31057207,
"distance": 48,
"spm": 19.8095238095
}
]
}
form_data = {
"totalDistance": 100,
"elapsedTime": 29000,
"title": "Test Workout (GO)",
"startdatetime": "2023-01-16 17:54:35.588838+00:00",
"workouttype": "water",
"boattype": "1x",
"notes": "some\nnotes",
"strokes": strokedata,
}
url = reverse('strokedatajson_v3')
request = self.factory.post(url,form_data,format='json')
request.user = self.u
request.data = json.dumps(form_data)
force_authenticate(request, user=self.u)
with patch('rowers.dataprep.getrowdata_db') as mock_getrowdata:
mock_getrowdata.return_value = (pd.DataFrame(),None)
response = strokedatajson_v3(request)
self.assertEqual(response.status_code,200)
response = json.loads(response.content)
x = response['workout id']
+2
View File
@@ -247,6 +247,8 @@ urlpatterns = [
name='strokedatajson'),
re_path(r'^api/v2/workouts/(?P<id>\b[0-9A-Fa-f]+\b)/strokedata/$', views.strokedatajson_v2,
name='strokedatajson_v2'),
re_path(r'^api/v3/workouts/$', views.strokedatajson_v3,
name='strokedatajson_v3'),
re_path(r'^500v/$', views.error500_view, name='error500_view'),
re_path(r'^500q/$', views.servererror_view, name='servererror_view'),
path('502/', TemplateView.as_view(template_name='502.html'), name='502'),
+256 -130
View File
@@ -3,6 +3,8 @@ from rowers.tasks import handle_calctrimp
from rowers.opaque import encoder
import arrow
import pendulum
from rowsandall_app.settings import UPLOAD_SERVICE_SECRET, UPLOAD_SERVICE_URL
# Stroke data form to test API upload
@@ -37,6 +39,138 @@ def strokedataform(request, id=0):
'workout': w,
}) # pragma: no cover
def api_get_dataframe(startdatetime, df):
try:
time = df['time']/1.e3
except KeyError: # pragma: no cover
try:
time = df['t']/10.
except KeyError:
return 400, "Missing time", pd.DataFrame()
try:
spm = df['spm']
except KeyError: # pragma: no cover
return 400, "Missing spm", pd.DataFrame()
try:
distance = df['distance']
except KeyError: # pragma: no cover
try:
distance = df['d']/10.
except KeyError:
return 400, "Missing distance", pd.DataFrame()
try:
pace = df['pace']/1.e3
except KeyError: # pragma: no cover
try:
pace = df['p']/10.
except KeyError:
return 400, "Missing pace", pd.DataFrame()
try:
power = df['power']
except KeyError: # pragma: no cover
power = 0*time
try:
drivelength = df['drivelength']
except KeyError:
drivelength = 0*time
try:
dragfactor = df['dragfactor']
except KeyError:
dragfactor = 0*time
try:
drivetime = df['drivetime']
except KeyError:
drivetime = 0*time
try:
strokerecoverytime = df['strokerecoverytime']
except KeyError:
strokerecoverytime = 0*time
try:
averagedriveforce = df['averagedriveforce']
except KeyError:
averagedriveforce = 0*time
try:
peakdriveforce = df['peakdriveforce']
except KeyError:
peakdriveforce = 0*time
try:
wash = df['wash']
except KeyError:
wash = 0*time
try:
catch = df['catch']
except KeyError:
catch = 0*time
try:
finish = df['finish']
except KeyError:
finish = 0*time
try:
peakforceangle = df['peakforceangle']
except KeyError:
peakforceangle = 0*time
try:
driveenergy = df['driveenergy']
except KeyError:
driveenergy = 60.*power/spm
try:
slip = df['slip']
except KeyError:
slip = 0*time
try:
lapidx = df['lapidx']
except KeyError:
lapidx = 0*time
try:
hr = df['hr']
except KeyError: # pragma: no cover
hr = 0*df['time']
try:
latitude = df['latitude']
except KeyError:
latitude = 0*df['time']
try:
longitude = df['longitude']
except KeyError:
longitude = 0*df['time']
starttime = totimestamp(startdatetime)+time[0]
unixtime = starttime+time
dologging('apilog.log',"(strokedatajson_v2/3 POST - data parsed)")
data = pd.DataFrame({'TimeStamp (sec)': unixtime,
' Horizontal (meters)': distance,
' Cadence (stokes/min)': spm,
' HRCur (bpm)': hr,
' DragFactor': dragfactor,
' Stroke500mPace (sec/500m)': pace,
' Power (watts)': power,
' DriveLength (meters)': drivelength,
' DriveTime (ms)': drivetime,
' StrokeRecoveryTime (ms)': strokerecoverytime,
' AverageDriveForce (lbs)': averagedriveforce,
' PeakDriveForce (lbs)': peakdriveforce,
' lapIdx': lapidx,
' ElapsedTime (sec)': time,
'catch': catch,
'slip': slip,
'finish': finish,
'wash': wash,
'driveenergy': driveenergy,
'peakforceangle': peakforceangle,
' latitude': latitude,
' longitude': longitude,
})
return 200, "", data
@login_required()
def strokedataform_v2(request, id=0):
@@ -69,10 +203,127 @@ def strokedataform_v2(request, id=0):
}) # pragma: no cover
@csrf_exempt
@login_required()
@api_view(["POST"])
@permission_classes([IsAuthenticated])
def strokedatajson_v3(request):
"""
POST: Add Stroke data to workout
GET: Get stroke data of workout
This v2 API works on stroke based data dict:
{
"totalDistance": 100,
"elapsedTime": 592,
"title": "Test Workout (GO)",
"startdatetime": "2023-01-16 17:54:35.588838+00:00",
"workouttype": "water",
"boattype": "1x",
"notes": "some\nnotes",
"strokes": {"data": [
{"distance":5, "power": 112, "hr": 132, "pace": 145800, "spm": 11, "time": 0, "latitude":52.2264097,"longitude":6.8493638},
{"distance":12, "power": 221, "hr": 131, "pace": 116400, "spm": 41, "time": 2200, "latitude":52.2263474,"longitude":6.8495814},
{"distance":19, "power": 511, "hr": 131, "pace": 88100, "spm": 56, "time": 4599, "latitude":52.2262715,"longitude":6.8496975},
{"distance":27, "power": 673, "hr": 132, "pace": 80400, "spm": 59, "time": 7000, "latitude":52.2262003,"longitude":6.8498095},
{"distance":35, "power": 744, "hr": 133, "pace": 77700, "spm": 55, "time": 9599, "latitude":52.2261312, "longitude":6.8499267},
{"distance":43, "power": 754, "hr": 136, "pace": 77400, "spm": 48, "time": 12000, "latitude":52.2260576,"longitude":6.8500366},
{"distance":51, "power": 754, "hr": 139, "pace": 77400, "spm": 48, "time": 14400, "latitude":52.2259872,"longitude":6.8501561},
{"distance":59, "power": 749, "hr": 142, "pace": 77600, "spm": 48, "time": 16799, "latitude":52.2259154,"longitude":6.8502769},
{"distance":67, "power": 729, "hr": 145, "pace": 78300, "spm": 48, "time": 19400, "latitude":52.2257749,"longitude":6.8503918},
{"distance":75, "power": 729, "hr": 147, "pace": 78300, "spm": 48, "time": 21799, "latitude":52.2259154,"longitude":6.8502769},
{"distance":82, "power": 726, "hr": 150, "pace": 78400, "spm": 48, "time": 24200, "latitude":52.2259872,"longitude":6.8501561},
{"distance":90, "power": 709, "hr": 152, "pace": 79000, "spm": 48, "time": 26599, "latitude":52.2260576,"longitude":6.8500366},
{"distance":100, "power": 707, "hr": 153, "pace": 79100, "spm": 49, "time": 29000, "latitude":52.2261312,"longitude":6.8499267}]}
}
"""
if request.method != 'POST':
return HttpResponseNotAllowed("Method not supported") # pragma: no cover
dologging('apilog.log', request.user.username+" (strokedatajson_v3 POST)")
title = request.data.get('title','')
try:
elapsedTime = request.data['elapsedTime']
except KeyError:
return HttpResponse("Missing Elapsed Time", status=400)
try:
totalDistance = request.data['totalDistance']
except KeyError:
return HttpResponse("Missing Total Distance", status=400)
timeZone = request.data.get('timezone','UTC')
workouttype = request.data.get('workouttype','rower')
boattype = request.data.get('boattype','1x')
notes = request.data.get('notes','')
rpe = request.data.get('rpe',0)
startdatetime = request.data.get('startdatetime',"%s" % timezone.now())
startdatetime = pendulum.parse(startdatetime)
df = pd.DataFrame()
try:
strokes = request.data['strokes']
except KeyError:
return HttpResponse("No Stroke Data in JSON", status=400)
try:
df = pd.DataFrame(strokes['data'])
except KeyError:
try:
df = pd.DataFrame(request.data['strokedata'])
except:
return HttpResponse("No JSON Object could be decoded", status=400)
df.index = df.index.astype(int)
df.sort_index(inplace=True)
status, comment, data = api_get_dataframe(startdatetime, df)
if status != 200:
return HttpResponse(comment, status=status)
csvfilename = 'media/{code}.csv.gz'.format(code=uuid4().hex[:16])
_ = data.to_csv(csvfilename, index_label='index', compression='gzip')
uploadoptions = {
'secret': UPLOAD_SERVICE_SECRET,
'user': request.user.id,
'file': csvfilename,
'title': title,
'workouttype': workouttype,
'boattype': boattype,
'elapsedTime': elapsedTime/1000., # in seconds
'totalDistance': totalDistance,
'rpe': rpe,
'notes': notes,
'timezone': timeZone,
}
session = requests.session()
newHeaders = {'Content-type': 'application/json', 'Accept': 'text/plain'}
session.headers.update(newHeaders)
response = session.post(UPLOAD_SERVICE_URL, json=uploadoptions)
if response.status_code != 200:
return HttpResponse(response.text, response.status_code)
try:
workoutid = response.json()['id']
except KeyError:
workoutid = 1
return JsonResponse(
{"workout public id": encoder.encode_hex(workoutid),
"workout id": workoutid,
"status": "success",
})
# Process the POSTed stroke data according to the API definition
# Return the GET stroke data according to the API definition
@csrf_exempt
@login_required()
@api_view(["GET", "POST"])
@@ -161,134 +412,9 @@ def strokedatajson_v2(request, id):
df.index = df.index.astype(int)
df.sort_index(inplace=True)
try:
time = df['time']/1.e3
except KeyError: # pragma: no cover
try:
time = df['t']/10.
except KeyError:
return HttpResponse("Missing time", status=400)
try:
spm = df['spm']
except KeyError: # pragma: no cover
return HttpResponse("Missing spm", status=400)
try:
distance = df['distance']
except KeyError: # pragma: no cover
try:
distance = df['d']/10.
except KeyError:
return HttpResponse("Missing distance", status=400)
try:
pace = df['pace']/1.e3
except KeyError: # pragma: no cover
try:
pace = df['p']/10.
except KeyError:
return HttpResponse("Missing pace", status=400)
try:
power = df['power']
except KeyError: # pragma: no cover
power = 0*time
try:
drivelength = df['drivelength']
except KeyError:
drivelength = 0*time
try:
dragfactor = df['dragfactor']
except KeyError:
dragfactor = 0*time
try:
drivetime = df['drivetime']
except KeyError:
drivetime = 0*time
try:
strokerecoverytime = df['strokerecoverytime']
except KeyError:
strokerecoverytime = 0*time
try:
averagedriveforce = df['averagedriveforce']
except KeyError:
averagedriveforce = 0*time
try:
peakdriveforce = df['peakdriveforce']
except KeyError:
peakdriveforce = 0*time
try:
wash = df['wash']
except KeyError:
wash = 0*time
try:
catch = df['catch']
except KeyError:
catch = 0*time
try:
finish = df['finish']
except KeyError:
finish = 0*time
try:
peakforceangle = df['peakforceangle']
except KeyError:
peakforceangle = 0*time
try:
driveenergy = df['driveenergy']
except KeyError:
driveenergy = 60.*power/spm
try:
slip = df['slip']
except KeyError:
slip = 0*time
try:
lapidx = df['lapidx']
except KeyError:
lapidx = 0*time
try:
hr = df['hr']
except KeyError: # pragma: no cover
hr = 0*df['time']
try:
latitude = df['latitude']
except KeyError:
latitude = 0*df['time']
try:
longitude = df['longitude']
except KeyError:
longitude = 0*df['time']
starttime = totimestamp(row.startdatetime)+time[0]
unixtime = starttime+time
dologging('apilog.log',"(strokedatajson_v2 POST - data parsed)")
data = pd.DataFrame({'TimeStamp (sec)': unixtime,
' Horizontal (meters)': distance,
' Cadence (stokes/min)': spm,
' HRCur (bpm)': hr,
' DragFactor': dragfactor,
' Stroke500mPace (sec/500m)': pace,
' Power (watts)': power,
' DriveLength (meters)': drivelength,
' DriveTime (ms)': drivetime,
' StrokeRecoveryTime (ms)': strokerecoverytime,
' AverageDriveForce (lbs)': averagedriveforce,
' PeakDriveForce (lbs)': peakdriveforce,
' lapIdx': lapidx,
' ElapsedTime (sec)': time,
'catch': catch,
'slip': slip,
'finish': finish,
'wash': wash,
'driveenergy': driveenergy,
'peakforceangle': peakforceangle,
' latitude': latitude,
' longitude': longitude,
})
status, comment, data = api_get_dataframe(row.startdatetime, df)
if status != 200:
return HttpResponse(comment, status=status)
r = getrower(request.user)
+1 -1
View File
@@ -33,7 +33,7 @@ from rq.exceptions import NoSuchJobError
import threading
import redis
import colorsys
import re
import zipfile
import bleach
import arrow
+6
View File
@@ -5234,6 +5234,8 @@ def workout_upload_api(request):
r = None
if form.is_valid():
t = form.cleaned_data['title']
t = re.sub('\r',' ',t)
t = re.sub('\n',' ',t)
boattype = form.cleaned_data['boattype']
workouttype = form.cleaned_data['workouttype']
try:
@@ -6701,6 +6703,7 @@ def workout_summary_edit_view(request, id, message="", successmessage=""
vals = None
# feeling lucky / ruptures
if request.method == 'POST' and "ruptures" in request.POST:
try:
df = pd.DataFrame({
'spm': rowdata.df[' Cadence (stokes/min)'],
'power': rowdata.df[' Power (watts)'],
@@ -6735,6 +6738,9 @@ def workout_summary_edit_view(request, id, message="", successmessage=""
intervalString = s
form = SummaryStringForm(initial={'intervalstring': intervalString})
except KeyError:
messages.error(request, "Nope, you were not lucky")
# We have submitted the mini language interpreter
if request.method == 'POST' and "intervalstring" in request.POST:
form = SummaryStringForm(request.POST)