From 40e18fee2172ff4b8edc02a5070b47670b937b44 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 9 Feb 2017 17:50:32 +0100 Subject: [PATCH] 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