From 45ac5f80a3135e0ec9a2688bd46bcd9423da4290 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Mon, 28 Nov 2016 15:49:46 +0100 Subject: [PATCH 1/6] Reponse Codes --- rowers/views.py | 41 ++++++++++++++++++++++---------------- rowsandall_app/settings.py | 5 +++-- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/rowers/views.py b/rowers/views.py index dd72faee..fefb5580 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -3,7 +3,11 @@ import operator from django.views.generic.base import TemplateView from django.db.models import Q from django.shortcuts import render -from django.http import HttpResponse, HttpResponseRedirect +from django.http import ( + HttpResponse, HttpResponseRedirect, + HttpResponseForbidden, HttpResponseNotAllowed, + HttpResponseNotFound, + ) from django.contrib.auth import authenticate, login, logout from rowers.forms import LoginForm,DocumentsForm,UploadOptionsForm from django.core.urlresolvers import reverse @@ -2019,7 +2023,7 @@ def workout_view(request,id=0): except Workout.DoesNotExist: - return HttpResponse("Workout doesn't exist") + return HttpResponseNotFound("Workout doesn't exist") @user_passes_test(promember,login_url="/login") @@ -4036,7 +4040,7 @@ def workout_delete_confirm_view(request, id=0): 'workout':row}) except Workout.DoesNotExist: - return HttpResponse("Workout doesn't exist") + return HttpResponseNotFound("Workout doesn't exist") @login_required() def workout_delete_view(request,id=0): @@ -4055,7 +4059,7 @@ def workout_delete_view(request,id=0): return HttpResponseRedirect(url) except Workout.DoesNotExist: - return HttpResponse("Workout doesn't exist") + return HttpResponseNotFound("Workout doesn't exist") @login_required() @@ -4071,7 +4075,7 @@ def graph_delete_confirm_view(request, id=0): 'graph':img}) except Workout.DoesNotExist: - return HttpResponse("Workout doesn't exist") + return HttpResponseNotFound("Workout doesn't exist") @login_required() def graph_delete_view(request,id=0): @@ -4168,7 +4172,7 @@ def workout_summary_restore_view(request,id,message="",successmessage=""): if (checkworkoutuser(request.user,row)==False): return HttpResponse("You are not allowed to edit this workout") except Workout.DoesNotExist: - return HttpResponse("Workout doesn't exist") + return HttpResponseNotFound("Workout doesn't exist") s = "" # still here - this is a workout we may edit @@ -4216,7 +4220,7 @@ def workout_summary_edit_view(request,id,message="",successmessage="" if (checkworkoutuser(request.user,row)==False): return HttpResponse("You are not allowed to edit this workout") except Workout.DoesNotExist: - return HttpResponse("Workout doesn't exist") + return HttpResponseNotFound("Workout doesn't exist") s = "" # still here - this is a workout we may edit @@ -4524,13 +4528,13 @@ def strokedatajson(request,id): try: row = Workout.objects.get(id=id) if (checkworkoutuser(request.user,row)==False): - return HttpResponse("Permission error") + return HttpResponseForbidden("Permission error") except Workout.DoesNotExist: - return HttpResponse("Workout doesn't exist") + return HttpResponseNotFound("Workout doesn't exist") try: id = int(id) except ValueError: - return HttpResponse("Not a valid workout number") + return HttpResponse("Not a valid workout number",status=400) if request.method == 'GET': @@ -4541,7 +4545,7 @@ def strokedatajson(request,id): if request.method == 'POST': checkdata,r = dataprep.getrowdata_db(id=row.id) if not checkdata.empty: - return "Not OK 1" + return HttpResponse("Duplicate Error",409) # strokedata = request.POST['strokedata'] print request.body received_json_data = json.loads(request.body) @@ -4549,7 +4553,7 @@ def strokedatajson(request,id): try: strokedata = json.loads(received_json_data['strokedata']) except: - return HttpResponse("Not OK 2") + return HttpResponse("No JSON object could be decoded",400) df = pd.DataFrame(strokedata) df.index = df.index.astype(int) @@ -4559,18 +4563,18 @@ def strokedatajson(request,id): aantal = len(time) pace = df['pseconds'] if len(pace) != aantal: - return "Not OK" + return HttpResponse("Pace array has incorrect length",status=400) distance = df['distance'] if len(distance) != aantal: - return "Not OK 3" + return HttpResponse("Distance array has incorrect length",status=400) spm = df['spm'] if len(spm) != aantal: - return "Not OK 4" + return HttpResponse("SPM array has incorrect length",status=400) res = dataprep.testdata(time,distance,pace,spm) if not res: - return HttpResponse("Not OK 5") + return HttpResponse("Data are not numerical",status=400) power = trydf(df,aantal,'power') drivelength = trydf(df,aantal,'drivelength') @@ -4621,4 +4625,7 @@ def strokedatajson(request,id): # mangling # - return HttpResponse("OK") + return HttpResponse(row.id,status=201) + + #Method not supported + return HttpResponseNotAllowed("Method not supported") diff --git a/rowsandall_app/settings.py b/rowsandall_app/settings.py index 1e78bec2..e9cb69e5 100644 --- a/rowsandall_app/settings.py +++ b/rowsandall_app/settings.py @@ -45,7 +45,7 @@ INSTALLED_APPS = [ 'cvkbrno', 'django_rq', 'translation_manager', - 'debug_toolbar', +# 'debug_toolbar', 'django_mailbox', 'rest_framework', 'rest_framework_swagger', @@ -71,7 +71,7 @@ MIDDLEWARE_CLASSES = [ 'oauth2_provider.middleware.OAuth2TokenMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'debug_toolbar.middleware.DebugToolbarMiddleware', +# 'debug_toolbar.middleware.DebugToolbarMiddleware', ] ROOT_URLCONF = 'rowsandall_app.urls' @@ -267,3 +267,4 @@ REST_FRAMEWORK = { ), 'PAGE_SIZE': 20, } + From 170d0e43a1d995140e67fa20ba79a986f8a68299 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 14 Dec 2016 22:55:23 +0100 Subject: [PATCH 2/6] permissions --- rowers/permissions.py | 2 +- rowers/urls.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rowers/permissions.py b/rowers/permissions.py index 03c1652b..b3872796 100644 --- a/rowers/permissions.py +++ b/rowers/permissions.py @@ -13,7 +13,7 @@ class IsOwnerOrReadOnly(permissions.BasePermission): return True # Write permissions are only allowed to the owner of the snippet. - return obj.owner == request.user + return obj.user == request.user class IsOwnerOrNot(permissions.BasePermission): def has_object_permission(self, request, view, obj): diff --git a/rowers/urls.py b/rowers/urls.py index 9e2c2e8d..f6b9beb5 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -18,7 +18,7 @@ from rowers.permissions import IsOwnerOrNot,IsOwnerOrReadOnly from rowers.serializers import WorkoutSerializer,RowerSerializer class WorkoutViewSet(viewsets.ModelViewSet): - queryset = Workout.objects.none().order_by("-date", "-starttime") + queryset = Workout.objects.all().order_by("-date", "-starttime") serializer_class = WorkoutSerializer permission_classes = (IsOwnerOrNot,) From 766f9bdc031441cd0c40a354e5377fcd045fe923 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Mon, 19 Dec 2016 16:07:30 +0100 Subject: [PATCH 3/6] tests --- rowers/tests.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rowers/tests.py b/rowers/tests.py index aa4666df..04439d70 100644 --- a/rowers/tests.py +++ b/rowers/tests.py @@ -874,6 +874,7 @@ class ViewTest(TestCase): f_to_be_deleted = w.csvfilename os.remove(f_to_be_deleted+'.gz') + def test_upload_view_sled_desktop(self): self.c.login(username='john',password='koeinsloot') From 24de5c681d573dd7dc56a65feee1999e3b4a06f6 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 22 Dec 2016 12:21:04 +0100 Subject: [PATCH 4/6] API seems to work ok --- rowers/ownapistuff.py | 8 ++++---- rowers/views.py | 4 ++-- rowsandall_app/settings.py | 13 ++++++++++++- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/rowers/ownapistuff.py b/rowers/ownapistuff.py index 4a1754bf..3ec7088e 100644 --- a/rowers/ownapistuff.py +++ b/rowers/ownapistuff.py @@ -85,9 +85,9 @@ def get_token(code): client_auth = requests.auth.HTTPBasicAuth(TEST_CLIENT_ID, TEST_CLIENT_SECRET) post_data = {"grant_type": "authorization_code", "code": code, - "redirect_uri": TEST_REDIRECT_URI, - "client_secret": TEST_CLIENT_SECRET, - "client_id":TEST_CLIENT_ID, + "redirect_uri": "http://localhost:8000/rowers/test_callback", + "client_secret": "aapnootmies", + "client_id":1, } headers = {'Accept': 'application/json', 'Content-Type': 'application/json'} @@ -99,12 +99,12 @@ def get_token(code): data=json.dumps(post_data), headers=headers) - print response.text token_json = response.json() thetoken = token_json['access_token'] expires_in = token_json['expires_in'] refresh_token = token_json['refresh_token'] + return [thetoken,expires_in,refresh_token] def make_authorization_url(request): diff --git a/rowers/views.py b/rowers/views.py index 56fb1961..f9ba1e97 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -105,7 +105,7 @@ from interactiveplots import * schema_view = get_swagger_view(title='Rowsandall API (Unstable)') - + def error500_view(request): response = render_to_response('500.html', {}, @@ -4830,7 +4830,7 @@ def strokedatajson(request,id): if request.method == 'GET': - columns = ['spm','timesecs','hr','pseconds','power','distance'] + columns = ['spm','time','hr','pace','power','distance'] datadf = dataprep.getsmallrowdata_db(columns,ids=[id]) return JSONResponse(datadf) diff --git a/rowsandall_app/settings.py b/rowsandall_app/settings.py index fed8a01c..a0b4513d 100644 --- a/rowsandall_app/settings.py +++ b/rowsandall_app/settings.py @@ -200,6 +200,7 @@ MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # user authentication LOGIN_REDIRECT_URL = '/rowers/list-workouts/' LOGIN_URL = '/login/' +LOGOUT_URL = '/logout/' # Concept 2 C2_CLIENT_ID = "bgTBbmjSyn8wbJb0JEdlYjDUfSZFAPQSzJV8YDwH" @@ -260,7 +261,8 @@ GMAPIKEY = "AIzaSyAgu1w9QSthaGPMLp8y9JedPoMc9sfEgJ8" OAUTH2_PROVIDER = { # this is the list of available scopes - 'SCOPES': {'read': 'Read scope', 'write': 'Write scope', 'groups': 'Access to your groups'} + 'SCOPES': {'read': 'Read scope', 'write': 'Write scope', 'groups': 'Access to your groups'}, + 'OAUTH2_BACKEND_CLASS': 'oauth2_provider.oauth2_backends.JSONOAuthLibCore' } @@ -280,6 +282,15 @@ REST_FRAMEWORK = { 'PAGE_SIZE': 20, } +SWAGGER_SETTINGS = { + 'SECURITY_DEFINITION': { + 'basic': { + 'type':'basic' + } + }, + 'SHOW_REQUEST_HEADERS': True, + } + # Analytics CLICKY_SITE_ID = '101011008' From 56fc3a52d3f13397621c231e461df20028b31a70 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 22 Dec 2016 15:39:57 +0100 Subject: [PATCH 5/6] sorted permissions for workouts-list --- rowers/permissions.py | 1 + rowers/serializers.py | 8 +++++--- rowers/templates/base.html | 1 - rowers/urls.py | 32 ++++++++++++++++++++++++-------- rowers/views.py | 18 ++++++++++-------- rowsandall_app/settings.py | 6 ++++++ 6 files changed, 46 insertions(+), 20 deletions(-) diff --git a/rowers/permissions.py b/rowers/permissions.py index b3872796..67afe16c 100644 --- a/rowers/permissions.py +++ b/rowers/permissions.py @@ -16,6 +16,7 @@ class IsOwnerOrReadOnly(permissions.BasePermission): return obj.user == request.user class IsOwnerOrNot(permissions.BasePermission): + def has_object_permission(self, request, view, obj): r = Rower.objects.get(user=request.user) return (obj.user == r) diff --git a/rowers/serializers.py b/rowers/serializers.py index b0890f54..e0c8b560 100644 --- a/rowers/serializers.py +++ b/rowers/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from rowers.models import Workout,Rower +from rowers.models import Workout,Rower,StrokeData import datetime @@ -93,8 +93,8 @@ class WorkoutSerializer(serializers.ModelSerializer): class StrokeDataSerializer(serializers.Serializer): workoutid = serializers.IntegerField strokedata = serializers.JSONField - - def create(self, validated_data): + + def create(self, workoutid, strokedata): """ Create and enter a new set of stroke data into the DB """ @@ -103,3 +103,5 @@ class StrokeDataSerializer(serializers.Serializer): print "fake serializer" return 1 + + diff --git a/rowers/templates/base.html b/rowers/templates/base.html index ed06f76e..4c5678a4 100644 --- a/rowers/templates/base.html +++ b/rowers/templates/base.html @@ -17,7 +17,6 @@ - {% block meta %} {% endblock %} {% analytical_head_bottom %} diff --git a/rowers/urls.py b/rowers/urls.py index f6b9beb5..10ca4c4a 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -2,11 +2,11 @@ from django.conf import settings from django.conf.urls import url, include from django.contrib.auth.models import User -from models import Workout,Rower +from models import Workout,Rower,StrokeData from rest_framework import routers, serializers, viewsets,permissions from rest_framework.urlpatterns import format_suffix_patterns - +from rest_framework.permissions import * from . import views from django.contrib.auth import views as auth_views from django.views.generic.base import TemplateView @@ -15,18 +15,34 @@ from django.conf.urls import ( ) from rowers.permissions import IsOwnerOrNot,IsOwnerOrReadOnly -from rowers.serializers import WorkoutSerializer,RowerSerializer +from rowers.serializers import ( + WorkoutSerializer, + RowerSerializer, + StrokeDataSerializer, + ) class WorkoutViewSet(viewsets.ModelViewSet): - queryset = Workout.objects.all().order_by("-date", "-starttime") + model = Workout + #queryset = Workout.objects.all().order_by("-date", "-starttime") serializer_class = WorkoutSerializer - permission_classes = (IsOwnerOrNot,) + def get_queryset(self): + r = Rower.objects.get(user=self.request.user) + return Workout.objects.filter(user=r).order_by("-date","-starttime") + + + permission_classes = ( + #DjangoModelPermissions, + IsOwnerOrNot, + ) + +class StrokeDataViewSet(viewsets.ModelViewSet): + serializer_class = StrokeDataSerializer # Routers provide an easy way of automatically determining the URL conf. router = routers.DefaultRouter() -router.register(r'api/workouts',WorkoutViewSet) -#router.register(r'api/rower',RowerViewSet) +router.register(r'api/workouts',WorkoutViewSet, 'workout') + handler500 = 'views.error500_view' handler404 = 'views.error404_view' @@ -40,7 +56,7 @@ urlpatterns = [ url(r'^', include(router.urls)), url(r'^api-docs$', views.schema_view), url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), - url(r'^api/workouts/(\d+)/strokedata$',views.strokedatajson), + url(r'^api/workouts/(?P\d+)/strokedata$',views.strokedatajson), url(r'^testbokeh$',views.testbokeh), url(r'^500/$', TemplateView.as_view(template_name='500.html'),name='500'), url(r'^404/$', TemplateView.as_view(template_name='404.html'),name='404'), diff --git a/rowers/views.py b/rowers/views.py index f9ba1e97..d13289c5 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -86,7 +86,7 @@ from rest_framework.parsers import JSONParser from rest_framework.response import Response from rowers.serializers import RowerSerializer,WorkoutSerializer from rest_framework import status,permissions,generics -from rest_framework.decorators import api_view +from rest_framework.decorators import api_view, renderer_classes from permissions import IsOwnerOrNot @@ -105,8 +105,6 @@ from interactiveplots import * schema_view = get_swagger_view(title='Rowsandall API (Unstable)') - - def error500_view(request): response = render_to_response('500.html', {}, context_instance = RequestContext(request)) @@ -4813,10 +4811,16 @@ def strokedataform(request,id=0): }) +from rest_framework_swagger.renderers import OpenAPIRenderer, SwaggerUIRenderer from django.views.decorators.csrf import csrf_exempt @csrf_exempt @login_required() +@api_view(['GET','POST']) def strokedatajson(request,id): + """ + POST: Add Stroke data to workout + GET: Get stroke data of workout + """ try: row = Workout.objects.get(id=id) if (checkworkoutuser(request.user,row)==False): @@ -4839,11 +4843,9 @@ def strokedatajson(request,id): if not checkdata.empty: return HttpResponse("Duplicate Error",409) # strokedata = request.POST['strokedata'] - print request.body - received_json_data = json.loads(request.body) # checking/validating and cleaning try: - strokedata = json.loads(received_json_data['strokedata']) + strokedata = json.loads(request.POST['strokedata']) except: return HttpResponse("No JSON object could be decoded",400) @@ -4851,9 +4853,9 @@ def strokedatajson(request,id): df.index = df.index.astype(int) df.sort_index(inplace=True) # time, hr, pace, spm, power, drivelength, distance, drivespeed, dragfactor, strokerecoverytime, averagedriveforce, peakdriveforce, lapidx - time = df['timesecs'] + time = df['time']/1.e3 aantal = len(time) - pace = df['pseconds'] + pace = df['pace']/1.e3 if len(pace) != aantal: return HttpResponse("Pace array has incorrect length",status=400) distance = df['distance'] diff --git a/rowsandall_app/settings.py b/rowsandall_app/settings.py index a0b4513d..c7af4887 100644 --- a/rowsandall_app/settings.py +++ b/rowsandall_app/settings.py @@ -273,6 +273,7 @@ REST_FRAMEWORK = { # or allow read-only access for unauthenticated users. 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.IsAuthenticated' +# 'rest_framework.permissions.DjangoModelPermissions' ], 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.BasicAuthentication', @@ -286,6 +287,11 @@ SWAGGER_SETTINGS = { 'SECURITY_DEFINITION': { 'basic': { 'type':'basic' + }, + 'oauth2': { + 'type':'oauth2', + 'authorizationUrl':'/rowers/o/authorize', + 'flow': 'implicit', } }, 'SHOW_REQUEST_HEADERS': True, From 8ed24fec63e59621c97bfe432eaed3d21f168a30 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 22 Dec 2016 17:24:58 +0100 Subject: [PATCH 6/6] Draft developers info --- rowers/templates/about_us.html | 1 + rowers/templates/base.html | 12 +- rowers/templates/developers.html | 198 +++++++++++++++++++++++++++++++ rowers/urls.py | 1 + 4 files changed, 208 insertions(+), 4 deletions(-) create mode 100644 rowers/templates/developers.html diff --git a/rowers/templates/about_us.html b/rowers/templates/about_us.html index 34c64464..e562c356 100644 --- a/rowers/templates/about_us.html +++ b/rowers/templates/about_us.html @@ -142,6 +142,7 @@ You will be taken to the secure PayPal payment site.

    +
  • 2016-12-19 Interactive stroke curve for NK Empower Oarlock
  • 2016-12-07 Favorite Flex Charts for Premium users
  • 2016-12-01 Support for NK Empower Oarlock parameters (catch and finish angles, slip and wash, and power as measured by the Oarlock
  • diff --git a/rowers/templates/base.html b/rowers/templates/base.html index 4c5678a4..2b209f35 100644 --- a/rowers/templates/base.html +++ b/rowers/templates/base.html @@ -155,18 +155,22 @@ {% block footer %} -
    + -
    + -
    +
    + +
    + -
    + diff --git a/rowers/templates/developers.html b/rowers/templates/developers.html new file mode 100644 index 00000000..f383230a --- /dev/null +++ b/rowers/templates/developers.html @@ -0,0 +1,198 @@ + +{% extends "base.html" %} +{% block title %}About us{% endblock title %} +{% block content %} + +
    +

    Resources for developers

    + +

    On this page, a work in progress, I will collect useful information + for developers of rowing data apps and hardware.

    + +

    I presume you have an app (smartphone app, dedicated hardware, web site) + where your users (customers) generate, collect or store their rowing + related workout data. You can now offer your users easy ways to get + their data on this site.

    + +

    There are three ways to allow your users to get data to Rowsandall.com.

    + +
    File based export from your app
    + +

    Enable export of TCX, FIT or CSV formatted files from your app. + The users + upload the file to Rowsandall.com.

    + +
      +
    • Advantages +
        +
      • User sees immediate results
      • +
      +
    • +
    • Disadvantages +
        +
      • It is a multi-step process: Download from your + app, store, upload.
      • +
      +
    • +
    + + +
    Email from your app
    + +

    Similar as above, generate TCX, FIT or CSV formatted files and + email them + to workouts@rowsandall.com directly from your app. The From: field + should be the email address of the registered user.

    + +
      +
    • Advantages +
        +
      • It may take up to five minutes for the workout to show up + on the site.
      • +
      +
    • +
    • Disadvantages +
        +
      • It's a simple process, which can be automated.
      • +
      +
    • +
    + +
    Using the REST API
    + +

    We are building a REST API which will allow you to post and + receive stroke + data from the site directly.

    + +

    The REST API is a work in progress.

    + +
      +
    • Advantages +
        +
      • Once it is set up, this is a one-click operation.
      • +
      • You can read a user's workout data from the site and use + them in your app.
      • +
      • This is not limited to workout data. You could make a full mobile + version of our site.
      • +
      +
    • + +
    • Disadvantages +
        +
      • The API is not stable and not fully tested yet.
      • +
      • You need to register your app with us. We can revoke your + permissions if you misuse them.
      • +
      • The first time user must grant permissions to your app.
      • +
      • You need to manage authorization tokens.
      • +
      +
    • +
    + +
    + + + +
    +
    +

    Quick Links

    + +
    Accepted file formats
    + +

    All files adhering to the standards TCX and FIT formats will be parsed.

    + +

    However, some rowing related parameters are not supported by TCX and FIT. Therefore, we are supporting the CSV format that is documented in the following link.

    + + + +

    Using this standard will guarantee that your user's data are accepted + without complaints.

    + +
    API related documentation
    + +
    Registering an app
    + +

    As a registered user, you can register an app.

    + + +
    Authentication
    + +

    Standard Oauth2 authentication. + Get authorization code by pointing your user to the authorization URL. + Exchange authorization code for an access token. When access token expires, + use the refresh token to refresh it.

    + +
      +
    • Authorization URL: https://rowsandall.com/rowers/o/authorize
    • +
    • Access Token request: https://rowsandall.com/rowers/o/token/
    • +
    • Access Token refresh: https://rowsandall.com/rowers/o/token/
    • +
    + +
    API documentation
    + +

    Once you have a registered app, you have gone through the authorization + and have successfully obtained an access token, you can use it to place + POST, GET and PUT requests.

    + +

    The workout summary data and the stroke data are obtained and sent + separately.

    + + + +

    You can only post stroke data to an existing workout with + workout number {id}. If the workout already has stroke data, you + will get a duplication error. This functionality will be expanded in the + future to enable updating stroke data. Stroke data for workout {id} are + posted to:

    + +
      +
    • https://rowsandall.com/rowers/api/workouts/{id}/strokedata
    • +
    + +

    The payload is application/json data and looks as follows:

    + +

    +    {
    +  "distance": [5,12,19,27,35,43,51,59,67,75,82,90,100],
    +  "power": [112,221,511,673,744,754,754,749,729,729,726,709,707],
    +  "hr": [132,131,131,132,133,136,139,142,145,147,150,152,153],
    +  "pace": [145800,116400,88100,80400,77700,77400,77400,77600,78300,78300,78400,79000,79100],
    +  "spm": [11,41,56,59,55,48,48,48,48,48,48,48,49],
    +  "time": [0,2200,4599,7000,9599,12000,14400,16799,19400,21799,24200,26599,2900],
    +}
    +    

    + +

    Mandatory data fields are:

    +
      +
    • time: Time (milliseconds since workout start)
    • +
    • distance: Distance (meters)
    • +
    • pace: Pace (milliseconds per 500m)
    • +
    • spm Stroke rate (strokes per minute)
    • +
    +

    Optional data fiels are:

    +
      +
    • power: Power (Watt)
    • +
    • drivelength: Drive length (meters)
    • +
    • dragfactor: Drag factor
    • +
    • drivetime: Drive time (ms)
    • +
    • strokerecoverytime: Recovery time (ms)
    • +
    • averagedriveforce: Average handle force (lbs)
    • +
    • peakdriveforce: Peak handle force (lbs)
    • +
    • lapidx: Lap identifier
    • +
    • hr: Heart rate (beats per minute)
    • +
    + +

    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.

    + +
    +
    + + {% endblock content %} diff --git a/rowers/urls.py b/rowers/urls.py index 10ca4c4a..8ca46c3e 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -168,6 +168,7 @@ urlpatterns = [ url(r'^email/thankyou/$', TemplateView.as_view(template_name='thankyou.html'), name='thankyou'), url(r'^email/$', TemplateView.as_view(template_name='email.html'), name='email'), url(r'^about', TemplateView.as_view(template_name='about_us.html'),name='about'), + url(r'^developers', TemplateView.as_view(template_name='developers.html'),name='about'), url(r'^compatibility', TemplateView.as_view(template_name='compatibility.html'),name='about'), url(r'^videos', TemplateView.as_view(template_name='videos.html'),name='videos'), url(r'^analysis', TemplateView.as_view(template_name='analysis.html'),name='analysis'),