Merge branch 'release/v10.11'
This commit is contained in:
172
rowers/alerts.py
Normal file
172
rowers/alerts.py
Normal file
@@ -0,0 +1,172 @@
|
||||
from rowers.models import Alert, Condition, User, Rower, Workout
|
||||
from rowers.teams import coach_getcoachees
|
||||
from rowers.dataprep import getsmallrowdata_db,getrowdata_db
|
||||
import datetime
|
||||
## BASIC operations
|
||||
|
||||
# create alert
|
||||
def create_alert(manager, rower, measured,period=7, emailalert=True,
|
||||
reststrokes=False, workouttype='water',
|
||||
name='',**kwargs):
|
||||
|
||||
# check if manager is coach of rower. If not return 0
|
||||
if manager.rower != rower:
|
||||
if rower not in coach_getcoachees(manager.rower):
|
||||
return 0,'You are not allowed to create this alert'
|
||||
|
||||
m = Condition(
|
||||
metric = measured['metric'],
|
||||
value1 = measured['value1'],
|
||||
value2 = measured['value2'],
|
||||
condition=measured['condition']
|
||||
)
|
||||
|
||||
m.save()
|
||||
|
||||
alert = Alert(name=name,
|
||||
manager=manager,
|
||||
rower=rower,
|
||||
measured=m,
|
||||
reststrokes=reststrokes,
|
||||
period=period,
|
||||
emailalert=emailalert,
|
||||
workouttype=workouttype
|
||||
)
|
||||
|
||||
alert.save()
|
||||
|
||||
if 'filter' in kwargs:
|
||||
filters = kwargs['filter']
|
||||
for f in filters:
|
||||
if f['metric'] and f['condition']:
|
||||
m = Condition(
|
||||
metric = f['metric'],
|
||||
value1 = f['value1'],
|
||||
value2 = f['value2'],
|
||||
condition = f['condition']
|
||||
)
|
||||
|
||||
m.save()
|
||||
|
||||
alert.filter.add(m)
|
||||
|
||||
|
||||
return alert.id,'Your alert was created'
|
||||
|
||||
|
||||
|
||||
# update alert
|
||||
def alert_add_filters(alert,filters):
|
||||
for f in alert.filter.all():
|
||||
alert.filter.remove(f)
|
||||
f.delete()
|
||||
|
||||
for f in filters:
|
||||
metric = f['metric']
|
||||
value1 = f['value1']
|
||||
value2 = f['value2']
|
||||
condition = f['condition']
|
||||
|
||||
if condition and metric and value1:
|
||||
m = Condition(
|
||||
metric = f['metric'],
|
||||
value1 = f['value1'],
|
||||
value2 = f['value2'],
|
||||
condition = f['condition']
|
||||
)
|
||||
|
||||
m.save()
|
||||
|
||||
alert.filter.add(m)
|
||||
|
||||
return 1
|
||||
|
||||
# get alert stats
|
||||
# nperiod = 0: current period, i.e. next_run - n days to today
|
||||
# nperiod = 1: 1 period ago , i.e. next_run -2n days to next_run -n days
|
||||
def alert_get_stats(alert,nperiod=0):
|
||||
# get strokes
|
||||
workstrokesonly = not alert.reststrokes
|
||||
startdate = (alert.next_run - datetime.timedelta(days=(nperiod+1)*alert.period-1))
|
||||
enddate = alert.next_run - datetime.timedelta(days=(nperiod)*alert.period)
|
||||
columns = [alert.measured.metric]
|
||||
|
||||
for condition in alert.filter.all():
|
||||
columns += condition.metric
|
||||
|
||||
workouts = Workout.objects.filter(date__gte=startdate,date__lte=enddate,user=alert.rower,
|
||||
workouttype=alert.workouttype)
|
||||
ids = [w.id for w in workouts]
|
||||
|
||||
df = getsmallrowdata_db(columns,ids=ids,doclean=True,workstrokesonly=workstrokesonly)
|
||||
|
||||
if df.empty:
|
||||
return {
|
||||
'workouts':len(workouts),
|
||||
'startdate':startdate,
|
||||
'enddate':enddate,
|
||||
'nr_strokes':0,
|
||||
'nr_strokes_qualifying':0,
|
||||
}
|
||||
|
||||
# check if filters are in columns list
|
||||
pdcolumns = set(df.columns)
|
||||
|
||||
# drop strokes through filter
|
||||
if set(columns) <= pdcolumns:
|
||||
for condition in alert.filter.all():
|
||||
if condition.condition == '>':
|
||||
mask = df[condition.metric] > condition.value1
|
||||
df.loc[mask,alert.measured.metric] = np.nan
|
||||
elif condition.condition == '<':
|
||||
mask = df[condition.metric] < condition.value1
|
||||
df.loc[mask,alert.measured.metric] = np.nan
|
||||
elif condition.condition == 'between':
|
||||
mask = df[condition.metric] > condition.value1
|
||||
mask2 = df[condition.metric] < condition.value2
|
||||
df.loc[mask & mask2,alert.measured.metric] = np.nan
|
||||
elif condition.condition == '=':
|
||||
mask = df[condition.metric] == condition.value1
|
||||
df.loc[mask,alert.measured.metric] = np.nan
|
||||
|
||||
df.dropna(inplace=True,axis=0)
|
||||
else:
|
||||
return {
|
||||
'workouts':len(workouts),
|
||||
'startdate':startdate,
|
||||
'enddate':enddate,
|
||||
'nr_strokes':0,
|
||||
'nr_strokes_qualifying':0,
|
||||
}
|
||||
|
||||
|
||||
# count strokes
|
||||
nr_strokes = len(df)
|
||||
|
||||
|
||||
# count qualifying
|
||||
if alert.measured.condition == '>':
|
||||
mask = df[alert.measured.metric] > alert.measured.value1
|
||||
df2 = df[mask].copy()
|
||||
elif alert.measured.condition == '<':
|
||||
mask = df[alert.measured.metric] > alert.measured.value1
|
||||
df2 = df[mask].copy()
|
||||
elif alert.measured.condition == 'between':
|
||||
mask = df[alert.measured.metric] > alert.measured.value1
|
||||
mask2 = df[alert.measured.metric] < alert.measured.value2
|
||||
df2 = df[mask & mask2].copy()
|
||||
else:
|
||||
mask = df[alert.measured.metric] == alert.measured.value1
|
||||
df2 = df[mask].copy()
|
||||
|
||||
nr_strokes_qualifying = len(df2)
|
||||
|
||||
return {
|
||||
'workouts':len(workouts),
|
||||
'startdate':startdate,
|
||||
'enddate':enddate,
|
||||
'nr_strokes':nr_strokes,
|
||||
'nr_strokes_qualifying':nr_strokes_qualifying
|
||||
}
|
||||
|
||||
# run alert report
|
||||
62
rowers/management/commands/processalerts.py
Normal file
62
rowers/management/commands/processalerts.py
Normal file
@@ -0,0 +1,62 @@
|
||||
#!/srv/venv/bin/python
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
PY3K = sys.version_info >= (3,0)
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from rowers.models import Alert, Condition, User
|
||||
from rowers.tasks import handle_send_email_alert
|
||||
|
||||
from rowers import alerts
|
||||
|
||||
from rowers.utils import myqueue
|
||||
|
||||
|
||||
import django_rq
|
||||
queue = django_rq.get_queue('default')
|
||||
queuelow = django_rq.get_queue('low')
|
||||
queuehigh = django_rq.get_queue('low')
|
||||
|
||||
|
||||
import datetime
|
||||
|
||||
class Command(BaseCommand):
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--testing',
|
||||
action='store_true',
|
||||
dest='testing',
|
||||
default=False,
|
||||
help="Run in testing mode, don't send emails",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if 'testing' in options:
|
||||
testing = options['testing']
|
||||
else:
|
||||
testing = False
|
||||
|
||||
todaysalerts = Alert.objects.filter(next_run <= datetime.date.today(),emailalert=True)
|
||||
|
||||
for alert in todaysalerts:
|
||||
stats = alerts.alert_get_stats(alert)
|
||||
|
||||
# send email
|
||||
job = myqueue(queue,handle_send_email_alert,
|
||||
alert.manager.email,
|
||||
alert.manager.first_name,
|
||||
alert.manager.last_name,
|
||||
stats,debug=True)
|
||||
|
||||
# advance next_run
|
||||
if not testing:
|
||||
alert.next_run = datetime.date.today() + datetime.timedelta(days=alert.period)
|
||||
alert.save()
|
||||
|
||||
if testing:
|
||||
print('{nr} alerts found'.format(nr = len(todaysalerts)))
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
'Successfully processed alerts'))
|
||||
@@ -1011,6 +1011,83 @@ class BaseFavoriteFormSet(BaseFormSet):
|
||||
if not yparam2:
|
||||
yparam2 = 'None'
|
||||
|
||||
|
||||
|
||||
class Condition(models.Model):
|
||||
conditionchoices = (
|
||||
('<','<'),
|
||||
('>','>'),
|
||||
('=','='),
|
||||
('between','between')
|
||||
)
|
||||
metric = models.CharField(max_length=50,choices=parchoicesy1,verbose_name='Metric')
|
||||
value1 = models.FloatField(default=0)
|
||||
value2 = models.FloatField(default=0,null=True,blank=True)
|
||||
condition = models.CharField(max_length=20,choices=conditionchoices,null=True)
|
||||
|
||||
class ConditionEditForm(ModelForm):
|
||||
class Meta:
|
||||
model = Condition
|
||||
fields = ['metric','condition','value1','value2']
|
||||
|
||||
def clean(self):
|
||||
cd = self.cleaned_data
|
||||
if cd['condition'] == 'between' and cd['value2'] is None:
|
||||
raise forms.ValidationError('When using between, you must fill value 1 and value 2')
|
||||
|
||||
class BaseConditionFormSet(BaseFormSet):
|
||||
def clean(self):
|
||||
if any(self.errors):
|
||||
return
|
||||
|
||||
for form in self.forms:
|
||||
if form.cleaned_data:
|
||||
metric = form.cleaned_data['metric']
|
||||
condition = form.cleaned_data['condition']
|
||||
value1 = form.cleaned_data['value1']
|
||||
value2 = form.cleaned_data['value2']
|
||||
|
||||
|
||||
|
||||
rowchoices = []
|
||||
for key,value in mytypes.workouttypes:
|
||||
if key in mytypes.rowtypes:
|
||||
rowchoices.append((key,value))
|
||||
|
||||
|
||||
|
||||
class Alert(models.Model):
|
||||
name = models.CharField(max_length=150,verbose_name='Alert Name',null=True,blank=True)
|
||||
manager = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
rower = models.ForeignKey(Rower, on_delete=models.CASCADE)
|
||||
measured = models.OneToOneField(Condition,verbose_name='Measuring',on_delete=models.CASCADE,
|
||||
related_name='measured')
|
||||
filter = models.ManyToManyField(Condition, related_name='filters',verbose_name='Filters')
|
||||
reststrokes = models.BooleanField(default=False,null=True,verbose_name='Include Rest Strokes')
|
||||
period = models.IntegerField(default=7,verbose_name='Reporting Period (days)')
|
||||
next_run = models.DateField(default=timezone.now)
|
||||
emailalert = models.BooleanField(default=True,verbose_name='Send email alerts')
|
||||
workouttype = models.CharField(choices=rowchoices,max_length=50,
|
||||
verbose_name='Exercise/Boat Class',default='water')
|
||||
|
||||
|
||||
def __str__(self):
|
||||
stri = u'Alert {name} on {metric} for {workouttype}'.format(
|
||||
name = self.name,
|
||||
metric = self.measured.metric,
|
||||
workouttype = self.workouttype
|
||||
)
|
||||
|
||||
return stri
|
||||
|
||||
class AlertEditForm(ModelForm):
|
||||
class Meta:
|
||||
model = Alert
|
||||
fields = ['name','reststrokes','period','emailalert','workouttype']
|
||||
widgets = {
|
||||
'reststrokes':forms.CheckboxInput()
|
||||
}
|
||||
|
||||
class BasePlannedSessionFormSet(BaseFormSet):
|
||||
def clean(self):
|
||||
if any(self.serrors):
|
||||
|
||||
@@ -54,7 +54,7 @@ class OpaqueEncoder:
|
||||
|
||||
def decode_hex(self, s):
|
||||
"""Decode an 8-character hex string, returning the original integer."""
|
||||
return self.transcode(int(s, 16))
|
||||
return self.transcode(int(str(s), 16))
|
||||
|
||||
def decode_base64(self, s):
|
||||
"""Decode a 6-character base64 string, returning the original integer."""
|
||||
|
||||
@@ -756,6 +756,35 @@ def handle_updatedps(useremail, workoutids, debug=False,**kwargs):
|
||||
|
||||
return 1
|
||||
|
||||
@app.task
|
||||
def handle_send_email_alert(
|
||||
useremail, userfirstname, userlastname, stats, **kwargs):
|
||||
|
||||
if 'debug' in kwargs:
|
||||
debug = kwargs['debug']
|
||||
else:
|
||||
debug = False
|
||||
|
||||
subject = "Your rowing performance on rowsandall.com ({startdate} to {enddate})".format(
|
||||
startdate = stats['startdate'],
|
||||
enddate = stats['enddate'],
|
||||
)
|
||||
|
||||
from_email = 'Rowsandall <info@rowsandall.com>'
|
||||
|
||||
d = {
|
||||
'report':stats,
|
||||
'first_name':userfirstname,
|
||||
'last_name':userlastname,
|
||||
'siteurl':siteurl,
|
||||
}
|
||||
|
||||
res = send_template_email(from_email,[useremail],subject,
|
||||
'alertemail.html',
|
||||
d,**kwargs)
|
||||
|
||||
return 1
|
||||
|
||||
@app.task
|
||||
def handle_send_email_transaction(
|
||||
username, useremail, amount, **kwargs):
|
||||
|
||||
72
rowers/templates/alert_create.html
Normal file
72
rowers/templates/alert_create.html
Normal file
@@ -0,0 +1,72 @@
|
||||
{% extends "newbase.html" %}
|
||||
{% load staticfiles %}
|
||||
|
||||
{% block title %}Metric Alert{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<h1>Alert Create</h1>
|
||||
|
||||
<p>
|
||||
Alerts are useful to give you a regular update on how you are doing. For example, if you are
|
||||
worried about rowing too short, you can set an alert on drive length, and the site will automatically
|
||||
tell you how well you are doing.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
To set an alert on a minimum drive length, you would select "Drive Length (degree)" as the metric in the
|
||||
form below, then set the condition to ">" (greater than), and value 1 to the minimum drive length
|
||||
that you find acceptable. The value 2 is only relevant for alerts where you want to have a metric
|
||||
between two values. Set the workout type to "Standard Racing Shell", or whatever boat class you
|
||||
want this metric to run for, select the period over which you want to monitor and get regular
|
||||
reports (7 days).
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Optionally, you can add filters. With filters, the alert considers only those strokes that
|
||||
fulfill all filters. For example, you could set a filter on power between 200 and 300 Watt,
|
||||
to only look at drive length in that power zone.
|
||||
</p>
|
||||
|
||||
<form action="" method="post">
|
||||
<ul class="main-content">
|
||||
<li class="grid_2">
|
||||
<h2>Alert</h2>
|
||||
<p>
|
||||
{{ formset.management_form }}
|
||||
{% csrf_token %}
|
||||
<table>
|
||||
{{ form.as_table }}
|
||||
{{ measuredform.as_table }}
|
||||
</table>
|
||||
<input type="submit" value="Save">
|
||||
</p>
|
||||
</li>
|
||||
<li class="grid_2">
|
||||
{% for filter_form in formset %}
|
||||
<div class="fav-formset">
|
||||
<h2>Filter {{ forloop.counter }}</h2>
|
||||
<table width=100%>
|
||||
{{ filter_form.as_table }}
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
|
||||
<!-- Include formset plugin - including jQuery dependency -->
|
||||
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
|
||||
<script src="/static/js/jquery.formset.js"></script>
|
||||
<script>
|
||||
$('.fav-formset').formset({
|
||||
addText: '<div> </div><div>add filter</div>',
|
||||
deleteText: '<div><p> </p></div><div>remove</div>'
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% include 'menu_analytics.html' %}
|
||||
{% endblock %}
|
||||
29
rowers/templates/alert_delete_confirm.html
Normal file
29
rowers/templates/alert_delete_confirm.html
Normal file
@@ -0,0 +1,29 @@
|
||||
{% extends "newbase.html" %}
|
||||
{% load staticfiles %}
|
||||
|
||||
{% block title %}Planned Session{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<h1>Confirm Delete</h1>
|
||||
<p>This will permanently delete the alert</p>
|
||||
|
||||
<ul class="main-content">
|
||||
<li class="grid_2">
|
||||
<p>
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<p>Are you sure you want to delete <em>{{ object }}</em>?</p>
|
||||
<input class="button red" type="submit" value="Confirm">
|
||||
</form>
|
||||
</p>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% include 'menu_analytics.html' %}
|
||||
{% endblock %}
|
||||
70
rowers/templates/alert_edit.html
Normal file
70
rowers/templates/alert_edit.html
Normal file
@@ -0,0 +1,70 @@
|
||||
{% extends "newbase.html" %}
|
||||
{% load staticfiles %}
|
||||
|
||||
{% block title %}Metric Alert{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<p>
|
||||
Alerts are useful to give you a regular update on how you are doing. For example, if you are
|
||||
worried about rowing too short, you can set an alert on drive length, and the site will automatically
|
||||
tell you how well you are doing.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
To set an alert on a minimum drive length, you would select "Drive Length (degree)" as the metric in the
|
||||
form below, then set the condition to ">" (greater than), and value 1 to the minimum drive length
|
||||
that you find acceptable. The value 2 is only relevant for alerts where you want to have a metric
|
||||
between two values. Set the workout type to "Standard Racing Shell", or whatever boat class you
|
||||
want this metric to run for, select the period over which you want to monitor and get regular
|
||||
reports (7 days).
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Optionally, you can add filters. With filters, the alert considers only those strokes that
|
||||
fulfill all filters. For example, you could set a filter on power between 200 and 300 Watt,
|
||||
to only look at drive length in that power zone.
|
||||
</p>
|
||||
|
||||
<form action="" method="post">
|
||||
<ul class="main-content">
|
||||
<li class="grid_2">
|
||||
<h2>Alert</h2>
|
||||
<p>
|
||||
{{ formset.management_form }}
|
||||
{% csrf_token %}
|
||||
<table>
|
||||
{{ form.as_table }}
|
||||
{{ measuredform.as_table }}
|
||||
</table>
|
||||
<input type="submit" value="Save">
|
||||
</p>
|
||||
</li>
|
||||
<li class="grid_2">
|
||||
{% for filter_form in formset %}
|
||||
<div class="fav-formset">
|
||||
<h2>Filter {{ forloop.counter }}</h2>
|
||||
<table width=100%>
|
||||
{{ filter_form.as_table }}
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
|
||||
<!-- Include formset plugin - including jQuery dependency -->
|
||||
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
|
||||
<script src="/static/js/jquery.formset.js"></script>
|
||||
<script>
|
||||
$('.fav-formset').formset({
|
||||
addText: '<div> </div><div>add filter</div>',
|
||||
deleteText: '<div><p> </p></div><div>remove</div>'
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% include 'menu_analytics.html' %}
|
||||
{% endblock %}
|
||||
35
rowers/templates/alert_stats.html
Normal file
35
rowers/templates/alert_stats.html
Normal file
@@ -0,0 +1,35 @@
|
||||
{% extends "newbase.html" %}
|
||||
{% load staticfiles %}
|
||||
|
||||
{% block title %}Metric Alert{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
<p>
|
||||
<a href="/rowers/alerts/{{ alert.id }}/report/{{ nperiod|add:1 }}/user/{{ rower.user.id }}/">Previous</a>
|
||||
{% if nperiod > 0 %}
|
||||
<a href="/rowers/alerts/{{ alert.id }}/report/{{ nperiod|add:-1 }}/user/{{ rower.user.id }}/">Next</a>
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
<ul class="main-content">
|
||||
|
||||
<li class="grid_2">
|
||||
<h2>Alert</h2>
|
||||
<p>{{ alert }}</p>
|
||||
<p>This is a page under construction. Currently with minimal information</p>
|
||||
</li>
|
||||
{% for key, value in stats.items %}
|
||||
<li>
|
||||
<h2>{{ key }}</h2>
|
||||
<p>{{ value }}</p>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% include 'menu_analytics.html' %}
|
||||
{% endblock %}
|
||||
20
rowers/templates/alertemail.html
Normal file
20
rowers/templates/alertemail.html
Normal file
@@ -0,0 +1,20 @@
|
||||
{% extends "emailbase.html" %}
|
||||
|
||||
{% block body %}
|
||||
<p>Dear <strong>{{ first_name }}</strong>,</p>
|
||||
|
||||
<p>
|
||||
Here is the report for your alert on <a href="{{ siteurl }}">rowsandall.com</a>.
|
||||
</p>
|
||||
|
||||
{% for key, value in report.items() %}
|
||||
<p>
|
||||
{{ key }}: {{ value }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
|
||||
<p>
|
||||
Best Regards, the Rowsandall Team
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
||||
73
rowers/templates/alerts.html
Normal file
73
rowers/templates/alerts.html
Normal file
@@ -0,0 +1,73 @@
|
||||
{% extends "newbase.html" %}
|
||||
{% load staticfiles %}
|
||||
{% load rowerfilters %}
|
||||
|
||||
{% block title %}Rowsandall - Analysis {% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
<h1>Alerts for {{ rower.user.first_name }} {{ rower.user.last_name }}</h1>
|
||||
<p>Set up automatic alerting for your workouts</p>
|
||||
|
||||
|
||||
<ul class="main-content">
|
||||
{% if alerts %}
|
||||
<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 %}
|
||||
<li class="grid_4">
|
||||
<p>You have not set any alerts for {{ rower.user.first_name }}</p>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="grid_4">
|
||||
<p>
|
||||
<a href="/rowers/alerts/new/">Create new alert</a>
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% include 'menu_analytics.html' %}
|
||||
{% endblock %}
|
||||
@@ -11,7 +11,9 @@
|
||||
|
||||
<p>Rower: {{ rower.user.first_name }}</p>
|
||||
|
||||
<a href="/rowers/user-analysis-select">Be adventurous and try our new Analysis page</a>
|
||||
<p>
|
||||
<a href="/rowers/alerts/">Try out Alerts</a>
|
||||
</p>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
BIN
rowers/tests/testdata/testdata.csv.gz
vendored
BIN
rowers/tests/testdata/testdata.csv.gz
vendored
Binary file not shown.
@@ -417,6 +417,15 @@ urlpatterns = [
|
||||
name='multi_compare_view'),
|
||||
re_path(r'^multi-compare/workout/(?P<id>\b[0-9A-Fa-f]+\b)/$',views.multi_compare_view,name='multi_compare_view'),
|
||||
re_path(r'^multi-compare/$',views.multi_compare_view,name='multi_compare_view'),
|
||||
re_path(r'^alerts/(?P<userid>\d+)/$',views.alerts_view,name='alerts_view'),
|
||||
re_path(r'^alerts/$',views.alerts_view,name='alerts_view'),
|
||||
re_path(r'^alerts/(?P<pk>\d+)/delete/$',views.AlertDelete.as_view(),name='alert_delete_view'),
|
||||
re_path(r'^alerts/(?P<id>\d+)/edit/user/(?P<userid>\d+)/$',views.alert_edit_view,name='alert_edit_view'),
|
||||
re_path(r'^alerts/(?P<id>\d+)/edit/$',views.alert_edit_view,name='alert_edit_view'),
|
||||
re_path(r'^alerts/new/$',views.alert_create_view, name='alert_create_view'),
|
||||
re_path(r'^alerts/(?P<id>\d+)/report/user/(?P<userid>\d+)/$',views.alert_report_view,name='alert_report_view'),
|
||||
re_path(r'^alerts/(?P<id>\d+)/report/(?P<nperiod>\d+)/user/(?P<userid>\d+)/$',views.alert_report_view,name='alert_report_view'),
|
||||
re_path(r'^alerts/(?P<id>\d+)/report/$',views.alert_report_view,name='alert_report_view'),
|
||||
re_path(r'^user-boxplot/user/(?P<userid>\d+)/$',views.boxplot_view,name='boxplot_view'),
|
||||
re_path(r'^user-boxplot/$',views.boxplot_view,name='boxplot_view'),
|
||||
re_path(r'^user-boxplot-data/$',views.boxplot_view_data,name='boxplot_view_data'),
|
||||
|
||||
@@ -4314,3 +4314,319 @@ def agegrouprecordview(request,sex='male',weightcategory='hwt',
|
||||
'active':'nav-analysis',
|
||||
'the_div':div,
|
||||
})
|
||||
|
||||
# alert overview view
|
||||
@user_passes_test(ispromember, login_url="/rowers/paidplans",
|
||||
message="This functionality requires a Pro plan or higher",
|
||||
redirect_field_name=None)
|
||||
def alerts_view(request,userid=0):
|
||||
r = getrequestrower(request,userid=userid)
|
||||
|
||||
alerts = Alert.objects.filter(rower=r).order_by('next_run')
|
||||
|
||||
breadcrumbs = [
|
||||
{
|
||||
'url':'/rowers/analysis',
|
||||
'name': 'Analysis'
|
||||
},
|
||||
{
|
||||
'url': reverse('alerts_view'),
|
||||
'name': 'Alerts',
|
||||
},
|
||||
]
|
||||
|
||||
return render(request,'alerts.html',
|
||||
{
|
||||
'breadcrumbs':breadcrumbs,
|
||||
'alerts':alerts,
|
||||
'rower':r,
|
||||
})
|
||||
|
||||
# alert create view
|
||||
@user_passes_test(ispromember, login_url="/rowers/paidplans",
|
||||
message="This functionality requires a Pro plan or higher",
|
||||
redirect_field_name=None)
|
||||
def alert_create_view(request,userid=0):
|
||||
r = getrequestrower(request,userid=userid)
|
||||
FilterFormSet = formset_factory(ConditionEditForm, formset=BaseConditionFormSet,extra=1)
|
||||
filter_formset = FilterFormSet()
|
||||
|
||||
if request.method == 'POST':
|
||||
form = AlertEditForm(request.POST)
|
||||
measuredform = ConditionEditForm(request.POST)
|
||||
filter_formset = FilterFormSet(request.POST)
|
||||
if form.is_valid() and measuredform.is_valid() and filter_formset.is_valid():
|
||||
ad = form.cleaned_data
|
||||
measured = measuredform.cleaned_data
|
||||
|
||||
period = ad['period']
|
||||
emailalert = ad['emailalert']
|
||||
reststrokes = ad['reststrokes']
|
||||
workouttype = ad['workouttype']
|
||||
name = ad['name']
|
||||
|
||||
filters = []
|
||||
|
||||
for filter_form in filter_formset:
|
||||
metric = filter_form.cleaned_data.get('metric')
|
||||
condition = filter_form.cleaned_data.get('condition')
|
||||
value1 = filter_form.cleaned_data.get('value1')
|
||||
value2 = filter_form.cleaned_data.get('value2')
|
||||
|
||||
filters.append(
|
||||
{
|
||||
'metric':metric,
|
||||
'condition':condition,
|
||||
'value1':value1,
|
||||
'value2':value2,
|
||||
}
|
||||
)
|
||||
|
||||
result,message = create_alert(request.user,r,measured,period=period,emailalert=emailalert,
|
||||
reststrokes=reststrokes,workouttype=workouttype,
|
||||
filter = filters,
|
||||
name=name)
|
||||
|
||||
if result:
|
||||
messages.info(request,message)
|
||||
|
||||
url = reverse('alert_edit_view',kwargs={'id':result})
|
||||
return HttpResponseRedirect(url)
|
||||
else:
|
||||
form = AlertEditForm()
|
||||
measuredform = ConditionEditForm()
|
||||
|
||||
breadcrumbs = [
|
||||
{
|
||||
'url':'/rowers/analysis',
|
||||
'name': 'Analysis'
|
||||
},
|
||||
{
|
||||
'url': reverse('alerts_view'),
|
||||
'name': 'Alerts',
|
||||
},
|
||||
{
|
||||
'url': reverse('alert_create_view'),
|
||||
'name': 'Create'
|
||||
}
|
||||
]
|
||||
|
||||
return render(request,'alert_create.html',
|
||||
{
|
||||
'breadcrumbs':breadcrumbs,
|
||||
'formset': filter_formset,
|
||||
'rower':r,
|
||||
'form':form,
|
||||
'measuredform':measuredform,
|
||||
})
|
||||
|
||||
# alert report view
|
||||
@user_passes_test(ispromember, login_url="/rowers/paidplans",
|
||||
message="This functionality requires a Pro plan or higher",
|
||||
redirect_field_name=None)
|
||||
def alert_report_view(request,id=0,userid=0,nperiod=0):
|
||||
r = getrequestrower(request,userid=userid)
|
||||
if userid == 0:
|
||||
userid = request.user.id
|
||||
|
||||
alert = Alert.objects.get(id=id)
|
||||
nperiod = int(nperiod)
|
||||
|
||||
try:
|
||||
alert = Alert.objects.get(id=id)
|
||||
except Alert.DoesNotExist:
|
||||
raise Http404("This alert doesn't exist")
|
||||
|
||||
|
||||
if alert.manager != request.user:
|
||||
raise PermissionDenied('You are not allowed to edit this Alert')
|
||||
|
||||
stats = alert_get_stats(alert,nperiod=nperiod)
|
||||
|
||||
breadcrumbs = [
|
||||
{
|
||||
'url':'/rowers/analysis',
|
||||
'name': 'Analysis'
|
||||
},
|
||||
{
|
||||
'url':reverse('alerts_view'),
|
||||
'name':'Alerts',
|
||||
},
|
||||
{
|
||||
'url': reverse('alert_edit_view',
|
||||
kwargs={'userid':userid,'id':alert.id}),
|
||||
'name': alert.name,
|
||||
},
|
||||
{
|
||||
'url': reverse('alert_report_view',
|
||||
kwargs={'userid':userid,'id':alert.id}),
|
||||
'name': 'Report',
|
||||
},
|
||||
]
|
||||
return render(request,'alert_stats.html',
|
||||
{
|
||||
'breadcrumbs':breadcrumbs,
|
||||
'stats':stats,
|
||||
'rower':r,
|
||||
'alert':alert,
|
||||
'nperiod':nperiod,
|
||||
})
|
||||
|
||||
# alert edit view
|
||||
@user_passes_test(ispromember, login_url="/rowers/paidplans",
|
||||
message="This functionality requires a Pro plan or higher",
|
||||
redirect_field_name=None)
|
||||
def alert_edit_view(request,id=0,userid=0):
|
||||
r = getrequestrower(request,userid=userid)
|
||||
|
||||
try:
|
||||
alert = Alert.objects.get(id=id)
|
||||
except Alert.DoesNotExist:
|
||||
raise Http404("This alert doesn't exist")
|
||||
|
||||
|
||||
if alert.manager != request.user:
|
||||
raise PermissionDenied('You are not allowed to edit this Alert')
|
||||
|
||||
FilterFormSet = formset_factory(ConditionEditForm, formset=BaseConditionFormSet,extra=0)
|
||||
if len(alert.filter.all()) == 0:
|
||||
FilterFormSet = formset_factory(ConditionEditForm, formset=BaseConditionFormSet, extra=1)
|
||||
|
||||
filter_data = [{'metric':m.metric,
|
||||
'value1':m.value1,
|
||||
'value2':m.value2,
|
||||
'condition':m.condition}
|
||||
for m in alert.filter.all()]
|
||||
|
||||
if request.method == 'POST':
|
||||
form = AlertEditForm(request.POST)
|
||||
measuredform = ConditionEditForm(request.POST)
|
||||
filter_formset = FilterFormSet(request.POST)
|
||||
if form.is_valid() and measuredform.is_valid() and filter_formset.is_valid():
|
||||
ad = form.cleaned_data
|
||||
measured = measuredform.cleaned_data
|
||||
|
||||
period = ad['period']
|
||||
emailalert = ad['emailalert']
|
||||
reststrokes = ad['reststrokes']
|
||||
workouttype = ad['workouttype']
|
||||
name = ad['name']
|
||||
|
||||
m = alert.measured
|
||||
m.metric = measured['metric']
|
||||
m.value1 = measured['value1']
|
||||
m.value2 = measured['value2']
|
||||
m.condition = measured['condition']
|
||||
m.save()
|
||||
|
||||
alert.period = period
|
||||
alert.emailalert = emailalert
|
||||
alert.reststrokes = reststrokes
|
||||
alert.workouttype = workouttype
|
||||
alert.name = name
|
||||
alert.save()
|
||||
|
||||
filters = []
|
||||
|
||||
for filter_form in filter_formset:
|
||||
metric = filter_form.cleaned_data.get('metric')
|
||||
condition = filter_form.cleaned_data.get('condition')
|
||||
value1 = filter_form.cleaned_data.get('value1')
|
||||
value2 = filter_form.cleaned_data.get('value2')
|
||||
|
||||
filters.append(
|
||||
{
|
||||
'metric':metric,
|
||||
'condition':condition,
|
||||
'value1':value1,
|
||||
'value2':value2,
|
||||
}
|
||||
)
|
||||
|
||||
res = alert_add_filters(alert, filters)
|
||||
messages.info(request,'Alert was changed')
|
||||
|
||||
else:
|
||||
form = AlertEditForm(instance=alert)
|
||||
measuredform = ConditionEditForm(instance=alert.measured)
|
||||
filter_formset = FilterFormSet(initial=filter_data)
|
||||
|
||||
|
||||
|
||||
breadcrumbs = [
|
||||
{
|
||||
'url':'/rowers/analysis',
|
||||
'name': 'Analysis'
|
||||
},
|
||||
{
|
||||
'url':reverse('alerts_view'),
|
||||
'name':'Alerts',
|
||||
},
|
||||
{
|
||||
'url': reverse('alert_edit_view',
|
||||
kwargs={'userid':userid,'id':alert.id}),
|
||||
'name': alert.name,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
return render(request,'alert_edit.html',
|
||||
{
|
||||
'breadcrumbs':breadcrumbs,
|
||||
'rower':r,
|
||||
'form':form,
|
||||
'measuredform':measuredform,
|
||||
'formset':filter_formset,
|
||||
})
|
||||
|
||||
# alert delete view
|
||||
class AlertDelete(DeleteView):
|
||||
login_requird = True
|
||||
model = Alert
|
||||
template_name = 'alert_delete_confirm.html'
|
||||
|
||||
# extra parameters
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(AlertDelete, self).get_context_data(**kwargs)
|
||||
|
||||
if 'userid' in kwargs:
|
||||
userid = kwargs['userid']
|
||||
else:
|
||||
userid = 0
|
||||
|
||||
context['rower'] = getrequestrower(self.request,userid=userid)
|
||||
context['alert'] = self.object
|
||||
|
||||
breadcrumbs = [
|
||||
{
|
||||
'url':'/rowers/analysis',
|
||||
'name': 'Analysis'
|
||||
},
|
||||
{
|
||||
'url':reverse('alerts_view'),
|
||||
'name':'Alerts',
|
||||
},
|
||||
{
|
||||
'url': reverse('alert_edit_view',
|
||||
kwargs={'userid':userid,'id':self.object.pk}),
|
||||
'name': 'Alert',
|
||||
},
|
||||
{
|
||||
'url': reverse('alert_delete_view',kwargs={'pk':self.object.pk}),
|
||||
'name': 'Delete'
|
||||
}
|
||||
]
|
||||
|
||||
context['breadcrumbs'] = breadcrumbs
|
||||
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('alerts_view')
|
||||
|
||||
def get_object(self, *args, **kwargs):
|
||||
obj = super(AlertDelete, self).get_object(*args, **kwargs)
|
||||
|
||||
# some checks
|
||||
|
||||
return obj
|
||||
|
||||
@@ -94,6 +94,7 @@ from rowers.models import (
|
||||
microcyclecheckdates,mesocyclecheckdates,macrocyclecheckdates,
|
||||
TrainingMesoCycleForm, TrainingMicroCycleForm,
|
||||
RaceLogo,RowerBillingAddressForm,PaidPlan,
|
||||
AlertEditForm, ConditionEditForm,
|
||||
PlannedSessionComment,CoachRequest,CoachOffer,checkaccessplanuser
|
||||
)
|
||||
from rowers.models import (
|
||||
@@ -108,10 +109,11 @@ from rowers.models import (
|
||||
VirtualRaceForm,VirtualRaceResultForm,RowerImportExportForm,
|
||||
IndoorVirtualRaceResultForm,IndoorVirtualRaceResult,
|
||||
IndoorVirtualRaceForm,PlannedSessionCommentForm,
|
||||
Alert, Condition
|
||||
)
|
||||
from rowers.models import (
|
||||
FavoriteForm,BaseFavoriteFormSet,SiteAnnouncement,BasePlannedSessionFormSet,
|
||||
get_course_timezone
|
||||
get_course_timezone,BaseConditionFormSet,
|
||||
)
|
||||
from rowers.metrics import rowingmetrics,defaultfavoritecharts,nometrics
|
||||
from rowers import metrics as metrics
|
||||
@@ -206,6 +208,7 @@ import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
from rowers.emails import send_template_email,htmlstrip
|
||||
from rowers.alerts import *
|
||||
|
||||
from pytz import timezone as tz,utc
|
||||
from timezonefinder import TimezoneFinder
|
||||
|
||||
Reference in New Issue
Block a user