From 75a37ff6427353fc2437c6e35c198bd495cb2dc1 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 8 Feb 2017 11:30:42 +0100 Subject: [PATCH 01/11] added gzip to OTW power calc --- rowers/tasks.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/rowers/tasks.py b/rowers/tasks.py index dea6dd32..b89d11a8 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -116,8 +116,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 +157,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." From 7e5d751aeffeb4414da9de635e1df66b26087fd5 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 8 Feb 2017 15:42:11 +0100 Subject: [PATCH 02/11] model changes to support team --- rowers/admin.py | 8 ++++++-- rowers/models.py | 8 ++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/rowers/admin.py b/rowers/admin.py index c5dd467e..887bbd25 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') 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/models.py b/rowers/models.py index 29fd5a4e..18591001 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -90,6 +90,13 @@ class Team(models.Model): def __unicode__(self): return self.name +class TeamInvite(models.Model): + team = models.ForeignKey(Team) + user = models.ForeignKey(User) + issuedate = models.DateField(default=timezone.now) + code = models.CharField(max_length=150) + + # Extension of User with rowing specific data class Rower(models.Model): weightcategories = ( @@ -149,6 +156,7 @@ class Rower(models.Model): choices=plans) planexpires = models.DateField(default=timezone.now) + clubsize = models.IntegerField(default=0) # Friends/Team friends = models.ManyToManyField("self",blank=True) From d279180d5de3c2f6afe63c5c8a089ba18f0eb7a7 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 8 Feb 2017 18:49:48 +0100 Subject: [PATCH 03/11] low level team functions --- rowers/admin.py | 2 +- rowers/models.py | 6 +- rowers/teams.py | 95 +++++++++++++++++++++++++++++ rowers/templates/advancededit.html | 10 +-- rowers/templates/advancedotw.html | 16 ++--- rowers/templates/analysis.html | 4 +- rowers/templates/base.html | 4 +- rowers/templates/bases.html | 4 +- rowers/templates/flexchart3.html | 2 +- rowers/templates/flexchart3otw.html | 2 +- rowers/templates/otwgeeky.html | 10 +-- rowers/views.py | 52 ++++++++-------- 12 files changed, 151 insertions(+), 56 deletions(-) create mode 100644 rowers/teams.py diff --git a/rowers/admin.py b/rowers/admin.py index 887bbd25..6c8da968 100644 --- a/rowers/admin.py +++ b/rowers/admin.py @@ -32,7 +32,7 @@ class TeamAdmin(admin.ModelAdmin): list_display = ('name','manager') class TeamInviteAdmin(admin.ModelAdmin): - list_display = ('issuedate','team','user') + list_display = ('issuedate','team','user','code') admin.site.unregister(User) admin.site.register(User,UserAdmin) diff --git a/rowers/models.py b/rowers/models.py index 18591001..c801799f 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -83,7 +83,7 @@ class PowerZonesField(models.TextField): # For future Team functionality class Team(models.Model): - name = models.CharField(max_length=150) + name = models.CharField(max_length=150,unique=True) notes = models.CharField(blank=True,max_length=200) manager = models.ForeignKey(User) @@ -92,9 +92,9 @@ class Team(models.Model): class TeamInvite(models.Model): team = models.ForeignKey(Team) - user = models.ForeignKey(User) + user = models.ForeignKey(User,null=True) issuedate = models.DateField(default=timezone.now) - code = models.CharField(max_length=150) + code = models.CharField(max_length=150,unique=True) # Extension of User with rowing specific data diff --git a/rowers/teams.py b/rowers/teams.py new file mode 100644 index 00000000..1d52b44a --- /dev/null +++ b/rowers/teams.py @@ -0,0 +1,95 @@ +# 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 rowers.models import ( + Rower, Workout, Team, TeamInvite,User + ) + +# Low level functions - to be called by higher level methods + +def create_team(name,manager,notes=''): + # needs some error testing + try: + t = Team(name=name,manager=manager,notes=notes) + 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 add_member(id,rower): + t= Team.objects.get(id=id) + rower.team.add(t) + return (1,'Member added') + +def remove_member(id,rower): + t = Team.objects.get(id=id) + rower.team.remove(t) + return (1,'Member removed') + +def mgr_remove_member(id,manager,rower): + t = Team.objects.get(id=id) + if t.manager == manager: + rower.team.remove(t) + return (1,'Member added') + else: + return (0,'You are not the team manager') + + return 0 + +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) + +# Medium level functionality + +def create_invite(team,manager,user=None): + 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') + if count_club_members(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) + invite.save() + return code + + + else: + return (0,'You are at your club size limit') + + return (0,'Nothing done') 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..57ee2294 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 %}

 

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/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/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/views.py b/rowers/views.py index 1f80dda1..2a49720c 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -199,7 +199,7 @@ def splitstdata(lijst): from utils import geo_distance,serialize_list,deserialize_list # 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 +1301,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 +1371,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 +1463,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 +1509,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 +1561,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 +1671,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 +2040,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 +2155,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 +2182,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 +2218,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 +2263,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 +2320,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 +2420,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 +2482,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 +2654,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 @@ -3086,7 +3086,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 +3113,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 +3161,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 +3316,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 +3353,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 +3555,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 +3818,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 +3871,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 +4934,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 = '' From 0969399e369f8dbc6ce0c8ea6db4b05f5077bd78 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 9 Feb 2017 08:43:07 +0100 Subject: [PATCH 04/11] moved data field prep for stats to dataprep from views --- rowers/dataprep.py | 94 +++++++++++++++++++++++++- rowers/models.py | 5 ++ rowers/views.py | 165 ++------------------------------------------- 3 files changed, 103 insertions(+), 161 deletions(-) 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 c801799f..8c75db10 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -83,9 +83,14 @@ class PowerZonesField(models.TextField): # For future Team functionality class Team(models.Model): + choices = ( + ('private','private'), + ('open','open'), + ) name = models.CharField(max_length=150,unique=True) notes = models.CharField(blank=True,max_length=200) manager = models.ForeignKey(User) + private = models.CharField(max_length=30,choices=choices,default='open') def __unicode__(self): return self.name diff --git a/rowers/views.py b/rowers/views.py index 2a49720c..402c6828 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -2738,97 +2738,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 +2828,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 +2838,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 = { From f393862a9296508e4a40a16def90fb304b72012c Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 9 Feb 2017 12:23:56 +0100 Subject: [PATCH 05/11] medium level and account page --- rowers/models.py | 21 ++- rowers/tasks.py | 34 ++++ rowers/teams.py | 88 ++++++++- rowers/templates/cumstats.html | 10 +- rowers/templates/rower_form.html | 291 ++++++++++++++++------------- rowers/templates/workoutstats.html | 12 +- 6 files changed, 311 insertions(+), 145 deletions(-) diff --git a/rowers/models.py b/rowers/models.py index 8c75db10..1e35b73e 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 @@ -63,6 +64,22 @@ class PowerZonesField(models.TextField): if not value: return if isinstance(value, list): return value + # remove double quotes and brackets + print value + 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) + print "aap" + print value + return value.split(self.token) def from_db_value(self,value, expression, connection, context): @@ -100,7 +117,7 @@ class TeamInvite(models.Model): 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) # Extension of User with rowing specific data class Rower(models.Model): @@ -310,7 +327,7 @@ class Workout(models.Model): ) user = models.ForeignKey(Rower) - team = models.ForeignKey(Team,blank=True,null=True) + team = models.ManyToManyField(Team,blank=True,null=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 b89d11a8..fc32e777 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -54,6 +54,40 @@ def handle_sendemail_unrecognized(unrecognizedfile,useremail): os.remove(unrecognizedfile) return 1 +# Send email with team invitation +@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 + + # Send email with TCX attachment @app.task def handle_sendemailtcx(first_name,last_name,email,tcxfile): diff --git a/rowers/teams.py b/rowers/teams.py index 1d52b44a..0b5b4b92 100644 --- a/rowers/teams.py +++ b/rowers/teams.py @@ -8,12 +8,16 @@ import time from django.db import IntegrityError import uuid +from rowers.tasks import handle_sendemail_invite + from rowers.models import ( Rower, Workout, Team, TeamInvite,User ) # Low level functions - to be called by higher level methods +inviteduration = 14 # days + def create_team(name,manager,notes=''): # needs some error testing try: @@ -32,23 +36,41 @@ def remove_team(id): 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) + for w in ws: + w.team.add(t) + return (1,'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) + for w in ws: + w.team.remove(t) + return (1,'Member removed') def mgr_remove_member(id,manager,rower): t = Team.objects.get(id=id) if t.manager == manager: - rower.team.remove(t) - return (1,'Member added') + remove_member(id,rower) + return (1,'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() @@ -65,9 +87,14 @@ 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): + r = Rower.objects.get(user=manager) if team.manager != manager: return (0,'Not the team manager') if user: @@ -77,7 +104,7 @@ def create_invite(team,manager,user=None): return (0,'Rower does not exist') if r2 in Rower.objects.filter(team=team): return (0,'Already member of that team') - if count_club_members(team.manager) < r.clubsize: + 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 @@ -86,10 +113,63 @@ def create_invite(team,manager,user=None): invite = TeamInvite(team=team,code=code,user=user) invite.save() - return code + 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 + + 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(email,name,code,teamname,manager) + else: + res = queue.enqueue(handle_sendemail_invite(email,name,code, + teamname, + manager)) + + return (1,'Invitation email sent') + +def process_invite_code(user,code): + 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/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 %} - + diff --git a/rowers/views.py b/rowers/views.py index 7e107de6..a5fb9dc0 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -5364,7 +5364,7 @@ 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).order_by('name') + 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)
{% 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/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/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 %} - + @@ -72,10 +94,38 @@
{% 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 %} From 887e7de53d213aef2ed8a82dabe561eec8c78b6c Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 9 Feb 2017 13:27:45 +0100 Subject: [PATCH 06/11] added teams and invitations placeholder views --- rowers/models.py | 3 --- rowers/templates/invitations.html | 27 +++++++++++++++++++++++++++ rowers/templates/teams.html | 27 +++++++++++++++++++++++++++ rowers/urls.py | 3 +++ rowers/views.py | 17 +++++++++++++++++ 5 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 rowers/templates/invitations.html create mode 100644 rowers/templates/teams.html diff --git a/rowers/models.py b/rowers/models.py index 1e35b73e..6d7f2c96 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -65,7 +65,6 @@ class PowerZonesField(models.TextField): if isinstance(value, list): return value # remove double quotes and brackets - print value value = re.sub(r'u\"','',value) value = re.sub(r'u\'','',value) value = re.sub(r'\\','',value) @@ -77,8 +76,6 @@ class PowerZonesField(models.TextField): value = re.sub(r'\]\]',']',value) value = re.sub(r'\ \ ',' ',value) value = re.sub(r', ',',',value) - print "aap" - print value return value.split(self.token) 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/teams.html b/rowers/templates/teams.html new file mode 100644 index 00000000..fd02b820 --- /dev/null +++ b/rowers/templates/teams.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} + +{% block title %}Teams {% endblock %} + +{% block content %} +
+
+

+

Teams

+ + Future teams page + +

+ + +
+
+

+

Invitations

+

+
+
+ + + + +{% endblock %} diff --git a/rowers/urls.py b/rowers/urls.py index 9d82dfc1..3ca3b8ab 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -199,6 +199,9 @@ 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'^me/invitation/$',views.rower_invitations_view), + url(r'^me/invitation/(\d+)/$',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 402c6828..c47625b9 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -5275,3 +5275,20 @@ def strokedatajson(request,id): #Method not supported return HttpResponseNotAllowed("Method not supported") + + +# Teams related views + +@login_required() +def rower_teams_view(request): + + return render(request, 'teams.html', + { + + }) + +@login_required() +def rower_invitations_view(request,code=None): + return render(request, 'invitations.html', + { + }) From f459cea2bd564e1e605282f5f1339ccf4ef5ce1c Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 9 Feb 2017 14:07:44 +0100 Subject: [PATCH 07/11] team expiry date added to Rower model --- rowers/models.py | 3 ++- rowers/teams.py | 19 ++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/rowers/models.py b/rowers/models.py index 6d7f2c96..343b9a22 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -175,6 +175,7 @@ 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 @@ -324,7 +325,7 @@ class Workout(models.Model): ) user = models.ForeignKey(Rower) - team = models.ManyToManyField(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/teams.py b/rowers/teams.py index 0b5b4b92..7223a378 100644 --- a/rowers/teams.py +++ b/rowers/teams.py @@ -33,6 +33,20 @@ 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) @@ -40,7 +54,9 @@ def add_member(id,rower): ws = Workout.objects.filter(user=rower) for w in ws: w.team.add(t) - + + set_teamplanexpires(rower) + return (1,'Member added') def remove_member(id,rower): @@ -51,6 +67,7 @@ def remove_member(id,rower): for w in ws: w.team.remove(t) + set_teamplanexpires(rower) return (1,'Member removed') def mgr_remove_member(id,manager,rower): From 40e18fee2172ff4b8edc02a5070b47670b937b44 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 9 Feb 2017 17:50:32 +0100 Subject: [PATCH 08/11] Team create, delete, leave, create invite --- rowers/models.py | 26 ++++- rowers/tasks.py | 81 +++++++++------ rowers/teams.py | 45 +++++--- rowers/templates/team.html | 61 +++++++++++ rowers/templates/teamcreate.html | 28 +++++ rowers/templates/teamdeleteconfirm.html | 43 ++++++++ rowers/templates/teamleaveconfirm.html | 45 ++++++++ rowers/templates/teams.html | 66 +++++++++++- rowers/urls.py | 8 +- rowers/views.py | 130 +++++++++++++++++++++++- 10 files changed, 473 insertions(+), 60 deletions(-) create mode 100644 rowers/templates/team.html create mode 100644 rowers/templates/teamcreate.html create mode 100644 rowers/templates/teamdeleteconfirm.html create mode 100644 rowers/templates/teamleaveconfirm.html diff --git a/rowers/models.py b/rowers/models.py index 343b9a22..bb34f99f 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -51,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 @@ -101,20 +104,35 @@ class Team(models.Model): ('private','private'), ('open','open'), ) - name = models.CharField(max_length=150,unique=True) - notes = models.CharField(blank=True,max_length=200) + 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') + 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()) + class Meta: + model = TeamInvite + fields = ['user','email'] # Extension of User with rowing specific data class Rower(models.Model): diff --git a/rowers/tasks.py b/rowers/tasks.py index fc32e777..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,39 +55,6 @@ def handle_sendemail_unrecognized(unrecognizedfile,useremail): os.remove(unrecognizedfile) return 1 -# Send email with team invitation -@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 - # Send email with TCX attachment @app.task @@ -267,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 index 7223a378..1cb5ba24 100644 --- a/rowers/teams.py +++ b/rowers/teams.py @@ -7,21 +7,26 @@ from datetime import timedelta import time from django.db import IntegrityError import uuid - -from rowers.tasks import handle_sendemail_invite +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,notes=''): +def create_team(name,manager,private='open',notes=''): # needs some error testing try: - t = Team(name=name,manager=manager,notes=notes) + t = Team(name=name,manager=manager,notes=notes, + private=private) t.save() r = Rower.objects.get(user=manager) r.team.add(t) @@ -52,8 +57,12 @@ def add_member(id,rower): rower.team.add(t) # code to add all workouts ws = Workout.objects.filter(user=rower) - for w in ws: - w.team.add(t) + + if settings.DEBUG: + res = handle_add_workouts_team(ws,t) + else: + res = queuehigh.enqueue(handle_add_workouts_team,ws,t) + set_teamplanexpires(rower) @@ -63,10 +72,13 @@ 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) - for w in ws: - w.team.remove(t) + 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 (1,'Member removed') @@ -110,7 +122,7 @@ def get_team_workouts(id): # Medium level functionality -def create_invite(team,manager,user=None): +def create_invite(team,manager,user=None,email=''): r = Rower.objects.get(user=manager) if team.manager != manager: return (0,'Not the team manager') @@ -121,6 +133,9 @@ def create_invite(team,manager,user=None): 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() @@ -128,7 +143,7 @@ def create_invite(team,manager,user=None): while code in codes: code = uuid.uuid4().hex[:10].upper() - invite = TeamInvite(team=team,code=code,user=user) + invite = TeamInvite(team=team,code=code,user=user,email=email) invite.save() return (invite.id,'Invitation created') @@ -144,6 +159,7 @@ def revoke_invite(id): return (1,'Invitation revoked') + def send_invite_email(id): invitation = TeamInvite.objects.get(id=id) if invitation.user: @@ -157,15 +173,14 @@ def send_invite_email(id): manager = invitation.team.manager.first_name+' '+invitation.team.manager.last_name if settings.DEBUG: - res = handle_sendemail_invite(email,name,code,teamname,manager) + res = handle_sendemail_invite.delay(email,name,code,teamname,manager) else: - res = queue.enqueue(handle_sendemail_invite(email,name,code, - teamname, - manager)) + 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: diff --git a/rowers/templates/team.html b/rowers/templates/team.html new file mode 100644 index 00000000..1a1bb618 --- /dev/null +++ b/rowers/templates/team.html @@ -0,0 +1,61 @@ +{% extends "base.html" %} + +{% block title %}Team {% endblock %} + +{% block content %} +
+

{{ team.name }}

+

{{ team.notes }}

+ +
+

+

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 index fd02b820..a26f6866 100644 --- a/rowers/templates/teams.html +++ b/rowers/templates/teams.html @@ -6,10 +6,33 @@

-

Teams

- - Future teams page - +

My Teams

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

You are not a member of any team.

+ {% endif %}

@@ -21,7 +44,40 @@
- +{% if user.rower.rowerplan == 'coach' %} +
+
+

Teams I manage

+ {% if myteams %} + + + + + + + + + {% for team in myteams %} + + + + + {% endfor %} + +
Name 
+ {{ team.name }} + +
+ Delete +
+
+ {% endif %} +
+ New Team +
+
+
+{% endif %} {% endblock %} diff --git a/rowers/urls.py b/rowers/urls.py index 3ca3b8ab..6832940e 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -200,8 +200,14 @@ urlpatterns = [ 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/(\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+)/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/(\d+)/$',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 c47625b9..1dcf51a1 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 ) 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,6 +200,12 @@ 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 ispromember(user): r = Rower.objects.get(user=user) @@ -5278,13 +5286,76 @@ def strokedatajson(request,id): # Teams related views +import teams + +@login_required() +def team_view(request,id=0): + + 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,message = teams.create_invite(t,t.manager, + user=newmember, + email=email) + if inviteid: + teams.send_invite_email(inviteid) + + 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') + + + + return render(request, 'team.html', + { + 'team':t, + 'members':members, + 'inviteform':inviteform, + }) + +@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) + teams = Team.objects.filter(rower=r) + myteams = Team.objects.filter(manager=request.user) + return render(request, 'teams.html', { - + 'teams':teams, + 'myteams':myteams, }) @login_required() @@ -5292,3 +5363,58 @@ def rower_invitations_view(request,code=None): return render(request, 'invitations.html', { }) + + +@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 From 32cadef2e5447cc84c53254995e4581329d21e0c Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 9 Feb 2017 17:56:03 +0100 Subject: [PATCH 09/11] bug fix --- rowers/models.py | 2 +- rowers/teams.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/rowers/models.py b/rowers/models.py index bb34f99f..b8c33184 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -129,7 +129,7 @@ class TeamInvite(models.Model): email = models.CharField(max_length=150,null=True,blank=True) class TeamInviteForm(ModelForm): - user = UserFullnameChoiceField(queryset=User.objects.all()) + user = UserFullnameChoiceField(queryset=User.objects.all(),required=False) class Meta: model = TeamInvite fields = ['user','email'] diff --git a/rowers/teams.py b/rowers/teams.py index 1cb5ba24..04609ebb 100644 --- a/rowers/teams.py +++ b/rowers/teams.py @@ -167,6 +167,7 @@ def send_invite_email(id): name = invitation.user.first_name + " " + invitation.user.last_name else: email = invitation.email + name = '' code = invitation.code teamname = invitation.team.name From e60320143a9027871f802f8effc696b2be2b822f Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 9 Feb 2017 20:45:10 +0100 Subject: [PATCH 10/11] Process team invites --- rowers/teams.py | 8 ++-- rowers/templates/base.html | 2 +- rowers/templates/team.html | 9 +++++ rowers/templates/teams.html | 54 ++++++++++++++++++++++++++- rowers/urls.py | 5 +++ rowers/views.py | 74 +++++++++++++++++++++++++++++++------ 6 files changed, 134 insertions(+), 18 deletions(-) diff --git a/rowers/teams.py b/rowers/teams.py index 04609ebb..19c0d06d 100644 --- a/rowers/teams.py +++ b/rowers/teams.py @@ -66,7 +66,7 @@ def add_member(id,rower): set_teamplanexpires(rower) - return (1,'Member added') + return (id,'Member added') def remove_member(id,rower): t = Team.objects.get(id=id) @@ -80,17 +80,17 @@ def remove_member(id,rower): res = queuehigh.enqueue(handle_remove_workouts_team,ws,t) set_teamplanexpires(rower) - return (1,'Member removed') + 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 (1,'Member removed') + return (id,'Member removed') else: return (0,'You are not the team manager') - return 0 + return (0,'') def count_invites(manager): ts = Team.objects.filter(manager=manager) diff --git a/rowers/templates/base.html b/rowers/templates/base.html index 57ee2294..48b6fde5 100644 --- a/rowers/templates/base.html +++ b/rowers/templates/base.html @@ -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/team.html b/rowers/templates/team.html index 1a1bb618..b5b8bcaf 100644 --- a/rowers/templates/team.html +++ b/rowers/templates/team.html @@ -6,9 +6,18 @@

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

diff --git a/rowers/templates/teams.html b/rowers/templates/teams.html index a26f6866..bb6e2213 100644 --- a/rowers/templates/teams.html +++ b/rowers/templates/teams.html @@ -38,9 +38,31 @@
+ {% 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 %}
@@ -53,7 +75,7 @@
Name Manager
{% endif %} - +
+ {% if otherteams %} +

Other Teams

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

 

+ {% endif %} +
{% endif %} diff --git a/rowers/urls.py b/rowers/urls.py index 6832940e..7c2c7bdc 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -200,13 +200,18 @@ urlpatterns = [ 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), diff --git a/rowers/views.py b/rowers/views.py index 1dcf51a1..7e107de6 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -31,7 +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 + Team,TeamForm,TeamInviteForm,TeamInvite ) from rowers.models import FavoriteForm,BaseFavoriteFormSet,SiteAnnouncement from django.forms.formsets import formset_factory @@ -5289,25 +5289,34 @@ def strokedatajson(request,id): import teams @login_required() -def team_view(request,id=0): +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,message = teams.create_invite(t,t.manager, + 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) @@ -5316,13 +5325,17 @@ def team_view(request,id=0): 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() @@ -5349,21 +5362,60 @@ def team_leave_view(request,id=0): @login_required() def rower_teams_view(request): r = Rower.objects.get(user=request.user) - teams = Team.objects.filter(rower=r) + ts = Team.objects.filter(rower=r) myteams = Team.objects.filter(manager=request.user) + otherteams = Team.objects.filter(private='open').exclude(rower=r).order_by('name') + teams.remove_expired_invites() + + invites = TeamInvite.objects.filter(user=request.user) return render(request, 'teams.html', { - 'teams':teams, + 'teams':ts, 'myteams':myteams, + 'invites':invites, + 'otherteams':otherteams, }) @login_required() -def rower_invitations_view(request,code=None): - return render(request, 'invitations.html', - { - }) +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): From 9e147528a4d8e2c7ab7840bb8332904fa9018506 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 9 Feb 2017 20:55:07 +0100 Subject: [PATCH 11/11] bug fix --- rowers/templates/teams.html | 2 +- rowers/views.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rowers/templates/teams.html b/rowers/templates/teams.html index bb6e2213..e651d31f 100644 --- a/rowers/templates/teams.html +++ b/rowers/templates/teams.html @@ -105,7 +105,7 @@
Name Manager