Private
Public Access
1
0

Merge branch 'develop' into feature/c2repair

This commit is contained in:
Sander Roosendaal
2016-12-22 20:25:20 +01:00
10 changed files with 299 additions and 50 deletions

View File

@@ -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):

View File

@@ -13,9 +13,10 @@ 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):
r = Rower.objects.get(user=request.user)
return (obj.user == r)

View File

@@ -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

View File

@@ -142,6 +142,7 @@ You will be taken to the secure PayPal payment site.
<p>
<ul>
<li>2016-12-19 Interactive stroke curve for NK Empower Oarlock</li>
<li>2016-12-07 Favorite Flex Charts for Premium users</li>
<li>2016-12-01 Support for NK Empower Oarlock parameters (catch and
finish angles, slip and wash, and power as measured by the Oarlock</li>

View File

@@ -17,7 +17,6 @@
<link rel="stylesheet" href="/static/css/text.css" />
<link rel="stylesheet" href="/static/css/960_12_col.css" />
<link rel="stylesheet" href="/static/css/rowsandall.css" />
<link rel="stylesheet" href="static/css/cookiecuttr.css" />
{% block meta %} {% endblock %}
{% analytical_head_bottom %}
</head>
@@ -156,18 +155,22 @@
{% block footer %}
<p id="footer"
>{{ versionstring }}</p>
<div class="grid_3 alpha">
<div class="grid_2 alpha">
<p id="footer"><a href="/rowers/email/">&copy; Sander Roosendaal</a></p>
</div>
<div class="grid_1 suffix_1">
<div class="grid_1 prefix_1">
<p id="footer">
<a href="/rowers/about">About</a></p>
</div>
<div class="grid_1 suffix_1">
<div class="grid_2">
<p id="footer">
<a href="/rowers/developers">Developers</a></p>
</div>
<div class="grid_1">
<p id="footer">
<a href="/rowers/legal">Legal</a></p>
</div>
<div class="grid_1">
<div class="grid_1 prefix_1">
<p id="footer">
<a href="/rowers/physics">Physics</a></p>
</div>

View File

@@ -0,0 +1,198 @@
{% extends "base.html" %}
{% block title %}About us{% endblock title %}
{% block content %}
<div class="grid_6 alpha">
<h2>Resources for developers</h2>
<p>On this page, a work in progress, I will collect useful information
for developers of rowing data apps and hardware.</p>
<p>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.</p>
<p>There are three ways to allow your users to get data to Rowsandall.com.</p>
<h5>File based export from your app</h5>
<p>Enable export of TCX, FIT or CSV formatted files from your app.
The users
upload the file to Rowsandall.com.</p>
<ul>
<li>Advantages
<ul>
<li>User sees immediate results</li>
</ul>
</li>
<li>Disadvantages
<ul>
<li>It is a multi-step process: Download from your
app, store, upload.</li>
</ul>
</li>
</ul>
<h5>Email from your app</h5>
<p>Similar as above, generate TCX, FIT or CSV formatted files and
email them
to <i>workouts@rowsandall.com</i> directly from your app. The From: field
should be the email address of the registered user.</p>
<ul>
<li>Advantages
<ul>
<li>It may take up to five minutes for the workout to show up
on the site.</li>
</ul>
</li>
<li>Disadvantages
<ul>
<li>It's a simple process, which can be automated.</li>
</ul>
</li>
</ul>
<h5>Using the REST API</h5>
<p>We are building a REST API which will allow you to post and
receive stroke
data from the site directly.</p>
<p>The REST API is a work in progress. </p>
<ul>
<li>Advantages
<ul>
<li>Once it is set up, this is a one-click operation.</li>
<li>You can read a user's workout data from the site and use
them in your app.</li>
<li>This is not limited to workout data. You could make a full mobile
version of our site.</li>
</ul>
</li>
<li>Disadvantages
<ul>
<li>The API is not stable and not fully tested yet.</li>
<li>You need to register your app with us. We can revoke your
permissions if you misuse them.</li>
<li>The first time user must grant permissions to your app.</li>
<li>You need to manage authorization tokens.</li>
</ul>
</li>
</ul>
</div>
<div class="grid_6 omega">
<div class="grid_6">
<h2>Quick Links</h2>
<h5>Accepted file formats</h5>
<p>All files adhering to the standards <a href="http://www8.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd">TCX</a> and <a href="https://www.thisisant.com/resources/fit/">FIT</a> formats will be parsed.</p>
<p>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.</p>
<ul><li><a href="http://rowingdata.readthedocs.io/en/latest/#csv-file-standard">Our standard rowing CSV file</a></li></ul>
<p>Using this standard will guarantee that your user's data are accepted
without complaints.</p>
<h5>API related documentation</h5>
<h6>Registering an app</h6>
<p>As a registered user, you can register an app.</p>
<ul><li><a href="/rowers/o/applications/">Self-service app link.</a></li></ul>
<h6>Authentication</h6>
<p>Standard <a href="https://oauth.net/2/">Oauth2</a> 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.</p>
<ul>
<li>Authorization URL: <b>https://rowsandall.com/rowers/o/authorize</b></li>
<li>Access Token request: <b>https://rowsandall.com/rowers/o/token/</b></li>
<li>Access Token refresh: <b>https://rowsandall.com/rowers/o/token/</b></li>
</ul>
<h6>API documentation</h6>
<p>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.</p>
<p>The workout summary data and the stroke data are obtained and sent
separately.</p>
<ul>
<li><a href="/rowers/api-docs">API documentation</a>
(But refer to the below for stroke data.)</li>
<li><a href="/rowers/api-docs#/workouts">Try out the workout summary API</a></li>
<li><a href="/rowers/api-docs#!/workouts/strokedata_list">GET stroke data</a></li>
</ul>
<p>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:</p>
<ul>
<li><b>https://rowsandall.com/rowers/api/workouts/{id}/strokedata</b></li>
</ul>
<p>The payload is application/json data and looks as follows:</p>
<p><pre>
{
"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],
}
</pre></p>
<p>Mandatory data fields are:</p>
<ul>
<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>Optional data fiels are:</p>
<ul>
<li><b>power</b>: Power (Watt)</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>
</ul>
<p>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>
</div>
</div>
{% endblock content %}

View File

@@ -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')

View File

@@ -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.none().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<id>\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'),
@@ -152,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'),

View File

@@ -5,7 +5,11 @@ from django.views.generic.base import TemplateView
from django.db.models import Q
from django.db import IntegrityError, transaction
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
@@ -82,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
@@ -101,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))
@@ -2123,7 +2125,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="/",redirect_field_name=None)
@@ -4263,7 +4265,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):
@@ -4282,7 +4284,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()
@@ -4298,7 +4300,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):
@@ -4395,7 +4397,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
@@ -4444,7 +4446,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
@@ -4809,60 +4811,64 @@ 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):
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':
columns = ['spm','timesecs','hr','pseconds','power','distance']
columns = ['spm','time','hr','pace','power','distance']
datadf = dataprep.getsmallrowdata_db(columns,ids=[id])
return JSONResponse(datadf)
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)
# checking/validating and cleaning
try:
strokedata = json.loads(received_json_data['strokedata'])
strokedata = json.loads(request.POST['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)
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 "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')
@@ -4914,4 +4920,7 @@ def strokedatajson(request,id):
# mangling
#
return HttpResponse("OK")
return HttpResponse(row.id,status=201)
#Method not supported
return HttpResponseNotAllowed("Method not supported")