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:
|
if not yparam2:
|
||||||
yparam2 = 'None'
|
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):
|
class BasePlannedSessionFormSet(BaseFormSet):
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if any(self.serrors):
|
if any(self.serrors):
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class OpaqueEncoder:
|
|||||||
|
|
||||||
def decode_hex(self, s):
|
def decode_hex(self, s):
|
||||||
"""Decode an 8-character hex string, returning the original integer."""
|
"""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):
|
def decode_base64(self, s):
|
||||||
"""Decode a 6-character base64 string, returning the original integer."""
|
"""Decode a 6-character base64 string, returning the original integer."""
|
||||||
|
|||||||
@@ -756,6 +756,35 @@ def handle_updatedps(useremail, workoutids, debug=False,**kwargs):
|
|||||||
|
|
||||||
return 1
|
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
|
@app.task
|
||||||
def handle_send_email_transaction(
|
def handle_send_email_transaction(
|
||||||
username, useremail, amount, **kwargs):
|
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>
|
<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 %}
|
{% 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'),
|
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/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'^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/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/$',views.boxplot_view,name='boxplot_view'),
|
||||||
re_path(r'^user-boxplot-data/$',views.boxplot_view_data,name='boxplot_view_data'),
|
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',
|
'active':'nav-analysis',
|
||||||
'the_div':div,
|
'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,
|
microcyclecheckdates,mesocyclecheckdates,macrocyclecheckdates,
|
||||||
TrainingMesoCycleForm, TrainingMicroCycleForm,
|
TrainingMesoCycleForm, TrainingMicroCycleForm,
|
||||||
RaceLogo,RowerBillingAddressForm,PaidPlan,
|
RaceLogo,RowerBillingAddressForm,PaidPlan,
|
||||||
|
AlertEditForm, ConditionEditForm,
|
||||||
PlannedSessionComment,CoachRequest,CoachOffer,checkaccessplanuser
|
PlannedSessionComment,CoachRequest,CoachOffer,checkaccessplanuser
|
||||||
)
|
)
|
||||||
from rowers.models import (
|
from rowers.models import (
|
||||||
@@ -108,10 +109,11 @@ from rowers.models import (
|
|||||||
VirtualRaceForm,VirtualRaceResultForm,RowerImportExportForm,
|
VirtualRaceForm,VirtualRaceResultForm,RowerImportExportForm,
|
||||||
IndoorVirtualRaceResultForm,IndoorVirtualRaceResult,
|
IndoorVirtualRaceResultForm,IndoorVirtualRaceResult,
|
||||||
IndoorVirtualRaceForm,PlannedSessionCommentForm,
|
IndoorVirtualRaceForm,PlannedSessionCommentForm,
|
||||||
|
Alert, Condition
|
||||||
)
|
)
|
||||||
from rowers.models import (
|
from rowers.models import (
|
||||||
FavoriteForm,BaseFavoriteFormSet,SiteAnnouncement,BasePlannedSessionFormSet,
|
FavoriteForm,BaseFavoriteFormSet,SiteAnnouncement,BasePlannedSessionFormSet,
|
||||||
get_course_timezone
|
get_course_timezone,BaseConditionFormSet,
|
||||||
)
|
)
|
||||||
from rowers.metrics import rowingmetrics,defaultfavoritecharts,nometrics
|
from rowers.metrics import rowingmetrics,defaultfavoritecharts,nometrics
|
||||||
from rowers import metrics as metrics
|
from rowers import metrics as metrics
|
||||||
@@ -206,6 +208,7 @@ import numpy as np
|
|||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
from rowers.emails import send_template_email,htmlstrip
|
from rowers.emails import send_template_email,htmlstrip
|
||||||
|
from rowers.alerts import *
|
||||||
|
|
||||||
from pytz import timezone as tz,utc
|
from pytz import timezone as tz,utc
|
||||||
from timezonefinder import TimezoneFinder
|
from timezonefinder import TimezoneFinder
|
||||||
|
|||||||
Reference in New Issue
Block a user