Merge branch 'feature/cleanup' into develop
This commit is contained in:
@@ -4,8 +4,9 @@ from django.contrib.auth.models import User
|
||||
|
||||
from .models import Rower, Workout,GraphImage,FavoriteChart,SiteAnnouncement
|
||||
|
||||
# Register your models here.
|
||||
# Register your models here so you can use them in the Admin module
|
||||
|
||||
# Rower details directly under the User
|
||||
class RowerInline(admin.StackedInline):
|
||||
model = Rower
|
||||
can_delete = False
|
||||
|
||||
@@ -2,6 +2,6 @@ from __future__ import unicode_literals
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
# Store metadata for the app
|
||||
class RowersConfig(AppConfig):
|
||||
name = 'rowers'
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
# The interactions with the Concept2 logbook API
|
||||
# All C2 related functions should be defined here
|
||||
# (There is still some stuff defined directly in views.py. Need to
|
||||
# move that here.)
|
||||
|
||||
# Python
|
||||
import oauth2 as oauth
|
||||
import cgi
|
||||
@@ -17,8 +22,7 @@ from django.contrib.auth import authenticate, login, logout
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
# Project
|
||||
# from .models import Profile
|
||||
|
||||
from rowingdata import rowingdata
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
@@ -27,9 +31,9 @@ import sys
|
||||
import urllib
|
||||
from requests import Request, Session
|
||||
|
||||
|
||||
from rowsandall_app.settings import C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET
|
||||
|
||||
# Custom error class - to raise a NoTokenError
|
||||
class C2NoTokenError(Exception):
|
||||
def __init__(self,value):
|
||||
self.value=value
|
||||
@@ -37,8 +41,8 @@ class C2NoTokenError(Exception):
|
||||
def __str__(self):
|
||||
return repr(self.value)
|
||||
|
||||
|
||||
|
||||
# Custom exception handler, returns a 401 HTTP message
|
||||
# with exception details in the json data
|
||||
def custom_exception_handler(exc,message):
|
||||
|
||||
response = {
|
||||
@@ -56,7 +60,7 @@ def custom_exception_handler(exc,message):
|
||||
|
||||
return res
|
||||
|
||||
|
||||
# Check if workout is owned by this user
|
||||
def checkworkoutuser(user,workout):
|
||||
try:
|
||||
r = Rower.objects.get(user=user)
|
||||
@@ -64,11 +68,12 @@ def checkworkoutuser(user,workout):
|
||||
except Rower.DoesNotExist:
|
||||
return(False)
|
||||
|
||||
|
||||
# convert datetime object to seconds
|
||||
def makeseconds(t):
|
||||
seconds = t.hour*3600.+t.minute*60.+t.second+0.1*int(t.microsecond/1.e5)
|
||||
return seconds
|
||||
|
||||
# convert our weight class code to Concept2 weight class code
|
||||
def c2wc(weightclass):
|
||||
if (weightclass=="lwt"):
|
||||
res = "L"
|
||||
@@ -77,7 +82,9 @@ def c2wc(weightclass):
|
||||
|
||||
return res
|
||||
|
||||
|
||||
# Concept2 logbook sends over split data for each interval
|
||||
# We use it here to generate a custom summary
|
||||
# Some users complained about small differences
|
||||
def summaryfromsplitdata(splitdata,data,filename,sep='|'):
|
||||
|
||||
totaldist = data['distance']
|
||||
@@ -177,6 +184,8 @@ def summaryfromsplitdata(splitdata,data,filename,sep='|'):
|
||||
|
||||
return sums,sa,results
|
||||
|
||||
# Not used now. Could be used to add workout split data to Concept2
|
||||
# logbook but needs to be reviewed.
|
||||
def createc2workoutdata_as_splits(w):
|
||||
filename = w.csvfilename
|
||||
row = rowingdata(filename)
|
||||
@@ -216,7 +225,6 @@ def createc2workoutdata_as_splits(w):
|
||||
|
||||
data = {
|
||||
"type": w.workouttype,
|
||||
# "date": str(w.date)+" "+str(w.starttime),
|
||||
"date": w.startdatetime.isoformat(),
|
||||
"distance": int(w.distance),
|
||||
"time": int(10*makeseconds(durationstr)),
|
||||
@@ -233,59 +241,8 @@ def createc2workoutdata_as_splits(w):
|
||||
|
||||
return data
|
||||
|
||||
def createc2workoutdata_grouped(w):
|
||||
filename = w.csvfilename
|
||||
row = rowingdata(filename)
|
||||
|
||||
# resize per minute
|
||||
df = row.df.groupby(lambda x:x/10).mean()
|
||||
|
||||
averagehr = int(df[' HRCur (bpm)'].mean())
|
||||
maxhr = int(df[' HRCur (bpm)'].max())
|
||||
|
||||
# adding diff, trying to see if this is valid
|
||||
t = 10*df.ix[:,' ElapsedTime (sec)'].values
|
||||
t[0] = t[1]
|
||||
d = df.ix[:,' Horizontal (meters)'].values
|
||||
d[0] = d[1]
|
||||
p = 10*df.ix[:,' Stroke500mPace (sec/500m)'].values
|
||||
t = t.astype(int)
|
||||
d = d.astype(int)
|
||||
p = p.astype(int)
|
||||
spm = df[' Cadence (stokes/min)'].astype(int)
|
||||
spm[0] = spm[1]
|
||||
hr = df[' HRCur (bpm)'].astype(int)
|
||||
stroke_data = []
|
||||
for i in range(len(t)):
|
||||
thisrecord = {"t":t[i],"d":d[i],"p":p[i],"spm":spm[i],"hr":hr[i]}
|
||||
stroke_data.append(thisrecord)
|
||||
|
||||
|
||||
try:
|
||||
durationstr = datetime.strptime(str(w.duration),"%H:%M:%S.%f")
|
||||
except ValueError:
|
||||
durationstr = datetime.strptime(str(w.duration),"%H:%M:%S")
|
||||
|
||||
|
||||
data = {
|
||||
"type": w.workouttype,
|
||||
# "date": str(w.date)+" "+str(w.starttime),
|
||||
"date": w.startdatetime.isoformat(),
|
||||
"distance": int(w.distance),
|
||||
"time": int(10*makeseconds(durationstr)),
|
||||
"weight_class": c2wc(w.weightcategory),
|
||||
"timezone": "Etc/UTC",
|
||||
"comments": w.notes,
|
||||
"heart_rate": {
|
||||
"average": averagehr,
|
||||
"max": maxhr,
|
||||
},
|
||||
"stroke_data": stroke_data,
|
||||
}
|
||||
|
||||
|
||||
return data
|
||||
|
||||
# Create the Data object for the stroke data to be sent to Concept2 logbook
|
||||
# API
|
||||
def createc2workoutdata(w):
|
||||
filename = w.csvfilename
|
||||
row = rowingdata(filename)
|
||||
@@ -318,7 +275,6 @@ def createc2workoutdata(w):
|
||||
|
||||
data = {
|
||||
"type": w.workouttype,
|
||||
# "date": str(w.date)+" "+str(w.starttime),
|
||||
"date": w.startdatetime.isoformat(),
|
||||
"timezone": "Etc/UTC",
|
||||
"distance": int(w.distance),
|
||||
@@ -335,6 +291,7 @@ def createc2workoutdata(w):
|
||||
|
||||
return data
|
||||
|
||||
# Refresh Concept2 authorization token
|
||||
def do_refresh_token(refreshtoken):
|
||||
scope = "results:write,user:read"
|
||||
client_auth = requests.auth.HTTPBasicAuth(C2_CLIENT_ID, C2_CLIENT_SECRET)
|
||||
@@ -347,10 +304,6 @@ def do_refresh_token(refreshtoken):
|
||||
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()
|
||||
prepped.body+="&scope="
|
||||
@@ -374,13 +327,12 @@ def do_refresh_token(refreshtoken):
|
||||
|
||||
return [thetoken,expires_in,refresh_token]
|
||||
|
||||
|
||||
# Exchange authorization code for authorization token
|
||||
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,
|
||||
@@ -396,9 +348,6 @@ def get_token(code):
|
||||
|
||||
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']
|
||||
@@ -406,6 +355,7 @@ def get_token(code):
|
||||
|
||||
return [thetoken,expires_in,refresh_token]
|
||||
|
||||
# Make URL for authorization and load it
|
||||
def make_authorization_url(request):
|
||||
# Generate a random string for the state parameter
|
||||
# Save it for use later to prevent xsrf attacks
|
||||
@@ -421,6 +371,7 @@ def make_authorization_url(request):
|
||||
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
# Get workout from C2 ID
|
||||
def get_c2_workout(user,c2id):
|
||||
r = Rower.objects.get(user=user)
|
||||
if (r.c2token == '') or (r.c2token is None):
|
||||
@@ -440,6 +391,7 @@ def get_c2_workout(user,c2id):
|
||||
|
||||
return s
|
||||
|
||||
# Get stroke data belonging to C2 ID
|
||||
def get_c2_workout_strokes(user,c2id):
|
||||
r = Rower.objects.get(user=user)
|
||||
if (r.c2token == '') or (r.c2token is None):
|
||||
@@ -459,6 +411,8 @@ def get_c2_workout_strokes(user,c2id):
|
||||
|
||||
return s
|
||||
|
||||
# Get list of C2 workouts. We load only the first page,
|
||||
# assuming that users don't want to import their old workouts
|
||||
def get_c2_workout_list(user):
|
||||
r = Rower.objects.get(user=user)
|
||||
if (r.c2token == '') or (r.c2token is None):
|
||||
@@ -479,7 +433,8 @@ def get_c2_workout_list(user):
|
||||
return s
|
||||
|
||||
|
||||
|
||||
# Get username, having access token.
|
||||
# Handy for checking if the API access is working
|
||||
def get_username(access_token):
|
||||
authorizationstring = str('Bearer ' + access_token)
|
||||
headers = {'Authorization': authorizationstring,
|
||||
@@ -495,6 +450,8 @@ def get_username(access_token):
|
||||
|
||||
return me_json['data']['username']
|
||||
|
||||
# Get user id, having access token
|
||||
# Handy for checking if the API access is working
|
||||
def get_userid(access_token):
|
||||
authorizationstring = str('Bearer ' + access_token)
|
||||
headers = {'Authorization': authorizationstring,
|
||||
@@ -510,6 +467,7 @@ def get_userid(access_token):
|
||||
|
||||
return me_json['data']['id']
|
||||
|
||||
# For debugging purposes
|
||||
def process_callback(request):
|
||||
# need error handling
|
||||
|
||||
@@ -521,6 +479,7 @@ def process_callback(request):
|
||||
|
||||
return HttpResponse("got a user name: %s" % username)
|
||||
|
||||
# Uploading workout
|
||||
def workout_c2_upload(user,w):
|
||||
response = 'trying C2 upload'
|
||||
r = Rower.objects.get(user=user)
|
||||
@@ -535,8 +494,6 @@ def workout_c2_upload(user,w):
|
||||
if (checkworkoutuser(user,w)):
|
||||
c2userid = get_userid(r.c2token)
|
||||
data = createc2workoutdata(w)
|
||||
# if (w.workouttype=='water'):
|
||||
# data = createc2workoutdata_as_splits(w)
|
||||
authorizationstring = str('Bearer ' + r.c2token)
|
||||
headers = {'Authorization': authorizationstring,
|
||||
'user-agent': 'sanderroosendaal',
|
||||
@@ -554,6 +511,7 @@ def workout_c2_upload(user,w):
|
||||
|
||||
return response
|
||||
|
||||
# This is token refresh. Looks for tokens in our database, then refreshes
|
||||
def rower_c2_token_refresh(user):
|
||||
r = Rower.objects.get(user=user)
|
||||
res = do_refresh_token(r.c2refreshtoken)
|
||||
|
||||
@@ -4,6 +4,9 @@ import os
|
||||
|
||||
from celery import Celery
|
||||
|
||||
# Only used for testing with Celery on localhost. RQ is not available
|
||||
# on Windows, so I use Celery on my notebook.
|
||||
|
||||
# set the default Django settings module for the 'celery' program.
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'rowsandall_app.settings')
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# All the data preparation, data cleaning and data mangling should
|
||||
# be defined here
|
||||
from rowers.models import Workout, User, Rower
|
||||
from rowingdata import rowingdata as rrdata
|
||||
|
||||
@@ -37,11 +39,13 @@ database_url = 'mysql://{user}:{password}@{host}:{port}/{database_name}'.format(
|
||||
port=port,
|
||||
)
|
||||
|
||||
# Use SQLite local database when we're in debug mode
|
||||
if settings.DEBUG or user=='':
|
||||
# database_url = 'sqlite:///db.sqlite3'
|
||||
database_url = 'sqlite:///'+database_name
|
||||
|
||||
|
||||
# mapping the DB column names to the CSV file column names
|
||||
columndict = {
|
||||
'time':'TimeStamp (sec)',
|
||||
'hr':' HRCur (bpm)',
|
||||
@@ -63,6 +67,7 @@ from scipy.signal import savgol_filter
|
||||
|
||||
import datetime
|
||||
|
||||
# A string representation for time deltas
|
||||
def niceformat(values):
|
||||
out = []
|
||||
for v in values:
|
||||
@@ -71,6 +76,7 @@ def niceformat(values):
|
||||
|
||||
return out
|
||||
|
||||
# A nice printable format for time delta values
|
||||
def strfdelta(tdelta):
|
||||
try:
|
||||
minutes,seconds = divmod(tdelta.seconds,60)
|
||||
@@ -87,6 +93,7 @@ def strfdelta(tdelta):
|
||||
|
||||
return res
|
||||
|
||||
# A nice printable format for pace values
|
||||
def nicepaceformat(values):
|
||||
out = []
|
||||
for v in values:
|
||||
@@ -96,6 +103,7 @@ def nicepaceformat(values):
|
||||
|
||||
return out
|
||||
|
||||
# Convert seconds to a Time Delta value, replacing NaN with a 5:50 pace
|
||||
def timedeltaconv(x):
|
||||
if not np.isnan(x):
|
||||
dt = datetime.timedelta(seconds=x)
|
||||
@@ -105,6 +113,9 @@ def timedeltaconv(x):
|
||||
|
||||
return dt
|
||||
|
||||
# Create new workout from file and store it in the database
|
||||
# This routine should be used everywhere in views.py and mailprocessing.pu
|
||||
# Currently there is code duplication
|
||||
def new_workout_from_file(r,f2,
|
||||
workouttype='rower',
|
||||
title='Workout',
|
||||
@@ -263,6 +274,9 @@ def new_workout_from_file(r,f2,
|
||||
|
||||
return True
|
||||
|
||||
# Compare the data from the CSV file and the database
|
||||
# Currently only calculates number of strokes. To be expanded with
|
||||
# more elaborate testing if needed
|
||||
def compare_data(id):
|
||||
row = Workout.objects.get(id=id)
|
||||
f1 = row.csvfilename
|
||||
@@ -288,6 +302,8 @@ def compare_data(id):
|
||||
ldb = l2
|
||||
return l1==l2,ldb,lfile
|
||||
|
||||
# Repair data for workouts where the CSV file is lost (or the DB entries
|
||||
# don't exist)
|
||||
def repair_data(verbose=False):
|
||||
ws = Workout.objects.all()
|
||||
for w in ws:
|
||||
@@ -319,6 +335,7 @@ def repair_data(verbose=False):
|
||||
print str(sys.exc_info()[0])
|
||||
pass
|
||||
|
||||
# A wrapper around the rowingdata class, with some error catching
|
||||
def rdata(file,rower=rrower()):
|
||||
try:
|
||||
res = rrdata(file,rower=rower)
|
||||
@@ -330,6 +347,7 @@ def rdata(file,rower=rrower()):
|
||||
|
||||
return res
|
||||
|
||||
# Remove all stroke data for workout ID from database
|
||||
def delete_strokedata(id):
|
||||
engine = create_engine(database_url, echo=False)
|
||||
query = sa.text('DELETE FROM strokedata WHERE workoutid={id};'.format(
|
||||
@@ -343,10 +361,12 @@ def delete_strokedata(id):
|
||||
conn.close()
|
||||
engine.dispose()
|
||||
|
||||
# Replace stroke data in DB with data from CSV file
|
||||
def update_strokedata(id,df):
|
||||
delete_strokedata(id)
|
||||
rowdata = dataprep(df,id=id,bands=True,barchart=True,otwpower=True)
|
||||
|
||||
|
||||
# Test that all data are of a numerical time
|
||||
def testdata(time,distance,pace,spm):
|
||||
t1 = np.issubdtype(time,np.number)
|
||||
t2 = np.issubdtype(distance,np.number)
|
||||
@@ -355,6 +375,8 @@ def testdata(time,distance,pace,spm):
|
||||
|
||||
return t1 and t2 and t3 and t4
|
||||
|
||||
# Get data from DB for one workout (fetches all data). If data
|
||||
# is not in DB, read from CSV file (and create DB entry)
|
||||
def getrowdata_db(id=0):
|
||||
data = read_df_sql(id)
|
||||
data['x_right'] = data['x_right']/1.0e6
|
||||
@@ -369,12 +391,14 @@ def getrowdata_db(id=0):
|
||||
|
||||
return data,row
|
||||
|
||||
# Fetch a subset of the data from the DB
|
||||
def getsmallrowdata_db(columns,ids=[]):
|
||||
prepmultipledata(ids)
|
||||
data = read_cols_df_sql(ids,columns)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
# Fetch both the workout and the workout stroke data (from CSV file)
|
||||
def getrowdata(id=0):
|
||||
|
||||
# check if valid ID exists (workout exists)
|
||||
@@ -395,7 +419,12 @@ def getrowdata(id=0):
|
||||
|
||||
return rowdata,row
|
||||
|
||||
|
||||
# Checks if all rows for a list of workout IDs have entries in the
|
||||
# stroke_data table. If this is not the case, it creates the stroke
|
||||
# data
|
||||
# In theory, this should never yield any work, but it's a good
|
||||
# safety net for programming errors elsewhere in the app
|
||||
# Also used heavily when I moved from CSV file only to CSV+Stroke data
|
||||
def prepmultipledata(ids,verbose=False):
|
||||
query = sa.text('SELECT DISTINCT workoutid FROM strokedata')
|
||||
engine = create_engine(database_url, echo=False)
|
||||
@@ -420,6 +449,8 @@ def prepmultipledata(ids,verbose=False):
|
||||
data = dataprep(rowdata.df,id=id,bands=True,barchart=True,otwpower=True)
|
||||
return res
|
||||
|
||||
# Read a set of columns for a set of workout ids, returns data as a
|
||||
# pandas dataframe
|
||||
def read_cols_df_sql(ids,columns):
|
||||
columns = list(columns)+['distance','spm']
|
||||
columns = [x for x in columns if x != 'None']
|
||||
@@ -450,7 +481,7 @@ def read_cols_df_sql(ids,columns):
|
||||
engine.dispose()
|
||||
return df
|
||||
|
||||
|
||||
# Read stroke data from the DB for a Workout ID. Returns a pandas dataframe
|
||||
def read_df_sql(id):
|
||||
engine = create_engine(database_url, echo=False)
|
||||
|
||||
@@ -460,10 +491,8 @@ def read_df_sql(id):
|
||||
engine.dispose()
|
||||
return df
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Get the necessary data from the strokedata table in the DB.
|
||||
# For the flex plot
|
||||
def smalldataprep(therows,xparam,yparam1,yparam2):
|
||||
df = pd.DataFrame()
|
||||
if yparam2 == 'None':
|
||||
@@ -503,7 +532,10 @@ def smalldataprep(therows,xparam,yparam1,yparam2):
|
||||
|
||||
return df
|
||||
|
||||
|
||||
# This is the main routine.
|
||||
# it reindexes, sorts, filters, and smooths the data, then
|
||||
# saves it to the stroke_data table in the database
|
||||
# Takes a rowingdata object's DataFrame as input
|
||||
def dataprep(rowdatadf,id=0,bands=True,barchart=True,otwpower=True,
|
||||
empower=True):
|
||||
rowdatadf.set_index([range(len(rowdatadf))],inplace=True)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# This is Data prep used for testing purposes (no Django environment)
|
||||
# Uses the debug SQLite database for stroke data
|
||||
from rowingdata import rowingdata as rrdata
|
||||
|
||||
from rowingdata import rower as rrower
|
||||
|
||||
@@ -27,7 +27,7 @@ from rowsandall_app.settings import C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SEC
|
||||
from rowsandall_app.settings import SPORTTRACKS_CLIENT_ID, SPORTTRACKS_REDIRECT_URI, SPORTTRACKS_CLIENT_SECRET
|
||||
import requests
|
||||
import json
|
||||
from rowsandall_app.rows import handle_uploaded_file
|
||||
from rowers.rows import handle_uploaded_file
|
||||
from rowers.tasks import handle_makeplot,handle_otwsetpower,handle_sendemailtcx
|
||||
from scipy.signal import savgol_filter
|
||||
|
||||
@@ -60,6 +60,7 @@ import plots
|
||||
from io import BytesIO
|
||||
from scipy.special import lambertw
|
||||
|
||||
# used in shell to send a newsletter to all Rowers
|
||||
def emailall(emailfile,subject):
|
||||
rowers = Rower.objects.all()
|
||||
for rower in rowers:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from django import forms
|
||||
from rowers.models import Workout
|
||||
from rowsandall_app.rows import validate_file_extension,must_be_csv
|
||||
from rowers.rows import validate_file_extension,must_be_csv
|
||||
from django.contrib.auth.forms import UserCreationForm
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.admin.widgets import AdminDateWidget
|
||||
@@ -9,10 +9,12 @@ from django.utils import timezone,translation
|
||||
|
||||
import datetime
|
||||
|
||||
# login form
|
||||
class LoginForm(forms.Form):
|
||||
username = forms.CharField()
|
||||
password = forms.CharField(widget=forms.PasswordInput())
|
||||
|
||||
|
||||
# simple form for Contact page. Sends email to info@rowsandall.com
|
||||
class EmailForm(forms.Form):
|
||||
firstname = forms.CharField(max_length=255)
|
||||
lastname = forms.CharField(max_length=255)
|
||||
@@ -21,16 +23,21 @@ class EmailForm(forms.Form):
|
||||
botcheck = forms.CharField(max_length=5)
|
||||
message = forms.CharField()
|
||||
|
||||
|
||||
# Upload the CrewNerd Summary CSV
|
||||
class CNsummaryForm(forms.Form):
|
||||
file = forms.FileField(required=True,validators=[must_be_csv])
|
||||
|
||||
# The little window to type '4x2000m/500m' to update the workout summary
|
||||
class SummaryStringForm(forms.Form):
|
||||
intervalstring = forms.CharField(max_length=255,label='Workout Description')
|
||||
|
||||
# Used for testing the POST API for StrokeData
|
||||
class StrokeDataForm(forms.Form):
|
||||
strokedata = forms.CharField(label='payload',
|
||||
widget=forms.Textarea)
|
||||
|
||||
|
||||
# The form used for uploading files
|
||||
class DocumentsForm(forms.Form):
|
||||
filetypechoices = (
|
||||
('csv' , 'Painsled iOS CSV'),
|
||||
@@ -50,9 +57,6 @@ class DocumentsForm(forms.Form):
|
||||
workouttype = forms.ChoiceField(required=True,
|
||||
choices=Workout.workouttypes,
|
||||
initial='rower')
|
||||
# fileformat = forms.ChoiceField(required=True,
|
||||
# choices=filetypechoices,
|
||||
# initial='csv')
|
||||
notes = forms.CharField(required=False,
|
||||
widget=forms.Textarea)
|
||||
|
||||
@@ -60,7 +64,8 @@ class DocumentsForm(forms.Form):
|
||||
fields = ['title','file','workouttype','fileformat']
|
||||
|
||||
|
||||
|
||||
# The form to indicate additional actions to be performed immediately
|
||||
# after a successful upload
|
||||
class UploadOptionsForm(forms.Form):
|
||||
plotchoices = (
|
||||
('timeplot','Time Plot'),
|
||||
@@ -76,6 +81,8 @@ class UploadOptionsForm(forms.Form):
|
||||
class Meta:
|
||||
fields = ['make_plot','plottype','upload_toc2']
|
||||
|
||||
# This form is used on the Analysis page to add a custom distance/time
|
||||
# trial and predict the pace
|
||||
class PredictedPieceForm(forms.Form):
|
||||
unitchoices = (
|
||||
('t','minutes'),
|
||||
@@ -88,6 +95,7 @@ class PredictedPieceForm(forms.Form):
|
||||
class Meta:
|
||||
fields = ['value','pieceunit']
|
||||
|
||||
# On the Geeky side, to update stream information for river dwellers
|
||||
class UpdateStreamForm(forms.Form):
|
||||
unitchoices = (
|
||||
('m','m/s'),
|
||||
@@ -107,6 +115,7 @@ class UpdateStreamForm(forms.Form):
|
||||
class Meta:
|
||||
fields = ['dist1','dist2','stream1', 'stream2','streamunit']
|
||||
|
||||
# add wind information to your workout
|
||||
class UpdateWindForm(forms.Form):
|
||||
unitchoices = (
|
||||
('m','m/s'),
|
||||
@@ -134,8 +143,7 @@ class UpdateWindForm(forms.Form):
|
||||
'windunit',
|
||||
'winddirection1','winddirection2']
|
||||
|
||||
|
||||
|
||||
# Form to select a data range to show workouts from a certain time period
|
||||
class DateRangeForm(forms.Form):
|
||||
startdate = forms.DateField(initial=timezone.now()-datetime.timedelta(days=365),
|
||||
widget=SelectDateWidget(years=range(1990,2050)),
|
||||
@@ -147,6 +155,7 @@ class DateRangeForm(forms.Form):
|
||||
class Meta:
|
||||
fields = ['startdate','enddate']
|
||||
|
||||
# Form used to select workouts for the past N days
|
||||
class DeltaDaysForm(forms.Form):
|
||||
deltadays = forms.IntegerField(initial=0,required=False,label='')
|
||||
|
||||
@@ -191,13 +200,14 @@ class RegistrationFormUniqueEmail(RegistrationFormTermsOfService):
|
||||
raise forms.ValidationError("This email address is already in use. Please supply a different email address.")
|
||||
return self.cleaned_data['email']
|
||||
|
||||
|
||||
# Time field supporting microseconds. Not used, I believe.
|
||||
class MyTimeField(forms.TimeField):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(MyTimeField, self).__init__(*args, **kwargs)
|
||||
supports_microseconds = True
|
||||
|
||||
# Form used to update interval stats
|
||||
class IntervalUpdateForm(forms.Form):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Processes emails sent to workouts@rowsandall.com
|
||||
import time
|
||||
from django.conf import settings
|
||||
from rowers.tasks import handle_sendemail_unrecognized
|
||||
@@ -21,7 +22,9 @@ from scipy.signal import savgol_filter
|
||||
|
||||
import zipfile
|
||||
import os
|
||||
import rowers.dataprep as dataprep
|
||||
|
||||
# Sends a confirmation with a link to the workout
|
||||
def send_confirm(u,name,link):
|
||||
fullemail = u.email
|
||||
subject = 'Workout added: '+name
|
||||
@@ -38,6 +41,7 @@ def send_confirm(u,name,link):
|
||||
|
||||
return 1
|
||||
|
||||
# Reads a "rowingdata" object, plus some error protections
|
||||
def rdata(file,rower=rrower()):
|
||||
try:
|
||||
res = rrdata(file,rower=rower)
|
||||
@@ -49,13 +53,18 @@ def rdata(file,rower=rrower()):
|
||||
|
||||
return res
|
||||
|
||||
# Some error protection around process attachments
|
||||
def safeprocessattachments():
|
||||
try:
|
||||
return processattachments()
|
||||
except:
|
||||
return [0]
|
||||
|
||||
# This is duplicated in management/commands/processemail
|
||||
# Need to double check the code there, update here, and only
|
||||
# use the code here.
|
||||
def processattachments():
|
||||
# in res, we store the ids of the new workouts
|
||||
res = []
|
||||
attachments = MessageAttachment.objects.all()
|
||||
for a in attachments:
|
||||
@@ -73,7 +82,6 @@ def processattachments():
|
||||
try:
|
||||
wid = [make_new_workout_from_email(rr,a.document,name)]
|
||||
res += wid
|
||||
print wid
|
||||
link = 'https://rowsandall.com/rowers/workout/'+str(wid[0])+'/edit'
|
||||
dd = send_confirm(u,name,link)
|
||||
except:
|
||||
@@ -92,6 +100,7 @@ def processattachments():
|
||||
# no attachments, so can be deleted
|
||||
m.delete()
|
||||
|
||||
# Delete remaining messages (which should not have attachments)
|
||||
mm = Message.objects.all()
|
||||
for m in mm:
|
||||
if m.attachments.exists()==False:
|
||||
@@ -99,6 +108,7 @@ def processattachments():
|
||||
|
||||
return res
|
||||
|
||||
# As above, but with some print commands for debugging purposes
|
||||
def processattachments_debug():
|
||||
res = []
|
||||
attachments = MessageAttachment.objects.all()
|
||||
@@ -144,7 +154,9 @@ def processattachments_debug():
|
||||
|
||||
return res
|
||||
|
||||
|
||||
# Process the attachment file, create new workout
|
||||
# The code here is duplication of the code in views.py (workout_upload_view)
|
||||
# Need to move the code to a subroutine used both in views.py and here
|
||||
def make_new_workout_from_email(rr,f2,name,cntr=0):
|
||||
workouttype = 'rower'
|
||||
f2 = f2.name
|
||||
@@ -305,6 +317,16 @@ def make_new_workout_from_email(rr,f2,name,cntr=0):
|
||||
startdatetime=row.rowdatetime)
|
||||
|
||||
w.save()
|
||||
|
||||
print w.id
|
||||
print row.df.info()
|
||||
|
||||
# put stroke data in database
|
||||
res = dataprep.dataprep(row.df,id=w.id,
|
||||
bands=True,barchart=True,
|
||||
otwpower=True,empower=True)
|
||||
|
||||
|
||||
return w.id
|
||||
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ from sqlite3 import OperationalError
|
||||
from django.utils import timezone
|
||||
import datetime
|
||||
|
||||
from rowers.rows import validate_file_extension
|
||||
|
||||
from rowsandall_app.settings import (
|
||||
TWEET_ACCESS_TOKEN_KEY,
|
||||
TWEET_ACCESS_TOKEN_SECRET,
|
||||
@@ -47,12 +49,12 @@ database_url = 'mysql://{user}:{password}@{host}:{port}/{database_name}'.format(
|
||||
if settings.DEBUG or user=='':
|
||||
database_url = 'sqlite:///db.sqlite3'
|
||||
|
||||
|
||||
# Create your models here.
|
||||
# For future Team functionality
|
||||
class Team(models.Model):
|
||||
name = models.CharField(max_length=150)
|
||||
notes = models.CharField(blank=True,max_length=200)
|
||||
|
||||
# Extension of User with rowing specific data
|
||||
class Rower(models.Model):
|
||||
weightcategories = (
|
||||
('hwt','heavy-weight'),
|
||||
@@ -95,6 +97,7 @@ class Rower(models.Model):
|
||||
def __str__(self):
|
||||
return self.user.username
|
||||
|
||||
# Saving a chart as a favorite chart
|
||||
class FavoriteChart(models.Model):
|
||||
y1params = (
|
||||
('hr','Heart Rate'),
|
||||
@@ -181,6 +184,7 @@ class FavoriteForm(ModelForm):
|
||||
fields = ['xparam','yparam1','yparam2',
|
||||
'plottype','workouttype','reststrokes']
|
||||
|
||||
# To generate favorite chart forms on the fly
|
||||
class BaseFavoriteFormSet(BaseFormSet):
|
||||
def clean(self):
|
||||
if any(self.errors):
|
||||
@@ -208,7 +212,8 @@ class BaseFavoriteFormSet(BaseFormSet):
|
||||
|
||||
if not yparam2:
|
||||
yparam2 = 'None'
|
||||
|
||||
|
||||
# Workout
|
||||
class Workout(models.Model):
|
||||
workouttypes = (
|
||||
('water','On-water'),
|
||||
@@ -273,7 +278,7 @@ def auto_delete_file_on_delete(sender, instance, **kwargs):
|
||||
if os.path.isfile(instance.csvfilename+'.gz'):
|
||||
os.remove(instance.csvfilename+'.gz')
|
||||
|
||||
|
||||
# Delete stroke data from the database when a workout is deleted
|
||||
@receiver(models.signals.post_delete,sender=Workout)
|
||||
def auto_delete_strokedata_on_delete(sender, instance, **kwargs):
|
||||
if instance.id:
|
||||
@@ -288,7 +293,12 @@ def auto_delete_strokedata_on_delete(sender, instance, **kwargs):
|
||||
print "Database Locked"
|
||||
conn.close()
|
||||
engine.dispose()
|
||||
|
||||
|
||||
# Model of StrokeData table
|
||||
# the definition here is used only to enable easy Django migration
|
||||
# when the StrokeData are expanded.
|
||||
# No Django Instances of this model are managed. Strokedata table is
|
||||
# accesssed directly with SQL commands
|
||||
class StrokeData(models.Model):
|
||||
class Meta:
|
||||
db_table = 'strokedata'
|
||||
@@ -329,7 +339,8 @@ class StrokeData(models.Model):
|
||||
finish = models.FloatField(default=0,null=True)
|
||||
wash = models.FloatField(default=0,null=True)
|
||||
peakforceangle = models.FloatField(default=0,null=True)
|
||||
|
||||
|
||||
# A wrapper around the png files
|
||||
class GraphImage(models.Model):
|
||||
filename = models.CharField(default='',max_length=150,blank=True,null=True)
|
||||
creationdatetime = models.DateTimeField()
|
||||
@@ -338,7 +349,7 @@ class GraphImage(models.Model):
|
||||
def __str__(self):
|
||||
return self.filename
|
||||
|
||||
# delete related file object
|
||||
# delete related file object when image is deleted
|
||||
@receiver(models.signals.post_delete,sender=GraphImage)
|
||||
def auto_delete_image_on_delete(sender,instance, **kwargs):
|
||||
if instance.filename:
|
||||
@@ -347,11 +358,11 @@ def auto_delete_image_on_delete(sender,instance, **kwargs):
|
||||
else:
|
||||
print "couldn't find the file "+instance.filename
|
||||
|
||||
|
||||
# Date input utility
|
||||
class DateInput(forms.DateInput):
|
||||
input_type = 'date'
|
||||
|
||||
|
||||
# Form to update Workout data
|
||||
class WorkoutForm(ModelForm):
|
||||
duration = forms.TimeInput(format='%H:%M:%S.%f')
|
||||
class Meta:
|
||||
@@ -368,16 +379,20 @@ class WorkoutForm(ModelForm):
|
||||
if self.instance.workouttype != 'water':
|
||||
del self.fields['boattype']
|
||||
|
||||
# Used for the rowing physics calculations
|
||||
class AdvancedWorkoutForm(ModelForm):
|
||||
class Meta:
|
||||
model = Workout
|
||||
fields = ['boattype','weightvalue']
|
||||
|
||||
# Simple form to set rower's Functional Threshold Power
|
||||
class RowerPowerForm(ModelForm):
|
||||
class Meta:
|
||||
model = Rower
|
||||
fields = ['ftp']
|
||||
|
||||
# Form to set rower's Heart Rate zones, including test routines
|
||||
# to enable consistency
|
||||
class RowerForm(ModelForm):
|
||||
class Meta:
|
||||
model = Rower
|
||||
@@ -519,6 +534,8 @@ class RowerForm(ModelForm):
|
||||
if an>=max:
|
||||
raise forms.ValidationError("AN should be lower than Max")
|
||||
|
||||
# An announcement that goes to the right of the workouts list
|
||||
# optionally sends a tweet to our twitter account
|
||||
class SiteAnnouncement(models.Model):
|
||||
created = models.DateField(default=timezone.now)
|
||||
announcement = models.TextField(max_length=140)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# Interactions with Rowsandall.com API. Not fully complete.
|
||||
|
||||
# Python
|
||||
import oauth2 as oauth
|
||||
import cgi
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# Defines permissions for objects (API related)
|
||||
|
||||
from rest_framework import permissions
|
||||
from rowers.models import Rower
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import matplotlib.pyplot as plt
|
||||
|
||||
import numpy as np
|
||||
|
||||
# Make pace ticks for pace axis '2:00', '1:50', etc
|
||||
def format_pace_tick(x,pos=None):
|
||||
min=int(x/60)
|
||||
sec=int(x-min*60.)
|
||||
@@ -10,6 +11,7 @@ def format_pace_tick(x,pos=None):
|
||||
template='%d:%s'
|
||||
return template % (min,sec_str)
|
||||
|
||||
# Returns a pace string from a pace in seconds/500m, '1:45.4'
|
||||
def format_pace(x,pos=None):
|
||||
if isinf(x) or isnan(x):
|
||||
x=0
|
||||
@@ -24,6 +26,7 @@ def format_pace(x,pos=None):
|
||||
|
||||
return str1
|
||||
|
||||
# Returns a time string from a time in seconds
|
||||
def format_time(x,pos=None):
|
||||
|
||||
|
||||
@@ -37,11 +40,13 @@ def format_time(x,pos=None):
|
||||
|
||||
return str1
|
||||
|
||||
# Formatting the distance tick marks
|
||||
def format_dist_tick(x,pos=None):
|
||||
km = x/1000.
|
||||
template='%6.3f'
|
||||
return template % (km)
|
||||
|
||||
# Formatting the time tick marks (h:mm)
|
||||
def format_time_tick(x,pos=None):
|
||||
hour=int(x/3600)
|
||||
min=int((x-hour*3600.)/60)
|
||||
@@ -49,6 +54,11 @@ def format_time_tick(x,pos=None):
|
||||
template='%d:%s'
|
||||
return template % (hour,min_str)
|
||||
|
||||
# Utility to select reasonable y axis range
|
||||
# Basically the data range plus some padding, but with ultimate
|
||||
# you can set the slowest paces to fall off the axis.
|
||||
# Useful for OTW rowing where you sometimes stops and pace runs out of
|
||||
# the boundaries
|
||||
def y_axis_range(ydata,miny=0,padding=.1,ultimate=[-1e9,1e9]):
|
||||
|
||||
# ydata must by a numpy array
|
||||
@@ -86,6 +96,7 @@ def y_axis_range(ydata,miny=0,padding=.1,ultimate=[-1e9,1e9]):
|
||||
|
||||
return [yrangemin,yrangemax]
|
||||
|
||||
# Make a plot (this one is only used for testing)
|
||||
def mkplot(row,title):
|
||||
df = row.df
|
||||
|
||||
|
||||
73
rowers/rows.py
Normal file
73
rowers/rows.py
Normal file
@@ -0,0 +1,73 @@
|
||||
import time
|
||||
import gzip
|
||||
import shutil
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
def format_pace_tick(x,pos=None):
|
||||
min=int(x/60)
|
||||
sec=int(x-min*60.)
|
||||
sec_str=str(sec).zfill(2)
|
||||
template='%d:%s'
|
||||
return template % (min,sec_str)
|
||||
|
||||
def format_time_tick(x,pos=None):
|
||||
hour=int(x/3600)
|
||||
min=int((x-hour*3600.)/60)
|
||||
min_str=str(min).zfill(2)
|
||||
template='%d:%s'
|
||||
return template % (hour,min_str)
|
||||
|
||||
def format_pace(x,pos=None):
|
||||
if isinf(x) or isnan(x):
|
||||
x=0
|
||||
|
||||
min=int(x/60)
|
||||
sec=(x-min*60.)
|
||||
|
||||
str1 = "{min:0>2}:{sec:0>4.1f}".format(
|
||||
min = min,
|
||||
sec = sec
|
||||
)
|
||||
|
||||
return str1
|
||||
|
||||
def format_time(x,pos=None):
|
||||
|
||||
|
||||
min = int(x/60.)
|
||||
sec = int(x-min*60)
|
||||
|
||||
str1 = "{min:0>2}:{sec:0>4.1f}".format(
|
||||
min=min,
|
||||
sec=sec,
|
||||
)
|
||||
|
||||
return str1
|
||||
|
||||
def validate_file_extension(value):
|
||||
import os
|
||||
ext = os.path.splitext(value.name)[1]
|
||||
valid_extensions = ['.tcx','.csv','.TCX','.CSV','.fit','.FIT','.zip','.ZIP']
|
||||
if not ext in valid_extensions:
|
||||
raise ValidationError(u'File not supported!')
|
||||
|
||||
def must_be_csv(value):
|
||||
import os
|
||||
ext = os.path.splitext(value.name)[1]
|
||||
valid_extensions = ['.csv','.CSV']
|
||||
if not ext in valid_extensions:
|
||||
raise ValidationError(u'File not supported!')
|
||||
|
||||
|
||||
def handle_uploaded_file(f):
|
||||
fname = f.name
|
||||
timestr = time.strftime("%Y%m%d-%H%M%S")
|
||||
fname = timestr+'-'+fname
|
||||
fname2 = 'media/'+fname
|
||||
with open(fname2,'wb+') as destination:
|
||||
for chunk in f.chunks():
|
||||
destination.write(chunk)
|
||||
|
||||
return fname,fname2
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# Serializers. Defines which fields from an object get to the JSON object
|
||||
# Also optionally define POST, PATCH methods (create, update)
|
||||
|
||||
from rest_framework import serializers
|
||||
from rowers.models import Workout,Rower,StrokeData,FavoriteChart
|
||||
|
||||
@@ -104,6 +107,7 @@ class WorkoutSerializer(serializers.ModelSerializer):
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
# This is just a fake one for URL registration purposes
|
||||
class StrokeDataSerializer(serializers.Serializer):
|
||||
workoutid = serializers.IntegerField
|
||||
strokedata = serializers.JSONField
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# All the functionality to connect to SportTracks
|
||||
|
||||
# Python
|
||||
import oauth2 as oauth
|
||||
import cgi
|
||||
@@ -31,6 +33,8 @@ from rowers.models import Rower,Workout
|
||||
|
||||
from rowsandall_app.settings import C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET, STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, STRAVA_CLIENT_SECRET, SPORTTRACKS_CLIENT_SECRET, SPORTTRACKS_CLIENT_ID, SPORTTRACKS_REDIRECT_URI
|
||||
|
||||
# Custom exception handler, returns a 401 HTTP message
|
||||
# with exception details in the json data
|
||||
def custom_exception_handler(exc,message):
|
||||
|
||||
response = {
|
||||
@@ -48,6 +52,7 @@ def custom_exception_handler(exc,message):
|
||||
|
||||
return res
|
||||
|
||||
# Refresh ST token using refresh token
|
||||
def do_refresh_token(refreshtoken):
|
||||
client_auth = requests.auth.HTTPBasicAuth(SPORTTRACKS_CLIENT_ID, SPORTTRACKS_CLIENT_SECRET)
|
||||
post_data = {"grant_type": "refresh_token",
|
||||
@@ -75,7 +80,7 @@ def do_refresh_token(refreshtoken):
|
||||
|
||||
return [thetoken,expires_in,refresh_token]
|
||||
|
||||
|
||||
# Exchange ST access code for long-lived ST access token
|
||||
def get_token(code):
|
||||
client_auth = requests.auth.HTTPBasicAuth(SPORTTRACKS_CLIENT_ID, SPORTTRACKS_CLIENT_SECRET)
|
||||
post_data = {"grant_type": "authorization_code",
|
||||
@@ -100,6 +105,7 @@ def get_token(code):
|
||||
|
||||
return [thetoken,expires_in,refresh_token]
|
||||
|
||||
# Make authorization URL including random string
|
||||
def make_authorization_url(request):
|
||||
# Generate a random string for the state parameter
|
||||
# Save it for use later to prevent xsrf attacks
|
||||
@@ -118,7 +124,7 @@ def make_authorization_url(request):
|
||||
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
# This is token refresh. Looks for tokens in our database, then refreshes
|
||||
def rower_sporttracks_token_refresh(user):
|
||||
r = Rower.objects.get(user=user)
|
||||
res = do_refresh_token(r.sporttracksrefreshtoken)
|
||||
@@ -135,6 +141,7 @@ def rower_sporttracks_token_refresh(user):
|
||||
r.save()
|
||||
return r.sporttrackstoken
|
||||
|
||||
# Get list of workouts available on SportTracks
|
||||
def get_sporttracks_workout_list(user):
|
||||
r = Rower.objects.get(user=user)
|
||||
if (r.sporttrackstoken == '') or (r.sporttrackstoken is None):
|
||||
@@ -154,7 +161,7 @@ def get_sporttracks_workout_list(user):
|
||||
|
||||
return s
|
||||
|
||||
|
||||
# Get workout summary data by SportTracks ID
|
||||
def get_sporttracks_workout(user,sporttracksid):
|
||||
r = Rower.objects.get(user=user)
|
||||
if (r.sporttrackstoken == '') or (r.sporttrackstoken is None):
|
||||
@@ -174,6 +181,7 @@ def get_sporttracks_workout(user,sporttracksid):
|
||||
|
||||
return s
|
||||
|
||||
# Create Workout Data for upload to SportTracks
|
||||
def createsporttracksworkoutdata(w):
|
||||
filename = w.csvfilename
|
||||
try:
|
||||
@@ -272,6 +280,8 @@ def createsporttracksworkoutdata(w):
|
||||
|
||||
return data
|
||||
|
||||
# Obtain SportTracks Workout ID from the response returned on successful
|
||||
# upload
|
||||
def getidfromresponse(response):
|
||||
t = json.loads(response.text)
|
||||
uri = t['uris'][0]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# All the functionality needed to connect to Strava
|
||||
|
||||
# Python
|
||||
import oauth2 as oauth
|
||||
import cgi
|
||||
@@ -30,6 +32,9 @@ import stravalib
|
||||
|
||||
from rowsandall_app.settings import C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET, STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, STRAVA_CLIENT_SECRET
|
||||
|
||||
# Exponentially weighted moving average
|
||||
# Used for data smoothing of the jagged data obtained by Strava
|
||||
# See bitbucket issue 72
|
||||
def ewmovingaverage(interval,window_size):
|
||||
# Experimental code using Exponential Weighted moving average
|
||||
|
||||
@@ -45,45 +50,11 @@ def ewmovingaverage(interval,window_size):
|
||||
|
||||
return interval2
|
||||
|
||||
def geo_distance(lat1,lon1,lat2,lon2):
|
||||
""" Approximate distance and bearing between two points
|
||||
defined by lat1,lon1 and lat2,lon2
|
||||
This is a slight underestimate but is close enough for our purposes,
|
||||
We're never moving more than 10 meters between trackpoints
|
||||
|
||||
Bearing calculation fails if one of the points is a pole.
|
||||
|
||||
"""
|
||||
|
||||
# radius of earth in km
|
||||
R = 6373.0
|
||||
|
||||
# pi
|
||||
pi = math.pi
|
||||
|
||||
lat1 = math.radians(lat1)
|
||||
lat2 = math.radians(lat2)
|
||||
lon1 = math.radians(lon1)
|
||||
lon2 = math.radians(lon2)
|
||||
|
||||
dlon = lon2 - lon1
|
||||
dlat = lat2 - lat1
|
||||
|
||||
a = sin(dlat / 2)**2 + cos(lat1) * cos(lat2) * sin(dlon / 2)**2
|
||||
c = 2 * atan2(sqrt(a), sqrt(1 - a))
|
||||
|
||||
distance = R * c
|
||||
|
||||
tc1 = atan2(sin(lon2-lon1)*cos(lat2),
|
||||
cos(lat1)*sin(lat2)-sin(lat1)*cos(lat2)*cos(lon2-lon1))
|
||||
|
||||
tc1 = tc1 % (2*pi)
|
||||
|
||||
bearing = math.degrees(tc1)
|
||||
|
||||
return [distance,bearing]
|
||||
from utils import geo_distance
|
||||
|
||||
|
||||
# Custom exception handler, returns a 401 HTTP message
|
||||
# with exception details in the json data
|
||||
def custom_exception_handler(exc,message):
|
||||
|
||||
response = {
|
||||
@@ -101,6 +72,7 @@ def custom_exception_handler(exc,message):
|
||||
|
||||
return res
|
||||
|
||||
# Exchange access code for long-lived access token
|
||||
def get_token(code):
|
||||
client_auth = requests.auth.HTTPBasicAuth(STRAVA_CLIENT_ID, STRAVA_CLIENT_SECRET)
|
||||
post_data = {"grant_type": "authorization_code",
|
||||
@@ -118,7 +90,7 @@ def get_token(code):
|
||||
|
||||
return [thetoken]
|
||||
|
||||
|
||||
# Make authorization URL including random string
|
||||
def make_authorization_url(request):
|
||||
# Generate a random string for the state parameter
|
||||
# Save it for use later to prevent xsrf attacks
|
||||
@@ -134,6 +106,7 @@ def make_authorization_url(request):
|
||||
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
# Get list of workouts available on Strava
|
||||
def get_strava_workout_list(user):
|
||||
r = Rower.objects.get(user=user)
|
||||
if (r.stravatoken == '') or (r.stravatoken is None):
|
||||
@@ -150,6 +123,7 @@ def get_strava_workout_list(user):
|
||||
|
||||
return s
|
||||
|
||||
# Get a Strava workout summary data and stroke data by ID
|
||||
def get_strava_workout(user,stravaid):
|
||||
r = Rower.objects.get(user=user)
|
||||
if (r.stravatoken == '') or (r.stravatoken is None):
|
||||
@@ -236,6 +210,7 @@ def get_strava_workout(user,stravaid):
|
||||
|
||||
return [workoutsummary,df]
|
||||
|
||||
# Generate Workout data for Strava (a TCX file)
|
||||
def createstravaworkoutdata(w):
|
||||
filename = w.csvfilename
|
||||
try:
|
||||
@@ -247,6 +222,8 @@ def createstravaworkoutdata(w):
|
||||
|
||||
return tcxfilename
|
||||
|
||||
# Upload the TCX file to Strava and set the workout activity type
|
||||
# to rowing on Strava
|
||||
def handle_stravaexport(file,workoutname,stravatoken,description=''):
|
||||
# w = Workout.objects.get(id=workoutid)
|
||||
client = stravalib.Client(access_token=stravatoken)
|
||||
@@ -264,12 +241,6 @@ def handle_stravaexport(file,workoutname,stravatoken,description=''):
|
||||
errorlog.write(timestr+errorstring+"\r\n")
|
||||
errorlog.write("stravastuff.py line 262\r\n")
|
||||
|
||||
|
||||
|
||||
# w.uploadedtostrava = res.id
|
||||
# w.save()
|
||||
# file.close()
|
||||
|
||||
return res.id
|
||||
|
||||
|
||||
|
||||
@@ -21,12 +21,12 @@ from rowers.dataprepnodjango import update_strokedata
|
||||
|
||||
from django.core.mail import send_mail, BadHeaderError,EmailMessage
|
||||
|
||||
|
||||
# testing task
|
||||
@app.task
|
||||
def add(x, y):
|
||||
return x + y
|
||||
|
||||
|
||||
# send email to me when an unrecognized file is uploaded
|
||||
@app.task
|
||||
def handle_sendemail_unrecognized(unrecognizedfile,useremail):
|
||||
|
||||
@@ -51,7 +51,7 @@ def handle_sendemail_unrecognized(unrecognizedfile,useremail):
|
||||
os.remove(unrecognizedfile)
|
||||
return 1
|
||||
|
||||
|
||||
# Send email with TCX attachment
|
||||
@app.task
|
||||
def handle_sendemailtcx(first_name,last_name,email,tcxfile):
|
||||
|
||||
@@ -75,6 +75,7 @@ def handle_sendemailtcx(first_name,last_name,email,tcxfile):
|
||||
os.remove(tcxfile)
|
||||
return 1
|
||||
|
||||
# Send email with CSV attachment
|
||||
@app.task
|
||||
def handle_sendemailcsv(first_name,last_name,email,csvfile):
|
||||
|
||||
@@ -104,6 +105,7 @@ def handle_sendemailcsv(first_name,last_name,email,csvfile):
|
||||
|
||||
return 1
|
||||
|
||||
# Calculate wind and stream corrections for OTW rowing
|
||||
@app.task
|
||||
def handle_otwsetpower(f1,boattype,weightvalue,
|
||||
first_name,last_name,email,workoutid,
|
||||
@@ -129,7 +131,7 @@ def handle_otwsetpower(f1,boattype,weightvalue,
|
||||
except KeyError:
|
||||
rg = rowingdata.getrigging('static/rigging/1x.txt')
|
||||
|
||||
# do calculation
|
||||
# do calculation, but do not overwrite NK Empower Power data
|
||||
powermeasured = False
|
||||
try:
|
||||
w = rowdata.df['wash']
|
||||
@@ -166,6 +168,7 @@ def handle_otwsetpower(f1,boattype,weightvalue,
|
||||
|
||||
return 1
|
||||
|
||||
# This function generates all the static (PNG image) plots
|
||||
@app.task
|
||||
def handle_makeplot(f1,f2,t,hrdata,plotnr,imagename):
|
||||
hrmax = hrdata['hrmax']
|
||||
@@ -222,5 +225,6 @@ def handle_makeplot(f1,f2,t,hrdata,plotnr,imagename):
|
||||
return imagename
|
||||
|
||||
|
||||
# Another simple task for debugging purposes
|
||||
def add2(x,y):
|
||||
return x+y
|
||||
|
||||
@@ -1,3 +1,59 @@
|
||||
# This is just a scratch pad to temporarily park code, just in case I need
|
||||
# it later. Hardly used since i have proper versioning
|
||||
|
||||
#
|
||||
def createc2workoutdata_grouped(w):
|
||||
filename = w.csvfilename
|
||||
row = rowingdata(filename)
|
||||
|
||||
# resize per minute
|
||||
df = row.df.groupby(lambda x:x/10).mean()
|
||||
|
||||
averagehr = int(df[' HRCur (bpm)'].mean())
|
||||
maxhr = int(df[' HRCur (bpm)'].max())
|
||||
|
||||
# adding diff, trying to see if this is valid
|
||||
t = 10*df.ix[:,' ElapsedTime (sec)'].values
|
||||
t[0] = t[1]
|
||||
d = df.ix[:,' Horizontal (meters)'].values
|
||||
d[0] = d[1]
|
||||
p = 10*df.ix[:,' Stroke500mPace (sec/500m)'].values
|
||||
t = t.astype(int)
|
||||
d = d.astype(int)
|
||||
p = p.astype(int)
|
||||
spm = df[' Cadence (stokes/min)'].astype(int)
|
||||
spm[0] = spm[1]
|
||||
hr = df[' HRCur (bpm)'].astype(int)
|
||||
stroke_data = []
|
||||
for i in range(len(t)):
|
||||
thisrecord = {"t":t[i],"d":d[i],"p":p[i],"spm":spm[i],"hr":hr[i]}
|
||||
stroke_data.append(thisrecord)
|
||||
|
||||
|
||||
try:
|
||||
durationstr = datetime.strptime(str(w.duration),"%H:%M:%S.%f")
|
||||
except ValueError:
|
||||
durationstr = datetime.strptime(str(w.duration),"%H:%M:%S")
|
||||
|
||||
|
||||
data = {
|
||||
"type": w.workouttype,
|
||||
"date": w.startdatetime.isoformat(),
|
||||
"distance": int(w.distance),
|
||||
"time": int(10*makeseconds(durationstr)),
|
||||
"weight_class": c2wc(w.weightcategory),
|
||||
"timezone": "Etc/UTC",
|
||||
"comments": w.notes,
|
||||
"heart_rate": {
|
||||
"average": averagehr,
|
||||
"max": maxhr,
|
||||
},
|
||||
"stroke_data": stroke_data,
|
||||
}
|
||||
|
||||
|
||||
return data
|
||||
|
||||
@login_required()
|
||||
def workout_edit_view(request,id=0):
|
||||
if request.method == 'POST':
|
||||
|
||||
@@ -8,7 +8,7 @@ import rowers.interactiveplots as iplots
|
||||
import datetime
|
||||
from rowingdata import rowingdata as rdata
|
||||
from rowingdata import rower as rrower
|
||||
from rowsandall_app.rows import handle_uploaded_file
|
||||
from rowers.rows import handle_uploaded_file
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from time import strftime,strptime,mktime,time,daylight
|
||||
import os
|
||||
@@ -1037,8 +1037,7 @@ class subroutinetests(TestCase):
|
||||
jsond = json.dumps(data)
|
||||
data = c2stuff.createc2workoutdata_as_splits(w)
|
||||
jsond = json.dumps(data)
|
||||
data = c2stuff.createc2workoutdata_as_grouped(w)
|
||||
jsond = json.dumps(data)
|
||||
|
||||
|
||||
|
||||
class PlotTests(TestCase):
|
||||
|
||||
@@ -100,7 +100,6 @@ urlpatterns = [
|
||||
url(r'^api-docs$', views.schema_view),
|
||||
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
|
||||
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'),
|
||||
url(r'^400/$', TemplateView.as_view(template_name='400.html'),name='400'),
|
||||
@@ -113,10 +112,6 @@ urlpatterns = [
|
||||
url(r'^list-workouts/(?P<startdatestring>\w+.*)/(?P<enddatestring>\w+.*)$',views.workouts_view),
|
||||
url(r'^list-workouts/$',views.workouts_view),
|
||||
url(r'^list-graphs/$',views.graphs_view),
|
||||
url(r'^dashboard/c/(?P<message>\w+.*)/$',views.dashboard_view),
|
||||
url(r'^dashboard/s/(?P<successmessage>\w+.*)/$',views.dashboard_view),
|
||||
url(r'^dashboard/c/(?P<message>\w+.*)/s/(?P<successmessage>\w+.*)$',views.dashboard_view),
|
||||
url(r'^dashboard/$',views.dashboard_view),
|
||||
url(r'^(?P<theuser>\d+)/ote-bests/(?P<startdatestring>\w+.*)/(?P<enddatestring>\w+.*)$',views.rankings_view),
|
||||
url(r'^(?P<theuser>\d+)/ote-bests/(?P<deltadays>\d+)$',views.rankings_view),
|
||||
url(r'^ote-bests/(?P<startdatestring>\w+.*)/(?P<enddatestring>\w+.*)$',views.rankings_view),
|
||||
@@ -194,7 +189,6 @@ urlpatterns = [
|
||||
url(r'^workout/sporttracksimport/$',views.workout_sporttracksimport_view),
|
||||
url(r'^workout/sporttracksimport/(\d+)/$',views.workout_getsporttracksworkout_view),
|
||||
url(r'^workout/(\d+)/deleteconfirm$',views.workout_delete_confirm_view),
|
||||
url(r'^workout/(\d+)/c2upload/$',views.list_c2_upload_view),
|
||||
url(r'^workout/(\d+)/c2uploadw/$',views.workout_c2_upload_view),
|
||||
url(r'^workout/(\d+)/stravauploadw/$',views.workout_strava_upload_view),
|
||||
url(r'^workout/(\d+)/recalcsummary/$',views.workout_recalcsummary_view),
|
||||
@@ -232,7 +226,6 @@ urlpatterns = [
|
||||
|
||||
if settings.DEBUG:
|
||||
urlpatterns += [
|
||||
url(r'^workout/uploadd/$',views.workout_upload_view_debug),
|
||||
url(r'^testreverse/$',views.test_reverse_view),
|
||||
url(r'^c2listug/$',views.c2listdebug_view),
|
||||
]
|
||||
|
||||
42
rowers/utils.py
Normal file
42
rowers/utils.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import math
|
||||
|
||||
def geo_distance(lat1,lon1,lat2,lon2):
|
||||
""" Approximate distance and bearing between two points
|
||||
defined by lat1,lon1 and lat2,lon2
|
||||
This is a slight underestimate but is close enough for our purposes,
|
||||
We're never moving more than 10 meters between trackpoints
|
||||
|
||||
Bearing calculation fails if one of the points is a pole.
|
||||
(Hey, from the North pole you can walk South, East, North and end up
|
||||
on the same spot!)
|
||||
|
||||
"""
|
||||
|
||||
# radius of our earth in km --> should be moved to settings if
|
||||
# rowing takes off on other planets
|
||||
R = 6373.0
|
||||
|
||||
# pi
|
||||
pi = math.pi
|
||||
|
||||
lat1 = math.radians(lat1)
|
||||
lat2 = math.radians(lat2)
|
||||
lon1 = math.radians(lon1)
|
||||
lon2 = math.radians(lon2)
|
||||
|
||||
dlon = lon2 - lon1
|
||||
dlat = lat2 - lat1
|
||||
|
||||
a = sin(dlat / 2)**2 + cos(lat1) * cos(lat2) * sin(dlon / 2)**2
|
||||
c = 2 * atan2(sqrt(a), sqrt(1 - a))
|
||||
|
||||
distance = R * c
|
||||
|
||||
tc1 = atan2(sin(lon2-lon1)*cos(lat2),
|
||||
cos(lat1)*sin(lat2)-sin(lat1)*cos(lat2)*cos(lon2-lon1))
|
||||
|
||||
tc1 = tc1 % (2*pi)
|
||||
|
||||
bearing = math.degrees(tc1)
|
||||
|
||||
return [distance,bearing]
|
||||
643
rowers/views.py
643
rowers/views.py
File diff suppressed because it is too large
Load Diff
@@ -8,9 +8,8 @@ from rowers.models import Rower, Workout
|
||||
|
||||
from rowsandall_app.settings import FORECAST_IO_KEY
|
||||
|
||||
|
||||
# Get weather data from the DarkSky API
|
||||
def get_weather_data(long,lat,unixtime):
|
||||
# url = "https://api.forecast.io/forecast/"+FORECAST_IO_KEY+"/"
|
||||
url = "https://api.darksky.net/forecast/"+FORECAST_IO_KEY+"/"
|
||||
url += str(long)+","+str(lat)+","+str(unixtime)
|
||||
|
||||
@@ -21,6 +20,7 @@ def get_weather_data(long,lat,unixtime):
|
||||
else:
|
||||
return 0
|
||||
|
||||
# Get wind data (and translate from knots to m/s)
|
||||
def get_wind_data(lat,long,unixtime):
|
||||
data = get_weather_data(lat,long,unixtime)
|
||||
if data:
|
||||
@@ -39,6 +39,7 @@ def get_wind_data(lat,long,unixtime):
|
||||
|
||||
try:
|
||||
temperature = data['currently']['temperature']
|
||||
# Temp is given in Fahrenheit, so convert to Celsius for Europeans
|
||||
temperaturec = (temperature-32.)*(5./9.)
|
||||
temperaturec = int(10*temperaturec)/10.
|
||||
except KeyError:
|
||||
@@ -54,7 +55,8 @@ def get_wind_data(lat,long,unixtime):
|
||||
windbearing = 0
|
||||
message = 'Not able to get weather data'
|
||||
|
||||
# apply Hellman's coefficient for neutral air above human inhabitated areas
|
||||
# apply Hellman's coefficient for neutral air above human
|
||||
# inhabitated areas
|
||||
windspeed = windspeed*(0.1)**0.34
|
||||
windspeed = 0.01*int(100*windspeed)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user