From 45ac5f80a3135e0ec9a2688bd46bcd9423da4290 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Mon, 28 Nov 2016 15:49:46 +0100 Subject: [PATCH 01/11] 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 02/11] 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 03/11] 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 29de886c875caafa76923033e1e4062a9ccfe6e2 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 21 Dec 2016 22:41:33 +0100 Subject: [PATCH 04/11] not working yet - added scope to request body --- rowers/c2stuff.py | 30 ++++++++++++++++++++++++++---- rowers/dataprep.py | 2 +- rowers/views.py | 2 ++ 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/rowers/c2stuff.py b/rowers/c2stuff.py index 0d0fedfd..1ba4c03d 100644 --- a/rowers/c2stuff.py +++ b/rowers/c2stuff.py @@ -24,6 +24,9 @@ import pandas as pd import numpy as np from rowers.models import Rower,Workout import sys +import urllib +from requests import Request, Session + from rowsandall_app.settings import C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET @@ -233,6 +236,7 @@ def createc2workoutdata(w): return data def do_refresh_token(refreshtoken): + scope = "results:write,user:read" client_auth = requests.auth.HTTPBasicAuth(C2_CLIENT_ID, C2_CLIENT_SECRET) post_data = {"grant_type": "refresh_token", "client_secret": C2_CLIENT_SECRET, @@ -240,10 +244,25 @@ def do_refresh_token(refreshtoken): "refresh_token": refreshtoken, } headers = {'user-agent': 'sanderroosendaal'} - response = requests.post("https://log.concept2.com/oauth/access_token", - data=post_data, - headers=headers) + url = "https://log.concept2.com/oauth/access_token" + s = Session() + req = Request('POST',url, data=post_data, headers=headers) + # response = requests.post("https://log.concept2.com/oauth/access_token", + # data=post_data, + # data=post_data, + # headers=headers) + + prepped = req.prepare() + print prepped.body + prepped.body+="&scope=" + prepped.body+=scope + print prepped.body + + response = s.send(prepped) + print response.status_code + + print response token_json = response.json() try: thetoken = token_json['access_token'] @@ -263,9 +282,11 @@ def do_refresh_token(refreshtoken): def get_token(code): + scope = "user:read,results:write" client_auth = requests.auth.HTTPBasicAuth(C2_CLIENT_ID, C2_CLIENT_SECRET) post_data = {"grant_type": "authorization_code", "code": code, + "scope": scope, "redirect_uri": C2_REDIRECT_URI, "client_secret": C2_CLIENT_SECRET, "client_id":C2_CLIENT_ID, @@ -286,11 +307,12 @@ def make_authorization_url(request): # Save it for use later to prevent xsrf attacks from uuid import uuid4 state = str(uuid4()) + scope = "user:read,results:write" params = {"client_id": CLIENT_ID, "response_type": "code", + "scope":scope, "redirect_uri": REDIRECT_URI} - import urllib url = "https://log.concept2.com/oauth/authorize?"+ urllib.urlencode(params) # url = "https://ssl.reddit.com/api/v1/authorize?" + urllib.urlencode(params) diff --git a/rowers/dataprep.py b/rowers/dataprep.py index e942da7e..9744b6b9 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -518,7 +518,7 @@ def dataprep(rowdatadf,id=0,bands=True,barchart=True,otwpower=True, try: drivespeed = drivelength/rowdatadf[' DriveTime (ms)']*1.0e3 except TypeError: - drivespeed = 0.0*drivelength + drivespeed = 0.0*rowdatadf['TimeStam (sec)'] drivespeed = drivespeed.fillna(value=0) driveenergy = drivelength*averageforce*4.44822 diff --git a/rowers/views.py b/rowers/views.py index 35b3233d..3a57a324 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -1095,9 +1095,11 @@ def rower_c2_authorize(request): # Save it for use later to prevent xsrf attacks from uuid import uuid4 state = str(uuid4()) + scope = "user:read,results:write" params = {"client_id": C2_CLIENT_ID, "response_type": "code", + "scope":scope, "redirect_uri": C2_REDIRECT_URI} import urllib url = "http://log.concept2.com/oauth/authorize?"+ urllib.urlencode(params) From 23c3ff70b51916de663efffb01a93e6a27860a99 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 22 Dec 2016 08:29:02 +0100 Subject: [PATCH 05/11] still not working --- rowers/c2stuff.py | 3 ++- rowers/views.py | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/rowers/c2stuff.py b/rowers/c2stuff.py index 1ba4c03d..2e499499 100644 --- a/rowers/c2stuff.py +++ b/rowers/c2stuff.py @@ -311,9 +311,10 @@ def make_authorization_url(request): params = {"client_id": CLIENT_ID, "response_type": "code", - "scope":scope, "redirect_uri": REDIRECT_URI} url = "https://log.concept2.com/oauth/authorize?"+ urllib.urlencode(params) + url += "&scope="+scope + print url # url = "https://ssl.reddit.com/api/v1/authorize?" + urllib.urlencode(params) return HttpResponseRedirect(url) diff --git a/rowers/views.py b/rowers/views.py index 3a57a324..9dc901ce 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -1096,14 +1096,12 @@ def rower_c2_authorize(request): from uuid import uuid4 state = str(uuid4()) scope = "user:read,results:write" - params = {"client_id": C2_CLIENT_ID, "response_type": "code", - "scope":scope, "redirect_uri": C2_REDIRECT_URI} import urllib url = "http://log.concept2.com/oauth/authorize?"+ urllib.urlencode(params) - + url += "&scope="+scope return HttpResponseRedirect(url) @login_required() From 24de5c681d573dd7dc56a65feee1999e3b4a06f6 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 22 Dec 2016 12:21:04 +0100 Subject: [PATCH 06/11] 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 07/11] 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 08/11] 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'), From 3f83e283e11e371e491ddcc9e3014c60f630c024 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 22 Dec 2016 20:33:55 +0100 Subject: [PATCH 09/11] added scope --- rowers/c2stuff.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/rowers/c2stuff.py b/rowers/c2stuff.py index 2e499499..13f3e0e4 100644 --- a/rowers/c2stuff.py +++ b/rowers/c2stuff.py @@ -253,16 +253,10 @@ def do_refresh_token(refreshtoken): # headers=headers) prepped = req.prepare() - print prepped.body prepped.body+="&scope=" prepped.body+=scope - print prepped.body - response = s.send(prepped) - print response.status_code - - print response token_json = response.json() try: thetoken = token_json['access_token'] @@ -286,15 +280,25 @@ def get_token(code): client_auth = requests.auth.HTTPBasicAuth(C2_CLIENT_ID, C2_CLIENT_SECRET) post_data = {"grant_type": "authorization_code", "code": code, - "scope": scope, +# "scope": scope, "redirect_uri": C2_REDIRECT_URI, "client_secret": C2_CLIENT_SECRET, "client_id":C2_CLIENT_ID, } headers = {'user-agent': 'sanderroosendaal'} - response = requests.post("https://log.concept2.com/oauth/access_token", - data=post_data, - headers=headers) + url = "https://log.concept2.com/oauth/access_token" + s = Session() + req = Request('POST',url, data=post_data, headers=headers) + + prepped = req.prepare() + prepped.body+="&scope=" + prepped.body+=scope + + response = s.send(prepped) + +# response = requests.post("https://log.concept2.com/oauth/access_token", +# data=post_data, +# headers=headers) token_json = response.json() thetoken = token_json['access_token'] expires_in = token_json['expires_in'] @@ -314,8 +318,6 @@ def make_authorization_url(request): "redirect_uri": REDIRECT_URI} url = "https://log.concept2.com/oauth/authorize?"+ urllib.urlencode(params) url += "&scope="+scope - print url - # url = "https://ssl.reddit.com/api/v1/authorize?" + urllib.urlencode(params) return HttpResponseRedirect(url) From d60f24c0404612cc3a2ffc846f3cbeda08aad1cf Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 23 Dec 2016 09:23:35 +0100 Subject: [PATCH 10/11] changed API to accept forms not json encoding --- rowers/templates/developers.html | 11 ++++++++++- rowsandall_app/settings.py | 2 +- rowsandall_app/urls.py | 6 ------ 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/rowers/templates/developers.html b/rowers/templates/developers.html index f383230a..e2907195 100644 --- a/rowers/templates/developers.html +++ b/rowers/templates/developers.html @@ -118,13 +118,22 @@

    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, + Exchange authorization code for an access token. When access token + expires, use the refresh token to refresh it.

    +

    The POST call must have content-type: x-www-form-urlencoded. + I set it this way to support the handy testing utility mentioned + belower. However, + I really would like to support application/json but with the + current framework I cannot support both at the same time. Expect + changes. Write to me if you want to be notified of changes.

    +
    • 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/
    • +
    • Handy utility for testing: http://django-oauth-toolkit.herokuapp.com/consumer/
    API documentation
    diff --git a/rowsandall_app/settings.py b/rowsandall_app/settings.py index c7af4887..58078fa3 100644 --- a/rowsandall_app/settings.py +++ b/rowsandall_app/settings.py @@ -262,7 +262,7 @@ GMAPIKEY = "AIzaSyAgu1w9QSthaGPMLp8y9JedPoMc9sfEgJ8" OAUTH2_PROVIDER = { # this is the list of available scopes 'SCOPES': {'read': 'Read scope', 'write': 'Write scope', 'groups': 'Access to your groups'}, - 'OAUTH2_BACKEND_CLASS': 'oauth2_provider.oauth2_backends.JSONOAuthLibCore' + # 'OAUTH2_BACKEND_CLASS': 'oauth2_provider.oauth2_backends.JSONOAuthLibCore' } diff --git a/rowsandall_app/urls.py b/rowsandall_app/urls.py index 6db8318d..3415cd36 100644 --- a/rowsandall_app/urls.py +++ b/rowsandall_app/urls.py @@ -27,15 +27,9 @@ from rowers import views as rowersviews urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), -# url('^', include('django.contrib.auth.urls')), url(r'^$',rootview), url(r'^version/$',version), -# url(r'^rowingdata/$',rowingdata,{'formloc':'/rowingdata/'}), -# url(r'^nrowingdata/$',nrowingdata,{'formloc':'/nrowingdata/'}), -# url(r'^upload/$',uploadfile,{'formloc':'/upload/'}), url(r'^addresult/(.+.*)/$',wait), -# url(r'^waitforplot/(.+.*)/$',waitforplot), -# url(r'^showplot/(.+.*)/$',showplot), url(r'^login/',auth_views.login, name='login'), url(r'^logout/',auth_views.logout_then_login,name='logout'), url(r'^password_change_done/$',auth_views.password_change_done,name='password_change_done'), From c3dbd71465ec9400dc22f289663b4d064e2d7c12 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sun, 25 Dec 2016 20:53:59 +0100 Subject: [PATCH 11/11] rowperfect3 --- rowers/models.py | 8 +++++++- rowers/views.py | 6 +++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/rowers/models.py b/rowers/models.py index 878929b4..303b4b76 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -532,5 +532,11 @@ class SiteAnnouncement(models.Model): self.expires = timezone.now()+datetime.timedelta(days=10) self.modified = timezone.now() if self.dotweet: - status = tweetapi.PostUpdate(self.announcement) + try: + status = tweetapi.PostUpdate(self.announcement) + except: + try: + status = tweetapi.PostUpdate(self.announcement[:135]) + except: + pass return super(SiteAnnouncement,self).save(*args, **kwargs) diff --git a/rowers/views.py b/rowers/views.py index 8a7d8cf3..6d480807 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -58,7 +58,7 @@ from rowingdata import rower as rrower from rowingdata import main as rmain from rowingdata import rowingdata as rrdata from rowingdata import TCXParser,RowProParser,ErgDataParser,TCXParserNoHR -from rowingdata import BoatCoachParser +from rowingdata import BoatCoachParser,RowPerfectParser from rowingdata import MysteryParser from rowingdata import painsledDesktopParser,speedcoachParser,ErgStickParser from rowingdata import SpeedCoach2Parser,FITParser,fitsummarydata @@ -3782,6 +3782,10 @@ def workout_upload_view(request,message=""): if (fileformat == 'mystery'): row = MysteryParser(f2) + # handle RowPerfect + if (fileformat == 'rowperfect3'): + row = RowPerfectParser(f2) + # handle TCX no HR if (fileformat == 'tcxnohr'): row = TCXParserNoHR(f2)