Private
Public Access
1
0

Merge branch 'feature/cleanup' into develop

This commit is contained in:
Sander Roosendaal
2017-01-16 17:36:31 +01:00
54 changed files with 470 additions and 3111 deletions

View File

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

View File

@@ -2,6 +2,6 @@ from __future__ import unicode_literals
from django.apps import AppConfig
# Store metadata for the app
class RowersConfig(AppConfig):
name = 'rowers'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,5 @@
# Interactions with Rowsandall.com API. Not fully complete.
# Python
import oauth2 as oauth
import cgi

View File

@@ -1,3 +1,5 @@
# Defines permissions for objects (API related)
from rest_framework import permissions
from rowers.models import Rower

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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