Private
Public Access
1
0

Merge branch 'release/v10.19'

This commit is contained in:
Sander Roosendaal
2019-08-29 15:48:49 +02:00
14 changed files with 275 additions and 83 deletions
+10
View File
@@ -108,6 +108,7 @@ def alert_get_stats(alert,nperiod=0):
'nr_strokes':0, 'nr_strokes':0,
'nr_strokes_qualifying':0, 'nr_strokes_qualifying':0,
'percentage':0, 'percentage':0,
'nperiod':nperiod,
} }
# check if filters are in columns list # check if filters are in columns list
@@ -139,6 +140,7 @@ def alert_get_stats(alert,nperiod=0):
'nr_strokes':0, 'nr_strokes':0,
'nr_strokes_qualifying':0, 'nr_strokes_qualifying':0,
'percentage':0, 'percentage':0,
'nperiod':nperiod,
} }
@@ -168,6 +170,10 @@ def alert_get_stats(alert,nperiod=0):
else: else:
percentage = 0 percentage = 0
median_q = df2[alert.measured.metric].median()
median = df[alert.measured.metric].median()
std = df[alert.measured.metric].std()
return { return {
'workouts':len(workouts), 'workouts':len(workouts),
'startdate':startdate, 'startdate':startdate,
@@ -175,6 +181,10 @@ def alert_get_stats(alert,nperiod=0):
'nr_strokes':nr_strokes, 'nr_strokes':nr_strokes,
'nr_strokes_qualifying':nr_strokes_qualifying, 'nr_strokes_qualifying':nr_strokes_qualifying,
'percentage': percentage, 'percentage': percentage,
'nperiod':nperiod,
'median':median,
'median_q':median_q,
'standard_dev':std,
} }
# run alert report # run alert report
+2
View File
@@ -136,6 +136,8 @@ class RowerPlanMiddleWare(object):
paymentprocessor='braintree') paymentprocessor='braintree')
r.paidplan = basicplans[0] r.paidplan = basicplans[0]
r.save() r.save()
# remove from Free Coach groups
# send email # send email
job = myqueue(queue, job = myqueue(queue,
handle_sendemail_expired, handle_sendemail_expired,
+36
View File
@@ -878,6 +878,19 @@ class Rower(models.Model):
def clean_email(self): def clean_email(self):
return self.user.email.lower() return self.user.email.lower()
def save(self, *args, **kwargs):
try:
for group in self.coachinggroups.all():
try:
coach = Rower.objects.get(mycoachgroup=group)
if coach.rowerplan == 'freecoach':
self.coachinggroups.remove(group)
except Rower.DoesNotExist:
pass
except ValueError:
pass
super(Rower, self).save(*args, **kwargs)
class DeactivateUserForm(forms.ModelForm): class DeactivateUserForm(forms.ModelForm):
class Meta: class Meta:
@@ -1083,6 +1096,11 @@ class Alert(models.Model):
return stri return stri
def metricname(self):
metricdict = {key:value for (key,value) in parchoicesy1}
return metricdict[self.measured.metric]
def description(self): def description(self):
metricdict = {key:value for (key,value) in parchoicesy1} metricdict = {key:value for (key,value) in parchoicesy1}
@@ -1105,6 +1123,24 @@ class Alert(models.Model):
return description return description
def shortdescription(self):
metricdict = {key:value for (key,value) in parchoicesy1}
if self.measured.condition == 'between':
description = '{value1} < {metric} < {value2}'.format(
metric = self.measured.metric,
value1 = self.measured.value1,
value2 = self.measured.value2,
)
else:
description = '{metric} {condition} {value1}'.format(
metric = self.measured.metric,
value1 = self.measured.value1,
condition = self.measured.condition
)
return description
class AlertEditForm(ModelForm): class AlertEditForm(ModelForm):
class Meta: class Meta:
+33 -2
View File
@@ -756,6 +756,26 @@ def handle_updatedps(useremail, workoutids, debug=False,**kwargs):
return 1 return 1
import math
def sigdig(value, digits = 3):
try:
order = int(math.floor(math.log10(math.fabs(value))))
except (ValueError,TypeError):
return value
# return integers as is
if value % 1 == 0:
return value
places = digits - order - 1
if places > 0:
fmtstr = "%%.%df" % (places)
else:
fmtstr = "%.0f"
return fmtstr % (round(value, places))
@app.task @app.task
def handle_send_email_alert( def handle_send_email_alert(
useremail, userfirstname, userlastname, rowerfirstname, alertname, stats, **kwargs): useremail, userfirstname, userlastname, rowerfirstname, alertname, stats, **kwargs):
@@ -770,15 +790,26 @@ def handle_send_email_alert(
else: else:
othertexts = None othertexts = None
subject = "Your rowing performance on rowsandall.com ({startdate} to {enddate})".format( report = {}
report['Percentage'] = int(stats['percentage'])
report['Number of workouts'] = int(stats['workouts'])
report['Data set'] = "{a} strokes out of {b}".format(
a = stats['nr_strokes_qualifying'],
b = stats['nr_strokes']
)
report['Median'] = sigdig(stats['median'])
report['Median of qualifying strokes'] = sigdig(stats['median_q'])
subject = "Rowsandall.com: {alertname} ({startdate} to {enddate})".format(
startdate = stats['startdate'], startdate = stats['startdate'],
enddate = stats['enddate'], enddate = stats['enddate'],
alertname=alertname,
) )
from_email = 'Rowsandall <info@rowsandall.com>' from_email = 'Rowsandall <info@rowsandall.com>'
d = { d = {
'report':stats, 'report':report,
'first_name':userfirstname, 'first_name':userfirstname,
'last_name':userlastname, 'last_name':userlastname,
'siteurl':siteurl, 'siteurl':siteurl,
+2 -2
View File
@@ -135,8 +135,8 @@ def add_member(id,rower):
t= Team.objects.get(id=id) t= Team.objects.get(id=id)
try: try:
rower.team.add(t) rower.team.add(t)
except ValidationError: except ValidationError as e:
return(0,"Couldn't add member") return(0,"Couldn't add member: "+str(e.message))
# code to add all workouts # code to add all workouts
ws = Workout.objects.filter(user=rower) ws = Workout.objects.filter(user=rower)
+21 -6
View File
@@ -1,5 +1,6 @@
{% extends "newbase.html" %} {% extends "newbase.html" %}
{% load staticfiles %} {% load staticfiles %}
{% load rowerfilters %}
{% block title %}Metric Alert{% endblock %} {% block title %}Metric Alert{% endblock %}
@@ -13,19 +14,33 @@
</p> </p>
<ul class="main-content"> <ul class="main-content">
<li>
{{ stats|lookuplong:'startdate' }} - {{ stats|lookuplong:'enddate' }}
</li>
<li class="grid_4"> <li class="grid_4">
<h2>Alert</h2> <h2>{{ alert.name }}</h2>
<p>{{ alert }}</p> <p>{{ alert }}</p>
<p>{{ alert.description }}</p> <p>{{ alert.description }}</p>
<p>This is a page under construction. Currently with minimal information</p> <p>This is a page under construction. Currently with minimal information</p>
</li> </li>
{% for key, value in stats.items %} <li class="rounder">
<li> <h2>Score</h2>
<h2>{{ key }}</h2> <hr>
<p>{{ value }}</p> <h2>{{ stats|lookup:'percentage' }}%</h2>
</li>
<li class="rounder">
<h2>Data set</h2>
<hr>
<p>{{ stats|lookup:'workouts' }} workouts</p>
<p>{{ stats|lookup:'nr_strokes_qualifying' }} strokes out of {{ stats|lookup:'nr_strokes' }}</p>
</li>
<li class="rounder">
<h2>Statistics</h2>
<hr>
<p>Median {{ alert.metricname }}: {{ stats|lookup:'median'|sigdig }}</p>
<p>Median {{ alert.metricname }}: {{ stats|lookup:'median_q'|sigdig }} ({{ alert.shortdescription }})</p>
</li> </li>
{% endfor %}
</ul> </ul>
+80 -46
View File
@@ -13,7 +13,7 @@
<ul class="main-content"> <ul class="main-content">
{% if alerts %} {% if alerts %}
{% for alert in alerts %} {% for alert in alerts %}
<li class="rounder"> <li class="rounder" id="alert_{{ alert.id }}">
<h2>{{ alert.name }}</h2> <h2>{{ alert.name }}</h2>
<a class="small" href="/rowers/alerts/{{ alert.id }}/edit/" title="Edit"> <a class="small" href="/rowers/alerts/{{ alert.id }}/edit/" title="Edit">
<i class="fas fa-pencil-alt fa-fw"></i> <i class="fas fa-pencil-alt fa-fw"></i>
@@ -22,10 +22,24 @@
title="Delete"> title="Delete">
<i class="fas fa-trash-alt fa-fw"></i> <i class="fas fa-trash-alt fa-fw"></i>
</a> </a>
<a class="small iteratorleft"
data-nperiod="{{ stats|alertnperiod:forloop.counter }}"
data-user="{{ rower.user.id }}"
data-alertid="{{ alert.id }}"
href="/rowers/alerts/{{ alert.id }}/report/">
<i class="fas fa-arrow-alt-left fa-fw"></i>
</a>
<a class="small iteratorright"
data-nperiod="{{ stats|alertnperiod:forloop.counter }}"
data-user="{{ rower.user.id }}"
data-alertid="{{ alert.id }}"
href="/rowers/alerts/{{ alert.id }}/report/">
<i class="fas fa-arrow-alt-right fa-fw"></i>
</a>
<hr> <hr>
<a href="/rowers/alerts/{{ alert.id }}/report/"> <a id="percentages" href="/rowers/alerts/{{ alert.id }}/report/">
<div id="id_alert_{{ forloop.counter }}"> <div>
<h1>{{ stats|alertstatspercentage:forloop.counter }}%</h1> <h1><span id="percentage">{{ stats|alertstatspercentage:forloop.counter }}</span>%</h1>
</div> </div>
</a> </a>
<p> <p>
@@ -34,51 +48,16 @@
<p> <p>
Workout type: {{ alert.workouttype }} Workout type: {{ alert.workouttype }}
</p> </p>
<p> <p id="dates">
Next Run: {{ alert.next_run }} <span id="startdate">
{{ stats|alertstartdate:forloop.counter }}
</span> -
<span id="enddate">
{{ stats|alertenddate:forloop.counter }}
</span>
</p> </p>
</li> </li>
{% endfor %} {% endfor %}
<li class="grid_4">
<table width="100%" class="listtable shortpadded">
<thead>
<tr>
<th>Name</th>
<th>metric</th>
<th>Workout type</th>
<th>Next Run</th>
</tr>
</thead>
<tbody>
{% for alert in alerts %}
<tr>
<td>{{ alert.name }}</td>
<td>{{ alert.measured.metric }}</td>
<td>{{ alert.workouttype }}</td>
<td>{{ alert.next_run }}</td>
<td>
<a class="small" href="/rowers/alerts/{{ alert.id }}/edit/" title="Edit">
<i class="fas fa-pencil-alt fa-fw"></i>
</a>
</td>
<td>
<a class="small"
href="/rowers/alerts/{{ alert.id }}/report/"
title="Report">
<i class="fal fa-table fa-fw"></i>
</a>
</td>
<td>
<a class="small" href="/rowers/alerts/{{ alert.id }}/delete/"
title="Delete">
<i class="fas fa-trash-alt fa-fw"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</li>
{% else %} {% else %}
<li class="grid_4"> <li class="grid_4">
<p>You have not set any alerts for {{ rower.user.first_name }}</p> <p>You have not set any alerts for {{ rower.user.first_name }}</p>
@@ -95,6 +74,61 @@
{% endblock %} {% endblock %}
{% block scripts %}
<script type='text/javascript'
src='https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js'>
</script>
<script>
$(document).ready(function(){
$(".iteratorleft").click(function( event ){
event.preventDefault();
var nperiod=$(this).data().nperiod+1;
var user = $(this).data().user;
var alertid = $(this).data().alertid;
var thediv = $(this);
url = "/rowers/alerts/"+alertid+"/report/"+nperiod+"/user/"+user+"/";
$.getJSON(window.location.protocol + '//' + window.location.host + url,
function(json) {
var percentage = json['stats']['percentage'];
var startdate = json['stats']['startdate'];
var enddate = json['stats']['enddate'];
thediv.siblings("#percentages").find("#percentage").text(percentage);
thediv.siblings("#dates").find("#startdate").text(startdate);
thediv.siblings("#dates").find("#enddate").text(enddate);
thediv.data().nperiod=nperiod;
if (nextperiod<0) {nextperiod=0};
var nextperiod = nperiod
thediv.siblings(".iteratorright").data().nperiod=nextperiod;
});
});
$(".iteratorright").click(function( event ){
event.preventDefault();
var nperiod=$(this).data().nperiod-1;
if ( nperiod<0 ) {
nperiod=0
};
var user = $(this).data().user;
var alertid = $(this).data().alertid;
var thediv = $(this)
url = "/rowers/alerts/"+alertid+"/report/"+nperiod+"/user/"+user+"/";
$.getJSON(window.location.protocol + '//' + window.location.host + url,
function(json) {
var percentage = json['stats']['percentage'];
var startdate = json['stats']['startdate'];
var enddate = json['stats']['enddate'];
thediv.siblings("#percentages").find("#percentage").text(percentage);
thediv.siblings("#dates").find("#startdate").text(startdate);
thediv.siblings("#dates").find("#enddate").text(enddate);
thediv.data().nperiod=nperiod;
var nextperiod = nperiod;
thediv.siblings(".iteratorleft").data().nperiod=nextperiod;
});
});
});
</script>
{% endblock %}
{% block sidebar %} {% block sidebar %}
{% include 'menu_analytics.html' %} {% include 'menu_analytics.html' %}
+1 -1
View File
@@ -86,7 +86,7 @@
</p> </p>
</li> </li>
<li class="rounder"> <li class="rounder">
<h1>Power Progress</h1> <h2>Power Progress</h2>
<a href="/rowers/fitness-progress/"> <a href="/rowers/fitness-progress/">
<div class="vignet"> <div class="vignet">
<img src="/static/img/powerprogress.png" alt="Power Progress"> <img src="/static/img/powerprogress.png" alt="Power Progress">
+12 -4
View File
@@ -1,13 +1,21 @@
<!doctype html>
<html> <html>
<head>
<meta name="viewport" content="width=device-width">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Rowsandall</title>
<style>
</style>
</head>
<body> <body>
<font face="verdana, sans-serif"> <font face="verdana, sans-serif">
<img src="https://rowsandall.com/static/img/logoemail.png" height="50"> <img src="https://rowsandall.com/static/img/logoemail.png" height="50">
<font face="verdana, sans-serif"> <font face="verdana, sans-serif">
{% block body %} {% block body %}
{% endblock %} {% endblock %}
</font> </font>
</body> </body>
+8
View File
@@ -54,6 +54,14 @@
{% if team.manager == user %} {% if team.manager == user %}
<p>Use the form to add a new user. You can either select a user from the list of your existing club members who are not on this team yet, or you can type the user's email address, which also works for users who have not registered to the site yet.</p> <p>Use the form to add a new user. You can either select a user from the list of your existing club members who are not on this team yet, or you can type the user's email address, which also works for users who have not registered to the site yet.</p>
{% if team.manager.rower.rowerplan == 'freecoach' %}
<p>
As a Free Coach user, your team members can only be users on a paid
plan. You can also upgrade to a
<a href="/rowers/paidplans/">Paid Coach Plan</a>
which allows you to coach users on a free Rower Plan.
</p>
{% endif %}
{% if inviteform.errors %} {% if inviteform.errors %}
<p style="color: red;"> <p style="color: red;">
Please correct the error{{ inviteform.errors|pluralize }} below. Please correct the error{{ inviteform.errors|pluralize }} below.
+48 -3
View File
@@ -4,6 +4,7 @@ from time import strftime
from django.utils import timezone from django.utils import timezone
import dateutil.parser import dateutil.parser
import json import json
import math
import datetime import datetime
import re import re
register = template.Library() register = template.Library()
@@ -37,6 +38,25 @@ from django.template.defaultfilters import stringfilter
from six import string_types from six import string_types
@register.filter
def sigdig(value, digits = 3):
try:
order = int(math.floor(math.log10(math.fabs(value))))
except (ValueError,TypeError):
return value
# return integers as is
if value % 1 == 0:
return value
places = digits - order - 1
if places > 0:
fmtstr = "%%.%df" % (places)
else:
fmtstr = "%.0f"
return fmtstr % (round(value, places))
@register.filter(is_safe=True, needs_autoescape=True) @register.filter(is_safe=True, needs_autoescape=True)
@stringfilter @stringfilter
def urlshorten(value, limit,autoescape=None): def urlshorten(value, limit,autoescape=None):
@@ -71,10 +91,27 @@ from rowers.teams import rower_get_managers
@register.filter @register.filter
def alertstatspercentage(list,i): def alertstatspercentage(list,i):
alertstats = list[i-1] alertstats = list[i-1]
print(alertstats)
return alertstats["percentage"] return alertstats["percentage"]
@register.filter
def alertstartdate(list,i):
alertstats = list[i-1]
return alertstats["startdate"]
@register.filter
def alertnperiod(list,i):
alertstats = list[i-1]
return alertstats["nperiod"]
@register.filter
def alertenddate(list,i):
alertstats = list[i-1]
return alertstats["enddate"]
@register.filter @register.filter
def is_coach(rower,rowers): def is_coach(rower,rowers):
for r in rowers: for r in rowers:
@@ -261,9 +298,14 @@ def jsdict(dict,key):
s = dict.get(key) s = dict.get(key)
return mark_safe(json.dumps(s)) return mark_safe(json.dumps(s))
@register.filter @register.filter
def lookup(dict, key): def lookup(dict, key):
s = dict.get(key) try:
s = dict.get(key)
except KeyError:
return None
if isinstance(s,string_types) and len(s) > 22: if isinstance(s,string_types) and len(s) > 22:
s = s[:22] s = s[:22]
@@ -271,7 +313,10 @@ def lookup(dict, key):
@register.filter @register.filter
def lookuplong(dict, key): def lookuplong(dict, key):
s = dict.get(key) try:
s = dict.get(key)
except KeyError:
return None
return s return s
Binary file not shown.
+1
View File
@@ -4329,6 +4329,7 @@ def alerts_view(request,userid=0):
for alert in alerts: for alert in alerts:
stats.append(alert_get_stats(alert)) stats.append(alert_get_stats(alert))
breadcrumbs = [ breadcrumbs = [
{ {
'url':'/rowers/analysis', 'url':'/rowers/analysis',
+14 -12
View File
@@ -16,11 +16,16 @@ def team_view(request,id=0,userid=0):
teams.remove_expired_invites() teams.remove_expired_invites()
try: try:
t = Team.objects.get(id=id) t = Team.objects.get(id=id)
except Team.DoesNotExist: except Team.DoesNotExist:
raise Http404("Team doesn't exist") raise Http404("Team doesn't exist")
if r.rowerplan == 'basic' and t.manager.rower.rowerplan != 'coach':
raise PermissionDenied("You need to be on a Paid Plan to see or join this team")
q = User.objects.filter(rower__isnull=False,rower__team__in=myteams).distinct().exclude(rower__team__name=t.name) q = User.objects.filter(rower__isnull=False,rower__team__in=myteams).distinct().exclude(rower__team__name=t.name)
mygroups = [request.user.rower.mycoachgroup] mygroups = [request.user.rower.mycoachgroup]
q2 = User.objects.filter(rower__isnull=False,rower__coachinggroups__in=mygroups).distinct().exclude(rower__team__name=t.name) q2 = User.objects.filter(rower__isnull=False,rower__coachinggroups__in=mygroups).distinct().exclude(rower__team__name=t.name)
@@ -168,19 +173,22 @@ def get_teams(request):
private='open').exclude( private='open').exclude(
rower=r).exclude(manager=request.user).order_by('name') rower=r).exclude(manager=request.user).order_by('name')
if r.rowerplan == 'basic':
otherteams = otherteams.filter(manager__rower__rowerplan='coach')
return myteams, memberteams, otherteams return myteams, memberteams, otherteams
@login_required() @login_required()
def rower_teams_view(request,message='',successmessage=''): def rower_teams_view(request):
if request.method == 'POST': if request.method == 'POST':
form = TeamInviteCodeForm(request.POST) form = TeamInviteCodeForm(request.POST)
if form.is_valid(): if form.is_valid():
code = form.cleaned_data['code'] code = form.cleaned_data['code']
res,text = teams.process_invite_code(request.user,code) res,text = teams.process_invite_code(request.user,code)
if res: if res:
successmessage = text messages.info(request,text)
else: else:
message = text messages.error(request,text)
else: else:
form = TeamInviteCodeForm() form = TeamInviteCodeForm()
@@ -249,8 +257,6 @@ def rower_teams_view(request,message='',successmessage=''):
# clubsize = teams.count_invites(request.user)+teams.count_club_members(request.user) # clubsize = teams.count_invites(request.user)+teams.count_club_members(request.user)
# max_clubsize = r.clubsize # max_clubsize = r.clubsize
messages.info(request,successmessage)
messages.error(request,message)
breadcrumbs = [ breadcrumbs = [
{ {
@@ -312,18 +318,14 @@ def manager_member_drop_view(request,teamid,userid,
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@login_required() @login_required()
def manager_requests_view(request,code=None,message='',successmessage=''): def manager_requests_view(request,code=None):
if code: if code:
res,text = teams.process_request_code(request.user,code) res,text = teams.process_request_code(request.user,code)
if res: if res:
successmessage = text messages.info(request,text)
message = ''
else: else:
message = text messages.error(request,text)
successmessage = ''
messages.info(request,successmessage)
messages.error(request,message)
url = reverse(rower_teams_view,kwargs={ url = reverse(rower_teams_view,kwargs={
}) })
return HttpResponseRedirect(url) return HttpResponseRedirect(url)