diff --git a/rowers/admin.py b/rowers/admin.py index c5dd467e..6c8da968 100644 --- a/rowers/admin.py +++ b/rowers/admin.py @@ -4,7 +4,7 @@ from django.contrib.auth.models import User from .models import ( Rower, Workout,GraphImage,FavoriteChart,SiteAnnouncement, - Team, + Team,TeamInvite ) # Register your models here so you can use them in the Admin module @@ -29,7 +29,10 @@ class SiteAnnouncementAdmin(admin.ModelAdmin): list_display = ('announcement','created','modified','expires','dotweet') class TeamAdmin(admin.ModelAdmin): - list_display = ('name',) + list_display = ('name','manager') + +class TeamInviteAdmin(admin.ModelAdmin): + list_display = ('issuedate','team','user','code') admin.site.unregister(User) admin.site.register(User,UserAdmin) @@ -38,3 +41,4 @@ admin.site.register(GraphImage) admin.site.register(Team,TeamAdmin) admin.site.register(FavoriteChart,FavoriteChartAdmin) admin.site.register(SiteAnnouncement,SiteAnnouncementAdmin) +admin.site.register(TeamInvite,TeamInviteAdmin) diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 2c5b46fa..12744d7f 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -1,6 +1,6 @@ # All the data preparation, data cleaning and data mangling should # be defined here -from rowers.models import Workout, User, Rower +from rowers.models import Workout, User, Rower,StrokeData from rowingdata import rowingdata as rrdata from rowers.tasks import handle_sendemail_unrecognized @@ -67,6 +67,98 @@ from scipy.signal import savgol_filter import datetime +def clean_df_stats(datadf,workstrokesonly=True): + # clean data remove zeros and negative values + datadf=datadf.clip(lower=0) + datadf.replace(to_replace=0,value=np.nan,inplace=True) + + # clean data for useful ranges per column + mask = datadf['hr'] < 30 + datadf.loc[mask,'hr'] = np.nan + + mask = datadf['rhythm'] < 5 + datadf.loc[mask,'rhythm'] = np.nan + + mask = datadf['rhythm'] > 70 + datadf.loc[mask,'rhythm'] = np.nan + + mask = datadf['power'] < 20 + datadf.loc[mask,'power'] = np.nan + + mask = datadf['drivelength'] < 0.5 + datadf.loc[mask,'drivelength'] = np.nan + + mask = datadf['forceratio'] < 0.2 + datadf.loc[mask,'forceratio'] = np.nan + + mask = datadf['forceratio'] > 1.0 + datadf.loc[mask,'forceratio'] = np.nan + + mask = datadf['spm'] < 10 + datadf.loc[mask,'spm'] = np.nan + + + mask = datadf['spm'] > 60 + datadf.loc[mask,'spm'] = np.nan + + mask = datadf['drivespeed'] < 0.5 + datadf.loc[mask,'drivespeed'] = np.nan + + mask = datadf['drivespeed'] > 4 + datadf.loc[mask,'drivespeed'] = np.nan + + mask = datadf['driveenergy'] > 2000 + datadf.loc[mask,'driveenergy'] = np.nan + + mask = datadf['driveenergy'] < 100 + datadf.loc[mask,'driveenergy'] = np.nan + + workoutstateswork = [1,4,5,8,9,6,7] + workoutstatesrest = [3] + workoutstatetransition = [0,2,10,11,12,13] + + if workstrokesonly=='True' or workstrokesonly==True: + try: + datadf = datadf[~datadf['workoutstate'].isin(workoutstatesrest)] + except: + pass + + return datadf + +def getstatsfields(): + # Get field names and remove those that are not useful in stats + fields = StrokeData._meta.get_fields() + + fielddict = {field.name:field.verbose_name for field in fields} + + fielddict.pop('workoutid') + fielddict.pop('ergpace') + fielddict.pop('hr_an') + fielddict.pop('hr_tr') + fielddict.pop('hr_at') + fielddict.pop('hr_ut2') + fielddict.pop('hr_ut1') + fielddict.pop('time') + fielddict.pop('distance') + fielddict.pop('nowindpace') + fielddict.pop('fnowindpace') + fielddict.pop('fergpace') + fielddict.pop('equivergpower') +# fielddict.pop('workoutstate') + fielddict.pop('fpace') + fielddict.pop('pace') + fielddict.pop('id') + fielddict.pop('ftime') + fielddict.pop('x_right') + fielddict.pop('hr_max') + fielddict.pop('hr_bottom') + fielddict.pop('cumdist') + + fieldlist = [field for field,value in fielddict.iteritems()] + + return fieldlist,fielddict + + # A string representation for time deltas def niceformat(values): out = [] diff --git a/rowers/models.py b/rowers/models.py index 29fd5a4e..b8c33184 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -11,6 +11,7 @@ from datetimewidget.widgets import DateTimeWidget from django.core.validators import validate_email import os import twitter +import re from django.conf import settings from sqlalchemy import create_engine @@ -50,7 +51,10 @@ database_url = 'mysql://{user}:{password}@{host}:{port}/{database_name}'.format( if settings.DEBUG or user=='': database_url = 'sqlite:///db.sqlite3' - +class UserFullnameChoiceField(forms.ModelChoiceField): + def label_from_instance(self,obj): + return obj.get_full_name() + # model for Power Zone names class PowerZonesField(models.TextField): # __metaclass__ = models.SubfieldBase @@ -63,6 +67,19 @@ class PowerZonesField(models.TextField): if not value: return if isinstance(value, list): return value + # remove double quotes and brackets + value = re.sub(r'u\"','',value) + value = re.sub(r'u\'','',value) + value = re.sub(r'\\','',value) + value = re.sub(r'\"','',value) + value = re.sub(r'\'','',value) + value = re.sub(r'\[','',value) + value = re.sub(r'\]','',value) + value = re.sub(r'\[\[','[',value) + value = re.sub(r'\]\]',']',value) + value = re.sub(r'\ \ ',' ',value) + value = re.sub(r', ',',',value) + return value.split(self.token) def from_db_value(self,value, expression, connection, context): @@ -83,13 +100,40 @@ class PowerZonesField(models.TextField): # For future Team functionality class Team(models.Model): - name = models.CharField(max_length=150) - notes = models.CharField(blank=True,max_length=200) + choices = ( + ('private','private'), + ('open','open'), + ) + name = models.CharField(max_length=150,unique=True,verbose_name='Team Name') + notes = models.CharField(blank=True,max_length=200,verbose_name='Team Purpose') manager = models.ForeignKey(User) + private = models.CharField(max_length=30,choices=choices,default='open', + verbose_name='Team Type') def __unicode__(self): return self.name +class TeamForm(ModelForm): + class Meta: + model = Team + fields = ['name','notes','private'] + widgets = { + 'notes': forms.Textarea, + } + +class TeamInvite(models.Model): + team = models.ForeignKey(Team) + user = models.ForeignKey(User,null=True) + issuedate = models.DateField(default=timezone.now) + code = models.CharField(max_length=150,unique=True) + email = models.CharField(max_length=150,null=True,blank=True) + +class TeamInviteForm(ModelForm): + user = UserFullnameChoiceField(queryset=User.objects.all(),required=False) + class Meta: + model = TeamInvite + fields = ['user','email'] + # Extension of User with rowing specific data class Rower(models.Model): weightcategories = ( @@ -149,6 +193,8 @@ class Rower(models.Model): choices=plans) planexpires = models.DateField(default=timezone.now) + teamplanexpires = models.DateField(default=timezone.now) + clubsize = models.IntegerField(default=0) # Friends/Team friends = models.ManyToManyField("self",blank=True) @@ -297,7 +343,7 @@ class Workout(models.Model): ) user = models.ForeignKey(Rower) - team = models.ForeignKey(Team,blank=True,null=True) + team = models.ManyToManyField(Team,blank=True) name = models.CharField(max_length=150) date = models.DateField() workouttype = models.CharField(choices=workouttypes,max_length=50) diff --git a/rowers/tasks.py b/rowers/tasks.py index dea6dd32..e9706889 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -22,6 +22,7 @@ from utils import serialize_list,deserialize_list from rowers.dataprepnodjango import update_strokedata + from django.core.mail import send_mail, BadHeaderError,EmailMessage # testing task @@ -54,6 +55,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): @@ -116,8 +118,11 @@ def handle_otwsetpower(f1,boattype,weightvalue, try: rowdata = rdata(f1) except IOError: - rowdata = rdata(f1+'.csv') - + try: + rowdata = rdata(f1+'.csv') + except IOError: + rowdata = rdata(f1+'.gz') + weightvalue = float(weightvalue) # do something with boat type @@ -154,14 +159,12 @@ def handle_otwsetpower(f1,boattype,weightvalue, subject = "Your Rowsandall OTW calculations are ready" message = "Dear "+first_name+",\n\n" message += "Your Rowsandall OTW calculations are ready.\n" - # message += "You can now create OTW plots with power information and wind corrections.\n\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 += "This functionality will be available soon, though.\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." @@ -232,6 +235,53 @@ def handle_makeplot(f1,f2,t,hrdata,plotnr,imagename): gc.collect() return 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 + message += ' on rowsandall.com\n\n' + 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 += '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 += 'You can also manually accept your team membership with the code.\n' + message += 'You will need to do this if you registered under a differnt email address than this one.\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 ', + [fullemail]) + + + res = email.send() + + return 1 + +@app.task +def handle_remove_workouts_team(ws,t): + for w in ws: + w.team.remove(t) + + return 1 + +@app.task +def handle_add_workouts_team(ws,t): + for w in ws: + w.team.add(t) + + return 1 # Another simple task for debugging purposes def add2(x,y): diff --git a/rowers/teams.py b/rowers/teams.py new file mode 100644 index 00000000..19c0d06d --- /dev/null +++ b/rowers/teams.py @@ -0,0 +1,208 @@ +# All the Team related methods + +# Python +from django.utils import timezone +from datetime import datetime +from datetime import timedelta +import time +from django.db import IntegrityError +import uuid +from django.conf import settings + +from rowers.models import ( + Rower, Workout, Team, TeamInvite,User + ) + +from rowers.tasks import ( + handle_remove_workouts_team,handle_sendemail_invite, + handle_add_workouts_team + ) + +# Low level functions - to be called by higher level methods + +inviteduration = 14 # days + +def create_team(name,manager,private='open',notes=''): + # needs some error testing + try: + t = Team(name=name,manager=manager,notes=notes, + private=private) + t.save() + r = Rower.objects.get(user=manager) + r.team.add(t) + except IntegrityError: + return (0,'Team name duplication') + return (t.id,'Team created') + +def remove_team(id): + t = Team.objects.get(id=id) + return t.delete() + +def set_teamplanexpires(rower): + ts = Team.objects.filter(rower=rower) + + texp = datetime.date(timezone.now()) + + for t in ts: + mr = Rower.objects.get(user=t.manager) + if mr.teamplanexpires > texp: + rower.teamplanexpires = mr.teamplanexpires + + t.save() + + return (1,'Updated rower team expiry') + +def add_member(id,rower): + t= Team.objects.get(id=id) + rower.team.add(t) + # code to add all workouts + ws = Workout.objects.filter(user=rower) + + if settings.DEBUG: + res = handle_add_workouts_team(ws,t) + else: + res = queuehigh.enqueue(handle_add_workouts_team,ws,t) + + + set_teamplanexpires(rower) + + return (id,'Member added') + +def remove_member(id,rower): + t = Team.objects.get(id=id) + rower.team.remove(t) + # remove the team from rower's workouts: + ws = Workout.objects.filter(user=rower,team=t) + + if settings.DEBUG: + res = handle_remove_workouts_team(ws,t) + else: + res = queuehigh.enqueue(handle_remove_workouts_team,ws,t) + + set_teamplanexpires(rower) + return (id,'Member removed') + +def mgr_remove_member(id,manager,rower): + t = Team.objects.get(id=id) + if t.manager == manager: + remove_member(id,rower) + return (id,'Member removed') + else: + return (0,'You are not the team manager') + + return (0,'') + +def count_invites(manager): + ts = Team.objects.filter(manager=manager) + count = 0 + for t in ts: + count += TeamInvite.objects.filter(team=t).count() + + return count + +def count_members(id): + t = Team.objects.get(id=id) + return Rower.objects.filter(team=t).count() + +def count_club_members(manager): + ts = Team.objects.filter(manager=manager) + return Rower.objects.filter(team__in=ts).distinct().count() + +def get_club_members(manager): + ts = Team.objects.filter(manager=manager) + return Rower.objects.filter(team__in=ts).distinct() + +def get_team_members(id): + t = Team.objects.get(id=id) + return Rower.objects.filter(team=t) + +def get_team_workouts(id): + t = Team.objects.get(id=id) + return Workout.objects.filter(team=t).order_by("-date", "-starttime") + +# Medium level functionality + +def create_invite(team,manager,user=None,email=''): + r = Rower.objects.get(user=manager) + if team.manager != manager: + return (0,'Not the team manager') + if user: + try: + r2 = Rower.objects.get(user=user) + except Rower.DoesNotExist: + return (0,'Rower does not exist') + if r2 in Rower.objects.filter(team=team): + return (0,'Already member of that team') + elif email==None or email=='': + return (0,'Invalid request - missing email or user') + + if count_club_members(team.manager)+count_invites(team.manager) < r.clubsize: + codes = [i.code for i in TeamInvite.objects.all()] + code = uuid.uuid4().hex[:10].upper() + # prevent duplicates + while code in codes: + code = uuid.uuid4().hex[:10].upper() + + invite = TeamInvite(team=team,code=code,user=user,email=email) + invite.save() + return (invite.id,'Invitation created') + + + else: + return (0,'You are at your club size limit') + + return (0,'Nothing done') + +def revoke_invite(id): + invitation = TeamInvite.objects.get(id=id) + invitation.delete() + + return (1,'Invitation revoked') + + +def send_invite_email(id): + invitation = TeamInvite.objects.get(id=id) + if invitation.user: + email = invitation.user.email + name = invitation.user.first_name + " " + invitation.user.last_name + else: + email = invitation.email + name = '' + + code = invitation.code + teamname = invitation.team.name + manager = invitation.team.manager.first_name+' '+invitation.team.manager.last_name + + if settings.DEBUG: + res = handle_sendemail_invite.delay(email,name,code,teamname,manager) + else: + queue.enqueue(handle_sendemail_invite,email,name,code,teamname,manager) + + return (1,'Invitation email sent') + +def process_invite_code(user,code): + code = code.upper() + try: + invitation = TeamInvite.objects.get(code=code) + except TeamInvite.DoesNotExist: + return (0,'The invitation has expired or the code is invalid') + + r = Rower.objects.get(user=user) + nu = datetime.date(timezone.now()) + if nu > invitation.issuedate+timedelta(days=inviteduration): + revoke_invite(invitation.id) + return (0,'The invitation has expired') + + t = invitation.team + result = add_member(t.id,r) + invitation.delete() + return result + +def remove_expired_invites(): + issuedate = timezone.now()-timedelta(days=inviteduration) + issuedate = datetime.date(issuedate) + invitations = TeamInvite.objects.filter(issuedate__lt=issuedate) + for i in invitations: + revoke_invite(i.id) + + return (1,'Expired invitations deleted') diff --git a/rowers/templates/advancededit.html b/rowers/templates/advancededit.html index 0e40362b..fe002787 100644 --- a/rowers/templates/advancededit.html +++ b/rowers/templates/advancededit.html @@ -51,7 +51,7 @@
- {% if user.rower.rowerplan == 'pro' %} + {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} Compare Workouts {% else %} Compare Workouts @@ -71,7 +71,7 @@

- {% if user.rower.rowerplan == 'pro' %} + {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} Edit Intervals {% else %} Edit Intervals @@ -88,7 +88,7 @@

- {% if user.rower.rowerplan == 'pro' %} + {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} Dist Metrics Plot {% else %} Dist Metrics Plot @@ -100,7 +100,7 @@

- {% if user.rower.rowerplan == 'pro' %} + {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} Time Metrics Plot {% else %} Time Metrics Plot @@ -123,7 +123,7 @@

- {% if user.rower.rowerplan == 'pro' %} + {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} Power Histogram {% else %} Dist Metrics Plot diff --git a/rowers/templates/advancedotw.html b/rowers/templates/advancedotw.html index 2892dcc4..e6c03f20 100644 --- a/rowers/templates/advancedotw.html +++ b/rowers/templates/advancedotw.html @@ -51,7 +51,7 @@

- {% if user.rower.rowerplan == 'pro' %} + {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} Compare Workouts {% else %} Compare Workouts @@ -73,7 +73,7 @@

- {% if user.rower.rowerplan == 'pro' %} + {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} Edit Intervals {% else %} Edit Intervals @@ -90,7 +90,7 @@

- {% if user.rower.rowerplan == 'pro' %} + {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} CrewNerd Summary {% else %} CrewNerd Summary @@ -104,7 +104,7 @@

- {% if user.rower.rowerplan == 'pro' %} + {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} Stroke Profile (Empower) {% else %} Stroke Profile (Empower) @@ -117,7 +117,7 @@

- {% if user.rower.rowerplan == 'pro' %} + {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} OTW Power Plot {% else %} OTW Power Plot @@ -137,7 +137,7 @@

- {% if user.rower.rowerplan == 'pro' %} + {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} Geeky Stuff {% else %} Geeky Stuff @@ -152,7 +152,7 @@

- {% if user.rower.rowerplan == 'pro' %} + {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} Smooth out Pace Data {% else %} Smooth out Pace Data @@ -169,7 +169,7 @@

- {% if user.rower.rowerplan == 'pro' %} + {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} Raw Data {% else %} Reset Smoothing diff --git a/rowers/templates/analysis.html b/rowers/templates/analysis.html index c0deb3cd..4cdbf511 100644 --- a/rowers/templates/analysis.html +++ b/rowers/templates/analysis.html @@ -41,7 +41,7 @@

Pro

- {% if user.rower.rowerplan == 'pro' %} + {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} Power Histogram {% else %} Power Histogram @@ -53,7 +53,7 @@

- {% if user.rower.rowerplan == 'pro' %} + {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} Statistics {% else %} Statistics diff --git a/rowers/templates/base.html b/rowers/templates/base.html index 1f583a2f..48b6fde5 100644 --- a/rowers/templates/base.html +++ b/rowers/templates/base.html @@ -24,7 +24,7 @@ {% analytical_body_top %}

- {% if user.rower.rowerplan == 'pro' %} + {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %}
Pro Member
{% else %}

 

@@ -120,7 +120,7 @@

{{ user.first_name }}

- Edit user data, e.g. heart rate zones + Edit user account, e.g. heart rate zones, power zones, email, teams {% else %}

login

diff --git a/rowers/templates/bases.html b/rowers/templates/bases.html index 57e283b6..a61c155d 100644 --- a/rowers/templates/bases.html +++ b/rowers/templates/bases.html @@ -19,7 +19,7 @@
- {% if user.rower.rowerplan == 'pro' %} + {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %}
Pro Member
{% else %}

 

diff --git a/rowers/templates/cumstats.html b/rowers/templates/cumstats.html index 564c160f..eb0b9654 100644 --- a/rowers/templates/cumstats.html +++ b/rowers/templates/cumstats.html @@ -98,7 +98,7 @@
{% if cordict %}

Correlation Matrix

-

This table indicates a positive (+) or negative (-) correlation between two parameters. The strong correlations are indicated with ++ and --. +

This matrix indicates a positive (+) or negative (-) correlation between two parameters. The Spearman correlation coefficient has values between +1 and -1. Positive correlation between two metrics means that if one metric increases, the other value is also likely to increase. Negative is the opposite. The further from zero, the higher the likelyhood.

@@ -116,13 +116,13 @@ {% for key2,value in thedict.items %}
{% if value > 0.5 %} -
++
+
{{ value|floatformat:-1 }}
{% elif value > 0.1 %} -
+
+
{{ value|floatformat:-1 }}
{% elif value < -0.5 %} -
--
+
{{ value|floatformat:-1 }}
{% elif value < -0.1 %} -
-
+
{{ value|floatformat:-1 }}
{% else %}   {% endif %} diff --git a/rowers/templates/flexchart3.html b/rowers/templates/flexchart3.html index 1fac349a..aa49ff65 100644 --- a/rowers/templates/flexchart3.html +++ b/rowers/templates/flexchart3.html @@ -168,7 +168,7 @@ -{% if user.rower.rowerplan == 'pro' %} +{% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %}
{% if maxfav >= 0 %} diff --git a/rowers/templates/flexchart3otw.html b/rowers/templates/flexchart3otw.html index 40738d31..4697f3a7 100644 --- a/rowers/templates/flexchart3otw.html +++ b/rowers/templates/flexchart3otw.html @@ -203,7 +203,7 @@
-{% if user.rower.rowerplan == 'pro' %} +{% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %}
{% if maxfav >= 0 %} diff --git a/rowers/templates/invitations.html b/rowers/templates/invitations.html new file mode 100644 index 00000000..fc1748c1 --- /dev/null +++ b/rowers/templates/invitations.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} + +{% block title %}Teams {% endblock %} + +{% block content %} +
+
+

+

Invitations

+ + Future invitations page + +

+ + +
+
+

+

Manual with Code

+

+
+
+ + + + +{% endblock %} diff --git a/rowers/templates/otwgeeky.html b/rowers/templates/otwgeeky.html index d26fe113..8248084d 100644 --- a/rowers/templates/otwgeeky.html +++ b/rowers/templates/otwgeeky.html @@ -57,7 +57,7 @@

- {% if user.rower.rowerplan == 'pro' %} + {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} Edit Wind Data {% else %} Edit Wind Data @@ -70,7 +70,7 @@

- {% if user.rower.rowerplan == 'pro' %} + {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} Edit Stream Data {% else %} Edit Stream Data @@ -83,7 +83,7 @@

- {% if user.rower.rowerplan == 'pro' %} + {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} OTW Power {% else %} OTW Power @@ -100,7 +100,7 @@

- {% if user.rower.rowerplan == 'pro' %} + {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} Corrected Pace Plot {% else %} Corrected Pace Plot @@ -159,4 +159,4 @@

-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/rowers/templates/rower_form.html b/rowers/templates/rower_form.html index 90c920e6..c17124ec 100644 --- a/rowers/templates/rower_form.html +++ b/rowers/templates/rower_form.html @@ -3,107 +3,124 @@ {% block title %}Change Rower {% endblock %} {% block content %} -
-

-

Heart Rate Zones

- {% if form.errors %} -

- Please correct the error{{ form.errors|pluralize }} below. -

- {% endif %} - -
- - {{ form.as_table }} -
- {% csrf_token %} -
- - -

-
-

-

Account Information

- {% if userform.errors %} -

- Please correct the error{{ form.errors|pluralize }} below. -

- {% endif %} - {% if accountform.errors %} -

- -

- {% endif %} -
- - {{ userform.as_table }} - {{ accountform.as_table }} - - - -
Plan{{ rower.rowerplan }}
- {% csrf_token %} -
- - -
-

-
-
-

-

Power Zones

+
+
+

+

Heart Rate Zones

+ {% if form.errors %} +

+ Please correct the error{{ form.errors|pluralize }} below. +

+ {% endif %} +
- {% if powerzonesform.errors %} -

- Please correct the error{{ powerzonesform.errors|pluralize }} below. - {{ powerzonesform.non_field_errors }} - -

- {% endif %} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {{ form.as_table }}
IDZone NameLower Boundary (Watt)
1{{ powerzonesform.ut3name }}
2{{ powerzonesform.ut2name }}{{ powerzonesform.pw_ut2 }}
3{{ powerzonesform.ut1name }}{{ powerzonesform.pw_ut1 }}
4{{ powerzonesform.atname }}{{ powerzonesform.pw_at }}
5{{ powerzonesform.trname }}{{ powerzonesform.pw_tr }}
6{{ powerzonesform.anname }}{{ powerzonesform.pw_an }}
{% csrf_token %}
-
-

-

-

Functional Threshold Power

-

Use this form to quickly change your zones based on the power of a - recent - full out 60 minutes effort. It will update all zones defined above.

+

+
+
+
+

+

Power Zones

+
+ {% if powerzonesform.errors %} +

+ Please correct the error{{ powerzonesform.errors|pluralize }} below. + {{ powerzonesform.non_field_errors }} + +

+ {% endif %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDZone NameLower Boundary (Watt)
1{{ powerzonesform.ut3name }}
2{{ powerzonesform.ut2name }}{{ powerzonesform.pw_ut2 }}
3{{ powerzonesform.ut1name }}{{ powerzonesform.pw_ut1 }}
4{{ powerzonesform.atname }}{{ powerzonesform.pw_at }}
5{{ powerzonesform.trname }}{{ powerzonesform.pw_tr }}
6{{ powerzonesform.anname }}{{ powerzonesform.pw_an }}
+ {% csrf_token %} +
+ +
+
+

+
+
+
+
+

+

Account Information

+ {% if userform.errors %} +

+ Please correct the error{{ form.errors|pluralize }} below. +

+ {% endif %} + {% if accountform.errors %} +

+ +

+ {% endif %} +
+ + {{ userform.as_table }} + {{ accountform.as_table }} + + + + + + +
Plan{{ rower.rowerplan }}
Plan Expiry{{ rower.planexpires }}
+ {% csrf_token %} +
+ {% if rower.rowerplan == 'basic' %} + Upgrade + {% else %} +   + {% endif %} +
+
+ + +
+

+
+
+

+

Functional Threshold Power

+

Use this form to quickly change your zones based on the power of a + recent + full out 60 minutes effort. It will update all zones defined above.

{{ powerform.as_table }} @@ -113,43 +130,61 @@ -

- {% if grants %} -

-

Applications

-
- - - - - - - - - {% for grant in grants %} - - - - - - {% endfor %} - -
ApplicationScopeRevoke
{{ grant.application }}{{ grant.scope }} - Revoke -
-

- {% endif %} +
+
+
+
+

+

Teams

+ +

+
+ +
+

+

Favorite Charts

+ +

+
- -
-

-

Favorite Charts

- -

+
+
+ {% if grants %} +

+

Applications

+ + + + + + + + + + {% for grant in grants %} + + + + + + {% endfor %} + +
ApplicationScopeRevoke
{{ grant.application }}{{ grant.scope }} + Revoke +
+

+ {% else %} +

 

+ {% endif %}
+
+ + {% endblock %} diff --git a/rowers/templates/team.html b/rowers/templates/team.html new file mode 100644 index 00000000..b5b8bcaf --- /dev/null +++ b/rowers/templates/team.html @@ -0,0 +1,70 @@ +{% extends "base.html" %} + +{% block title %}Team {% endblock %} + +{% block content %} +
+

{{ team.name }}

+

{{ team.notes }}

+

Manager: {{ team.manager.first_name }} {{ team.manager.last_name }}

+ {% if ismember %} + + {% else %} +
+ Join + A request will be sent to the team manager + +
+ {% endif %} +
+

+

Members

+ + + + + + + + {% for member in members %} + + + + {% endfor %} + +
Name
{{ member.user.first_name }} {{ member.user.last_name }}
+

+ + +
+
+ {% if team.manager == user %} +

Use the form to add a new user. You can either select a user from the slist, or you can type his email address, which also works for users who have not registered to the site yet.

+ {% if inviteform.errors %} +

+ Please correct the error{{ inviteform.errors|pluralize }} below. +

+ {% endif %} +
+ + {{ inviteform.as_table }} +
+ {% csrf_token %} +
+ +
+
+ {% else %} +

+   +

+ {% endif %} +
+
+ + + + +{% endblock %} diff --git a/rowers/templates/teamcreate.html b/rowers/templates/teamcreate.html new file mode 100644 index 00000000..557e366b --- /dev/null +++ b/rowers/templates/teamcreate.html @@ -0,0 +1,28 @@ +{% extends "base.html" %} +{% load staticfiles %} + +{% block title %}New Team{% endblock %} + +{% block content %} +
+
+

Create a new Team

+ {% if form.errors %} +

+ Please correct the error{{ form.errors|pluralize }} below. +

+ {% endif %} + + + {{ form.as_table }} +
+ {% csrf_token %} +
+ +
+
+ + + +
+{% endblock %} diff --git a/rowers/templates/teamdeleteconfirm.html b/rowers/templates/teamdeleteconfirm.html new file mode 100644 index 00000000..56088353 --- /dev/null +++ b/rowers/templates/teamdeleteconfirm.html @@ -0,0 +1,43 @@ +{% extends "base.html" %} +{% load staticfiles %} +{% load rowerfilters %} + +{% block title %}Leave Team {% endblock %} + +{% block content %} +
+ + {% if form.errors %} +

+ Please correct the error{{ form.errors|pluralize }} below. +

+ {% endif %} + +

Confirm Deleting the team {{ team.name }}

+

This will remove the team. Your team members and their workouts + will not be deleted. +

+ + +
+

+ Cancel +

+ +
+

+ Delete +

+
+ +
+ +
+

+  +

+ +
+ + +{% endblock %} diff --git a/rowers/templates/teamleaveconfirm.html b/rowers/templates/teamleaveconfirm.html new file mode 100644 index 00000000..4210a404 --- /dev/null +++ b/rowers/templates/teamleaveconfirm.html @@ -0,0 +1,45 @@ +{% extends "base.html" %} +{% load staticfiles %} +{% load rowerfilters %} + +{% block title %}Leave Team {% endblock %} + +{% block content %} +
+ + {% if form.errors %} +

+ Please correct the error{{ form.errors|pluralize }} below. +

+ {% endif %} + +

Confirm Leaving the team {{ team.name }}

+

This will remove you and all your workouts from the team. If this + is a closed team, you can only return when the team manager + reinvites you. + If this is an open team, you can return by applying for team membership. +

+ + +
+

+ Cancel +

+ +
+

+ Leave +

+
+ +
+ +
+

+  +

+ +
+ + +{% endblock %} diff --git a/rowers/templates/teams.html b/rowers/templates/teams.html new file mode 100644 index 00000000..e651d31f --- /dev/null +++ b/rowers/templates/teams.html @@ -0,0 +1,133 @@ +{% extends "base.html" %} + +{% block title %}Teams {% endblock %} + +{% block content %} +
+
+

+

My Teams

+ {% if teams %} + + + + + + + + + {% for team in teams %} + + + + + {% endfor %} + +
Name 
+ {{ team.name }} + +
+ Leave +
+
+ {% else %} +

You are not a member of any team.

+ {% endif %} +

+ + +
+
+ {% if invites %} +

+

Invitations

+ + + + + + + + + + {% for i in invites %} + + + + + + {% endfor %} + +
NameManager 
{{ i.team.name }}{{ i.team.manager.first_name }} {{ i.team.manager.last_name }}Accept
+

+ {% else %} +

 

+ {% endif %} +
+
+ +{% if user.rower.rowerplan == 'coach' %} +
+
+

Teams I manage

+ {% if myteams %} + + + + + + + + + {% for team in myteams %} + + + + + {% endfor %} + +
NameManager
+ {{ team.name }} + +
+ Delete +
+
+ {% endif %} +
+ New Team +
+
+
+ {% if otherteams %} +

Other Teams

+ + + + + + + + + {% for team in otherteams %} + + + + + {% endfor %} + +
NameManager
+ {{ team.name }} + + {{ team.manager.first_name }} {{ team.manager.last_name }} +
+ + {% else %} +

 

+ {% endif %} +
+
+{% endif %} + + +{% endblock %} diff --git a/rowers/templates/workoutstats.html b/rowers/templates/workoutstats.html index 3f470672..6d413dea 100644 --- a/rowers/templates/workoutstats.html +++ b/rowers/templates/workoutstats.html @@ -75,8 +75,8 @@
{% if cordict %} -

Correlation table

-

This table indicates a positive (+) or negative (-) correlation between two parameters. The strong correlations are indicated with ++ and --. +

Correlation matrix

+

This matrix indicates a positive (+) or negative (-) correlation between two parameters. The Spearman correlation coefficient has values between +1 and -1. Positive correlation between two metrics means that if one metric increases, the other value is also likely to increase. Negative is the opposite. The further from zero, the higher the likelyhood.

@@ -94,13 +94,13 @@ {% for key2,value in thedict.items %}
{% if value > 0.5 %} -
++
+
{{ value|floatformat:-1 }}
{% elif value > 0.1 %} -
+
+
{{ value|floatformat:-1 }}
{% elif value < -0.5 %} -
--
+
{{ value|floatformat:-1 }}
{% elif value < -0.1 %} -
-
+
{{ value|floatformat:-1 }}
{% else %}   {% endif %} diff --git a/rowers/urls.py b/rowers/urls.py index 9d82dfc1..7c2c7bdc 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -199,6 +199,20 @@ urlpatterns = [ url(r'^workout/(\d+)/stravauploadw/$',views.workout_strava_upload_view), url(r'^workout/(\d+)/recalcsummary/$',views.workout_recalcsummary_view), url(r'^workout/(\d+)/sporttracksuploadw/$',views.workout_sporttracks_upload_view), + url(r'^me/teams/$',views.rower_teams_view), + url(r'^team/(?P\d+)/s/(?P\w+.*)/c/(?P\w+.*)$',views.team_view), + url(r'^team/(?P\d+)/c/(?P\w+.*)$',views.team_view), + url(r'^team/(?P\d+)/s/(?P\w+.*)$',views.team_view), + url(r'^team/(\d+)/$',views.team_view), + url(r'^team/(\d+)/leaveconfirm/$',views.team_leaveconfirm_view), + url(r'^team/(\d+)/leave/$',views.team_leave_view), + url(r'^team/(\d+)/deleteconfirm/$',views.team_deleteconfirm_view), + url(r'^team/(\d+)/requestmembership/(\d+)$',views.team_requestmembership_view), + url(r'^team/(\d+)/delete/$',views.team_delete_view), + url(r'^team/create/$',views.team_create_view), + url(r'^me/invitation/$',views.rower_invitations_view), + url(r'^me/invitation/c/(?P\w+.*)/$',views.rower_invitations_view), + url(r'^me/invitation/(\w+.*)/$',views.rower_invitations_view), url(r'^me/edit/c/(?P\w+.*)$',views.rower_edit_view), url(r'^me/edit/s/(?P\w+.*)$',views.rower_edit_view), url(r'^me/edit/$',views.rower_edit_view), diff --git a/rowers/views.py b/rowers/views.py index 1f80dda1..a5fb9dc0 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -31,6 +31,7 @@ from rowers.models import Workout, User, Rower, WorkoutForm,FavoriteChart from rowers.models import ( RowerPowerForm,RowerForm,GraphImage,AdvancedWorkoutForm, RowerPowerZonesForm,AccountRowerForm,UserForm,StrokeData, + Team,TeamForm,TeamInviteForm,TeamInvite ) from rowers.models import FavoriteForm,BaseFavoriteFormSet,SiteAnnouncement from django.forms.formsets import formset_factory @@ -57,6 +58,7 @@ from rest_framework.parsers import JSONParser from rowers.rows import handle_uploaded_file from rowers.tasks import handle_makeplot,handle_otwsetpower,handle_sendemailtcx,handle_sendemailcsv from rowers.tasks import handle_sendemail_unrecognized + from scipy.signal import savgol_filter from django.shortcuts import render_to_response from Cookie import SimpleCookie @@ -198,8 +200,14 @@ def splitstdata(lijst): from utils import geo_distance,serialize_list,deserialize_list +# Check if a user is a Coach member +def iscoachmember(user): + r = Rower.objects.get(user=user) + result = user.is_authenticated() and (r.rowerplan=='coach') + return result + # Check if a user is a Pro member -def promember(user): +def ispromember(user): r = Rower.objects.get(user=user) result = user.is_authenticated() and (r.rowerplan=='pro' or r.rowerplan=='coach') return result @@ -1301,7 +1309,7 @@ def histo_all(request,theuser=0): if not request.user.is_anonymous(): r = Rower.objects.get(user=request.user) - result = request.user.is_authenticated() and r.rowerplan=='pro' + result = request.user.is_authenticated() and ispromember(request.user) if result: promember=1 @@ -1371,7 +1379,7 @@ def cum_flex(request,theuser=0, if not request.user.is_anonymous(): r = Rower.objects.get(user=request.user) - result = request.user.is_authenticated() and r.rowerplan=='pro' + result = request.user.is_authenticated() and ispromember(request.user) if result: promember=1 @@ -1463,14 +1471,14 @@ def cum_flex(request,theuser=0, }) # Show the EMpower Oarlock generated Stroke Profile -@user_passes_test(promember,login_url="/",redirect_field_name=None) +@user_passes_test(ispromember,login_url="/",redirect_field_name=None) def workout_forcecurve_view(request,id=0,workstrokesonly=False): row = Workout.objects.get(id=id) promember=0 mayedit=0 if not request.user.is_anonymous(): r = Rower.objects.get(user=request.user) - result = request.user.is_authenticated() and r.rowerplan=='pro' + result = request.user.is_authenticated() and ispromember(request.user) if result: promember=1 if request.user == row.user.user: @@ -1509,7 +1517,7 @@ def workout_histo_view(request,id=0): mayedit=0 if not request.user.is_anonymous(): r = Rower.objects.get(user=request.user) - result = request.user.is_authenticated() and r.rowerplan=='pro' + result = request.user.is_authenticated() and ispromember(request.user) if result: promember=1 if request.user == row.user.user: @@ -1561,7 +1569,7 @@ def histo(request,theuser=0, if not request.user.is_anonymous(): r = Rower.objects.get(user=request.user) - result = request.user.is_authenticated() and r.rowerplan=='pro' + result = request.user.is_authenticated() and ispromember(request.user) if result: promember=1 @@ -1671,7 +1679,7 @@ def rankings_view(request,theuser=0, promember=0 if not request.user.is_anonymous(): r = Rower.objects.get(user=request.user) - result = request.user.is_authenticated() and r.rowerplan=='pro' + result = request.user.is_authenticated() and ispromember(request.user) if result: promember=1 @@ -2040,7 +2048,7 @@ def workouts_view(request,message='',successmessage='', return HttpResponse("User has no rower instance") # List of workouts to compare a selected workout to -@user_passes_test(promember,login_url="/",redirect_field_name=None) +@user_passes_test(ispromember,login_url="/",redirect_field_name=None) def workout_comparison_list(request,id=0,message='',successmessage='', startdatestring="",enddatestring="", startdate=timezone.now()-datetime.timedelta(days=365), @@ -2155,7 +2163,7 @@ def workout_view(request,id=0): return HttpResponseNotFound("Workout doesn't exist") # Resets stroke data to raw data (pace) -@user_passes_test(promember,login_url="/",redirect_field_name=None) +@user_passes_test(ispromember,login_url="/",redirect_field_name=None) def workout_undo_smoothenpace_view(request,id=0,message="",successmessage=""): row = Workout.objects.get(id=id) if (checkworkoutuser(request.user,row)==False): @@ -2182,7 +2190,7 @@ def workout_undo_smoothenpace_view(request,id=0,message="",successmessage=""): # Data smoothing of pace data -@user_passes_test(promember,login_url="/",redirect_field_name=None) +@user_passes_test(ispromember,login_url="/",redirect_field_name=None) def workout_smoothenpace_view(request,id=0,message="",successmessage=""): row = Workout.objects.get(id=id) if (checkworkoutuser(request.user,row)==False): @@ -2218,7 +2226,7 @@ def workout_smoothenpace_view(request,id=0,message="",successmessage=""): return HttpResponseRedirect(url) # Process CrewNerd Summary CSV and update summary -@user_passes_test(promember,login_url="/",redirect_field_name=None) +@user_passes_test(ispromember,login_url="/",redirect_field_name=None) def workout_crewnerd_summary_view(request,id=0,message="",successmessage=""): row = Workout.objects.get(id=id) if request.method == 'POST': @@ -2263,7 +2271,7 @@ def workout_crewnerd_summary_view(request,id=0,message="",successmessage=""): 'id':row.id}) # Get weather for given location and date/time -@user_passes_test(promember,login_url="/",redirect_field_name=None) +@user_passes_test(ispromember,login_url="/",redirect_field_name=None) def workout_downloadwind_view(request,id=0,message="",successmessage=""): row = Workout.objects.get(id=id) f1 = row.csvfilename @@ -2320,7 +2328,7 @@ def workout_downloadwind_view(request,id=0,message="",successmessage=""): return response # Show form to update wind data -@user_passes_test(promember,login_url="/",redirect_field_name=None) +@user_passes_test(ispromember,login_url="/",redirect_field_name=None) def workout_wind_view(request,id=0,message="",successmessage=""): row = Workout.objects.get(id=id) if (checkworkoutuser(request.user,row)==False): @@ -2420,7 +2428,7 @@ def workout_wind_view(request,id=0,message="",successmessage=""): # Show form to update River stream data (for river dwellers) -@user_passes_test(promember,login_url="/",redirect_field_name=None) +@user_passes_test(ispromember,login_url="/",redirect_field_name=None) def workout_stream_view(request,id=0,message="",successmessage=""): row = Workout.objects.get(id=id) if (checkworkoutuser(request.user,row)==False): @@ -2482,7 +2490,7 @@ def workout_stream_view(request,id=0,message="",successmessage=""): 'the_div':div}) # Form to set average crew weight and boat type, then run power calcs -@user_passes_test(promember, login_url="/",redirect_field_name=None) +@user_passes_test(ispromember, login_url="/",redirect_field_name=None) def workout_otwsetpower_view(request,id=0,message="",successmessage=""): row = Workout.objects.get(id=id) if (checkworkoutuser(request.user,row)==False): @@ -2654,7 +2662,7 @@ def cumstats(request,theuser=0, if not request.user.is_anonymous(): r = Rower.objects.get(user=request.user) - result = request.user.is_authenticated() and r.rowerplan=='pro' + result = request.user.is_authenticated() and ispromember(request.user) if result: promember=1 @@ -2738,97 +2746,17 @@ def cumstats(request,theuser=0, ids = [int(workout.id) for workout in allergworkouts] - # Get field names and remove those that are not useful in stats - fields = StrokeData._meta.get_fields() - - fielddict = {field.name:field.verbose_name for field in fields} - - - fielddict.pop('workoutid') - fielddict.pop('ergpace') - fielddict.pop('hr_an') - fielddict.pop('hr_tr') - fielddict.pop('hr_at') - fielddict.pop('hr_ut2') - fielddict.pop('hr_ut1') - fielddict.pop('time') - fielddict.pop('distance') - fielddict.pop('nowindpace') - fielddict.pop('fnowindpace') - fielddict.pop('fergpace') - fielddict.pop('equivergpower') -# fielddict.pop('workoutstate') - fielddict.pop('fpace') - fielddict.pop('pace') - fielddict.pop('id') - fielddict.pop('ftime') - fielddict.pop('x_right') - fielddict.pop('hr_max') - fielddict.pop('hr_bottom') - fielddict.pop('cumdist') - - fieldlist = [field for field,value in fielddict.iteritems()] + fieldlist,fielddict = dataprep.getstatsfields() # prepare data frame datadf = dataprep.read_cols_df_sql(ids,fieldlist) - # clean data remove zeros and negative values - datadf=datadf.clip(lower=0) - datadf.replace(to_replace=0,value=np.nan,inplace=True) - - # clean data for useful ranges per column - mask = datadf['hr'] < 30 - datadf.loc[mask,'hr'] = np.nan - - mask = datadf['rhythm'] < 5 - datadf.loc[mask,'rhythm'] = np.nan - - mask = datadf['rhythm'] > 70 - datadf.loc[mask,'rhythm'] = np.nan - - mask = datadf['power'] < 20 - datadf.loc[mask,'power'] = np.nan - - mask = datadf['drivelength'] < 0.5 - datadf.loc[mask,'drivelength'] = np.nan - - mask = datadf['forceratio'] < 0.2 - datadf.loc[mask,'forceratio'] = np.nan + datadf = dataprep.clean_df_stats(datadf,workstrokesonly=workstrokesonly) - mask = datadf['forceratio'] > 1.0 - datadf.loc[mask,'forceratio'] = np.nan - - mask = datadf['spm'] < 10 - datadf.loc[mask,'spm'] = np.nan - - - mask = datadf['spm'] > 60 - datadf.loc[mask,'spm'] = np.nan - - mask = datadf['drivespeed'] < 0.5 - datadf.loc[mask,'drivespeed'] = np.nan - - mask = datadf['drivespeed'] > 4 - datadf.loc[mask,'drivespeed'] = np.nan - - mask = datadf['driveenergy'] > 2000 - datadf.loc[mask,'driveenergy'] = np.nan - - mask = datadf['driveenergy'] < 100 - datadf.loc[mask,'driveenergy'] = np.nan if datadf.empty: return HttpResponse("No data found") - workoutstateswork = [1,4,5,8,9,6,7] - workoutstatesrest = [3] - workoutstatetransition = [0,2,10,11,12,13] - - if workstrokesonly=='True' or workstrokesonly==True: - try: - datadf = datadf[~datadf['workoutstate'].isin(workoutstatesrest)] - except: - pass # Create stats stats = {} @@ -2908,51 +2836,8 @@ def workout_stats_view(request,id=0,message="",successmessage=""): url = reverse(workouts_view,args=[str(message)]) return HttpResponseRedirect(url) + datadf = dataprep.clean_df_stats(datadf,workstrokesonly=workstrokesonly) - # clean data remove zeros and negative values - datadf=datadf.clip(lower=0) - datadf.replace(to_replace=0,value=np.nan,inplace=True) - - # clean data for useful ranges per column - mask = datadf['hr'] < 30 - datadf.loc[mask,'hr'] = np.nan - - mask = datadf['rhythm'] < 5 - datadf.loc[mask,'rhythm'] = np.nan - - mask = datadf['rhythm'] > 70 - datadf.loc[mask,'rhythm'] = np.nan - - mask = datadf['power'] < 20 - datadf.loc[mask,'power'] = np.nan - - mask = datadf['drivelength'] < 0.5 - datadf.loc[mask,'drivelength'] = np.nan - - mask = datadf['forceratio'] < 0.2 - datadf.loc[mask,'forceratio'] = np.nan - - mask = datadf['forceratio'] > 1.0 - datadf.loc[mask,'forceratio'] = np.nan - - mask = datadf['spm'] < 10 - datadf.loc[mask,'spm'] = np.nan - - - mask = datadf['spm'] > 60 - datadf.loc[mask,'spm'] = np.nan - - mask = datadf['drivespeed'] < 0.5 - datadf.loc[mask,'drivespeed'] = np.nan - - mask = datadf['drivespeed'] > 4 - datadf.loc[mask,'drivespeed'] = np.nan - - mask = datadf['driveenergy'] > 2000 - datadf.loc[mask,'driveenergy'] = np.nan - - mask = datadf['driveenergy'] < 100 - datadf.loc[mask,'driveenergy'] = np.nan if datadf.empty: return HttpResponse("CSV data file not found") @@ -2961,44 +2846,12 @@ def workout_stats_view(request,id=0,message="",successmessage=""): workoutstatesrest = [3] workoutstatetransition = [0,2,10,11,12,13] - if workstrokesonly=='True' or workstrokesonly==True: - try: - datadf = datadf[~datadf['workoutstate'].isin(workoutstatesrest)] - except: - pass - workstrokesonly = True # Create stats stats = {} - # Get field names and remove those that are not useful in stats - fields = StrokeData._meta.get_fields() - - fielddict = {field.name:field.verbose_name for field in fields} - - - fielddict.pop('workoutid') - fielddict.pop('ergpace') - fielddict.pop('hr_an') - fielddict.pop('hr_tr') - fielddict.pop('hr_at') - fielddict.pop('hr_ut2') - fielddict.pop('hr_ut1') - fielddict.pop('time') - fielddict.pop('distance') - fielddict.pop('nowindpace') - fielddict.pop('fnowindpace') - fielddict.pop('fergpace') - fielddict.pop('equivergpower') - fielddict.pop('workoutstate') - fielddict.pop('fpace') - fielddict.pop('pace') - fielddict.pop('id') - fielddict.pop('ftime') - fielddict.pop('x_right') - fielddict.pop('hr_max') - fielddict.pop('hr_bottom') - fielddict.pop('cumdist') + fieldlist,fielddict = dataprep.getstatsfields() + for field,verbosename in fielddict.iteritems(): thedict = { @@ -3086,7 +2939,7 @@ def workout_comparison_view(request,id1=0,id2=0,xparam='distance',yparam='spm'): promember=0 if not request.user.is_anonymous(): r = Rower.objects.get(user=request.user) - result = request.user.is_authenticated() and r.rowerplan=='pro' + result = request.user.is_authenticated() and ispromember(request.user) if result: promember=1 @@ -3113,7 +2966,7 @@ def workout_comparison_view2(request,id1=0,id2=0,xparam='distance', promember=0 if not request.user.is_anonymous(): r = Rower.objects.get(user=request.user) - result = request.user.is_authenticated() and r.rowerplan=='pro' + result = request.user.is_authenticated() and ispromember(request.user) if result: promember=1 @@ -3161,7 +3014,7 @@ def workout_flexchart3_view(request,*args,**kwargs): mayedit=0 if not request.user.is_anonymous(): r = Rower.objects.get(user=request.user) - result = request.user.is_authenticated() and r.rowerplan=='pro' + result = request.user.is_authenticated() and ispromember(request.user) if result: promember=1 if request.user == row.user.user: @@ -3316,7 +3169,7 @@ def workout_biginteractive_view(request,id=0,message="",successmessage=""): mayedit=0 if not request.user.is_anonymous(): r = Rower.objects.get(user=request.user) - result = request.user.is_authenticated() and r.rowerplan=='pro' + result = request.user.is_authenticated() and ispromember(request.user) if result: promember=1 if request.user == row.user.user: @@ -3353,7 +3206,7 @@ def workout_otwpowerplot_view(request,id=0,message="",successmessage=""): mayedit=0 if not request.user.is_anonymous(): r = Rower.objects.get(user=request.user) - result = request.user.is_authenticated() and r.rowerplan=='pro' + result = request.user.is_authenticated() and ispromember(request.user) if result: promember=1 if request.user == row.user.user: @@ -3555,7 +3408,7 @@ def workout_edit_view(request,id=0,message="",successmessage=""): return HttpResponseRedirect(url) # Create the chart image with wind corrected pace (OTW) -@user_passes_test(promember,login_url="/",redirect_field_name=None) +@user_passes_test(ispromember,login_url="/",redirect_field_name=None) def workout_add_otw_powerplot_view(request,id): w = Workout.objects.get(id=id) if (checkworkoutuser(request.user,w)==False): @@ -3818,7 +3671,7 @@ def workout_add_distanceplot_view(request,id): return HttpResponseRedirect(url) # Create the advanced parameters distance overview chart -@user_passes_test(promember,login_url="/",redirect_field_name=None) +@user_passes_test(ispromember,login_url="/",redirect_field_name=None) def workout_add_distanceplot2_view(request,id): w = Workout.objects.get(id=id) if (checkworkoutuser(request.user,w)==False): @@ -3871,7 +3724,7 @@ def workout_add_distanceplot2_view(request,id): # Create the advanced parameters time based overview chart -@user_passes_test(promember,login_url="/",redirect_field_name=None) +@user_passes_test(ispromember,login_url="/",redirect_field_name=None) def workout_add_timeplot2_view(request,id): w = Workout.objects.get(id=id) if (checkworkoutuser(request.user,w)==False): @@ -4934,7 +4787,7 @@ def workout_summary_edit_view(request,id,message="",successmessage="" }) # Page where user can manage his favorite charts -@user_passes_test(promember,login_url="/rowers/me/edit",redirect_field_name=None) +@user_passes_test(ispromember,login_url="/rowers/me/edit",redirect_field_name=None) def rower_favoritecharts_view(request): message = '' successmessage = '' @@ -5430,3 +5283,190 @@ def strokedatajson(request,id): #Method not supported return HttpResponseNotAllowed("Method not supported") + + +# Teams related views +import teams + +@login_required() +def team_view(request,id=0,message='',successmessage=''): + ismember = 0 + + r = Rower.objects.get(user=request.user) + + + + try: + t = Team.objects.get(id=id) + except Team.DoesNotExist: + return HttpResponse("Team doesn't exist") + + + if request.method == 'POST' and request.user == t.manager: + inviteform = TeamInviteForm(request.POST) + if inviteform.is_valid(): + cd = inviteform.cleaned_data + newmember = cd['user'] + email = cd['email'] + inviteid,text = teams.create_invite(t,t.manager, + user=newmember, + email=email) + if inviteid: + teams.send_invite_email(inviteid) + successmessage = text + else: + message = text + + elif request.user == t.manager: + inviteform = TeamInviteForm() + inviteform.fields['user'].queryset = User.objects.filter(rower__isnull=False) + else: + inviteform = '' + + members = Rower.objects.filter(team=t).order_by('user__last_name','user__first_name') + + if r in members: + ismember = 1 + + return render(request, 'team.html', + { + 'team':t, + 'members':members, + 'inviteform':inviteform, + 'message':message, + 'successmessage':successmessage, + 'ismember':ismember, + }) + +@login_required() +def team_leaveconfirm_view(request,id=0): + try: + t = Team.objects.get(id=id) + except Team.DoesNotExist: + return HttpResponse("Team doesn't exist") + + return render(request,'teamleaveconfirm.html', + { + 'team':t + }) + +@login_required() +def team_leave_view(request,id=0): + r = Rower.objects.get(user=request.user) + teams.remove_member(id,r) + + url = reverse(rower_teams_view) + response = HttpResponseRedirect(url) + return response + +@login_required() +def rower_teams_view(request): + r = Rower.objects.get(user=request.user) + ts = Team.objects.filter(rower=r) + myteams = Team.objects.filter(manager=request.user) + otherteams = Team.objects.filter(private='open').exclude(rower=r).exclude(manager=request.user).order_by('name') + teams.remove_expired_invites() + + invites = TeamInvite.objects.filter(user=request.user) + + return render(request, 'teams.html', + { + 'teams':ts, + 'myteams':myteams, + 'invites':invites, + 'otherteams':otherteams, + }) + +@login_required() +def rower_invitations_view(request,code=None,message='',successmessage=''): + + if code: + teams.remove_expired_invites() + res,text = teams.process_invite_code(request.user,code) + if res: + successmessage = text + teamid=res + url = reverse(team_view,kwargs={ + 'id':teamid, + 'successmessage': successmessage, + }) + else: + message = text + url = reverse(rower_invitations_view,kwargs={ + 'message':message + }) + + return HttpResponseRedirect(url) + + + r = Rower.objects.get(user=request.user) + ts = Team.objects.filter(rower=r) + myteams = Team.objects.filter(manager=request.user) + invites = TeamInvite.objects.filter(user=request.user) + otherteams = Team.objects.filter(private='open').drop(rower=r) + + return render(request, 'teams.html', + { + 'teams':ts, + 'myteams':myteams, + 'invites':invites, + 'otherteams':otherteams, + }) + +@login_required() +def team_requestmembership_view(request,teamid,userid): + return HttpResponse("Not yet implemented") + +@user_passes_test(iscoachmember,login_url="/",redirect_field_name=None) +def team_create_view(request): + if request.method == 'POST': + teamcreateform = TeamForm(request.POST) + if teamcreateform.is_valid(): + cd = teamcreateform.cleaned_data + name = cd['name'] + notes = cd['notes'] + manager = request.user + private = cd['private'] + res,message=teams.create_team(name,manager,private,notes) + url = reverse(rower_teams_view) + response = HttpResponseRedirect(url) + return response + + else: + teamcreateform = TeamForm() + + return render(request,'teamcreate.html', + { + 'form':teamcreateform, + }) + +@user_passes_test(iscoachmember,login_url="/",redirect_field_name=None) +def team_deleteconfirm_view(request,id): + r = Rower.objects.get(user=request.user) + try: + t = Team.objects.get(id=id) + except Team.DoesNotExist: + return HttpResponse("This team doesn't exist") + if t.manager != request.user: + return HttpResponse("You are not allowed to delete this team") + + return render(request,'teamdeleteconfirm.html', + { + 'team':t + }) + +@user_passes_test(iscoachmember,login_url="/",redirect_field_name=None) +def team_delete_view(request,id): + r = Rower.objects.get(user=request.user) + try: + t = Team.objects.get(id=id) + except Team.DoesNotExist: + return HttpResponse("This team doesn't exist") + if t.manager != request.user: + return HttpResponse("You are not allowed to delete this team") + + teams.remove_team(t.id) + + url = reverse(rower_teams_view) + response = HttpResponseRedirect(url) + return response