Merge branch 'release/v22.3.0'
This commit is contained in:
15
rowers/authentication.py
Normal file
15
rowers/authentication.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from rest_framework.authentication import BaseAuthentication
|
||||||
|
from rest_framework.exceptions import AuthenticationFailed
|
||||||
|
from rowers.models import APIKey
|
||||||
|
|
||||||
|
class APIKeyAuthentication(BaseAuthentication):
|
||||||
|
def authenticate(self, request):
|
||||||
|
api_key = request.META.get('HTTP_AUTHORIZATION')
|
||||||
|
if not api_key:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
api_key = APIKey.objects.get(key=api_key, is_active=True)
|
||||||
|
except APIKey.DoesNotExist:
|
||||||
|
raise AuthenticationFailed('Invalid API key')
|
||||||
|
|
||||||
|
return (api_key.user, None)
|
||||||
@@ -176,6 +176,7 @@ columndict = {
|
|||||||
'slip': 'slip',
|
'slip': 'slip',
|
||||||
'workoutstate': ' WorkoutState',
|
'workoutstate': ' WorkoutState',
|
||||||
'cumdist': 'cum_dist',
|
'cumdist': 'cum_dist',
|
||||||
|
'check_factor': 'check_factor',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1599,6 +1600,10 @@ def read_data(columns, ids=[], doclean=True, workstrokesonly=True, debug=False,
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
datadf = pl.concat(data).select(columns)
|
datadf = pl.concat(data).select(columns)
|
||||||
|
except ColumnNotFoundError:
|
||||||
|
datadf = pl.concat(data)
|
||||||
|
existing_columns = [col for col in columns if col in datadf.columns]
|
||||||
|
datadf = datadf.select(existing_columns)
|
||||||
except (ShapeError, SchemaError):
|
except (ShapeError, SchemaError):
|
||||||
data = [
|
data = [
|
||||||
df.select(columns)
|
df.select(columns)
|
||||||
@@ -1620,6 +1625,7 @@ def read_data(columns, ids=[], doclean=True, workstrokesonly=True, debug=False,
|
|||||||
else:
|
else:
|
||||||
intcolumns.append(c)
|
intcolumns.append(c)
|
||||||
|
|
||||||
|
try:
|
||||||
data = [
|
data = [
|
||||||
df.with_columns(
|
df.with_columns(
|
||||||
cs.float().cast(pl.Float64)
|
cs.float().cast(pl.Float64)
|
||||||
@@ -1634,6 +1640,8 @@ def read_data(columns, ids=[], doclean=True, workstrokesonly=True, debug=False,
|
|||||||
)
|
)
|
||||||
for df in data
|
for df in data
|
||||||
]
|
]
|
||||||
|
except ComputeError:
|
||||||
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
datadf = pl.concat(data)
|
datadf = pl.concat(data)
|
||||||
@@ -2299,6 +2307,16 @@ def dataplep(rowdatadf, id=0, inboard=0.88, forceunit='lbs', bands=True, barchar
|
|||||||
hr_bottom = 0.0*df[' HRCur (bpm)'],
|
hr_bottom = 0.0*df[' HRCur (bpm)'],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if 'check_factor' not in df.columns:
|
||||||
|
data = data.with_columns(
|
||||||
|
check_factor = pl.lit(0.0),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
data = data.with_columns(
|
||||||
|
check_factor = df['check_factor'],
|
||||||
|
)
|
||||||
|
|
||||||
if 'wash' not in df.columns:
|
if 'wash' not in df.columns:
|
||||||
data = data.with_columns(
|
data = data.with_columns(
|
||||||
wash = pl.lit(0.0),
|
wash = pl.lit(0.0),
|
||||||
|
|||||||
@@ -1478,11 +1478,14 @@ class FusionMetricChoiceForm(ModelForm):
|
|||||||
value in self.fields['columns'].choices}
|
value in self.fields['columns'].choices}
|
||||||
|
|
||||||
for label in labeldict:
|
for label in labeldict:
|
||||||
|
try:
|
||||||
if df.loc[:, label].std() == 0:
|
if df.loc[:, label].std() == 0:
|
||||||
try:
|
try:
|
||||||
formaxlabels2.pop(label)
|
formaxlabels2.pop(label)
|
||||||
except KeyError: # pragma: no cover
|
except KeyError: # pragma: no cover
|
||||||
pass
|
pass
|
||||||
|
except KeyError: # pragma: no cover
|
||||||
|
formaxlabels2.pop(label)
|
||||||
|
|
||||||
metricchoices = list(
|
metricchoices = list(
|
||||||
sorted(formaxlabels2.items(), key=lambda x: x[1]))
|
sorted(formaxlabels2.items(), key=lambda x: x[1]))
|
||||||
|
|||||||
@@ -1841,6 +1841,8 @@ def interactive_flex_chart2(id, r, promember=0,
|
|||||||
rowdata[column], 5))
|
rowdata[column], 5))
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
except ColumnNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
if len(rowdata) < 2:
|
if len(rowdata) < 2:
|
||||||
if promember:
|
if promember:
|
||||||
@@ -1938,10 +1940,10 @@ def interactive_flex_chart2(id, r, promember=0,
|
|||||||
rowdata = rowdata.with_columns((pl.lit(axlabels[yparam2])).alias("yname2"))
|
rowdata = rowdata.with_columns((pl.lit(axlabels[yparam2])).alias("yname2"))
|
||||||
except (KeyError, ColumnNotFoundError): # pragma: no cover
|
except (KeyError, ColumnNotFoundError): # pragma: no cover
|
||||||
rowdata = rowdata.with_columns((pl.lit(yparam2)).alias("yname2"))
|
rowdata = rowdata.with_columns((pl.lit(yparam2)).alias("yname2"))
|
||||||
|
|
||||||
else: # pragma: no cover
|
else: # pragma: no cover
|
||||||
rowdata = rowdata.with_columns((pl.col("yname1")).alias("yname2"))
|
rowdata = rowdata.with_columns((pl.col("yname1")).alias("yname2"))
|
||||||
|
|
||||||
|
|
||||||
def func(x, a, b):
|
def func(x, a, b):
|
||||||
return a*x+b
|
return a*x+b
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
from rowers.utils import lbstoN
|
from rowers.utils import lbstoN
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import yaml
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from scipy import optimize
|
from scipy import optimize
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@@ -44,322 +44,16 @@ nometrics = [
|
|||||||
'cum_dist.1'
|
'cum_dist.1'
|
||||||
]
|
]
|
||||||
|
|
||||||
rowingmetrics = (
|
with open("rowingmetrics.yaml", "r") as file:
|
||||||
('time', {
|
td = yaml.safe_load(file)
|
||||||
'numtype': 'float',
|
|
||||||
'null': True,
|
|
||||||
'verbose_name': 'Time',
|
|
||||||
'ax_min': 0,
|
|
||||||
'ax_max': 1e5,
|
|
||||||
'mode': 'both',
|
|
||||||
'type': 'basic',
|
|
||||||
'group': 'basic',
|
|
||||||
'maysmooth': False,
|
|
||||||
'sigfigs': -1}),
|
|
||||||
|
|
||||||
('hr', {
|
|
||||||
'numtype': 'integer',
|
|
||||||
'null': True,
|
|
||||||
'verbose_name': 'Heart Rate (bpm)',
|
|
||||||
'ax_min': 100,
|
|
||||||
'ax_max': 200,
|
|
||||||
'mode': 'both',
|
|
||||||
'type': 'basic',
|
|
||||||
'maysmooth': False,
|
|
||||||
'group': 'athlete',
|
|
||||||
'sigfigs': 0, }),
|
|
||||||
|
|
||||||
('pace', {
|
|
||||||
'numtype': 'float',
|
|
||||||
'null': True,
|
|
||||||
'verbose_name': 'Pace (/500m)',
|
|
||||||
'ax_min': 1.0e3*210,
|
|
||||||
'ax_max': 1.0e3*75,
|
|
||||||
'mode': 'both',
|
|
||||||
'type': 'basic',
|
|
||||||
'sigfigs': 1,
|
|
||||||
'maysmooth': True,
|
|
||||||
'group': 'basic'}),
|
|
||||||
|
|
||||||
('velo', {
|
|
||||||
'numtype': 'float',
|
|
||||||
'null': True,
|
|
||||||
'verbose_name': 'Boat Speed (m/s)',
|
|
||||||
'ax_min': 0,
|
|
||||||
'ax_max': 8,
|
|
||||||
'default': 0,
|
|
||||||
'mode': 'both',
|
|
||||||
'sigfigs': 1,
|
|
||||||
'maysmooth': True,
|
|
||||||
'type': 'pro',
|
|
||||||
'group': 'basic'}),
|
|
||||||
|
|
||||||
# ('powerhr',{
|
|
||||||
# 'numtype':'float',
|
|
||||||
# 'null':True,
|
|
||||||
# 'verbose_name': 'Power Heart Rate Efficiency (J/beat)',
|
|
||||||
# 'ax_min':0,
|
|
||||||
# 'ax_max':300,
|
|
||||||
# 'mode':'both',
|
|
||||||
# 'type':'pro',
|
|
||||||
# 'group':'athlete'}),
|
|
||||||
|
|
||||||
('spm', {
|
|
||||||
'numtype': 'float',
|
|
||||||
'null': True,
|
|
||||||
'verbose_name': 'Stroke Rate (spm)',
|
|
||||||
'ax_min': 15,
|
|
||||||
'ax_max': 45,
|
|
||||||
'mode': 'both',
|
|
||||||
'sigfigs': 1,
|
|
||||||
'type': 'basic',
|
|
||||||
'maysmooth': True,
|
|
||||||
'group': 'basic'}),
|
|
||||||
|
|
||||||
('driveenergy', {
|
|
||||||
'numtype': 'float',
|
|
||||||
'null': True,
|
|
||||||
'verbose_name': 'Work Per Stroke (J)',
|
|
||||||
'ax_min': 0,
|
|
||||||
'ax_max': 1000,
|
|
||||||
'mode': 'both',
|
|
||||||
'sigfigs': 0,
|
|
||||||
'type': 'pro',
|
|
||||||
'maysmooth': True,
|
|
||||||
'group': 'forcepower'}),
|
|
||||||
|
|
||||||
('power', {
|
|
||||||
'numtype': 'float',
|
|
||||||
'null': True,
|
|
||||||
'verbose_name': 'Power (W)',
|
|
||||||
'ax_min': 0,
|
|
||||||
'ax_max': 600,
|
|
||||||
'mode': 'both',
|
|
||||||
'sigfigs': 0,
|
|
||||||
'type': 'basic',
|
|
||||||
'maysmooth': True,
|
|
||||||
'group': 'forcepower'}),
|
|
||||||
|
|
||||||
('averageforce', {
|
|
||||||
'numtype': 'float',
|
|
||||||
'null': True,
|
|
||||||
'verbose_name': 'Average Drive Force (N)',
|
|
||||||
'ax_min': 0,
|
|
||||||
'ax_max': 1200,
|
|
||||||
'mode': 'both',
|
|
||||||
'sigfigs': 0,
|
|
||||||
'maysmooth': True,
|
|
||||||
'type': 'pro',
|
|
||||||
'group': 'forcepower'}),
|
|
||||||
|
|
||||||
('peakforce', {
|
|
||||||
'numtype': 'float',
|
|
||||||
'null': True,
|
|
||||||
'verbose_name': 'Peak Drive Force (N)',
|
|
||||||
'ax_min': 0,
|
|
||||||
'ax_max': 1500,
|
|
||||||
'sigfigs': 0,
|
|
||||||
'mode': 'both',
|
|
||||||
'maysmooth': True,
|
|
||||||
'type': 'pro',
|
|
||||||
'group': 'forcepower'}),
|
|
||||||
|
|
||||||
('drivelength', {
|
|
||||||
'numtype': 'float',
|
|
||||||
'null': True,
|
|
||||||
'verbose_name': 'Drive Length (m)',
|
|
||||||
'ax_min': 0,
|
|
||||||
'ax_max': 2.5,
|
|
||||||
'mode': 'rower',
|
|
||||||
'sigfigs': 2,
|
|
||||||
'maysmooth': False,
|
|
||||||
'type': 'pro',
|
|
||||||
'group': 'stroke'}),
|
|
||||||
|
|
||||||
('forceratio', {
|
|
||||||
'numtype': 'float',
|
|
||||||
'null': True,
|
|
||||||
'verbose_name': 'Average/Peak Force Ratio',
|
|
||||||
'ax_min': 0,
|
|
||||||
'ax_max': 1,
|
|
||||||
'sigfigs': 2,
|
|
||||||
'maysmooth': True,
|
|
||||||
'mode': 'both',
|
|
||||||
'type': 'pro',
|
|
||||||
'group': 'forcepower'}),
|
|
||||||
|
|
||||||
('distance', {
|
|
||||||
'numtype': 'float',
|
|
||||||
'null': True,
|
|
||||||
'verbose_name': 'Interval Distance (m)',
|
|
||||||
'ax_min': 0,
|
|
||||||
'ax_max': 1e5,
|
|
||||||
'sigfigs': 0,
|
|
||||||
'mode': 'both',
|
|
||||||
'maysmooth': False,
|
|
||||||
'type': 'basic',
|
|
||||||
'group': 'basic'}),
|
|
||||||
|
|
||||||
('cumdist', {
|
|
||||||
'numtype': 'float',
|
|
||||||
'null': True,
|
|
||||||
'verbose_name': 'Cumulative Distance (m)',
|
|
||||||
'ax_min': 0,
|
|
||||||
'ax_max': 1e5,
|
|
||||||
'mode': 'both',
|
|
||||||
'sigfigs': 0,
|
|
||||||
'maysmooth': False,
|
|
||||||
'type': 'basic',
|
|
||||||
'group': 'basic'}),
|
|
||||||
|
|
||||||
('drivespeed', {
|
|
||||||
'numtype': 'float',
|
|
||||||
'null': True,
|
|
||||||
'verbose_name': 'Drive Speed (m/s)',
|
|
||||||
'ax_min': 0,
|
|
||||||
'ax_max': 4,
|
|
||||||
'default': 0,
|
|
||||||
'sigfigs': 2,
|
|
||||||
'maysmooth': True,
|
|
||||||
'mode': 'both',
|
|
||||||
'type': 'pro',
|
|
||||||
'group': 'stroke'}),
|
|
||||||
|
|
||||||
|
|
||||||
('catch', {
|
|
||||||
'numtype': 'float',
|
|
||||||
'null': True,
|
|
||||||
'verbose_name': 'Catch Angle (degrees)',
|
|
||||||
'ax_min': -40,
|
|
||||||
'ax_max': -75,
|
|
||||||
'default': 0,
|
|
||||||
'sigfigs': 0,
|
|
||||||
'mode': 'water',
|
|
||||||
'maysmooth': True,
|
|
||||||
'type': 'pro',
|
|
||||||
'group': 'stroke'}),
|
|
||||||
|
|
||||||
('slip', {
|
|
||||||
'numtype': 'float',
|
|
||||||
'null': True,
|
|
||||||
'verbose_name': 'Slip (degrees)',
|
|
||||||
'ax_min': 0,
|
|
||||||
'ax_max': 20,
|
|
||||||
'default': 0,
|
|
||||||
'sigfigs': 1,
|
|
||||||
'maysmooth': True,
|
|
||||||
'mode': 'water',
|
|
||||||
'type': 'pro',
|
|
||||||
'group': 'stroke'}),
|
|
||||||
|
|
||||||
('finish', {
|
|
||||||
'numtype': 'float',
|
|
||||||
'null': True,
|
|
||||||
'verbose_name': 'Finish Angle (degrees)',
|
|
||||||
'ax_min': 20,
|
|
||||||
'ax_max': 55,
|
|
||||||
'default': 0,
|
|
||||||
'sigfigs': 0,
|
|
||||||
'maysmooth': True,
|
|
||||||
'mode': 'water',
|
|
||||||
'type': 'pro',
|
|
||||||
'group': 'stroke'}),
|
|
||||||
|
|
||||||
('wash', {
|
|
||||||
'numtype': 'float',
|
|
||||||
'null': True,
|
|
||||||
'verbose_name': 'Wash (degrees)',
|
|
||||||
'ax_min': 0,
|
|
||||||
'ax_max': 30,
|
|
||||||
'default': 0,
|
|
||||||
'sigfigs': 1,
|
|
||||||
'maysmooth': True,
|
|
||||||
'mode': 'water',
|
|
||||||
'type': 'pro',
|
|
||||||
'group': 'stroke'}),
|
|
||||||
|
|
||||||
('peakforceangle', {
|
|
||||||
'numtype': 'float',
|
|
||||||
'null': True,
|
|
||||||
'verbose_name': 'Peak Force Angle',
|
|
||||||
'ax_min': -50,
|
|
||||||
'ax_max': 50,
|
|
||||||
'default': 0,
|
|
||||||
'sigfigs': 0,
|
|
||||||
'maysmooth': True,
|
|
||||||
'mode': 'water',
|
|
||||||
'type': 'pro',
|
|
||||||
'group': 'stroke'}),
|
|
||||||
|
|
||||||
|
|
||||||
('totalangle', {
|
|
||||||
'numtype': 'float',
|
|
||||||
'null': True,
|
|
||||||
'verbose_name': 'Drive Length (deg)',
|
|
||||||
'ax_min': 40,
|
|
||||||
'ax_max': 140,
|
|
||||||
'default': 0,
|
|
||||||
'sigfigs': 0,
|
|
||||||
'maysmooth': True,
|
|
||||||
'mode': 'water',
|
|
||||||
'type': 'pro',
|
|
||||||
'group': 'stroke'}),
|
|
||||||
|
|
||||||
|
|
||||||
('effectiveangle', {
|
|
||||||
'numtype': 'float',
|
|
||||||
'null': True,
|
|
||||||
'verbose_name': 'Effective Drive Length (deg)',
|
|
||||||
'ax_min': 40,
|
|
||||||
'ax_max': 140,
|
|
||||||
'default': 0,
|
|
||||||
'sigfigs': 0,
|
|
||||||
'mode': 'water',
|
|
||||||
'maysmooth': True,
|
|
||||||
'type': 'pro',
|
|
||||||
'group': 'stroke'}),
|
|
||||||
|
|
||||||
('rhythm', {
|
|
||||||
'numtype': 'float',
|
|
||||||
'null': True,
|
|
||||||
'verbose_name': 'Stroke Rhythm',
|
|
||||||
'ax_min': 20,
|
|
||||||
'ax_max': 55,
|
|
||||||
'default': 1.0,
|
|
||||||
'sigfigs': 0,
|
|
||||||
'mode': 'erg',
|
|
||||||
'maysmooth': True,
|
|
||||||
'type': 'pro',
|
|
||||||
'group': 'stroke'}),
|
|
||||||
|
|
||||||
('efficiency', {
|
|
||||||
'numtype': 'float',
|
|
||||||
'null': True,
|
|
||||||
'verbose_name': 'OTW Efficiency (%)',
|
|
||||||
'ax_min': 0,
|
|
||||||
'ax_max': 110,
|
|
||||||
'default': 0,
|
|
||||||
'sigfigs': 0,
|
|
||||||
'mode': 'water',
|
|
||||||
'maysmooth': True,
|
|
||||||
'type': 'pro',
|
|
||||||
'group': 'forcepower'}),
|
|
||||||
|
|
||||||
('distanceperstroke', {
|
|
||||||
'numtype': 'float',
|
|
||||||
'null': True,
|
|
||||||
'verbose_name': 'Distance per Stroke (m)',
|
|
||||||
'ax_min': 0,
|
|
||||||
'ax_max': 15,
|
|
||||||
'default': 0,
|
|
||||||
'sigfigs': 1,
|
|
||||||
'maysmooth': True,
|
|
||||||
'mode': 'both',
|
|
||||||
'type': 'basic',
|
|
||||||
'group': 'basic'}),
|
|
||||||
|
|
||||||
|
rowingmetrics = tuple(
|
||||||
|
(key, value)
|
||||||
|
for key, value in td.items()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
metricsdicts = {}
|
metricsdicts = {}
|
||||||
for key, dict in rowingmetrics:
|
for key, dict in rowingmetrics:
|
||||||
metricsdicts[key] = dict
|
metricsdicts[key] = dict
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from django.contrib.admin.widgets import AdminDateWidget, AdminTimeWidget, Admin
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import ssl
|
import ssl
|
||||||
|
import secrets
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import pytz
|
import pytz
|
||||||
@@ -923,7 +923,18 @@ class CoachingGroup(models.Model):
|
|||||||
return Rower.objects.filter(mycoachgroup=self)
|
return Rower.objects.filter(mycoachgroup=self)
|
||||||
|
|
||||||
# Extension of User with rowing specific data
|
# Extension of User with rowing specific data
|
||||||
|
class APIKey(models.Model):
|
||||||
|
key = models.CharField(max_length=50, unique=True, default=secrets.token_urlsafe)
|
||||||
|
user = models.ForeignKey('auth.User', on_delete=models.CASCADE)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
is_active = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.user.username}:{self.key}"
|
||||||
|
|
||||||
|
def regenerate_key(self):
|
||||||
|
self.key = secrets.token_urlsafe()
|
||||||
|
self.save()
|
||||||
|
|
||||||
class Rower(models.Model):
|
class Rower(models.Model):
|
||||||
adaptivetypes = mytypes.adaptivetypes
|
adaptivetypes = mytypes.adaptivetypes
|
||||||
|
|||||||
@@ -888,6 +888,10 @@ def get_dates_timeperiod(request, startdatestring='', enddatestring='',
|
|||||||
startdate = timezone.now()-timezone.timedelta(days=5)
|
startdate = timezone.now()-timezone.timedelta(days=5)
|
||||||
startdate = startdate.date()
|
startdate = startdate.date()
|
||||||
enddate = timezone.now().date()
|
enddate = timezone.now().date()
|
||||||
|
except parser.ParserError:
|
||||||
|
startdate = timezone.now()-timezone.timedelta(days=5)
|
||||||
|
startdate = startdate.date()
|
||||||
|
enddate = timezone.now().date()
|
||||||
|
|
||||||
if startdate > enddate:
|
if startdate > enddate:
|
||||||
e = startdate
|
e = startdate
|
||||||
|
|||||||
@@ -7,7 +7,9 @@
|
|||||||
{% block main %}
|
{% block main %}
|
||||||
<h1>Import and Export Settings for {{ rower.user.first_name }} {{ rower.user.last_name }}</h1>
|
<h1>Import and Export Settings for {{ rower.user.first_name }} {{ rower.user.last_name }}</h1>
|
||||||
|
|
||||||
<p>You are currently connected to:
|
<ul class="main-content">
|
||||||
|
<li class="grid_2">
|
||||||
|
<p>You are currently connected to:
|
||||||
{% if rower.c2token is not None and rower.c2token != '' %}
|
{% if rower.c2token is not None and rower.c2token != '' %}
|
||||||
Concept2 Logbook,
|
Concept2 Logbook,
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -35,8 +37,7 @@
|
|||||||
{% if rower.rojabo_token is not None and rower.rojabo_token != '' %}
|
{% if rower.rojabo_token is not None and rower.rojabo_token != '' %}
|
||||||
Rojabo
|
Rojabo
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</p>
|
||||||
</p>
|
|
||||||
|
|
||||||
{% if form.errors %}
|
{% if form.errors %}
|
||||||
<p style="color: red;">
|
<p style="color: red;">
|
||||||
@@ -71,29 +72,58 @@
|
|||||||
a workout on Strava. If you want Deletions to propagate to Rowsandall, tick the Strava Auto Delete
|
a workout on Strava. If you want Deletions to propagate to Rowsandall, tick the Strava Auto Delete
|
||||||
check box.
|
check box.
|
||||||
</p>
|
</p>
|
||||||
<p>Click on one of the icons below to connect to the service of your
|
</li>
|
||||||
|
<li class="grid_2">
|
||||||
|
{% if grants %}
|
||||||
|
<h2>Applications</h2>
|
||||||
|
<table width="100%">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Application</th>
|
||||||
|
<th>Scope</th>
|
||||||
|
<th>Revoke</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for grant in grants %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ grant.application }}</td>
|
||||||
|
<td>{{ grant.scope }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="/rowers/me/revokeapp/{{ grant.application.id }}/">Revoke</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
<h2>API Key</h2>
|
||||||
|
<p>{{ apikey }}</p>
|
||||||
|
<p>
|
||||||
|
<a href="/rowers/me/regenerateapikey/">Regenerate</a>
|
||||||
|
</p>
|
||||||
|
This API key can be used to access the Rowsandall API. It is used by some third party applications to access your data. Keep it secret.
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
<p>Click on one of the icons below to connect to the service of your
|
||||||
choice or to renew the authorization.</p>
|
choice or to renew the authorization.</p>
|
||||||
<p><a href="/rowers/me/stravaauthorize/"><img src="/static/img/ConnectWithStrava.png" alt="connect with strava" width="120"></a></p>
|
<p><a href="/rowers/me/stravaauthorize/"><img src="/static/img/ConnectWithStrava.png" alt="connect with strava" width="120"></a></p>
|
||||||
<p><a href="/rowers/me/c2authorize/"><img src="/static/img/blueC2logo.png" alt="connect with Concept2" width="120"></a></p>
|
<p><a href="/rowers/me/c2authorize/"><img src="/static/img/blueC2logo.png" alt="connect with Concept2" width="120"></a></p>
|
||||||
<p><a href="/rowers/me/nkauthorize/"><img src="/static/img/NKLiNKLogbook.png" alt="connect with NK Logbook" width="120"></a></p>
|
<p><a href="/rowers/me/nkauthorize/"><img src="/static/img/NKLiNKLogbook.png" alt="connect with NK Logbook" width="120"></a></p>
|
||||||
<p><a href="/rowers/me/sporttracksauthorize/"><img src="/static/img/sporttracks-button.png" alt="connect with SportTracks" width="120"></a></p>
|
<p><a href="/rowers/me/sporttracksauthorize/"><img src="/static/img/sporttracks-button.png" alt="connect with SportTracks" width="120"></a></p>
|
||||||
<p><a href="/rowers/me/polarauthorize/"><img src="/static/img/Polar_connectwith_btn_white.png"
|
<p><a href="/rowers/me/polarauthorize/"><img src="/static/img/Polar_connectwith_btn_white.png"
|
||||||
alt="connect with Polar" width="130"></a></p>
|
alt="connect with Polar" width="130"></a></p>
|
||||||
<p><a href="/rowers/me/tpauthorize/"><img src="/static/img/TP_logo_horz_2_color.png"
|
<p><a href="/rowers/me/tpauthorize/"><img src="/static/img/TP_logo_horz_2_color.png"
|
||||||
alt="connect with Polar" width="130"></a></p>
|
alt="connect with Polar" width="130"></a></p>
|
||||||
|
|
||||||
<p><a href="/rowers/me/garminauthorize"><img src="/static/img/garmin_badge_130.png"
|
<p><a href="/rowers/me/garminauthorize"><img src="/static/img/garmin_badge_130.png"
|
||||||
alt="connect with Garmin" width="130"></a></p>
|
alt="connect with Garmin" width="130"></a></p>
|
||||||
<p><a href="/rowers/me/rp3authorize"><img src="/static/img/logo-rp3-full-black.png"
|
<p><a href="/rowers/me/rp3authorize"><img src="/static/img/logo-rp3-full-black.png"
|
||||||
alt="connect with RP3" width="130"></a></p>
|
alt="connect with RP3" width="130"></a></p>
|
||||||
<p><a href="/rowers/me/rojaboauthorize"><img src="/static/img/rojabo.png"
|
<p><a href="/rowers/me/rojaboauthorize"><img src="/static/img/rojabo.png"
|
||||||
alt="connect with Rojabo" width="130"></a></p>
|
alt="connect with Rojabo" width="130"></a></p>
|
||||||
|
|
||||||
{% if user.is_staff %}
|
|
||||||
<p><a href="/rowers/me/idokladauthorize/">iDoklad authorize</a></p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block sidebar %}
|
{% block sidebar %}
|
||||||
|
|||||||
@@ -116,6 +116,14 @@
|
|||||||
</table>
|
</table>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
|
<li class="grid_2">
|
||||||
|
<h2>API Key</h2>
|
||||||
|
<p>{{ apikey }}</p>
|
||||||
|
<p>
|
||||||
|
<a href="/rowers/me/regenerateapikey/">Regenerate</a>
|
||||||
|
</p>
|
||||||
|
This API key can be used to access the Rowsandall API. It is used by some third party applications to access your data. Keep it secret.
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import json
|
|||||||
|
|
||||||
from rowers.ownapistuff import *
|
from rowers.ownapistuff import *
|
||||||
from rowers.views.apiviews import *
|
from rowers.views.apiviews import *
|
||||||
|
from rowers.models import APIKey
|
||||||
|
|
||||||
class OwnApi(TestCase):
|
class OwnApi(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@@ -44,9 +45,8 @@ class OwnApi(TestCase):
|
|||||||
self.password = faker.word()
|
self.password = faker.word()
|
||||||
self.u.set_password(self.password)
|
self.u.set_password(self.password)
|
||||||
self.u.save()
|
self.u.save()
|
||||||
|
|
||||||
self.factory = APIRequestFactory()
|
self.factory = APIRequestFactory()
|
||||||
|
self.apikey = APIKey.objects.create(user=self.u)
|
||||||
|
|
||||||
def test_strokedataform(self):
|
def test_strokedataform(self):
|
||||||
login = self.c.login(username=self.u.username, password=self.password)
|
login = self.c.login(username=self.u.username, password=self.password)
|
||||||
@@ -210,6 +210,75 @@ class OwnApi(TestCase):
|
|||||||
response = strokedata_tcx(request)
|
response = strokedata_tcx(request)
|
||||||
self.assertEqual(response.status_code,200)
|
self.assertEqual(response.status_code,200)
|
||||||
|
|
||||||
|
def test_strokedataform_rowingdata(self):
|
||||||
|
|
||||||
|
url = reverse('strokedata_rowingdata')
|
||||||
|
|
||||||
|
filename = 'rowers/tests/testdata/testdata.csv'
|
||||||
|
f = open(filename, 'rb')
|
||||||
|
|
||||||
|
# Use Basic Auth header (alternative if using a custom view)
|
||||||
|
credentials = f"{self.u.username}:{self.password}"
|
||||||
|
base64_credentials = base64.b64encode(credentials.encode()).decode()
|
||||||
|
|
||||||
|
# Use Basic Auth header
|
||||||
|
headers = {
|
||||||
|
"HTTP_AUTHORIZATION": f"Basic {base64_credentials}"
|
||||||
|
}
|
||||||
|
|
||||||
|
form_data = {
|
||||||
|
"workouttype": "rower",
|
||||||
|
"boattype": "1x",
|
||||||
|
"notes": "A test file upload",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Send POST request
|
||||||
|
response = self.client.post(
|
||||||
|
url,
|
||||||
|
{"file": f, **form_data},
|
||||||
|
format="multipart", # Ensure multipart/form-data is used
|
||||||
|
**headers, # Optional if login doesn't suffice
|
||||||
|
)
|
||||||
|
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
# Assertions
|
||||||
|
self.assertEqual(response.status_code, 201)
|
||||||
|
|
||||||
|
|
||||||
|
def test_strokedataform_rowingdata_apikey(self):
|
||||||
|
|
||||||
|
url = reverse('strokedata_rowingdata_apikey')
|
||||||
|
|
||||||
|
filename = 'rowers/tests/testdata/testdata.csv'
|
||||||
|
f = open(filename, 'rb')
|
||||||
|
|
||||||
|
# Use API Key header
|
||||||
|
headers = {
|
||||||
|
"HTTP_AUTHORIZATION": self.apikey.key,
|
||||||
|
}
|
||||||
|
|
||||||
|
form_data = {
|
||||||
|
"workouttype": "rower",
|
||||||
|
"boattype": "1x",
|
||||||
|
"notes": "A test file upload",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Send POST request
|
||||||
|
response = self.client.post(
|
||||||
|
url,
|
||||||
|
{"file": f, **form_data},
|
||||||
|
format="multipart", # Ensure multipart/form-data is used
|
||||||
|
**headers, # Optional if login doesn't suffice
|
||||||
|
)
|
||||||
|
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
# Assertions
|
||||||
|
self.assertEqual(response.status_code, 201)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def test_strokedataform_empty(self):
|
def test_strokedataform_empty(self):
|
||||||
login = self.c.login(username=self.u.username, password=self.password)
|
login = self.c.login(username=self.u.username, password=self.password)
|
||||||
self.assertTrue(login)
|
self.assertTrue(login)
|
||||||
|
|||||||
@@ -254,6 +254,10 @@ urlpatterns = [
|
|||||||
name='strokedatajson_v3'),
|
name='strokedatajson_v3'),
|
||||||
re_path(r'^api/TCX/workouts/$', views.strokedata_tcx,
|
re_path(r'^api/TCX/workouts/$', views.strokedata_tcx,
|
||||||
name='strokedata_tcx'),
|
name='strokedata_tcx'),
|
||||||
|
re_path(r'^api/rowingdata/workouts/$', views.strokedata_rowingdata,
|
||||||
|
name='strokedata_rowingdata'),
|
||||||
|
re_path(r'^api/rowingdata/$', views.strokedata_rowingdata_apikey,
|
||||||
|
name='strokedata_rowingdata_apikey'),
|
||||||
re_path(r'^api/courses/$', views.course_list, name='course_list'),
|
re_path(r'^api/courses/$', views.course_list, name='course_list'),
|
||||||
re_path(r'^api/courses/(?P<id>\d+)/$', views.get_crewnerd_kml, name='get_crewnerd_kml'),
|
re_path(r'^api/courses/(?P<id>\d+)/$', views.get_crewnerd_kml, name='get_crewnerd_kml'),
|
||||||
re_path(r'^api/courses/kml/liked/$', views.get_crewnerd_liked, name='get_crewnerd_liked'),
|
re_path(r'^api/courses/kml/liked/$', views.get_crewnerd_liked, name='get_crewnerd_liked'),
|
||||||
@@ -733,6 +737,7 @@ urlpatterns = [
|
|||||||
re_path(r'^me/request/$', views.manager_requests_view,
|
re_path(r'^me/request/$', views.manager_requests_view,
|
||||||
name='manager_requests_view'),
|
name='manager_requests_view'),
|
||||||
re_path(r'^me/edit/$', views.rower_edit_view, name='rower_edit_view'),
|
re_path(r'^me/edit/$', views.rower_edit_view, name='rower_edit_view'),
|
||||||
|
re_path(r'^me/regenerateapikey/$', views.rower_regenerate_apikey, name='rower_regenerate_apikey'),
|
||||||
re_path(r'^me/edit/user/(?P<userid>\d+)/$',
|
re_path(r'^me/edit/user/(?P<userid>\d+)/$',
|
||||||
views.rower_edit_view, name='rower_edit_view'),
|
views.rower_edit_view, name='rower_edit_view'),
|
||||||
re_path(r'^me/preferences/$', views.rower_prefs_view,
|
re_path(r'^me/preferences/$', views.rower_prefs_view,
|
||||||
|
|||||||
@@ -2282,11 +2282,11 @@ def history_view_data(request, userid=0):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
ddict['hrmean'] = int(wavg(ddf, 'hr', 'deltat'))
|
ddict['hrmean'] = int(wavg(ddf, 'hr', 'deltat'))
|
||||||
except (KeyError, ValueError, AttributeError): # pragma: no cover
|
except (KeyError, ValueError, AttributeError, ColumnNotFoundError): # pragma: no cover
|
||||||
ddict['hrmean'] = 0
|
ddict['hrmean'] = 0
|
||||||
try:
|
try:
|
||||||
ddict['hrmax'] = int(ddf['hr'].max())
|
ddict['hrmax'] = int(ddf['hr'].max())
|
||||||
except (KeyError, ValueError, AttributeError): # pragma: no cover
|
except (KeyError, ValueError, AttributeError, ColumnNotFoundError): # pragma: no cover
|
||||||
ddict['hrmax'] = 0
|
ddict['hrmax'] = 0
|
||||||
|
|
||||||
ddict['powermean'] = int(wavg(ddf, 'power', 'deltat'))
|
ddict['powermean'] = int(wavg(ddf, 'power', 'deltat'))
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ from rowers.views.statements import *
|
|||||||
from rowers.tasks import handle_calctrimp
|
from rowers.tasks import handle_calctrimp
|
||||||
from rowers.opaque import encoder
|
from rowers.opaque import encoder
|
||||||
from rowers.courses import coursetokml, coursestokml
|
from rowers.courses import coursetokml, coursestokml
|
||||||
from xml.etree import ElementTree as ET
|
#from xml.etree import ElementTree as ET
|
||||||
|
from defusedxml import ElementTree as ET
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import pendulum
|
import pendulum
|
||||||
@@ -21,6 +22,8 @@ import rowingdata.tcxtools as tcxtools
|
|||||||
from rowingdata import TCXParser, rowingdata
|
from rowingdata import TCXParser, rowingdata
|
||||||
import arrow
|
import arrow
|
||||||
|
|
||||||
|
import base64
|
||||||
|
|
||||||
class XMLParser(BaseParser):
|
class XMLParser(BaseParser):
|
||||||
media_type = "application/xml"
|
media_type = "application/xml"
|
||||||
|
|
||||||
@@ -28,6 +31,8 @@ class XMLParser(BaseParser):
|
|||||||
dologging("apilog.log", "XML Parser")
|
dologging("apilog.log", "XML Parser")
|
||||||
try:
|
try:
|
||||||
s = ET.parse(stream).getroot()
|
s = ET.parse(stream).getroot()
|
||||||
|
except ET.XMLSyntaxError:
|
||||||
|
return HttpResponse(status=400)
|
||||||
except Exception as e: # pragma: no cover
|
except Exception as e: # pragma: no cover
|
||||||
dologging("apilog.log",e)
|
dologging("apilog.log",e)
|
||||||
return HttpResponse(status=500)
|
return HttpResponse(status=500)
|
||||||
@@ -422,6 +427,110 @@ def get_crewnerd_liked(request):
|
|||||||
|
|
||||||
# Stroke data views
|
# Stroke data views
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
@logged_in_or_basicauth()
|
||||||
|
#@api_view(["POST"])
|
||||||
|
#@permission_required('rower.is_not_freecoach', fn=get_user_by_userid, raise_exception=True)
|
||||||
|
#@permission_classes([IsAuthenticated])
|
||||||
|
def strokedata_rowingdata(request):
|
||||||
|
"""
|
||||||
|
Upload a .csv file (rowingdata standard) through API, using Basic Auth
|
||||||
|
"""
|
||||||
|
r = getrower(request.user)
|
||||||
|
if r.rowerplan == 'freecoach':
|
||||||
|
return HttpResponseNotAllowed("This endpoint is for users, not for free coach accounts")
|
||||||
|
|
||||||
|
if request.method != 'POST':
|
||||||
|
return HttpResponseNotAllowed("Method not supported") # pragma: no cover
|
||||||
|
|
||||||
|
form = DocumentsForm(request.POST, request.FILES)
|
||||||
|
if not form.is_valid():
|
||||||
|
return HttpResponseBadRequest(json.dumps(form.errors))
|
||||||
|
|
||||||
|
f = form.cleaned_data['file']
|
||||||
|
if f is None:
|
||||||
|
return HttpResponseBadRequest("Missing file")
|
||||||
|
|
||||||
|
filename, completefilename = handle_uploaded_file(f)
|
||||||
|
|
||||||
|
uploadoptions = {
|
||||||
|
'secret': settings.UPLOAD_SERVICE_SECRET,
|
||||||
|
'user': r.user.id,
|
||||||
|
'file': completefilename,
|
||||||
|
'workouttype': form.cleaned_data['workouttype'],
|
||||||
|
'boattype': form.cleaned_data['boattype'],
|
||||||
|
'title': form.cleaned_data['title'],
|
||||||
|
'rpe': form.cleaned_data['rpe'],
|
||||||
|
'notes': form.cleaned_data['notes']
|
||||||
|
}
|
||||||
|
|
||||||
|
url = settings.UPLOAD_SERVICE_URL
|
||||||
|
|
||||||
|
_ = myqueue(queuehigh,
|
||||||
|
handle_request_post,
|
||||||
|
url,
|
||||||
|
uploadoptions)
|
||||||
|
response = JsonResponse(
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
response.status_code = 201
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
@api_view(["POST"])
|
||||||
|
@permission_required('rower.is_not_freecoach', fn=get_user_by_userid, raise_exception=True)
|
||||||
|
@permission_classes([IsAuthenticated])
|
||||||
|
def strokedata_rowingdata_apikey(request):
|
||||||
|
"""
|
||||||
|
Upload a .csv file (rowingdata standard) through API, using
|
||||||
|
"""
|
||||||
|
|
||||||
|
r = getrower(request.user)
|
||||||
|
if r.rowerplan == 'freecoach':
|
||||||
|
return HttpResponseNotAllowed("This endpoint is for users, not for free coach accounts")
|
||||||
|
|
||||||
|
if request.method != 'POST':
|
||||||
|
return HttpResponseNotAllowed("Method not supported")
|
||||||
|
|
||||||
|
form = DocumentsForm(request.POST, request.FILES)
|
||||||
|
if not form.is_valid():
|
||||||
|
return HttpResponseBadRequest(json.dumps(form.errors))
|
||||||
|
|
||||||
|
f = form.cleaned_data['file']
|
||||||
|
if f is None:
|
||||||
|
return HttpResponseBadRequest("Missing file")
|
||||||
|
|
||||||
|
filename, completefilename = handle_uploaded_file(f)
|
||||||
|
|
||||||
|
uploadoptions = {
|
||||||
|
'secret': settings.UPLOAD_SERVICE_SECRET,
|
||||||
|
'user': r.user.id,
|
||||||
|
'file': completefilename,
|
||||||
|
'workouttype': form.cleaned_data['workouttype'],
|
||||||
|
'boattype': form.cleaned_data['boattype'],
|
||||||
|
'title': form.cleaned_data['title'],
|
||||||
|
'rpe': form.cleaned_data['rpe'],
|
||||||
|
'notes': form.cleaned_data['notes']
|
||||||
|
}
|
||||||
|
|
||||||
|
url = settings.UPLOAD_SERVICE_URL
|
||||||
|
|
||||||
|
_ = myqueue(queuehigh,
|
||||||
|
handle_request_post,
|
||||||
|
url,
|
||||||
|
uploadoptions)
|
||||||
|
response = JsonResponse(
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
response.status_code = 201
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
#@login_required()
|
#@login_required()
|
||||||
@api_view(["POST"])
|
@api_view(["POST"])
|
||||||
|
|||||||
@@ -93,7 +93,8 @@ from django.http import (
|
|||||||
HttpResponse, HttpResponseRedirect,
|
HttpResponse, HttpResponseRedirect,
|
||||||
JsonResponse,
|
JsonResponse,
|
||||||
HttpResponseForbidden, HttpResponseNotAllowed,
|
HttpResponseForbidden, HttpResponseNotAllowed,
|
||||||
HttpResponseNotFound, Http404
|
HttpResponseNotFound, Http404,
|
||||||
|
HttpResponseBadRequest,
|
||||||
)
|
)
|
||||||
from django.contrib.auth import authenticate, login, logout
|
from django.contrib.auth import authenticate, login, logout
|
||||||
from rowers.forms import (
|
from rowers.forms import (
|
||||||
@@ -163,7 +164,7 @@ from rowers.models import (
|
|||||||
StandardCollection, CourseStandard,
|
StandardCollection, CourseStandard,
|
||||||
VirtualRaceFollower, TombStone, InstantPlan,
|
VirtualRaceFollower, TombStone, InstantPlan,
|
||||||
PlannedSessionStep,InStrokeAnalysis, ForceCurveAnalysis, SyncRecord,
|
PlannedSessionStep,InStrokeAnalysis, ForceCurveAnalysis, SyncRecord,
|
||||||
UserMessage,
|
UserMessage,APIKey,
|
||||||
)
|
)
|
||||||
from rowers.models import ( RowerPowerForm, RowerHRZonesForm, SimpleRowerPowerForm,
|
from rowers.models import ( RowerPowerForm, RowerHRZonesForm, SimpleRowerPowerForm,
|
||||||
RowerForm, RowerCPForm, GraphImage, AdvancedWorkoutForm,
|
RowerForm, RowerCPForm, GraphImage, AdvancedWorkoutForm,
|
||||||
@@ -303,6 +304,142 @@ from rowers.weather import get_wind_data, get_airport_code, get_metar_data
|
|||||||
|
|
||||||
from oauth2_provider.models import Application, Grant, AccessToken
|
from oauth2_provider.models import Application, Grant, AccessToken
|
||||||
|
|
||||||
|
import base64
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.contrib.auth import authenticate, login
|
||||||
|
|
||||||
|
def view_or_apikey(view, request, test_func, realm = "", *args, **kwargs):
|
||||||
|
if test_func(request.user):
|
||||||
|
return view(request, *args, **kwargs)
|
||||||
|
|
||||||
|
if 'Authorization' in request.META:
|
||||||
|
api_key = request.META.get('Authorization')
|
||||||
|
if api_key:
|
||||||
|
try:
|
||||||
|
api_key = APIKey.objects.get(key=api_key, is_active=True)
|
||||||
|
except APIKey.DoesNotExist:
|
||||||
|
raise AuthenticationFailed('Invalid API key')
|
||||||
|
|
||||||
|
login(request, api_key.user, backend='django.contrib.auth.backends.ModelBackend')
|
||||||
|
request.user = api_key.user
|
||||||
|
return view(request, *args, **kwargs)
|
||||||
|
|
||||||
|
response = HttpResponse()
|
||||||
|
response.status_code = 401
|
||||||
|
response['WWW-Authenticate'] = 'Basic realm="%s"' % realm
|
||||||
|
return response
|
||||||
|
|
||||||
|
#############################################################################
|
||||||
|
#
|
||||||
|
def view_or_basicauth(view, request, test_func, realm = "", *args, **kwargs):
|
||||||
|
"""
|
||||||
|
This is a helper function used by both 'logged_in_or_basicauth' and
|
||||||
|
'has_perm_or_basicauth' that does the nitty of determining if they
|
||||||
|
are already logged in or if they have provided proper http-authorization
|
||||||
|
and returning the view if all goes well, otherwise responding with a 401.
|
||||||
|
"""
|
||||||
|
if test_func(request.user):
|
||||||
|
# Already logged in, just return the view.
|
||||||
|
#
|
||||||
|
return view(request, *args, **kwargs)
|
||||||
|
|
||||||
|
# They are not logged in. See if they provided login credentials
|
||||||
|
#
|
||||||
|
if 'HTTP_AUTHORIZATION' in request.META:
|
||||||
|
auth = request.META['HTTP_AUTHORIZATION'].split()
|
||||||
|
if len(auth) == 2:
|
||||||
|
# NOTE: We are only support basic authentication for now.
|
||||||
|
#
|
||||||
|
if auth[0].lower() == "basic":
|
||||||
|
uname, passwd = base64.b64decode(auth[1]).decode("utf-8").split(':')
|
||||||
|
user = authenticate(username=uname, password=passwd)
|
||||||
|
if user is not None:
|
||||||
|
if user.is_active:
|
||||||
|
login(request, user)
|
||||||
|
request.user = user
|
||||||
|
return view(request, *args, **kwargs)
|
||||||
|
|
||||||
|
# Either they did not provide an authorization header or
|
||||||
|
# something in the authorization attempt failed. Send a 401
|
||||||
|
# back to them to ask them to authenticate.
|
||||||
|
#
|
||||||
|
response = HttpResponse()
|
||||||
|
response.status_code = 401
|
||||||
|
response['WWW-Authenticate'] = 'Basic realm="%s"' % realm
|
||||||
|
return response
|
||||||
|
|
||||||
|
#############################################################################
|
||||||
|
#
|
||||||
|
def logged_in_or_apikey(realm = ""):
|
||||||
|
def view_decorator(func):
|
||||||
|
def wrapper(request, *args, **kwargs):
|
||||||
|
return view_or_apikey(func, request,
|
||||||
|
lambda u: u.is_authenticated,
|
||||||
|
realm, *args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
return view_decorator
|
||||||
|
|
||||||
|
def logged_in_or_basicauth(realm = ""):
|
||||||
|
"""
|
||||||
|
A simple decorator that requires a user to be logged in. If they are not
|
||||||
|
logged in the request is examined for a 'authorization' header.
|
||||||
|
|
||||||
|
If the header is present it is tested for basic authentication and
|
||||||
|
the user is logged in with the provided credentials.
|
||||||
|
|
||||||
|
If the header is not present a http 401 is sent back to the
|
||||||
|
requestor to provide credentials.
|
||||||
|
|
||||||
|
The purpose of this is that in several django projects I have needed
|
||||||
|
several specific views that need to support basic authentication, yet the
|
||||||
|
web site as a whole used django's provided authentication.
|
||||||
|
|
||||||
|
The uses for this are for urls that are access programmatically such as
|
||||||
|
by rss feed readers, yet the view requires a user to be logged in. Many rss
|
||||||
|
readers support supplying the authentication credentials via http basic
|
||||||
|
auth (and they do NOT support a redirect to a form where they post a
|
||||||
|
username/password.)
|
||||||
|
|
||||||
|
Use is simple:
|
||||||
|
|
||||||
|
@logged_in_or_basicauth
|
||||||
|
def your_view:
|
||||||
|
...
|
||||||
|
|
||||||
|
You can provide the name of the realm to ask for authentication within.
|
||||||
|
"""
|
||||||
|
def view_decorator(func):
|
||||||
|
def wrapper(request, *args, **kwargs):
|
||||||
|
return view_or_basicauth(func, request,
|
||||||
|
lambda u: u.is_authenticated,
|
||||||
|
realm, *args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
return view_decorator
|
||||||
|
|
||||||
|
#############################################################################
|
||||||
|
#
|
||||||
|
def has_perm_or_basicauth(perm, realm = ""):
|
||||||
|
"""
|
||||||
|
This is similar to the above decorator 'logged_in_or_basicauth'
|
||||||
|
except that it requires the logged in user to have a specific
|
||||||
|
permission.
|
||||||
|
|
||||||
|
Use:
|
||||||
|
|
||||||
|
@logged_in_or_basicauth('asforums.view_forumcollection')
|
||||||
|
def your_view:
|
||||||
|
...
|
||||||
|
|
||||||
|
"""
|
||||||
|
def view_decorator(func):
|
||||||
|
def wrapper(request, *args, **kwargs):
|
||||||
|
return view_or_basicauth(func, request,
|
||||||
|
lambda u: u.has_perm(perm),
|
||||||
|
realm, *args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
return view_decorator
|
||||||
|
|
||||||
|
|
||||||
import django_rq
|
import django_rq
|
||||||
queue = django_rq.get_queue('default')
|
queue = django_rq.get_queue('default')
|
||||||
queuelow = django_rq.get_queue('low')
|
queuelow = django_rq.get_queue('low')
|
||||||
|
|||||||
@@ -520,10 +520,19 @@ def rower_exportsettings_view(request, userid=0):
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
grants = AccessToken.objects.filter(user=request.user)
|
||||||
|
try:
|
||||||
|
apikey = APIKey.objects.get(user=request.user)
|
||||||
|
except APIKey.DoesNotExist:
|
||||||
|
apikey = APIKey.objects.create(user=request.user)
|
||||||
|
|
||||||
|
|
||||||
return render(request, 'rower_exportsettings.html',
|
return render(request, 'rower_exportsettings.html',
|
||||||
{'form': form,
|
{'form': form,
|
||||||
'rower': r,
|
'rower': r,
|
||||||
'breadcrumbs': breadcrumbs,
|
'breadcrumbs': breadcrumbs,
|
||||||
|
'grants': grants,
|
||||||
|
'apikey': apikey.key,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -632,6 +641,12 @@ def rower_edit_view(request, rowerid=0, userid=0, message=""):
|
|||||||
userform = UserForm(instance=r.user)
|
userform = UserForm(instance=r.user)
|
||||||
|
|
||||||
grants = AccessToken.objects.filter(user=request.user)
|
grants = AccessToken.objects.filter(user=request.user)
|
||||||
|
try:
|
||||||
|
apikey = APIKey.objects.get(user=request.user)
|
||||||
|
except APIKey.DoesNotExist:
|
||||||
|
apikey = APIKey.objects.create(user=request.user)
|
||||||
|
|
||||||
|
|
||||||
return render(request, 'rower_form.html',
|
return render(request, 'rower_form.html',
|
||||||
{
|
{
|
||||||
'teams': get_my_teams(request.user),
|
'teams': get_my_teams(request.user),
|
||||||
@@ -640,8 +655,24 @@ def rower_edit_view(request, rowerid=0, userid=0, message=""):
|
|||||||
'userform': userform,
|
'userform': userform,
|
||||||
'accountform': accountform,
|
'accountform': accountform,
|
||||||
'rower': r,
|
'rower': r,
|
||||||
|
'apikey': apikey.key,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@login_required()
|
||||||
|
def rower_regenerate_apikey(request):
|
||||||
|
try:
|
||||||
|
referer = request.META['HTTP_REFERER']
|
||||||
|
except KeyError:
|
||||||
|
referer = '/rowers/me/edit/'
|
||||||
|
try:
|
||||||
|
apikey = APIKey.objects.get(user=request.user)
|
||||||
|
except APIKey.DoesNotExist:
|
||||||
|
apikey = APIKey.objects.create(user=request.user)
|
||||||
|
|
||||||
|
apikey.regenerate_key()
|
||||||
|
|
||||||
|
return HttpResponseRedirect(referer)
|
||||||
|
|
||||||
#simple initial settings page
|
#simple initial settings page
|
||||||
@login_required()
|
@login_required()
|
||||||
@permission_required('rower.is_coach', fn=get_user_by_userid, raise_exception=True)
|
@permission_required('rower.is_coach', fn=get_user_by_userid, raise_exception=True)
|
||||||
|
|||||||
@@ -4931,7 +4931,6 @@ def workout_upload_api(request):
|
|||||||
post_data = {k: q.getlist(k) if len(
|
post_data = {k: q.getlist(k) if len(
|
||||||
q.getlist(k)) > 1 else v for k, v in q.items()}
|
q.getlist(k)) > 1 else v for k, v in q.items()}
|
||||||
|
|
||||||
|
|
||||||
# only allow local host
|
# only allow local host
|
||||||
hostt = request.get_host().split(':')
|
hostt = request.get_host().split(':')
|
||||||
if hostt[0] not in ['localhost', '127.0.0.1', 'dev.rowsandall.com', 'rowsandall.com']:
|
if hostt[0] not in ['localhost', '127.0.0.1', 'dev.rowsandall.com', 'rowsandall.com']:
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import requests
|
|||||||
from requests.exceptions import ConnectionError
|
from requests.exceptions import ConnectionError
|
||||||
import json
|
import json
|
||||||
from lxml import objectify, etree
|
from lxml import objectify, etree
|
||||||
import xml.etree.ElementTree as ET
|
#import xml.etree.ElementTree as ET
|
||||||
|
from defusedxml import ElementTree as ET
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from rowingdata import rowingdata, geo_distance
|
from rowingdata import rowingdata, geo_distance
|
||||||
|
|||||||
288
rowingmetrics.yaml
Normal file
288
rowingmetrics.yaml
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
time:
|
||||||
|
numtype: float
|
||||||
|
'null': true
|
||||||
|
verbose_name: Time
|
||||||
|
ax_min: 0
|
||||||
|
ax_max: 100000.0
|
||||||
|
mode: both
|
||||||
|
type: basic
|
||||||
|
group: basic
|
||||||
|
maysmooth: false
|
||||||
|
sigfigs: -1
|
||||||
|
hr:
|
||||||
|
numtype: integer
|
||||||
|
'null': true
|
||||||
|
verbose_name: Heart Rate (bpm)
|
||||||
|
ax_min: 100
|
||||||
|
ax_max: 200
|
||||||
|
mode: both
|
||||||
|
type: basic
|
||||||
|
maysmooth: false
|
||||||
|
group: athlete
|
||||||
|
sigfigs: 0
|
||||||
|
pace:
|
||||||
|
numtype: float
|
||||||
|
'null': true
|
||||||
|
verbose_name: Pace (/500m)
|
||||||
|
ax_min: 210000.0
|
||||||
|
ax_max: 75000.0
|
||||||
|
mode: both
|
||||||
|
type: basic
|
||||||
|
sigfigs: 1
|
||||||
|
maysmooth: true
|
||||||
|
group: basic
|
||||||
|
velo:
|
||||||
|
numtype: float
|
||||||
|
'null': true
|
||||||
|
verbose_name: Boat Speed (m/s)
|
||||||
|
ax_min: 0
|
||||||
|
ax_max: 8
|
||||||
|
default: 0
|
||||||
|
mode: both
|
||||||
|
sigfigs: 1
|
||||||
|
maysmooth: true
|
||||||
|
type: pro
|
||||||
|
group: basic
|
||||||
|
spm:
|
||||||
|
numtype: float
|
||||||
|
'null': true
|
||||||
|
verbose_name: Stroke Rate (spm)
|
||||||
|
ax_min: 15
|
||||||
|
ax_max: 45
|
||||||
|
mode: both
|
||||||
|
sigfigs: 1
|
||||||
|
type: basic
|
||||||
|
maysmooth: true
|
||||||
|
group: basic
|
||||||
|
driveenergy:
|
||||||
|
numtype: float
|
||||||
|
'null': true
|
||||||
|
verbose_name: Work Per Stroke (J)
|
||||||
|
ax_min: 0
|
||||||
|
ax_max: 1000
|
||||||
|
mode: both
|
||||||
|
sigfigs: 0
|
||||||
|
type: pro
|
||||||
|
maysmooth: true
|
||||||
|
group: forcepower
|
||||||
|
power:
|
||||||
|
numtype: float
|
||||||
|
'null': true
|
||||||
|
verbose_name: Power (W)
|
||||||
|
ax_min: 0
|
||||||
|
ax_max: 600
|
||||||
|
mode: both
|
||||||
|
sigfigs: 0
|
||||||
|
type: basic
|
||||||
|
maysmooth: true
|
||||||
|
group: forcepower
|
||||||
|
averageforce:
|
||||||
|
numtype: float
|
||||||
|
'null': true
|
||||||
|
verbose_name: Average Drive Force (N)
|
||||||
|
ax_min: 0
|
||||||
|
ax_max: 1200
|
||||||
|
mode: both
|
||||||
|
sigfigs: 0
|
||||||
|
maysmooth: true
|
||||||
|
type: pro
|
||||||
|
group: forcepower
|
||||||
|
peakforce:
|
||||||
|
numtype: float
|
||||||
|
'null': true
|
||||||
|
verbose_name: Peak Drive Force (N)
|
||||||
|
ax_min: 0
|
||||||
|
ax_max: 1500
|
||||||
|
sigfigs: 0
|
||||||
|
mode: both
|
||||||
|
maysmooth: true
|
||||||
|
type: pro
|
||||||
|
group: forcepower
|
||||||
|
drivelength:
|
||||||
|
numtype: float
|
||||||
|
'null': true
|
||||||
|
verbose_name: Drive Length (m)
|
||||||
|
ax_min: 0
|
||||||
|
ax_max: 2.5
|
||||||
|
mode: rower
|
||||||
|
sigfigs: 2
|
||||||
|
maysmooth: false
|
||||||
|
type: pro
|
||||||
|
group: stroke
|
||||||
|
forceratio:
|
||||||
|
numtype: float
|
||||||
|
'null': true
|
||||||
|
verbose_name: Average/Peak Force Ratio
|
||||||
|
ax_min: 0
|
||||||
|
ax_max: 1
|
||||||
|
sigfigs: 2
|
||||||
|
maysmooth: true
|
||||||
|
mode: both
|
||||||
|
type: pro
|
||||||
|
group: forcepower
|
||||||
|
distance:
|
||||||
|
numtype: float
|
||||||
|
'null': true
|
||||||
|
verbose_name: Interval Distance (m)
|
||||||
|
ax_min: 0
|
||||||
|
ax_max: 100000.0
|
||||||
|
sigfigs: 0
|
||||||
|
mode: both
|
||||||
|
maysmooth: false
|
||||||
|
type: basic
|
||||||
|
group: basic
|
||||||
|
cumdist:
|
||||||
|
numtype: float
|
||||||
|
'null': true
|
||||||
|
verbose_name: Cumulative Distance (m)
|
||||||
|
ax_min: 0
|
||||||
|
ax_max: 100000.0
|
||||||
|
mode: both
|
||||||
|
sigfigs: 0
|
||||||
|
maysmooth: false
|
||||||
|
type: basic
|
||||||
|
group: basic
|
||||||
|
drivespeed:
|
||||||
|
numtype: float
|
||||||
|
'null': true
|
||||||
|
verbose_name: Drive Speed (m/s)
|
||||||
|
ax_min: 0
|
||||||
|
ax_max: 4
|
||||||
|
default: 0
|
||||||
|
sigfigs: 2
|
||||||
|
maysmooth: true
|
||||||
|
mode: both
|
||||||
|
type: pro
|
||||||
|
group: stroke
|
||||||
|
catch:
|
||||||
|
numtype: float
|
||||||
|
'null': true
|
||||||
|
verbose_name: Catch Angle (degrees)
|
||||||
|
ax_min: -40
|
||||||
|
ax_max: -75
|
||||||
|
default: 0
|
||||||
|
sigfigs: 0
|
||||||
|
mode: water
|
||||||
|
maysmooth: true
|
||||||
|
type: pro
|
||||||
|
group: stroke
|
||||||
|
slip:
|
||||||
|
numtype: float
|
||||||
|
'null': true
|
||||||
|
verbose_name: Slip (degrees)
|
||||||
|
ax_min: 0
|
||||||
|
ax_max: 20
|
||||||
|
default: 0
|
||||||
|
sigfigs: 1
|
||||||
|
maysmooth: true
|
||||||
|
mode: water
|
||||||
|
type: pro
|
||||||
|
group: stroke
|
||||||
|
finish:
|
||||||
|
numtype: float
|
||||||
|
'null': true
|
||||||
|
verbose_name: Finish Angle (degrees)
|
||||||
|
ax_min: 20
|
||||||
|
ax_max: 55
|
||||||
|
default: 0
|
||||||
|
sigfigs: 0
|
||||||
|
maysmooth: true
|
||||||
|
mode: water
|
||||||
|
type: pro
|
||||||
|
group: stroke
|
||||||
|
wash:
|
||||||
|
numtype: float
|
||||||
|
'null': true
|
||||||
|
verbose_name: Wash (degrees)
|
||||||
|
ax_min: 0
|
||||||
|
ax_max: 30
|
||||||
|
default: 0
|
||||||
|
sigfigs: 1
|
||||||
|
maysmooth: true
|
||||||
|
mode: water
|
||||||
|
type: pro
|
||||||
|
group: stroke
|
||||||
|
peakforceangle:
|
||||||
|
numtype: float
|
||||||
|
'null': true
|
||||||
|
verbose_name: Peak Force Angle
|
||||||
|
ax_min: -50
|
||||||
|
ax_max: 50
|
||||||
|
default: 0
|
||||||
|
sigfigs: 0
|
||||||
|
maysmooth: true
|
||||||
|
mode: water
|
||||||
|
type: pro
|
||||||
|
group: stroke
|
||||||
|
totalangle:
|
||||||
|
numtype: float
|
||||||
|
'null': true
|
||||||
|
verbose_name: Drive Length (deg)
|
||||||
|
ax_min: 40
|
||||||
|
ax_max: 140
|
||||||
|
default: 0
|
||||||
|
sigfigs: 0
|
||||||
|
maysmooth: true
|
||||||
|
mode: water
|
||||||
|
type: pro
|
||||||
|
group: stroke
|
||||||
|
check_factor:
|
||||||
|
numtype: float
|
||||||
|
'null': true
|
||||||
|
verbose_name: Check Factor
|
||||||
|
ax_min: 0
|
||||||
|
ax_max: 100
|
||||||
|
default: 0
|
||||||
|
sigfigs: 1
|
||||||
|
maysmooth: true
|
||||||
|
mode: water
|
||||||
|
type: pro
|
||||||
|
group: stroke
|
||||||
|
effectiveangle:
|
||||||
|
numtype: float
|
||||||
|
'null': true
|
||||||
|
verbose_name: Effective Drive Length (deg)
|
||||||
|
ax_min: 40
|
||||||
|
ax_max: 140
|
||||||
|
default: 0
|
||||||
|
sigfigs: 0
|
||||||
|
mode: water
|
||||||
|
maysmooth: true
|
||||||
|
type: pro
|
||||||
|
group: stroke
|
||||||
|
rhythm:
|
||||||
|
numtype: float
|
||||||
|
'null': true
|
||||||
|
verbose_name: Stroke Rhythm
|
||||||
|
ax_min: 20
|
||||||
|
ax_max: 55
|
||||||
|
default: 1.0
|
||||||
|
sigfigs: 0
|
||||||
|
mode: erg
|
||||||
|
maysmooth: true
|
||||||
|
type: pro
|
||||||
|
group: stroke
|
||||||
|
efficiency:
|
||||||
|
numtype: float
|
||||||
|
'null': true
|
||||||
|
verbose_name: OTW Efficiency (%)
|
||||||
|
ax_min: 0
|
||||||
|
ax_max: 110
|
||||||
|
default: 0
|
||||||
|
sigfigs: 0
|
||||||
|
mode: water
|
||||||
|
maysmooth: true
|
||||||
|
type: pro
|
||||||
|
group: forcepower
|
||||||
|
distanceperstroke:
|
||||||
|
numtype: float
|
||||||
|
'null': true
|
||||||
|
verbose_name: Distance per Stroke (m)
|
||||||
|
ax_min: 0
|
||||||
|
ax_max: 15
|
||||||
|
default: 0
|
||||||
|
sigfigs: 1
|
||||||
|
maysmooth: true
|
||||||
|
mode: both
|
||||||
|
type: basic
|
||||||
|
group: basic
|
||||||
@@ -483,10 +483,11 @@ REST_FRAMEWORK = {
|
|||||||
# 'rest_framework.permissions.DjangoModelPermissions'
|
# 'rest_framework.permissions.DjangoModelPermissions'
|
||||||
],
|
],
|
||||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||||
'rest_framework.authentication.BasicAuthentication',
|
#'rest_framework.authentication.BasicAuthentication',
|
||||||
# 'rest_framework.authentication.SessionAuthentication',
|
# 'rest_framework.authentication.SessionAuthentication',
|
||||||
# 'rest_framework.authentication.TokenAuthentication',
|
# 'rest_framework.authentication.TokenAuthentication',
|
||||||
'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
|
'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
|
||||||
|
'rowers.authentication.APIKeyAuthentication',
|
||||||
),
|
),
|
||||||
'PAGE_SIZE': 20,
|
'PAGE_SIZE': 20,
|
||||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
|
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
|
||||||
|
|||||||
Reference in New Issue
Block a user