Private
Public Access
1
0

Merge branch 'release/v4.5'

This commit is contained in:
Sander Roosendaal
2017-10-24 15:05:57 +02:00
36 changed files with 1443 additions and 1280 deletions
+576 -575
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -12,6 +12,7 @@ import dataprep
import types
import datetime
from django.forms import formset_factory
from utils import landingpages
# login form
class LoginForm(forms.Form):
@@ -180,6 +181,10 @@ class UploadOptionsForm(forms.Form):
makeprivate = forms.BooleanField(initial=False,required=False,
label='Make Workout Private')
landingpage = forms.ChoiceField(choices=landingpages,
initial='workout_edit_view',
label='Landing Page')
class Meta:
fields = ['make_plot','plottype','upload_toc2','makeprivate']
+10 -2
View File
@@ -2046,7 +2046,6 @@ def interactive_flex_chart2(id=0,promember=0,
plottype='line',
workstrokesonly=False):
#rowdata,row = dataprep.getrowdata_db(id=id)
columns = [xparam,yparam1,yparam2,
'ftime','distance','fpace',
@@ -2510,7 +2509,16 @@ def thumbnails_set(r,id,favorites):
columns += [f.yparam2 for f in favorites]
columns += ['time']
rowdata = dataprep.getsmallrowdata_db(columns,ids=[id],doclean=True)
try:
rowdata = dataprep.getsmallrowdata_db(columns,ids=[id],doclean=True)
except:
return [
{'script':"",
'div':"",
'notes':""
}]
rowdata.dropna(axis=1,how='all',inplace=True)
if rowdata.empty:
+85 -181
View File
@@ -1,24 +1,19 @@
# Processes emails sent to workouts@rowsandall.com
""" Processes emails sent to workouts@rowsandall.com """
import shutil
import time
from django.conf import settings
from rowers.tasks import handle_sendemail_unrecognized
from django_mailbox.models import Mailbox,Message,MessageAttachment
from rowers.models import Workout, User, Rower, WorkoutForm,RowerForm,GraphImage,AdvancedWorkoutForm
from django.core.files.base import ContentFile
from django.core.mail import send_mail, BadHeaderError,EmailMessage
from rowsandall_app.settings import BASE_DIR
from django_mailbox.models import Message, MessageAttachment
from rowers.models import User, Rower, RowerForm
from django.core.mail import EmailMessage
from rowingdata import rower as rrower
from rowingdata import main as rmain
from rowingdata import rowingdata as rrdata
from rowingdata import TCXParser,RowProParser,ErgDataParser
from rowingdata import MysteryParser,BoatCoachParser
from rowingdata import painsledDesktopParser,speedcoachParser,ErgStickParser
from rowingdata import SpeedCoach2Parser,FITParser,fitsummarydata
from rowingdata import make_cumvalues
from rowingdata import summarydata,get_file_type
from scipy.signal import savgol_filter
from rowingdata import rowingdata as rrdata
from rowingdata import get_file_type
import zipfile
import os
@@ -30,19 +25,21 @@ queuelow = django_rq.get_queue('low')
queuehigh = django_rq.get_queue('default')
# Sends a confirmation with a link to the workout
def send_confirm(u,name,link,options):
fullemail = u.email
subject = 'Workout added: '+name
message = 'Dear '+u.first_name+',\n\n'
def send_confirm(user, name, link, options):
""" Send confirmation email to user when email has been processed """
fullemail = user.email
subject = 'Workout added: ' + name
message = 'Dear ' + user.first_name + ',\n\n'
message += "Your workout has been added to Rowsandall.com.\n"
message += "Link to workout: "+link+"\n\n"
message += "Link to workout: " + link + "\n\n"
message += "Best Regards, the Rowsandall Team"
if options:
message += "\n\n"+str(options)
message += "\n\n" + str(options)
email = EmailMessage(subject,message,
email = EmailMessage(subject, message,
'Rowsandall <info@rowsandall.com>',
[fullemail])
@@ -51,203 +48,110 @@ def send_confirm(u,name,link,options):
return 1
# Reads a "rowingdata" object, plus some error protections
def rdata(file,rower=rrower()):
def rdata(file, rower=rrower()):
""" Reads rowingdata data or returns 0 on Error """
try:
res = rrdata(file,rower=rower)
result = rrdata(file, rower=rower)
except IOError:
try:
res = rrdata(file+'.gz',rower=rower)
result = rrdata(file + '.gz', rower=rower)
except IOError:
res = 0
result = 0
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:
donotdelete = 0
m = Message.objects.get(id=a.message_id)
from_address = m.from_address[0]
name = m.subject
# get a list of users
theusers = User.objects.filter(email=from_address)
for u in theusers:
try:
rr = Rower.objects.get(user=u.id)
# move attachment and make workout
try:
wid = [make_new_workout_from_email(rr,a.document,name)]
res += wid
link = 'https://rowsandall.com/rowers/workout/'+str(wid[0])+'/edit'
if wid != 1:
dd = send_confirm(u,name,link)
except:
# replace with code to process error
res += ['fail: '+name]
donotdelete = 1
except Rower.DoesNotExist:
pass
# remove attachment
if donotdelete == 0:
a.delete()
if m.attachments.exists()==False:
# 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:
m.delete()
return res
# As above, but with some print commands for debugging purposes
def processattachments_debug():
res = []
attachments = MessageAttachment.objects.all()
for a in attachments:
donotdelete = 1
m = Message.objects.get(id=a.message_id)
from_address = m.from_address[0]
name = m.subject
# get a list of users
theusers = User.objects.filter(email=from_address)
print theusers
for u in theusers:
try:
rr = Rower.objects.get(user=u.id)
doorgaan = 1
except:
doorgaan = 0
if doorgaan:
# move attachment and make workout
print a.document
print name
wid = [make_new_workout_from_email(rr,a.document,name)]
res += wid
link = 'https://rowsandall.com/rowers/workout/'+str(wid[0])+'/edit'
if wid != 1:
dd = send_confirm(u,name,link)
return result
# remove attachment
if donotdelete == 0:
a.delete()
if m.attachments.exists()==False:
# no attachments, so can be deleted
m.delete()
mm = Message.objects.all()
for m in mm:
if m.attachments.exists()==False:
m.delete()
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):
def make_new_workout_from_email(rower, datafile, name, cntr=0):
""" This one is used in processemail """
workouttype = 'rower'
try:
f2 = f2.name
fileformat = get_file_type('media/'+f2)
datafilename = datafile.name
fileformat = get_file_type('media/' + datafilename)
except IOError:
f2 = f2.name+'.gz'
fileformat = get_file_type('media/'+f2)
datafilename = datafile.name + '.gz'
fileformat = get_file_type('media/' + datafilename)
except AttributeError:
fileformat = get_file_type('media/'+f2)
datafilename = datafile
fileformat = get_file_type('media/' + datafile)
if len(fileformat)==3 and fileformat[0]=='zip':
f_to_be_deleted = f2
with zipfile.ZipFile('media/'+f2) as z:
f2 = z.extract(z.namelist()[0],path='media/')[6:]
if len(fileformat) == 3 and fileformat[0] == 'zip':
with zipfile.ZipFile('media/' + datafilename) as zip_file:
datafilename = zip_file.extract(
zip_file.namelist()[0],
path='media/')[6:]
fileformat = fileformat[2]
if fileformat == 'unknown':
if settings.DEBUG:
res = handle_sendemail_unrecognized.delay(f2,
"roosendaalsander@gmail.com")
else:
res = queuehigh.enqueue(handle_sendemail_unrecognized,
f2,"roosendaalsander@gmail.com")
return 1
# extension = datafilename[-4:].lower()
# fcopy = "media/"+datafilename[:-4]+"_copy"+extension
# with open('media/'+datafilename, 'r') as f_in, open(fcopy, 'w') as f_out:
# shutil.copyfileobj(f_in,f_out)
fcopy = "media/"+datafilename
if settings.DEBUG:
res = handle_sendemail_unrecognized.delay(
fcopy,
rower.user.email
)
res = handle_sendemail_unrecognizedowner.delay(
rower.user.email,
rower.user.first_name
)
else:
res = queuehigh.enqueue(handle_sendemail_unrecognized,
fcopy,
rower.user.email)
res = queuehigh.enqueue(handle_sendemail_unrecognizedowner,
rower.user.email,
rower.user.first_name
)
return 0
summary = ''
# handle non-Painsled
if fileformat != 'csv':
f3,summary,oarlength,inboard = dataprep.handle_nonpainsled('media/'+f2,fileformat,summary)
filename_mediadir, summary, oarlength, inboard = dataprep.handle_nonpainsled(
'media/' + datafilename, fileformat, summary)
else:
f3 = 'media/'+f2
filename_mediadir = 'media/' + datafilename
inboard = 0.88
oarlength = 2.89
# make workout and put in database
#r = rrower(hrmax=rr.max,hrut2=rr.ut2,
# hrut1=rr.ut1,hrat=rr.at,
# hrtr=rr.tr,hran=rr.an,ftp=r.ftp)
row = rdata(f3) #,rower=r)
row = rdata(filename_mediadir)
if row == 0:
return 0
return 0
# change filename
if f2[:5] != 'media':
timestr = time.strftime("%Y%m%d-%H%M%S")
f2 = 'media/'+timestr+str(cntr)+'o.csv'
if datafilename[:5] != 'media':
timestr = time.strftime("%Y%m%d-%H%M%S")
datafilename = 'media/' + timestr + str(cntr) + 'o.csv'
try:
avglat = row.df[' latitude'].mean()
avglon = row.df[' longitude'].mean()
avglat = row.df[' latitude'].mean()
avglon = row.df[' longitude'].mean()
if avglat != 0 or avglon != 0:
workouttype = 'water'
except KeyError:
pass
row.write_csv(f2,gzip=True)
row.write_csv(datafilename, gzip=True)
dosummary = (fileformat != 'fit')
if name == '':
name = 'imported through email'
id,message = dataprep.save_workout_database(f2,rr,
workouttype=workouttype,
dosummary=dosummary,
inboard=inboard,
oarlength=oarlength,
title=name,
workoutsource=fileformat,
notes='imported through email')
name = 'imported through email'
id, message = dataprep.save_workout_database(
datafilename, rower,
workouttype=workouttype,
dosummary=dosummary,
inboard=inboard,
oarlength=oarlength,
title=name,
workoutsource=fileformat,
notes='imported through email'
)
return id
+118 -132
View File
@@ -1,162 +1,148 @@
#!/srv/venv/bin/python
""" Process emails """
import sys
import os
import zipfile
import time
from time import strftime
from django.core.management.base import BaseCommand
from django_mailbox.models import Message, MessageAttachment
from django.core.urlresolvers import reverse
from django.conf import settings
from rowers.models import Workout, Rower
from rowingdata import rower as rrower
from rowingdata import rowingdata as rrdata
import rowers.uploads as uploads
from rowers.mailprocessing import make_new_workout_from_email, send_confirm
# If you find a solution that does not need the two paths, please comment!
sys.path.append('$path_to_root_of_project$')
sys.path.append('$path_to_root_of_project$/$project_name$')
os.environ['DJANGO_SETTINGS_MODULE'] = '$project_name$.settings'
import zipfile
if not getattr(__builtins__, "WindowsError", None):
class WindowsError(OSError): pass
from django.core.management.base import BaseCommand, CommandError
from django.conf import settings
#from rowers.mailprocessing import processattachments
import time
from time import strftime
from django.conf import settings
from rowers.tasks import handle_sendemail_unrecognized
from django_mailbox.models import Mailbox,Message,MessageAttachment
from rowers.models import Workout, User, Rower, WorkoutForm,RowerForm,GraphImage,AdvancedWorkoutForm
from django.core.files.base import ContentFile
from rowsandall_app.settings import BASE_DIR
from rowingdata import rower as rrower
from rowingdata import main as rmain
from rowingdata import rowingdata as rrdata
from rowingdata import make_cumvalues
from rowingdata import summarydata,get_file_type
from scipy.signal import savgol_filter
from rowers.mailprocessing import make_new_workout_from_email,send_confirm
import rowers.uploads as uploads
def rdata(file,rower=rrower()):
def rdata(file_obj, rower=rrower()):
""" Read rowing data file and return 0 if file doesn't exist"""
try:
res = rrdata(file,rower=rower)
result = rrdata(file_obj, rower=rower)
except IOError:
res = 0
result = 0
return res
return result
def processattachment(rower, fileobj, title, uploadoptions):
try:
filename = fileobj.name
except AttributeError:
filename = fileobj[6:]
workoutid = [
make_new_workout_from_email(rower, filename, title)
]
if workoutid[0]:
link = settings.SITE_URL+reverse(
rower.defaultlandingpage,
kwargs = {
'id':workoutid[0],
}
)
if uploadoptions and not 'error' in uploadoptions:
workout = Workout.objects.get(id=workoutid[0])
uploads.do_sync(workout, uploadoptions)
uploads.make_private(workout, uploadoptions)
if 'make_plot' in uploadoptions:
plottype = uploadoptions['plottype']
workoutcsvfilename = workout.csvfilename[6:-4]
timestr = strftime("%Y%m%d-%H%M%S")
imagename = workoutcsvfilename + timestr + '.png'
result = uploads.make_plot(
workout.user, workout, workoutcsvfilename,
workout.csvfilename,
plottype, title,
imagename=imagename
)
try:
if workoutid:
email_sent = send_confirm(
rower.user, title, link,
uploadoptions
)
time.sleep(10)
except:
try:
time.sleep(10)
if workoutid:
email_sent = send_confirm(
rower.user, title, link,
uploadoptions
)
except:
pass
return workoutid
class Command(BaseCommand):
"""Run the Email processing command """
def handle(self, *args, **options):
res = []
attachments = MessageAttachment.objects.all()
cntr = 0
for a in attachments:
extension = a.document.name[-3:].lower()
donotdelete = 0
m = Message.objects.get(id=a.message_id)
body = "\n".join(m.text.splitlines())
attachments = MessageAttachment.objects.all()
cntr = 0
for attachment in attachments:
extension = attachment.document.name[-3:].lower()
message = Message.objects.get(id=attachment.message_id)
body = "\n".join(message.text.splitlines())
uploadoptions = uploads.upload_options(body)
from_address = m.from_address[0].lower()
name = m.subject
cntr += 1
# get a list of users
# theusers = User.objects.filter(email=from_address)
ther = [
from_address = message.from_address[0].lower()
name = message.subject
# get a list of users
# theusers = User.objects.filter(email=from_address)
rowers = [
r for r in Rower.objects.all() if r.user.email.lower() == from_address
]
for rr in ther:
for rower in rowers:
if extension == 'zip':
z = zipfile.ZipFile(a.document)
for f in z.namelist():
f2 = z.extract(f,path='media/')
title = os.path.basename(f2)
wid = [
make_new_workout_from_email(rr,f2[6:],title)
]
res += wid
link = 'http://rowsandall.com/rowers/workout/'+str(wid[0])+'/edit'
if uploadoptions and not 'error' in uploadoptions:
w = Workout.objects.get(id=wid[0])
r = w.user
uploads.do_sync(w,uploadoptions)
uploads.make_private(w,uploadoptions)
if 'make_plot' in uploadoptions:
plottype = uploadoptions['plottype']
f1 = w.csvfilename[6:-4]
timestr = strftime("%Y%m%d-%H%M%S")
imagename = f1+timestr+'.png'
resu = uploads.make_plot(r,w,f1,
w.csvfilename,
plottype,name,
imagename=imagename)
try:
if wid != 1:
dd = send_confirm(rr.user,title,link,
uploadoptions)
time.sleep(10)
except:
try:
time.sleep(10)
if wid != 1:
dd = send_confirm(rr.user,title,link,
uploadoptions)
except:
pass
zip_file = zipfile.ZipFile(attachment.document)
for filename in zip_file.namelist():
datafile = zip_file.extract(filename, path='media/')
title = os.path.basename(datafile)
workoutid = processattachment(
rower, datafile, title, uploadoptions
)
else:
# move attachment and make workout
try:
wid = [
make_new_workout_from_email(rr,
a.document,
name)
]
res += wid
link = 'http://rowsandall.com/rowers/workout/'+str(wid[0])+'/edit'
if uploadoptions:
w = Workout.objects.get(id=wid[0])
r = w.user
uploads.do_sync(w,uploadoptions)
uploads.make_private(w,uploadoptions)
if 'make_plot' in uploadoptions:
plottype = uploadoptions['plottype']
f1 = w.csvfilename[6:-4]
timestr = strftime("%Y%m%d-%H%M%S")
imagename = f1+timestr+'.png'
resu = uploads.make_plot(r,w,f1,
w.csvfilename,
plottype,name,
imagename=imagename)
except:
# replace with code to process error
res += ['fail: '+name]
donotdelete = 1
wid = 1
try:
if wid != 1:
dd = send_confirm(rr.user,name,link,
uploadoptions)
time.sleep(10)
except:
pass
# move attachment and make workout
workoutid = processattachment(
rower, attachment.document, name, uploadoptions
)
# We're done with the attachment. It can be deleted
try:
a.delete()
attachment.delete()
except IOError:
pass
# remove attachment
#if donotdelete == 0:
if m.attachments.exists()==False:
# no attachments, so can be deleted
m.delete()
mm = Message.objects.all()
for m in mm:
if m.attachments.exists()==False:
m.delete()
self.stdout.write(self.style.SUCCESS('Successfully processed email attachments'))
except WindowsError:
time.sleep(2)
try:
attachment.delete()
except WindowsError:
pass
if message.attachments.exists() is False:
# no attachments, so can be deleted
message.delete()
messages = Message.objects.all()
for message in messages:
if message.attachments.exists() is False:
message.delete()
self.stdout.write(self.style.SUCCESS(
'Successfully processed email attachments'))
+2 -2
View File
@@ -269,7 +269,7 @@ in case you recorded your heart rate during your workout""",
heart rate versus time. """,
},
{
'yparam1':'strokeenergy',
'yparam1':'driveenergy',
'yparam2':'hr',
'xparam':'time',
'plottype':'line',
@@ -291,7 +291,7 @@ as you increase stroke rate. Typical values are > 10m for steady state
dropping to 8m for race pace in the single.""",
},
{
'yparam1':'strokeenergy',
'yparam1':'driveenergy',
'yparam2':'None',
'xparam':'spm',
'plottype':'line',
+7 -3
View File
@@ -198,7 +198,7 @@ class TeamRequest(models.Model):
from utils import (
workflowleftpanel,workflowmiddlepanel,
defaultleft,defaultmiddle
defaultleft,defaultmiddle,landingpages
)
# Extension of User with rowing specific data
@@ -281,8 +281,11 @@ class Rower(models.Model):
# Site Settings
workflowleftpanel = TemplateListField(default=defaultleft)
workflowmiddlepanel = TemplateListField(default=defaultmiddle)
defaultlandingpage = models.CharField(default='workout_edit_view',
max_length=200,
choices=landingpages,
verbose_name="Default Landing Page")
# Access tokens
c2token = models.CharField(default='',max_length=200,blank=True,null=True)
@@ -889,7 +892,8 @@ class AccountRowerForm(ModelForm):
class Meta:
model = Rower
fields = ['weightcategory','getemailnotifications',
'defaulttimezone','showfavoritechartnotes']
'defaulttimezone','showfavoritechartnotes',
'defaultlandingpage']
class UserForm(ModelForm):
class Meta:
+267 -222
View File
@@ -1,4 +1,4 @@
from celery import Celery,app
""" Background tasks done by Celery (develop) or QR (production) """
import os
import time
import gc
@@ -7,67 +7,73 @@ import shutil
import numpy as np
import rowingdata
from rowingdata import main as rmain
from rowingdata import rowingdata as rdata
import rowingdata
from async_messages import message_user,messages
from celery import app
from matplotlib.backends.backend_agg import FigureCanvas
#from matplotlib.backends.backend_cairo import FigureCanvasCairo as FigureCanvas
import matplotlib.pyplot as plt
from matplotlib import figure
import stravalib
import pandas as pd
from django_rq import job
from utils import serialize_list,deserialize_list
from utils import deserialize_list
from rowers.dataprepnodjango import (
update_strokedata, new_workout_from_file,
getsmallrowdata_db,read_df_sql,columndict,
getsmallrowdata_db,
)
from django.core.mail import send_mail, BadHeaderError,EmailMessage
from django.core.mail import send_mail, EmailMessage
import datautils
import utils
# testing task
@app.task
def add(x, y):
return x + y
# create workout
@app.task
def handle_new_workout_from_file(r,f2,
workouttype='rower',
title='Workout',
makeprivate=False,
notes=''):
return new_workout_from_file(r,f2,workouttype,
title,makeprivate,notes)
def handle_new_workout_from_file(r, f2,
workouttype='rower',
title='Workout',
makeprivate=False,
notes=''):
return new_workout_from_file(r, f2, workouttype,
title, makeprivate, notes)
# process and update workouts
@app.task
def handle_updatedps(useremail,workoutids,debug=False):
for wid,f1 in workoutids:
def handle_updatedps(useremail, workoutids, debug=False):
for wid, f1 in workoutids:
havedata = 1
try:
rowdata = rdata(f1)
except IOError:
try:
rowdata = rdata(f1+'.csv')
rowdata = rdata(f1 + '.csv')
except IOError:
try:
rowdata = rdata(f1+'.gz')
rowdata = rdata(f1 + '.gz')
except IOError:
havedata = 0
if havedata:
update_strokedata(wid,rowdata.df,debug=debug)
update_strokedata(wid, rowdata.df, debug=debug)
subject = "Rowsandall.com Your Distance per Stroke metric has been updated"
message = "All your workouts now have Distance per Stroke"
@@ -81,14 +87,16 @@ def handle_updatedps(useremail,workoutids,debug=False):
return 1
# send email when a breakthrough workout is uploaded
@app.task
def handle_sendemail_breakthrough(workoutid,useremail,
userfirstname,userlastname,
btvalues = pd.DataFrame().to_json()):
def handle_sendemail_breakthrough(workoutid, useremail,
userfirstname, userlastname,
btvalues=pd.DataFrame().to_json()):
# send email with attachment
subject = "A breakthrough workout on rowsandall.com"
message = "Dear "+userfirstname+",\n"
message = "Dear " + userfirstname + ",\n"
message += "Congratulations! Your recent workout has been analyzed"
message += " by Rowsandall.com and it appears your fitness,"
message += " as measured by Critical Power, has improved!"
@@ -98,14 +106,14 @@ def handle_sendemail_breakthrough(workoutid,useremail,
message += " http://analytics.rowsandall.com/2017/06/17/how-do-we-calculate-critical-power/ \n\n"
message += "Link to the workout http://rowsandall.com/rowers/workout/"
message += str(workoutid)
message +="/edit\n\n"
message +="To add the workout to your Ranking workouts and see the updated CP plot, click the following link:\n"
message += "/edit\n\n"
message += "To add the workout to your Ranking workouts and see the updated CP plot, click the following link:\n"
message += "http://rowsandall.com/rowers/workout/"
message += str(workoutid)
message += "/updatecp\n\n"
btvalues = pd.read_json(btvalues)
btvalues.sort_values('delta',axis=0,inplace=True)
btvalues.sort_values('delta', axis=0, inplace=True)
if not btvalues.empty:
message += "These were the breakthrough values:\n"
@@ -116,23 +124,21 @@ def handle_sendemail_breakthrough(workoutid,useremail,
message += "Time: {delta} seconds\n".format(
delta=delta
)
)
message += "New: {cpvalue:.0f} Watt\n".format(
cpvalue=cpvalue
)
)
message += "Old: {pwr:.0f} Watt\n\n".format(
pwr=pwr
)
)
message += "To opt out of these email notifications, deselect the checkbox on your Profile page under Account Information.\n\n"
message += "Best Regards, the Rowsandall Team"
email = EmailMessage(subject, message,
'Rowsandall <info@rowsandall.com>',
[useremail])
'Rowsandall <info@rowsandall.com>',
[useremail])
res = email.send()
@@ -140,14 +146,16 @@ def handle_sendemail_breakthrough(workoutid,useremail,
return 1
# send email when a breakthrough workout is uploaded
@app.task
def handle_sendemail_hard(workoutid,useremail,
userfirstname,userlastname,
btvalues = pd.DataFrame().to_json()):
def handle_sendemail_hard(workoutid, useremail,
userfirstname, userlastname,
btvalues=pd.DataFrame().to_json()):
# send email with attachment
subject = "That was a pretty hard workout on rowsandall.com"
message = "Dear "+userfirstname+",\n"
message = "Dear " + userfirstname + ",\n"
message += "Congratulations! Your recent workout has been analyzed"
message += " by Rowsandall.com and it appears that it was pretty hard work."
message += " You were working pretty close to your Critical Power\n\n"
@@ -157,16 +165,15 @@ def handle_sendemail_hard(workoutid,useremail,
message += " http://analytics.rowsandall.com/2017/06/17/how-do-we-calculate-critical-power/ \n\n"
message += "Link to the workout http://rowsandall.com/rowers/workout/"
message += str(workoutid)
message +="/edit\n\n"
message += "/edit\n\n"
message += "To opt out of these email notifications, deselect the checkbox on your Profile page under Account Information.\n\n"
message += "Best Regards, the Rowsandall Team"
email = EmailMessage(subject, message,
'Rowsandall <info@rowsandall.com>',
[useremail])
'Rowsandall <info@rowsandall.com>',
[useremail])
res = email.send()
@@ -176,19 +183,19 @@ def handle_sendemail_hard(workoutid,useremail,
# send email to me when an unrecognized file is uploaded
@app.task
def handle_sendemail_unrecognized(unrecognizedfile,useremail):
def handle_sendemail_unrecognized(unrecognizedfile, useremail):
# send email with attachment
fullemail = 'roosendaalsander@gmail.com'
subject = "Unrecognized file from Rowsandall.com"
message = "Dear Sander,\n\n"
message += "Please find attached a file that someone tried to upload to rowsandall.com. The file was not recognized as a valid file type.\n\n"
message += "User Email "+useremail+"\n\n"
message += "User Email " + useremail + "\n\n"
message += "Best Regards, the Rowsandall Team"
email = EmailMessage(subject, message,
'Rowsandall <info@rowsandall.com>',
[fullemail])
'Rowsandall <info@rowsandall.com>',
[fullemail])
try:
email.attach_file(unrecognizedfile)
@@ -206,21 +213,51 @@ def handle_sendemail_unrecognized(unrecognizedfile,useremail):
return 1
# send email to owner when an unrecognized file is uploaded
@app.task
def handle_sendemail_unrecognizedowner(useremail, userfirstname):
# send email with attachment
fullemail = useremail
subject = "Unrecognized file from Rowsandall.com"
message = "Dear " + userfirstname + ",\n\n"
message += """
The file you tried to send to rowsandall.com was not recognized by
our email processing system. You may have sent a file in a format
that is not supported. Sometimes, rowing apps make file format changes.
When that happens, it takes some time for rowsandall.comm to make
the necessary changes on our side and support the app again.
The file has been sent to the developer at rowsandall.com for evaluation.
"""
message += "Best Regards, the Rowsandall Team"
email = EmailMessage(subject, message,
'Rowsandall <info@rowsandall.com>',
[fullemail])
res = email.send()
return 1
# Send email with TCX attachment
@app.task
def handle_sendemailtcx(first_name,last_name,email,tcxfile):
def handle_sendemailtcx(first_name, last_name, email, tcxfile):
# send email with attachment
fullemail = first_name + " " + last_name + " " + "<" + email + ">"
subject = "File from Rowsandall.com"
message = "Dear "+first_name+",\n\n"
message = "Dear " + first_name + ",\n\n"
message += "Please find attached the requested file for your workout.\n\n"
message += "Best Regards, the Rowsandall Team"
email = EmailMessage(subject, message,
'Rowsandall <info@rowsandall.com>',
[fullemail])
'Rowsandall <info@rowsandall.com>',
[fullemail])
email.attach_file(tcxfile)
@@ -230,10 +267,11 @@ def handle_sendemailtcx(first_name,last_name,email,tcxfile):
os.remove(tcxfile)
return 1
@app.task
def handle_zip_file(emailfrom,subject,file):
def handle_zip_file(emailfrom, subject, file):
message = "... zip processing ... "
email = EmailMessage(subject,message,
email = EmailMessage(subject, message,
emailfrom,
['workouts@rowsandall.com'])
email.attach_file(file)
@@ -242,26 +280,27 @@ def handle_zip_file(emailfrom,subject,file):
return 1
# Send email with CSV attachment
@app.task
def handle_sendemailcsv(first_name,last_name,email,csvfile):
def handle_sendemailcsv(first_name, last_name, email, csvfile):
# send email with attachment
fullemail = first_name + " " + last_name + " " + "<" + email + ">"
subject = "File from Rowsandall.com"
message = "Dear "+first_name+",\n\n"
message = "Dear " + first_name + ",\n\n"
message += "Please find attached the requested file for your workout.\n\n"
message += "Best Regards, the Rowsandall Team"
email = EmailMessage(subject, message,
'Rowsandall <info@rowsandall.com>',
[fullemail])
'Rowsandall <info@rowsandall.com>',
[fullemail])
if os.path.isfile(csvfile):
email.attach_file(csvfile)
else:
csvfile2 = csvfile
with gzip.open(csvfile+'.gz','rb') as f_in, open(csvfile2,'wb') as f_out:
with gzip.open(csvfile + '.gz', 'rb') as f_in, open(csvfile2, 'wb') as f_out:
shutil.copyfileobj(f_in, f_out)
email.attach_file(csvfile2)
@@ -272,34 +311,37 @@ 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,ps=[1,1,1,1],
def handle_otwsetpower(f1, boattype, weightvalue,
first_name, last_name, email, workoutid, ps=[
1, 1, 1, 1],
ratio=1.0,
debug=False):
try:
rowdata = rdata(f1)
except IOError:
try:
rowdata = rdata(f1+'.csv')
rowdata = rdata(f1 + '.csv')
except IOError:
rowdata = rdata(f1+'.gz')
rowdata = rdata(f1 + '.gz')
weightvalue = float(weightvalue)
# do something with boat type
boatfile = {
'1x':'static/rigging/1x.txt',
'2x':'static/rigging/2x.txt',
'2-':'static/rigging/2-.txt',
'4x':'static/rigging/4x.txt',
'4-':'static/rigging/4-.txt',
'8+':'static/rigging/8+.txt',
}
'1x': 'static/rigging/1x.txt',
'2x': 'static/rigging/2x.txt',
'2-': 'static/rigging/2-.txt',
'4x': 'static/rigging/4x.txt',
'4-': 'static/rigging/4-.txt',
'8+': 'static/rigging/8+.txt',
}
try:
rg = rowingdata.getrigging(boatfile[boattype])
rg = rowingdata.getrigging(boatfile[boattype])
except KeyError:
rg = rowingdata.getrigging('static/rigging/1x.txt')
rg = rowingdata.getrigging('static/rigging/1x.txt')
# do calculation, but do not overwrite NK Empower Power data
powermeasured = False
@@ -310,117 +352,119 @@ def handle_otwsetpower(f1,boattype,weightvalue,
except KeyError:
pass
rowdata.otw_setpower_silent(skiprows=5,mc=weightvalue,rg=rg,
rowdata.otw_setpower_silent(skiprows=5, mc=weightvalue, rg=rg,
powermeasured=powermeasured)
# save data
rowdata.write_csv(f1,gzip=True)
update_strokedata(workoutid,rowdata.df,debug=debug)
rowdata.write_csv(f1, gzip=True)
update_strokedata(workoutid, rowdata.df, debug=debug)
totaltime = rowdata.df['TimeStamp (sec)'].max()-rowdata.df['TimeStamp (sec)'].min()
totaltime = rowdata.df['TimeStamp (sec)'].max(
) - rowdata.df['TimeStamp (sec)'].min()
try:
totaltime = totaltime+rowdata.df.ix[0,' ElapsedTime (sec)']
totaltime = totaltime + rowdata.df.ix[0, ' ElapsedTime (sec)']
except KeyError:
pass
df = getsmallrowdata_db(['power','workoutid','time'],ids=[workoutid])
df = getsmallrowdata_db(['power', 'workoutid', 'time'], ids=[workoutid])
thesecs = totaltime
maxt = 1.05*thesecs
maxt = 1.05 * thesecs
logarr = datautils.getlogarr(maxt)
dfgrouped = df.groupby(['workoutid'])
delta,cpvalues,avgpower = datautils.getcp(dfgrouped,logarr)
delta, cpvalues, avgpower = datautils.getcp(dfgrouped, logarr)
#delta,cpvalues,avgpower = datautils.getsinglecp(rowdata.df)
res,btvalues,res2 = utils.isbreakthrough(delta,cpvalues,ps[0],ps[1],ps[2],ps[3],ratio)
res, btvalues, res2 = utils.isbreakthrough(
delta, cpvalues, ps[0], ps[1], ps[2], ps[3], ratio)
if res:
handle_sendemail_breakthrough(workoutid,email,
handle_sendemail_breakthrough(workoutid, email,
first_name,
last_name,btvalues=btvalues.to_json())
last_name, btvalues=btvalues.to_json())
# send email
fullemail = first_name + " " + last_name + " " + "<" + email + ">"
subject = "Your Rowsandall OTW calculations are ready"
message = "Dear "+first_name+",\n\n"
message = "Dear " + first_name + ",\n\n"
message += "Your Rowsandall OTW calculations are ready.\n"
message += "Thank you for using rowsandall.com.\n\n"
message += "Rowsandall OTW calculations have not been fully implemented yet.\n"
message += "We are now running an experimental version for debugging purposes. \n"
message += "Your wind/stream corrected plot is available here: http://rowsandall.com/rowers/workout/"
message += str(workoutid)
message +="/interactiveotwplot\n\n"
message += "/interactiveotwplot\n\n"
message += "Please report any bugs/inconsistencies/unexpected results at rowsandall.slack.com or by reply to this email.\n\n"
message += "Best Regards, The Rowsandall Physics Department."
send_mail(subject, message,
'Rowsandall Physics Department <info@rowsandall.com>',
[fullemail])
'Rowsandall Physics Department <info@rowsandall.com>',
[fullemail])
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']
hrut2 = hrdata['hrut2']
hrut1 = hrdata['hrut1']
hrat = hrdata['hrat']
hrtr = hrdata['hrtr']
hran = hrdata['hran']
@app.task
def handle_makeplot(f1, f2, t, hrdata, plotnr, imagename):
hrmax = hrdata['hrmax']
hrut2 = hrdata['hrut2']
hrut1 = hrdata['hrut1']
hrat = hrdata['hrat']
hrtr = hrdata['hrtr']
hran = hrdata['hran']
ftp = hrdata['ftp']
powerzones = deserialize_list(hrdata['powerzones'])
powerperc = np.array(deserialize_list(hrdata['powerperc'])).astype(int)
rr = rowingdata.rower(hrmax=hrmax,hrut2=hrut2,
hrut1=hrut1,hrat=hrat,
hrtr=hrtr,hran=hran,
ftp=ftp,powerperc=powerperc,
rr = rowingdata.rower(hrmax=hrmax, hrut2=hrut2,
hrut1=hrut1, hrat=hrat,
hrtr=hrtr, hran=hran,
ftp=ftp, powerperc=powerperc,
powerzones=powerzones)
try:
row = rdata(f2,rower=rr)
row = rdata(f2, rower=rr)
except IOError:
row = rdata(f2+'.gz',rower=rr)
row = rdata(f2 + '.gz', rower=rr)
haspower = row.df[' Power (watts)'].mean() > 50
nr_rows = len(row.df)
if (plotnr in [1,2,4,5,8,11,9,12]) and (nr_rows > 1200):
bin = int(nr_rows/1200.)
df = row.df.groupby(lambda x:x/bin).mean()
row.df = df
if (plotnr in [1, 2, 4, 5, 8, 11, 9, 12]) and (nr_rows > 1200):
bin = int(nr_rows / 1200.)
df = row.df.groupby(lambda x: x / bin).mean()
row.df = df
nr_rows = len(row.df)
if (plotnr==1):
fig1 = row.get_timeplot_erg(t)
elif (plotnr==2):
fig1 = row.get_metersplot_erg(t)
elif (plotnr==3):
fig1 = row.get_piechart(t)
elif (plotnr==4):
if (plotnr == 1):
fig1 = row.get_timeplot_erg(t)
elif (plotnr == 2):
fig1 = row.get_metersplot_erg(t)
elif (plotnr == 3):
fig1 = row.get_piechart(t)
elif (plotnr == 4):
if haspower:
fig1 = row.get_timeplot_otwempower(t)
else:
fig1 = row.get_timeplot_otw(t)
elif (plotnr==5):
fig1 = row.get_timeplot_otw(t)
elif (plotnr == 5):
if haspower:
fig1 = row.get_metersplot_otwempower(t)
else:
fig1 = row.get_metersplot_otw(t)
elif (plotnr==6):
fig1 = row.get_piechart(t)
elif (plotnr==7) or (plotnr==10):
fig1 = row.get_metersplot_erg2(t)
elif (plotnr==8) or (plotnr==11):
fig1 = row.get_timeplot_erg2(t)
elif (plotnr==9) or (plotnr==12):
fig1 = row.get_time_otwpower(t)
elif (plotnr==13) or (plotnr==16):
fig1 = row.get_power_piechart(t)
fig1 = row.get_metersplot_otw(t)
elif (plotnr == 6):
fig1 = row.get_piechart(t)
elif (plotnr == 7) or (plotnr == 10):
fig1 = row.get_metersplot_erg2(t)
elif (plotnr == 8) or (plotnr == 11):
fig1 = row.get_timeplot_erg2(t)
elif (plotnr == 9) or (plotnr == 12):
fig1 = row.get_time_otwpower(t)
elif (plotnr == 13) or (plotnr == 16):
fig1 = row.get_power_piechart(t)
canvas = FigureCanvas(fig1)
# plt.savefig('static/plots/'+imagename,format='png')
canvas.print_figure('static/plots/'+imagename)
canvas.print_figure('static/plots/' + imagename)
# plt.imsave(fname='static/plots/'+imagename)
plt.close(fig1)
fig1.clf()
@@ -430,12 +474,13 @@ def handle_makeplot(f1,f2,t,hrdata,plotnr,imagename):
# Team related remote tasks
@app.task
def handle_sendemail_invite(email,name,code,teamname,manager):
fullemail = name+' <'+email+'>'
subject = 'Invitation to join team '+teamname
message = 'Dear '+name+',\n\n'
message += manager+' is inviting you to join his team '+teamname
def handle_sendemail_invite(email, name, code, teamname, manager):
fullemail = name + ' <' + email + '>'
subject = 'Invitation to join team ' + teamname
message = 'Dear ' + name + ',\n\n'
message += manager + ' is inviting you to join his team ' + teamname
message += ' on rowsandall.com\n\n'
message += 'By accepting the invite, you will have access to your'
message += " team's workouts on rowsandall.com and your workouts will "
@@ -444,82 +489,84 @@ def handle_sendemail_invite(email,name,code,teamname,manager):
message += 'If you already have an account on rowsandall.com, you can login to the site and you will find the invitation here on the Teams page:\n'
message += ' https://rowsandall.com/rowers/me/teams \n\n'
message += 'You can also click the direct link: \n'
message += 'https://rowsandall.com/rowers/me/invitation/'+code+' \n\n'
message += 'https://rowsandall.com/rowers/me/invitation/' + code + ' \n\n'
message += 'If you are not yet registered to rowsandall.com, '
message += 'you can register for free at https://rowsandall.com/rowers/register\n'
message += 'After you set up your account, you can use the direct link: '
message += 'https://rowsandall.com/rowers/me/invitation/'+code+' \n\n'
message += 'https://rowsandall.com/rowers/me/invitation/' + code + ' \n\n'
message += 'You can also manually accept your team membership with the code.\n'
message += 'You will need to do this if you registered under a different email address than this one.\n'
message += 'Code: '+code+'\n'
message += 'Code: ' + code + '\n'
message += 'Link to manually accept your team membership: '
message += 'https://rowsandall.com/rowers/me/invitation\n\n'
message += "Best Regards, the Rowsandall Team"
email = EmailMessage(subject, message,
'Rowsandall <info@rowsandall.com>',
[fullemail])
'Rowsandall <info@rowsandall.com>',
[fullemail])
res = email.send()
return 1
@app.task
def handle_sendemailnewresponse(first_name,last_name,
def handle_sendemailnewresponse(first_name, last_name,
email,
commenter_first_name,
commenter_last_name,
comment,
workoutname,workoutid,commentid):
fullemail = first_name+' '+last_name+' <'+email+'>'
subject = 'New comment on workout '+workoutname
message = 'Dear '+first_name+',\n\n'
message += commenter_first_name+' '+commenter_last_name
workoutname, workoutid, commentid):
fullemail = first_name + ' ' + last_name + ' <' + email + '>'
subject = 'New comment on workout ' + workoutname
message = 'Dear ' + first_name + ',\n\n'
message += commenter_first_name + ' ' + commenter_last_name
message += ' has written a new comment on the workout '
message += workoutname+'\n\n'
message += workoutname + '\n\n'
message += comment
message += '\n\n'
message += 'You can read the comment here:\n'
message += 'https://rowsandall.com/rowers/workout/'+str(workoutid)+'/comment'
message += 'https://rowsandall.com/rowers/workout/' + \
str(workoutid) + '/comment'
message += '\n\n'
message += 'You are receiving this email because you are subscribed '
message += 'to comments on this workout. To unsubscribe, follow this link:\n'
message += 'https://rowsandall.com/rowers/workout/'+str(workoutid)+'/unsubscribe'
message += 'https://rowsandall.com/rowers/workout/' + \
str(workoutid) + '/unsubscribe'
email = EmailMessage(subject, message,
'Rowsandall <info@rowsandall.com>',
[fullemail])
'Rowsandall <info@rowsandall.com>',
[fullemail])
res = email.send()
return 1
@app.task
def handle_sendemailnewcomment(first_name,
last_name,
email,
last_name,
email,
commenter_first_name,
commenter_last_name,
comment,workoutname,
comment, workoutname,
workoutid):
fullemail = first_name+' '+last_name+' <'+email+'>'
subject = 'New comment on workout '+workoutname
message = 'Dear '+first_name+',\n\n'
message += commenter_first_name+' '+commenter_last_name
fullemail = first_name + ' ' + last_name + ' <' + email + '>'
subject = 'New comment on workout ' + workoutname
message = 'Dear ' + first_name + ',\n\n'
message += commenter_first_name + ' ' + commenter_last_name
message += ' has written a new comment on your workout '
message += workoutname+'\n\n'
message += workoutname + '\n\n'
message += comment
message += '\n\n'
message += 'You can read the comment here:\n'
message += 'https://rowsandall.com/rowers/workout/'+str(workoutid)+'/comment'
message += 'https://rowsandall.com/rowers/workout/' + \
str(workoutid) + '/comment'
email = EmailMessage(subject, message,
'Rowsandall <info@rowsandall.com>',
[fullemail])
'Rowsandall <info@rowsandall.com>',
[fullemail])
res = email.send()
@@ -527,34 +574,34 @@ def handle_sendemailnewcomment(first_name,
@app.task
def handle_sendemail_request(email,name,code,teamname,requestor,id):
fullemail = name+' <'+email+'>'
subject = 'Request to join team '+teamname
message = 'Dear '+name+',\n\n'
message += requestor+' is requesting admission to your team '+teamname
def handle_sendemail_request(email, name, code, teamname, requestor, id):
fullemail = name + ' <' + email + '>'
subject = 'Request to join team ' + teamname
message = 'Dear ' + name + ',\n\n'
message += requestor + ' is requesting admission to your team ' + teamname
message += ' on rowsandall.com\n\n'
message += 'Click the direct link to accept: \n'
message += 'https://rowsandall.com/rowers/me/request/'+code+' \n\n'
message += 'https://rowsandall.com/rowers/me/request/' + code + ' \n\n'
message += 'Click the following link to reject the request: \n'
message += 'https://rowsandall.com/rowers/me/request/'+str(id)+' \n\n'
message += 'https://rowsandall.com/rowers/me/request/' + str(id) + ' \n\n'
message += 'You can find all pending requests on your team management page:\n'
message += 'https://rowsandall.com/rowers/me/teams\n\n'
message += "Best Regards, the Rowsandall Team"
email = EmailMessage(subject, message,
'Rowsandall <info@rowsandall.com>',
[fullemail])
'Rowsandall <info@rowsandall.com>',
[fullemail])
res = email.send()
return 1
@app.task
def handle_sendemail_request_accept(email,name,teamname,managername):
fullemail = name+' <'+email+'>'
subject = 'Welcome to '+teamname
message = 'Dear '+name+',\n\n'
def handle_sendemail_request_accept(email, name, teamname, managername):
fullemail = name + ' <' + email + '>'
subject = 'Welcome to ' + teamname
message = 'Dear ' + name + ',\n\n'
message += managername
message += ' has accepted your request to be part of the team '
message += teamname
@@ -562,19 +609,19 @@ def handle_sendemail_request_accept(email,name,teamname,managername):
message += "Best Regards, the Rowsandall Team"
email = EmailMessage(subject, message,
'Rowsandall <info@rowsandall.com>',
[fullemail])
'Rowsandall <info@rowsandall.com>',
[fullemail])
res = email.send()
return 1
@app.task
def handle_sendemail_request_reject(email,name,teamname,managername):
fullemail = name+' <'+email+'>'
subject = 'Your application to '+teamname+' was rejected'
message = 'Dear '+name+',\n\n'
def handle_sendemail_request_reject(email, name, teamname, managername):
fullemail = name + ' <' + email + '>'
subject = 'Your application to ' + teamname + ' was rejected'
message = 'Dear ' + name + ',\n\n'
message += 'Unfortunately, '
message += managername
message += ' has rejected your request to be part of the team '
@@ -583,19 +630,19 @@ def handle_sendemail_request_reject(email,name,teamname,managername):
message += "Best Regards, the Rowsandall Team"
email = EmailMessage(subject, message,
'Rowsandall <info@rowsandall.com>',
[fullemail])
'Rowsandall <info@rowsandall.com>',
[fullemail])
res = email.send()
return 1
@app.task
def handle_sendemail_member_dropped(email,name,teamname,managername):
fullemail = name+' <'+email+'>'
subject = 'You were removed from '+teamname
message = 'Dear '+name+',\n\n'
def handle_sendemail_member_dropped(email, name, teamname, managername):
fullemail = name + ' <' + email + '>'
subject = 'You were removed from ' + teamname
message = 'Dear ' + name + ',\n\n'
message += 'Unfortunately, '
message += managername
message += ' has removed you from the team '
@@ -604,42 +651,42 @@ def handle_sendemail_member_dropped(email,name,teamname,managername):
message += "Best Regards, the Rowsandall Team"
email = EmailMessage(subject, message,
'Rowsandall <info@rowsandall.com>',
[fullemail])
'Rowsandall <info@rowsandall.com>',
[fullemail])
res = email.send()
return 1
@app.task
def handle_sendemail_team_removed(email,name,teamname,managername):
fullemail = name+' <'+email+'>'
subject = 'Team '+teamname+' was deleted'
message = 'Dear '+name+',\n\n'
def handle_sendemail_team_removed(email, name, teamname, managername):
fullemail = name + ' <' + email + '>'
subject = 'Team ' + teamname + ' was deleted'
message = 'Dear ' + name + ',\n\n'
message += managername
message += ' has decided to delete the team '
message += teamname
message += '\n\n'
message += 'The '+teamname+' tag has been removed from all your '
message += 'The ' + teamname + ' tag has been removed from all your '
message += 'workouts on rowsandall.com.\n\n'
message += "Best Regards, the Rowsandall Team"
email = EmailMessage(subject, message,
'Rowsandall <info@rowsandall.com>',
[fullemail])
'Rowsandall <info@rowsandall.com>',
[fullemail])
res = email.send()
return 1
@app.task
def handle_sendemail_invite_reject(email,name,teamname,managername):
fullemail = managername+' <'+email+'>'
subject = 'Your invitation to '+name+' was rejected'
message = 'Dear '+managername+',\n\n'
message += 'Unfortunately, '
def handle_sendemail_invite_reject(email, name, teamname, managername):
fullemail = managername + ' <' + email + '>'
subject = 'Your invitation to ' + name + ' was rejected'
message = 'Dear ' + managername + ',\n\n'
message += 'Unfortunately, '
message += name
message += ' has rejected your invitation to be part of the team '
message += teamname
@@ -647,33 +694,31 @@ def handle_sendemail_invite_reject(email,name,teamname,managername):
message += "Best Regards, the Rowsandall Team"
email = EmailMessage(subject, message,
'Rowsandall <info@rowsandall.com>',
[fullemail])
'Rowsandall <info@rowsandall.com>',
[fullemail])
res = email.send()
return 1
@app.task
def handle_sendemail_invite_accept(email,name,teamname,managername):
fullemail = managername+' <'+email+'>'
subject = 'Your invitation to '+name+' was accepted'
message = 'Dear '+managername+',\n\n'
message += name+' has accepted your invitation to be part of the team '+teamname+'\n\n'
def handle_sendemail_invite_accept(email, name, teamname, managername):
fullemail = managername + ' <' + email + '>'
subject = 'Your invitation to ' + name + ' was accepted'
message = 'Dear ' + managername + ',\n\n'
message += name + ' has accepted your invitation to be part of the team ' + teamname + '\n\n'
message += "Best Regards, the Rowsandall Team"
email = EmailMessage(subject, message,
'Rowsandall <info@rowsandall.com>',
[fullemail])
'Rowsandall <info@rowsandall.com>',
[fullemail])
res = email.send()
return 1
# Another simple task for debugging purposes
def add2(x,y):
return x+y
def add2(x, y):
return x + y
+6 -1
View File
@@ -24,7 +24,12 @@
<a class="button gray small" href="/rowers/workout/{{ workout.id }}/edit">Edit Workout</a>
</p>
</div>
<div class="grid_2 suffix_2 omega">
<div class="grid_2">
<p>
<a class="button gray small" href="/rowers/workout/{{ workout.id }}/workflow">Workflow View</a>
</p>
</div>
<div class="grid_2 omega">
<p>
<a class="button gray small" href="/rowers/workout/{{ workout.id }}/export">Export</a>
</p>
+6 -1
View File
@@ -23,7 +23,12 @@
<a class="button gray small" href="/rowers/workout/{{ workout.id }}/edit">Edit Workout</a>
</p>
</div>
<div class="grid_2 suffix_2 omega">
<div class="grid_2">
<p>
<a class="button gray small" href="/rowers/workout/{{ workout.id }}/workflow">Workflow View</a>
</p>
</div>
<div class="grid_2 omega">
<p>
<a class="button gray small" href="/rowers/workout/{{ workout.id }}/export">Export</a>
</p>
+2 -15
View File
@@ -38,23 +38,10 @@
You can select one static plot to be generated immediately for
this workout. You can select to export to major fitness
platforms automatically.
If you check "make private", this workout will not be visible to your followers and will not show up in your teams' workouts list.
If you check "make private", this workout will not be visible to your followers and will not show up in your teams' workouts list. With the Landing Page option, you can select to which (workout related) page you will be
taken after a successfull upload.
</p>
<p>
Valid file types are:
<ul>
<li>Painsled iOS Stroke Export (CSV)</li>
<li>Painsled desktop version Stroke Export (CSV)</li>
<li>A TCX file with location data (lat,long) - with or without Heart Rate value, for example from RiM or CrewNerd</li>
<li>RowPro CSV export</li>
<li>SpeedCoach GPS and SpeedCoach GPS 2 CSV export</li>
<li>ErgData CSV export</li>
<li>ErgStick CSV export</li>
<li>BoatCoach CSV export</li>
<li>A FIT file with location data (experimental)</li>
</ul>
</p>
</div>
+6 -1
View File
@@ -15,7 +15,12 @@
<a class="button gray small" href="/rowers/workout/{{ workout.id }}/edit">Edit Workout</a>
</p>
</div>
<div class="grid_2 suffix_2 omega">
<div class="grid_2">
<p>
<a class="button gray small" href="/rowers/workout/{{ workout.id }}/workflow">Workflow View</a>
</p>
</div>
<div class="grid_2 omega">
<p>
<a class="button gray small" href="/rowers/workout/{{ workout.id }}/advanced">Advanced Edit</a>
</p>
+6 -1
View File
@@ -32,7 +32,12 @@
<a class="button gray small" href="/rowers/workout/{{ id }}/edit">Edit Workout</a>
</p>
</div>
<div class="grid_2 suffix_8 omega">
<div class="grid_2">
<p>
<a class="button gray small" href="/rowers/workout/{{ id }}/workflow">Workflow View</a>
</p>
</div>
<div class="grid_2 suffix_6 omega">
<p>
<a class="button gray small" href="/rowers/workout/{{ id }}/advanced">Advanced Edit</a>
</p>
+1
View File
@@ -1,5 +1,6 @@
{% if charts %}
<h2>Flex Charts</h2>
<p>Click on the thumbnails to view the full chart.</p>
{% for chart in charts %}
<div class="grid_3 alpha">
<big>{{ forloop.counter }}</big>
+2 -2
View File
@@ -84,9 +84,9 @@
[RANKING PIECE]
{% endif %}
{% if workout.name != '' %}
<a href="/rowers/workout/{{ workout.id }}/edit">{{ workout.name }}</a> </td>
<a href={% url rower.defaultlandingpage id=workout.id %}>{{ workout.name }}</a> </td>
{% else %}
<a href="/rowers/workout/{{ workout.id }}/edit">No Name</a> </td>
<a href={% url rower.defaultlandingpage id=workout.id %}>No Name</a> </td>
{% endif %}
{% else %}
{% if workout.name != '' %}
+7 -1
View File
@@ -43,7 +43,13 @@
<a class="button gray small" href="/rowers/workout/{{ workout.id }}/edit">Edit Workout</a>
</p>
</div>
<div class="grid_2 suffix_2 omega">
<div class="grid_2">
<p>
<a class="button gray small" href="/rowers/workout/{{ workout.id }}/workflow">Workflow View</a>
</p>
</div>
<div class="grid_2 omega">
<p>
<a class="button gray small" href="/rowers/workout/{{ workout.id }}/advanced">Advanced Edit</a>
</p>
+7 -1
View File
@@ -14,7 +14,13 @@
<a class="button gray small" href="/rowers/workout/{{ workout.id }}/edit">Edit Workout</a>
</p>
</div>
<div class="grid_2 suffix_2 omega">
<div class="grid_2">
<p>
<a class="button gray small" href="/rowers/workout/{{ workout.id }}/workflow">Workflow View</a>
</p>
</div>
<div class="grid_2 omega">
<p>
<a class="button gray small" href="/rowers/workout/{{ workout.id }}/advanced">Advanced Edit</a>
</p>
+31
View File
@@ -0,0 +1,31 @@
{% load rowerfilters %}
{% load tz %}
<div class="grid_6 suffix_3 alpha">
<table width=100%>
<tr>
{% localtime on %}
<th>Date/Time:</th><td>{{ workout.startdatetime|localtime}}</td>
{% endlocaltime %}
</tr><tr>
<th>Distance:</th><td>{{ workout.distance }}m</td>
</tr><tr>
<th>Duration:</th><td>{{ workout.duration |durationprint:"%H:%M:%S.%f" }}</td>
</tr><tr>
<th>Public link to this workout</th>
<td>
<a href="/rowers/workout/{{ workout.id }}">https://rowsandall.com/rowers/workout/{{ workout.id }}</a>
</td>
</tr><tr>
<th>Comments</th>
<td>
<a href="/rowers/workout/{{ workout.id }}/comment">Comment ({{ aantalcomments }})</a>
</td>
</tr><tr>
<th>Public link to interactive chart</th>
<td>
<a href="/rowers/workout/{{ workout.id }}/interactiveplot">https://rowsandall.com/rowers/workout/{{ workout.id }}/interactiveplot</a>
<td>
</tr>
</table>
</div>
+5
View File
@@ -0,0 +1,5 @@
<div class="grid_2 alpha">
<p>
<a class="button red small" href="/rowers/workout/{{ workout.id }}/deleteconfirm">Delete</a>
</p>
</div>
+5
View File
@@ -0,0 +1,5 @@
<div class="grid_2 alpha">
<p>
<a class="button gray small" href="/rowers/workout/{{ workout.id }}/export">Export Workout</a>
</p>
</div>
+9
View File
@@ -0,0 +1,9 @@
<div style="height:100%;" id="theplot" class="grid_9 alpha flexplot">
{{ mapdiv|safe }}
{{ mapscript|safe }}
</div>
+5
View File
@@ -0,0 +1,5 @@
<div class="grid_2 alpha">
<p>
<a class="button gray small" href="/rowers/workout/{{ workout.id }}/map">Map View</a>
</p>
</div>
+14
View File
@@ -0,0 +1,14 @@
<div class="grid_9">
<div class="grid_1 alpha">
<div class="fb-share-button" data-href="https://rowsandall.com/rowers/workout/{{ workout.id }}" data-layout="button" data-size="small" data-mobile-iframe="false">
<a class="fb-xfbml-parse-ignore" target="_blank" href="https://www.facebook.com/sharer/sharer.php?u=https://rowsandall.com/rowers/workout/{{ workout.id }}">Share</a>
</div>
</div>
<div class="grid_1 suffix_7 omega">
<a class="twitter-share-button"
href="https://twitter.com/intent/tweet"
data-url="https://rowsandall.com/rowers/workout/{{ workout.id }}"
data-text="@rowsandall #rowingdata">Tweet</a>
</div>
+14
View File
@@ -0,0 +1,14 @@
<div class="grid_2 alpha">
<div class="fb-share-button" data-href="https://rowsandall.com/rowers/workout/{{ workout.id }}" data-layout="button" data-size="small" data-mobile-iframe="false">
<a class="fb-xfbml-parse-ignore" target="_blank" href="https://www.facebook.com/sharer/sharer.php?u=https://rowsandall.com/rowers/workout/{{ workout.id }}">Share</a>
</div>
</div>
<div class="grid_2 alpha">
<p>&nbsp;</p>
<a class="twitter-share-button"
href="https://twitter.com/intent/tweet"
data-url="https://rowsandall.com/rowers/workout/{{ workout.id }}"
data-text="@rowsandall #rowingdata">Tweet</a>
</div>
+4 -1
View File
@@ -29,7 +29,10 @@
</div>
</div>
<div class="grid_6 alpha">
<div class="grid_2 prefix_4 alpha">
<div class="grid_2 prefix_2 alpha">
<p><a class="button gray small" href="/rowers/workout/{{ workout.id }}/workflow">Workflow View</a></p>
</div>
<div class="grid_2 omega">
<p><a class="button blue small" href="/rowers/workout/{{ workout.id }}/wind">Wind Edit</a></p>
</div>
</div>
+1 -1
View File
@@ -23,7 +23,7 @@
</div>
<div class="grid_2">
<p>
<a class="button gray small" href="/rowers/workout/{{ workout.id }}/export">Export</a>
<a class="button gray small" href="/rowers/workout/{{ workout.id }}/workflow">Workflow View</a>
</p>
</div>
+4 -1
View File
@@ -28,7 +28,10 @@
</div>
</div>
<div class="grid_6 alpha">
<div class="grid_2 prefix_4 alpha">
<div class="grid_2 prefix_2 alpha">
<p><a class="button gray small" href="/rowers/workout/{{ workout.id }}/workflow">Workflow View</a></p>
</div>
<div class="grid_2 omega">
<p><a class="button blue small" href="/rowers/workout/{{ workout.id }}/stream">Stream Edit</a></p>
</div>
</div>
-4
View File
@@ -3,7 +3,6 @@
{% load rowerfilters %}
{% load tz %}
{% get_current_timezone as TIME_ZONE %}
{% block title %}{{ workout.name }}{% endblock %}
@@ -40,9 +39,6 @@
{% include templateName %}
{% endfor %}
{% endblock %}
<div class="grid_2 alpha">
<p>Click on the thumbnails to view the full chart</p>
</div>
</div>
<div id="middlepanel" class="grid_9">
{% block middle_panel %}
+6 -1
View File
@@ -44,7 +44,12 @@
</div>
</div>
<div class="grid_6 alpha">
<div class="grid_2 prefix_2 alpha">
<div class="grid_2 alpha">
<p>
<a class="button gray small" href="/rowers/workout/{{ workout.id }}/workflow">Workflow View</a>
</p>
</div>
<div class="grid_2">
<p>
<a class="button gray small" href="/rowers/workout/{{ workout.id }}/map">Map View</a>
</p>
+1 -1
View File
@@ -18,7 +18,7 @@
</div>
<div class="grid_2">
<p>
<a class="button gray small" href="/rowers/workout/{{ workout.id }}/export">Export</a>
<a class="button gray small" href="/rowers/workout/{{ workout.id }}/workflow">Workflow View</a>
</p>
</div>
-1
View File
@@ -118,7 +118,6 @@ def user_teams(user):
return teams
@register.filter
def has_teams(user):
try:
+4 -2
View File
@@ -191,7 +191,8 @@ urlpatterns = [
url(r'^workout/compare/(?P<id>\d+)/$',views.workout_comparison_list),
url(r'^workout/compare2/(?P<id1>\d+)/(?P<id2>\d+)/(?P<xparam>\w+.*)/(?P<yparam>\w+.*)/$',views.workout_comparison_view),
url(r'^workout/compare/(?P<id>\d+)/(?P<startdatestring>\d+-\d+-\d+)/(?P<enddatestring>\w+.*)$',views.workout_comparison_list),
url(r'^workout/(?P<id>\d+)/edit$',views.workout_edit_view),
url(r'^workout/(?P<id>\d+)/edit$',views.workout_edit_view,
name='workout_edit_view'),
url(r'^workout/(?P<id>\d+)/navionics$',views.workout_edit_view_navionics),
url(r'^workout/(?P<id>\d+)/map$',views.workout_map_view),
url(r'^workout/(?P<id>\d+)/setprivate$',views.workout_setprivate_view),
@@ -329,7 +330,8 @@ urlpatterns = [
url(r'^legal', TemplateView.as_view(template_name='legal.html'),name='legal'),
url(r'^register$',views.rower_register_view),
url(r'^register/thankyou/$', TemplateView.as_view(template_name='registerthankyou.html'), name='registerthankyou'),
url(r'^workout/(?P<id>\d+)/workflow$',views.workout_workflow_view),
url(r'^workout/(?P<id>\d+)/workflow$',views.workout_workflow_view,
name='workout_workflow_view'),
url(r'^workout/(?P<id>\d+)/flexchart/(?P<xparam>\w+.*)/(?P<yparam1>\w+.*)/(?P<yparam2>\w+.*)/(?P<plottype>\w+)/$',views.workout_flexchart3_view),
url(r'^workout/(?P<id>\d+)/flexchart/(?P<xparam>\w+.*)/(?P<yparam1>\w+.*)/(?P<yparam2>\w+.*)/(?P<plottype>\w+.*)$',views.workout_flexchart3_view),
url(r'^workout/(?P<id>\d+)/flexchart/(?P<xparam>\w+.*)/(?P<yparam1>\w+.*)/(?P<yparam2>\w+.*)$',views.workout_flexchart3_view),
+18 -3
View File
@@ -5,19 +5,33 @@ import colorsys
lbstoN = 4.44822
landingpages = (
('workout_edit_view','Edit View'),
('workout_workflow_view','Workflow View'),
)
workflowmiddlepanel = (
('panel_statcharts.html','Static Charts'),
('flexthumbnails.html','Flex Charts'),
('panel_summary.html','Summary'),
('panel_map.html','Map'),
('panel_comments.html','Basic Info and Links'),
('panel_middlesocial.html','Social Media Share Buttons'),
)
defaultmiddle = ['panel_statcharts.html',
defaultmiddle = ['panel_middlesocial.html',
'panel_statcharts.html',
'flexthumbnails.html',
'panel_summary.html']
'panel_summary.html',
'panel_map.html']
workflowleftpanel = (
('panel_navigationheader.html','Navigation Header'),
('panel_editbuttons.html','Edit Workout Button'),
('panel_delete.html','Delete Workout Button'),
('panel_export.html','Export Workout Button'),
('panel_social.html','Social Media Share Buttons'),
('panel_advancededit.html','Advanced Workout Edit Button'),
('panel_editintervals.html','Edit Intervals Button'),
('panel_stats.html','Workout Statistics Button'),
@@ -25,7 +39,8 @@ workflowleftpanel = (
('panel_geekyheader.html','Geeky Header'),
('panel_editwind.html','Edit Wind Data'),
('panel_editstream.html','Edit Stream Data'),
('panel_otwpower.html','Run OTW Power Calculations')
('panel_otwpower.html','Run OTW Power Calculations'),
('panel_mapview.html','Map'),
)
defaultleft = [
+100 -21
View File
@@ -12,6 +12,7 @@ from PIL import Image
from numbers import Number
from django.views.generic.base import TemplateView
from django.db.models import Q
from django import template
from django.db import IntegrityError, transaction
from django.shortcuts import render
from django.http import (
@@ -157,6 +158,27 @@ from interactiveplots import *
# Define the API documentation
schema_view = get_swagger_view(title='Rowsandall API')
# Test if row data include candidates
def rowhascoordinates(row):
# create interactive plot
f1 = row.csvfilename
u = row.user.user
r = getrower(u)
rowdata = rdata(f1)
hascoordinates = 1
if rowdata != 0:
try:
latitude = rowdata.df[' latitude']
if not latitude.std():
hascoordinates = 0
except KeyError,AttributeError:
hascoordinates = 0
else:
hascoordinates = 0
return hascoordinates
# Custom error pages with Rowsandall headers
def error500_view(request):
response = render_to_response('500.html', {},
@@ -4432,6 +4454,7 @@ def workouts_view(request,message='',successmessage='',
return render(request, 'list_workouts.html',
{'workouts': workouts,
'rower':r,
'dateform':dateform,
'startdate':startdate,
'enddate':enddate,
@@ -5231,7 +5254,11 @@ def workout_otwsetpower_view(request,id=0,message="",successmessage=""):
kwargs = {
'id':int(id)}
url = reverse(workout_advanced_view,kwargs=kwargs)
try:
url = request.session['referer']
except KeyError:
url = reverse(workout_advanced_view,kwargs=kwargs)
response = HttpResponseRedirect(url)
return response
@@ -6006,15 +6033,41 @@ def workout_workflow_view(request,id):
charts = []
if favorites:
if favorites and 'flexthumbnails.html' in r.workflowmiddlepanel:
charts = thumbnails_set(r,id,favorites)
if charts[0]['script'] == '':
charts = []
if 'panel_map.html' in r.workflowmiddlepanel and rowhascoordinates(row):
rowdata = rdata(row.csvfilename)
mapscript,mapdiv = leaflet_chart2(rowdata.df[' latitude'],
rowdata.df[' longitude'],
row.name)
else:
mapscript = ''
mapdiv = ''
statcharts = GraphImage.objects.filter(workout=row)
# This will be user configurable in the future
middleTemplates = r.workflowmiddlepanel
leftTemplates = r.workflowleftpanel
middleTemplates = []
for t in r.workflowmiddlepanel:
try:
template.loader.get_template(t)
middleTemplates.append(t)
except template.TemplateDoesNotExist:
pass
leftTemplates = []
for t in r.workflowleftpanel:
try:
template.loader.get_template(t)
leftTemplates.append(t)
except template.TemplateDoesNotExist:
pass
return render(request,
'workflow.html',
@@ -6023,6 +6076,8 @@ def workout_workflow_view(request,id):
'leftTemplates':leftTemplates,
'charts':charts,
'workout':row,
'mapscript':mapscript,
'mapdiv':mapdiv,
'statcharts':statcharts,
})
@@ -6142,6 +6197,19 @@ def workout_flexchart3_view(request,*args,**kwargs):
else:
workstrokesonly = False
if not promember:
for name,d in rowingmetrics:
if d['type'] != 'basic':
if xparam == name:
xparam = 'time'
messages.info(request,'To use '+d['verbose_name']+', you have to be Pro member')
if yparam1 == name:
yparam1 = 'pace'
messages.info(request,'To use '+d['verbose_name']+', you have to be Pro member')
if yparam2 == name:
yparam2 = 'spm'
messages.info(request,'To use '+d['verbose_name']+', you have to be Pro member')
# create interactive plot
try:
script,div,js_resources,css_resources,workstrokesonly = interactive_flex_chart2(id,xparam=xparam,yparam1=yparam1,
@@ -7533,13 +7601,17 @@ def workout_upload_view(request,
'make_plot':False,
'upload_to_C2':False,
'plottype':'timeplot',
'landingpage':'workout_edit_view',
},
docformoptions={
'workouttype':'rower',
}):
r = getrower(request.user)
if 'uploadoptions' in request.session:
uploadoptions = request.session['uploadoptions']
uploadoptions['landingpage'] = r.defaultlandingpage
else:
request.session['uploadoptions'] = uploadoptions
@@ -7568,36 +7640,40 @@ def workout_upload_view(request,
plottype = 'timeplot'
try:
upload_toc2 = uploadoptions['upload_to_C2']
landingpage = uploadoptions['landingpage']
except KeyError:
upload_toc2 = False
landingpage = r.defaultlandingpage
try:
upload_tostrava = uploadoptions['upload_to_Strava']
upload_to_c2 = uploadoptions['upload_to_C2']
except KeyError:
upload_tostrava = False
upload_to_c2 = False
try:
upload_tost = uploadoptions['upload_to_SportTracks']
upload_to_strava = uploadoptions['upload_to_Strava']
except KeyError:
upload_tost = False
upload_to_strava = False
try:
upload_tork = uploadoptions['upload_to_RunKeeper']
upload_to_st = uploadoptions['upload_to_SportTracks']
except KeyError:
upload_tork = False
upload_to_st = False
try:
upload_toua = uploadoptions['upload_to_MapMyFitness']
upload_to_rk = uploadoptions['upload_to_RunKeeper']
except KeyError:
upload_toua = False
upload_to_rk = False
try:
upload_totp = uploadoptions['upload_to_TrainingPeaks']
upload_to_ua = uploadoptions['upload_to_MapMyFitness']
except KeyError:
upload_totp = False
upload_to_ua = False
try:
upload_to_tp = uploadoptions['upload_to_TrainingPeaks']
except KeyError:
upload_to_tp = False
r = getrower(request.user)
if request.method == 'POST':
form = DocumentsForm(request.POST,request.FILES)
optionsform = UploadOptionsForm(request.POST)
@@ -7622,6 +7698,7 @@ def workout_upload_view(request,
upload_to_ua = optionsform.cleaned_data['upload_to_MapMyFitness']
upload_to_tp = optionsform.cleaned_data['upload_to_TrainingPeaks']
makeprivate = optionsform.cleaned_data['makeprivate']
landingpage = optionsform.cleaned_data['landingpage']
uploadoptions = {
'makeprivate':makeprivate,
@@ -7633,6 +7710,7 @@ def workout_upload_view(request,
'upload_to_RunKeeper':upload_to_rk,
'upload_to_MapMyFitness':upload_to_ua,
'upload_to_TrainingPeaks':upload_to_tp,
'landingpage':r.defaultlandingpage,
}
@@ -7756,7 +7834,7 @@ def workout_upload_view(request,
else:
messages.error(request,message)
url = reverse(workout_edit_view,
url = reverse(landingpage,
kwargs = {
'id':w.id,
})
@@ -8474,10 +8552,10 @@ def rower_favoritecharts_view(request):
if request.method == 'POST':
favorites_formset = FavoriteChartFormSet(request.POST)
if favorites_formset.is_valid():
new_instances = []
for favorites_form in favorites_formset:
print 'mies'
yparam1 = favorites_form.cleaned_data.get('yparam1')
yparam2 = favorites_form.cleaned_data.get('yparam2')
xparam = favorites_form.cleaned_data.get('xparam')
@@ -8503,7 +8581,6 @@ def rower_favoritecharts_view(request):
except IntegrityError:
message = "something went wrong"
messages.error(request,message)
else:
favorites_formset = FavoriteChartFormSet(initial=favorites_data)
@@ -8724,6 +8801,7 @@ def rower_edit_view(request,message=""):
first_name = ucd['first_name']
last_name = ucd['last_name']
email = ucd['email']
defaultlandingpage = cd['defaultlandingpage']
weightcategory = cd['weightcategory']
getemailnotifications = cd['getemailnotifications']
defaulttimezone=cd['defaulttimezone']
@@ -8740,6 +8818,7 @@ def rower_edit_view(request,message=""):
r.defaulttimezone=defaulttimezone
r.weightcategory = weightcategory
r.getemailnotifications = getemailnotifications
r.defaultlandingpage = defaultlandingpage
r.save()
form = RowerForm(instance=r)
powerform = RowerPowerForm(instance=r)
+3
View File
@@ -261,6 +261,9 @@ TP_CLIENT_SECRET = CFG["tp_client_secret"]
TP_REDIRECT_URI = CFG["tp_redirect_uri"]
TP_CLIENT_KEY = TP_CLIENT_ID
# Full Site URL
SITE_URL = "https://rowsandall.com"
# RQ stuff
RQ_QUEUES = {
+2
View File
@@ -76,6 +76,8 @@ LOGIN_REDIRECT_URL = '/rowers/list-workouts/'
SESSION_ENGINE = "django.contrib.sessions.backends.signed_cookies"
SITE_URL = "http://localhost:8000"
#EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
#EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend'