From d0a8abc85b332c4013457e84f088f7dae4371719 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 7 Aug 2019 11:51:46 +0200 Subject: [PATCH 01/17] models for alerts and conditions --- rowers/models.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/rowers/models.py b/rowers/models.py index 7b42bf24..d10dfa8e 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -1011,6 +1011,36 @@ class BaseFavoriteFormSet(BaseFormSet): if not yparam2: yparam2 = 'None' + + +class Condition(models.Model): + conditionchoices = ( + ('<','<'), + ('>','>'), + ('=','='), + ) + metric = models.CharField(max_length=50,choices=parchoicesy1,verbose_name='Metric') + value = models.FloatField(default=0) + condition = models.CharField(max_length=2,choices=conditionchoices,null=True) + alert = models.ForeignKey('Alert',on_delete=models.CASCADE,null=True) + +rowchoices = [] +for key,value in mytypes.workouttypes: + if key in mytypes.rowtypes: + rowchoices.append((key,value)) + + +class Alert(models.Model): + measured = models.OneToOneField(Condition,verbose_name='Measuring',on_delete=models.CASCADE, + related_name='measured') + reststrokes = models.BooleanField(default=False,null=True,verbose_name='Include Rest Strokes') + period = models.IntegerField(default=7,verbose_name='Reporting Period') + 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') + + + class BasePlannedSessionFormSet(BaseFormSet): def clean(self): if any(self.serrors): From 9024aa7686bd19b2dff9fb8b015caae42d8111bf Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 9 Aug 2019 16:12:50 +0200 Subject: [PATCH 02/17] first model of basic alert functions --- rowers/alerts.py | 81 ++++++++++++++++++++++++++++++++++++++++++++++++ rowers/models.py | 10 ++++-- 2 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 rowers/alerts.py diff --git a/rowers/alerts.py b/rowers/alerts.py new file mode 100644 index 00000000..96101d3b --- /dev/null +++ b/rowers/alerts.py @@ -0,0 +1,81 @@ +from rowers.models import Alert, Condition, User, Rower +from rowers.teams import coach_getcoachees + +## BASIC operations + +# create alert +def create_alert(manager, rower, measured,period=7, emailalert=True, + reststrokes=False, workouttype='water', + name=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 + + 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, + restrokes=reststrokes, + period=period, + emailalert=emailalert, + workouttype=workouttype + ) + + alert.save() + + if 'filter' in kwargs: + for f in filter: + m = Condition( + metric = f['metric'], + value1 = f['value1'], + value2 = f['value2'], + condition = f['condition'] + ) + + m.save() + + alert.filter.add(m) + + + return 1 + + + +# update alert +def alert_add_filters(alert,filter): + for f in alert.filter.all(): + alert.filter.remove(f) + f.delete() + + for f in filter: + m = Condition( + metric = f['metric'], + value1 = f['value1'], + value2 = f['value2'], + condition = f['condition'] + ) + + m.save() + + alert.filter.add(m) + + + +# 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): + return {} + +# run alert report diff --git a/rowers/models.py b/rowers/models.py index d10dfa8e..e97e5d47 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -1018,11 +1018,12 @@ class Condition(models.Model): ('<','<'), ('>','>'), ('=','='), + ('between','between') ) metric = models.CharField(max_length=50,choices=parchoicesy1,verbose_name='Metric') - value = models.FloatField(default=0) + value1 = models.FloatField(default=0) + value2 = models.FloatField(default=0) condition = models.CharField(max_length=2,choices=conditionchoices,null=True) - alert = models.ForeignKey('Alert',on_delete=models.CASCADE,null=True) rowchoices = [] for key,value in mytypes.workouttypes: @@ -1031,10 +1032,15 @@ for key,value in mytypes.workouttypes: class Alert(models.Model): + name = models.CharField(max_length=150,verbose_name='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') + 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') From 8716bf2562cacbb7663f774d260608180c09fa1a Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sun, 11 Aug 2019 10:02:38 +0200 Subject: [PATCH 03/17] fixed opaque --- rowers/opaque.py | 2 +- rowers/tests/testdata/testdata.csv.gz | Bin 12543 -> 12543 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/rowers/opaque.py b/rowers/opaque.py index 949572d9..0927df99 100644 --- a/rowers/opaque.py +++ b/rowers/opaque.py @@ -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.""" diff --git a/rowers/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index 9676956fa2c2f4e9acbcebd9750b3ef3dd9e0d73..dc175d117e5ce37e1f82c70f5e2729a6db20c2b6 100644 GIT binary patch delta 16 XcmeyL_&^}?uKYs@h delta 16 XcmeyL_& Date: Sun, 11 Aug 2019 10:25:46 +0200 Subject: [PATCH 04/17] fd --- rowers/tests/testdata/testdata.csv.gz | Bin 12543 -> 12642 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/rowers/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index dc175d117e5ce37e1f82c70f5e2729a6db20c2b6..b7174123956e1f05b89cf6324a401c467ab1b57e 100644 GIT binary patch literal 12642 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 Q0RR630LQKMsTYd?0Iw-yN&o-= literal 12543 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 zʩ#=1hb_}VRZ>xe7ZaMW6yPtn@>A+Q@%LOfFfy)Fm_xH(`J$-2~OP)gF7NL@BK zX0A0p)Ouqy?~`6Ze@x_iBV@Ythd_bDd?3o@(y^vncAri7*&srq8U};h9kQ*SHC+q? zSFPodmwHUJon=2mtfOLmJkM4%f8tX*#xWm9+V%sVDwi@y%~tj!F~@$kTHHHR61RDA zI%hJiJ$;c3sk3edR33runKoOS($lLE;&@q`gHXIzj>LDI)^$Dpk+eV7-Z zaHxEQYRVHT>>J&^a Date: Wed, 14 Aug 2019 21:42:30 +0200 Subject: [PATCH 05/17] minimal working version of alert_get_stats --- rowers/alerts.py | 80 +++++++++++++++++++++++++++++++++++++++++++++--- rowers/models.py | 2 +- 2 files changed, 76 insertions(+), 6 deletions(-) diff --git a/rowers/alerts.py b/rowers/alerts.py index 96101d3b..2bbb0e81 100644 --- a/rowers/alerts.py +++ b/rowers/alerts.py @@ -1,12 +1,13 @@ -from rowers.models import Alert, Condition, User, Rower +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=name,**kwargs): + name='',**kwargs): # check if manager is coach of rower. If not return 0 if manager.rower != rower: @@ -26,7 +27,7 @@ def create_alert(manager, rower, measured,period=7, emailalert=True, manager=manager, rower=rower, measured=m, - restrokes=reststrokes, + reststrokes=reststrokes, period=period, emailalert=emailalert, workouttype=workouttype @@ -76,6 +77,75 @@ def alert_add_filters(alert,filter): # 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): - return {} + # 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, + } + + + # drop strokes through filter + 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) + + # 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 diff --git a/rowers/models.py b/rowers/models.py index e97e5d47..dfc5f807 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -1023,7 +1023,7 @@ class Condition(models.Model): metric = models.CharField(max_length=50,choices=parchoicesy1,verbose_name='Metric') value1 = models.FloatField(default=0) value2 = models.FloatField(default=0) - condition = models.CharField(max_length=2,choices=conditionchoices,null=True) + condition = models.CharField(max_length=20,choices=conditionchoices,null=True) rowchoices = [] for key,value in mytypes.workouttypes: From b7aa7f863cbe8135e657d6b29f34ca1c9b3a2000 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 15 Aug 2019 15:16:15 +0200 Subject: [PATCH 06/17] just removed old link from laboratory --- rowers/templates/.#laboratory.html | 1 + rowers/templates/laboratory.html | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 rowers/templates/.#laboratory.html diff --git a/rowers/templates/.#laboratory.html b/rowers/templates/.#laboratory.html new file mode 100644 index 00000000..25889a2a --- /dev/null +++ b/rowers/templates/.#laboratory.html @@ -0,0 +1 @@ +E408191@CZ27LT9RCGN72.13140:1565793987 \ No newline at end of file diff --git a/rowers/templates/laboratory.html b/rowers/templates/laboratory.html index bc9021f7..73d2368e 100644 --- a/rowers/templates/laboratory.html +++ b/rowers/templates/laboratory.html @@ -11,7 +11,7 @@

Rower: {{ rower.user.first_name }}

-Be adventurous and try our new Analysis page + {% endblock %} From de6d498717fe81d0115dad9e8b1530734d629ffb Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 16 Aug 2019 14:52:42 +0200 Subject: [PATCH 07/17] basic views (not complete) --- rowers/models.py | 10 ++- rowers/templates/alert_delete_confirm.html | 29 +++++++ rowers/templates/alerts.html | 73 ++++++++++++++++++ rowers/urls.py | 5 ++ rowers/views/analysisviews.py | 89 ++++++++++++++++++++++ rowers/views/statements.py | 1 + 6 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 rowers/templates/alert_delete_confirm.html create mode 100644 rowers/templates/alerts.html diff --git a/rowers/models.py b/rowers/models.py index dfc5f807..9d81610f 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -1045,7 +1045,15 @@ class Alert(models.Model): 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 BasePlannedSessionFormSet(BaseFormSet): def clean(self): diff --git a/rowers/templates/alert_delete_confirm.html b/rowers/templates/alert_delete_confirm.html new file mode 100644 index 00000000..dc9a5417 --- /dev/null +++ b/rowers/templates/alert_delete_confirm.html @@ -0,0 +1,29 @@ +{% extends "newbase.html" %} +{% load staticfiles %} + +{% block title %}Planned Session{% endblock %} + +{% block main %} +

Confirm Delete

+

This will permanently delete the alert

+ +
    +
  • +

    +

    + {% csrf_token %} +

    Are you sure you want to delete {{ object }}?

    + +
    +

    +
  • + +
+ + + +{% endblock %} + +{% block sidebar %} +{% include 'menu_analytics.html' %} +{% endblock %} diff --git a/rowers/templates/alerts.html b/rowers/templates/alerts.html new file mode 100644 index 00000000..4693136d --- /dev/null +++ b/rowers/templates/alerts.html @@ -0,0 +1,73 @@ +{% extends "newbase.html" %} +{% load staticfiles %} +{% load rowerfilters %} + +{% block title %}Rowsandall - Analysis {% endblock %} + +{% block main %} + +

Alerts for {{ rower.user.first_name }} {{ rower.user.last_name }}

+

Set up automatic alerting for your workouts

+ + +
    + {% if alerts %} +
  • + + + + + + + + + + + {% for alert in alerts %} + + + + + + + + + + {% endfor %} + +
    NamemetricWorkout typeNext Run
    {{ alert.name }}{{ alert.measured.metric }}{{ alert.workouttype }}{{ alert.next_run }} + + + + + + + + + + + +
    +
  • + {% else %} +
  • +

    You have not set any alerts for {{ rower.user.first_name }}

    +
  • + {% endif %} +
  • +

    + Create new alert +

    +
  • +
+ + + +{% endblock %} + +{% block sidebar %} +{% include 'menu_analytics.html' %} +{% endblock %} diff --git a/rowers/urls.py b/rowers/urls.py index 02d7b112..d6150c1a 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -417,6 +417,11 @@ urlpatterns = [ name='multi_compare_view'), re_path(r'^multi-compare/workout/(?P\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\d+)/$',views.alerts_view,name='alerts_view'), + re_path(r'^alerts/$',views.alerts_view,name='alerts_view'), + re_path(r'^alerts/(?P\d+)/delete/$',views.AlertDelete.as_view(),name='alert_delete_view'), + re_path(r'^alerts/(?P\d+)/edit/user/(?P\d+)/$',views.alert_edit_view,name='alert_edit_view'), + re_path(r'^alerts/(?P\d+)/edit/$',views.alert_edit_view,name='alert_edit_view'), re_path(r'^user-boxplot/user/(?P\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'), diff --git a/rowers/views/analysisviews.py b/rowers/views/analysisviews.py index 07c4d8a5..13fa1b89 100644 --- a/rowers/views/analysisviews.py +++ b/rowers/views/analysisviews.py @@ -4314,3 +4314,92 @@ 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') + + return render(request,'alerts.html', + { + '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) + + return render(request,'alert_create.html', + { + 'rower':r, + }) + +# alert report view + +# 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) + + return render(request,'alert_edit.html', + { + 'rower':r, + }) + +# 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('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 diff --git a/rowers/views/statements.py b/rowers/views/statements.py index b3a0745f..23d6e377 100644 --- a/rowers/views/statements.py +++ b/rowers/views/statements.py @@ -108,6 +108,7 @@ from rowers.models import ( VirtualRaceForm,VirtualRaceResultForm,RowerImportExportForm, IndoorVirtualRaceResultForm,IndoorVirtualRaceResult, IndoorVirtualRaceForm,PlannedSessionCommentForm, + Alert, Condition ) from rowers.models import ( FavoriteForm,BaseFavoriteFormSet,SiteAnnouncement,BasePlannedSessionFormSet, From 40ad973b33ce29d2be0503bedb1b21dcd55b332e Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 16 Aug 2019 18:18:54 +0200 Subject: [PATCH 08/17] alert form --- rowers/models.py | 5 +++++ rowers/templates/alert_create.html | 30 ++++++++++++++++++++++++++++++ rowers/templates/alert_edit.html | 30 ++++++++++++++++++++++++++++++ rowers/templates/alerts.html | 2 +- rowers/urls.py | 1 + rowers/views/analysisviews.py | 6 ++++++ rowers/views/statements.py | 1 + 7 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 rowers/templates/alert_create.html create mode 100644 rowers/templates/alert_edit.html diff --git a/rowers/models.py b/rowers/models.py index 9d81610f..1d0fcc42 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -1054,6 +1054,11 @@ class Alert(models.Model): ) return stri + +class AlertEditForm(ModelForm): + class Meta: + model = Alert + fields = ['name','measured','reststrokes','period','emailalert','workouttype'] class BasePlannedSessionFormSet(BaseFormSet): def clean(self): diff --git a/rowers/templates/alert_create.html b/rowers/templates/alert_create.html new file mode 100644 index 00000000..a637b3bd --- /dev/null +++ b/rowers/templates/alert_create.html @@ -0,0 +1,30 @@ +{% extends "newbase.html" %} +{% load staticfiles %} + +{% block title %}Planned Session{% endblock %} + +{% block main %} +

Alert Edit

+ +
    +
  • +

    +

    + {% csrf_token %} + + {{ form.as_table }} +
    + +
    +

    +
  • + +
+ + + +{% endblock %} + +{% block sidebar %} +{% include 'menu_analytics.html' %} +{% endblock %} diff --git a/rowers/templates/alert_edit.html b/rowers/templates/alert_edit.html new file mode 100644 index 00000000..a637b3bd --- /dev/null +++ b/rowers/templates/alert_edit.html @@ -0,0 +1,30 @@ +{% extends "newbase.html" %} +{% load staticfiles %} + +{% block title %}Planned Session{% endblock %} + +{% block main %} +

Alert Edit

+ +
    +
  • +

    +

    + {% csrf_token %} + + {{ form.as_table }} +
    + +
    +

    +
  • + +
+ + + +{% endblock %} + +{% block sidebar %} +{% include 'menu_analytics.html' %} +{% endblock %} diff --git a/rowers/templates/alerts.html b/rowers/templates/alerts.html index 4693136d..5740d620 100644 --- a/rowers/templates/alerts.html +++ b/rowers/templates/alerts.html @@ -59,7 +59,7 @@ {% endif %}
  • - Create new alert + Create new alert

  • diff --git a/rowers/urls.py b/rowers/urls.py index d6150c1a..14e04c6f 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -422,6 +422,7 @@ urlpatterns = [ re_path(r'^alerts/(?P\d+)/delete/$',views.AlertDelete.as_view(),name='alert_delete_view'), re_path(r'^alerts/(?P\d+)/edit/user/(?P\d+)/$',views.alert_edit_view,name='alert_edit_view'), re_path(r'^alerts/(?P\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'^user-boxplot/user/(?P\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'), diff --git a/rowers/views/analysisviews.py b/rowers/views/analysisviews.py index 13fa1b89..035946cc 100644 --- a/rowers/views/analysisviews.py +++ b/rowers/views/analysisviews.py @@ -4337,9 +4337,12 @@ def alerts_view(request,userid=0): def alert_create_view(request,userid=0): r = getrequestrower(request,userid=userid) + form = AlertEditForm() + return render(request,'alert_create.html', { 'rower':r, + 'form':form, }) # alert report view @@ -4351,9 +4354,12 @@ def alert_create_view(request,userid=0): def alert_edit_view(request,id=0,userid=0): r = getrequestrower(request,userid=userid) + form = AlertEditForm() + return render(request,'alert_edit.html', { 'rower':r, + 'form':form, }) # alert delete view diff --git a/rowers/views/statements.py b/rowers/views/statements.py index 23d6e377..2f62d5c1 100644 --- a/rowers/views/statements.py +++ b/rowers/views/statements.py @@ -94,6 +94,7 @@ from rowers.models import ( microcyclecheckdates,mesocyclecheckdates,macrocyclecheckdates, TrainingMesoCycleForm, TrainingMicroCycleForm, RaceLogo,RowerBillingAddressForm,PaidPlan, + AlertEditForm, PlannedSessionComment,CoachRequest,CoachOffer,checkaccessplanuser ) from rowers.models import ( From 451d2a419b9211847efe3477719efd015594d7e3 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 16 Aug 2019 19:38:42 +0200 Subject: [PATCH 09/17] more progress on alerts --- rowers/alerts.py | 2 +- rowers/models.py | 16 ++++-- rowers/templates/alert_create.html | 1 + rowers/templates/alert_edit.html | 1 + rowers/views/analysisviews.py | 82 +++++++++++++++++++++++++++++- rowers/views/statements.py | 3 +- 6 files changed, 98 insertions(+), 7 deletions(-) diff --git a/rowers/alerts.py b/rowers/alerts.py index 2bbb0e81..fcd7468b 100644 --- a/rowers/alerts.py +++ b/rowers/alerts.py @@ -49,7 +49,7 @@ def create_alert(manager, rower, measured,period=7, emailalert=True, alert.filter.add(m) - return 1 + return m.id diff --git a/rowers/models.py b/rowers/models.py index 1d0fcc42..7b6d824e 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -1025,21 +1025,28 @@ class Condition(models.Model): value2 = models.FloatField(default=0) condition = models.CharField(max_length=20,choices=conditionchoices,null=True) +class ConditionEditForm(ModelForm): + class Meta: + model = Condition + fields = ['metric','condition','value1','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='Name',null=True,blank=True) + 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') + 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, @@ -1058,7 +1065,10 @@ class Alert(models.Model): class AlertEditForm(ModelForm): class Meta: model = Alert - fields = ['name','measured','reststrokes','period','emailalert','workouttype'] + fields = ['name','reststrokes','period','emailalert','workouttype'] + widgets = { + 'reststrokes':forms.CheckboxInput() + } class BasePlannedSessionFormSet(BaseFormSet): def clean(self): diff --git a/rowers/templates/alert_create.html b/rowers/templates/alert_create.html index a637b3bd..267abf40 100644 --- a/rowers/templates/alert_create.html +++ b/rowers/templates/alert_create.html @@ -13,6 +13,7 @@ {% csrf_token %} {{ form.as_table }} + {{ measuredform.as_table }}
    diff --git a/rowers/templates/alert_edit.html b/rowers/templates/alert_edit.html index a637b3bd..267abf40 100644 --- a/rowers/templates/alert_edit.html +++ b/rowers/templates/alert_edit.html @@ -13,6 +13,7 @@ {% csrf_token %} {{ form.as_table }} + {{ measuredform.as_table }}
    diff --git a/rowers/views/analysisviews.py b/rowers/views/analysisviews.py index 035946cc..7aee9332 100644 --- a/rowers/views/analysisviews.py +++ b/rowers/views/analysisviews.py @@ -4324,8 +4324,20 @@ def alerts_view(request,userid=0): 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, }) @@ -4337,12 +4349,51 @@ def alerts_view(request,userid=0): def alert_create_view(request,userid=0): r = getrequestrower(request,userid=userid) - form = AlertEditForm() + if request.method == 'POST': + form = AlertEditForm(request.POST) + measuredform = ConditionEditForm(request.POST) + if form.is_valid() and measuredform.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'] + + result = create_alert(request.user,r,measured,period=period,emailalert=emailalert, + reststrokes=reststrokes,workouttype=workouttype, + name=name) + + if result: + 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, 'rower':r, 'form':form, + 'measuredform':measuredform, }) # alert report view @@ -4354,12 +4405,35 @@ def alert_create_view(request,userid=0): def alert_edit_view(request,id=0,userid=0): r = getrequestrower(request,userid=userid) - form = AlertEditForm() + alert = Alert.objects.get(id=id) + + + form = AlertEditForm(instance=alert) + measuredform = ConditionEditForm(instance=alert.measured) + + 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, }) # alert delete view @@ -4385,6 +4459,10 @@ class AlertDelete(DeleteView): 'url':'/rowers/analysis', 'name': 'Analysis' }, + { + 'url':reverse('alerts_view'), + 'name':'Alerts', + }, { 'url': reverse('alert_edit_view', kwargs={'userid':userid,'id':self.object.pk}), diff --git a/rowers/views/statements.py b/rowers/views/statements.py index 2f62d5c1..5392e757 100644 --- a/rowers/views/statements.py +++ b/rowers/views/statements.py @@ -94,7 +94,7 @@ from rowers.models import ( microcyclecheckdates,mesocyclecheckdates,macrocyclecheckdates, TrainingMesoCycleForm, TrainingMicroCycleForm, RaceLogo,RowerBillingAddressForm,PaidPlan, - AlertEditForm, + AlertEditForm, ConditionEditForm, PlannedSessionComment,CoachRequest,CoachOffer,checkaccessplanuser ) from rowers.models import ( @@ -208,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 From 8009831ab14e0bba5f39d9054abfa7e365ca5d45 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sat, 17 Aug 2019 15:17:11 +0200 Subject: [PATCH 10/17] alert create including filters --- rowers/alerts.py | 9 +++-- rowers/models.py | 15 +++++++- rowers/templates/.#laboratory.html | 1 - rowers/templates/alert_create.html | 59 +++++++++++++++++++++++++----- rowers/templates/laboratory.html | 4 +- rowers/views/analysisviews.py | 34 +++++++++++++++-- rowers/views/statements.py | 2 +- 7 files changed, 103 insertions(+), 21 deletions(-) delete mode 100644 rowers/templates/.#laboratory.html diff --git a/rowers/alerts.py b/rowers/alerts.py index fcd7468b..688600fc 100644 --- a/rowers/alerts.py +++ b/rowers/alerts.py @@ -12,7 +12,7 @@ def create_alert(manager, rower, measured,period=7, emailalert=True, # 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 + return 0,'You are not allowed to create this alert' m = Condition( metric = measured['metric'], @@ -36,7 +36,8 @@ def create_alert(manager, rower, measured,period=7, emailalert=True, alert.save() if 'filter' in kwargs: - for f in filter: + filters = kwargs['filter'] + for f in filters: m = Condition( metric = f['metric'], value1 = f['value1'], @@ -49,7 +50,7 @@ def create_alert(manager, rower, measured,period=7, emailalert=True, alert.filter.add(m) - return m.id + return alert.id,'Your alert was created' @@ -71,7 +72,7 @@ def alert_add_filters(alert,filter): alert.filter.add(m) - + return 1 # get alert stats # nperiod = 0: current period, i.e. next_run - n days to today diff --git a/rowers/models.py b/rowers/models.py index 7b6d824e..06bfb562 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -1030,7 +1030,20 @@ class ConditionEditForm(ModelForm): model = Condition fields = ['metric','condition','value1','value2'] - +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: diff --git a/rowers/templates/.#laboratory.html b/rowers/templates/.#laboratory.html deleted file mode 100644 index 25889a2a..00000000 --- a/rowers/templates/.#laboratory.html +++ /dev/null @@ -1 +0,0 @@ -E408191@CZ27LT9RCGN72.13140:1565793987 \ No newline at end of file diff --git a/rowers/templates/alert_create.html b/rowers/templates/alert_create.html index 267abf40..24099c36 100644 --- a/rowers/templates/alert_create.html +++ b/rowers/templates/alert_create.html @@ -5,23 +5,64 @@ {% block main %}

    Alert Edit

    + +

    + 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. +

    + +

    + 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). +

    + +

    + 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. +

    -
      -
    • -

      -

      + +
        +
      • +

        Alert

        +

        + {{ formset.management_form }} {% csrf_token %} {{ form.as_table }} {{ measuredform.as_table }}
        -

      • -

        - +

        + + {% for filter_form in formset %} +
      • +
        +

        Filter {{ forloop.counter }}

        + + {{ filter_form.as_table }} +
        +
        +
      • + {% endfor %} +
      + -
    - + + + + {% endblock %} diff --git a/rowers/templates/laboratory.html b/rowers/templates/laboratory.html index 73d2368e..41fe9cb0 100644 --- a/rowers/templates/laboratory.html +++ b/rowers/templates/laboratory.html @@ -11,7 +11,9 @@

    Rower: {{ rower.user.first_name }}

    - +

    + Try out Alerts +

    {% endblock %} diff --git a/rowers/views/analysisviews.py b/rowers/views/analysisviews.py index 7aee9332..5d099933 100644 --- a/rowers/views/analysisviews.py +++ b/rowers/views/analysisviews.py @@ -4348,11 +4348,14 @@ def alerts_view(request,userid=0): 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) - if form.is_valid() and measuredform.is_valid(): + 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 @@ -4362,11 +4365,31 @@ def alert_create_view(request,userid=0): workouttype = ad['workouttype'] name = ad['name'] - result = create_alert(request.user,r,measured,period=period,emailalert=emailalert, - reststrokes=reststrokes,workouttype=workouttype, - name=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: @@ -4391,6 +4414,7 @@ def alert_create_view(request,userid=0): return render(request,'alert_create.html', { 'breadcrumbs':breadcrumbs, + 'formset': filter_formset, 'rower':r, 'form':form, 'measuredform':measuredform, @@ -4411,6 +4435,8 @@ def alert_edit_view(request,id=0,userid=0): form = AlertEditForm(instance=alert) measuredform = ConditionEditForm(instance=alert.measured) + + breadcrumbs = [ { 'url':'/rowers/analysis', diff --git a/rowers/views/statements.py b/rowers/views/statements.py index 5392e757..06c33325 100644 --- a/rowers/views/statements.py +++ b/rowers/views/statements.py @@ -113,7 +113,7 @@ from rowers.models import ( ) 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 From 729ed0d4f84d3287d16a9b3afe31f0b3d0720d43 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sat, 17 Aug 2019 16:19:09 +0200 Subject: [PATCH 11/17] alert edit including filters --- rowers/alerts.py | 4 +- rowers/templates/alert_create.html | 4 +- rowers/templates/alert_edit.html | 61 ++++++++++++++++++++++----- rowers/views/analysisviews.py | 66 ++++++++++++++++++++++++++++-- 4 files changed, 117 insertions(+), 18 deletions(-) diff --git a/rowers/alerts.py b/rowers/alerts.py index 688600fc..5ac22e57 100644 --- a/rowers/alerts.py +++ b/rowers/alerts.py @@ -55,12 +55,12 @@ def create_alert(manager, rower, measured,period=7, emailalert=True, # update alert -def alert_add_filters(alert,filter): +def alert_add_filters(alert,filters): for f in alert.filter.all(): alert.filter.remove(f) f.delete() - for f in filter: + for f in filters: m = Condition( metric = f['metric'], value1 = f['value1'], diff --git a/rowers/templates/alert_create.html b/rowers/templates/alert_create.html index 24099c36..138e66c0 100644 --- a/rowers/templates/alert_create.html +++ b/rowers/templates/alert_create.html @@ -1,10 +1,10 @@ {% extends "newbase.html" %} {% load staticfiles %} -{% block title %}Planned Session{% endblock %} +{% block title %}Metric Alert{% endblock %} {% block main %} -

    Alert Edit

    +

    Alert Create

    Alerts are useful to give you a regular update on how you are doing. For example, if you are diff --git a/rowers/templates/alert_edit.html b/rowers/templates/alert_edit.html index 267abf40..7c15abda 100644 --- a/rowers/templates/alert_edit.html +++ b/rowers/templates/alert_edit.html @@ -1,27 +1,66 @@ {% extends "newbase.html" %} {% load staticfiles %} -{% block title %}Planned Session{% endblock %} +{% block title %}Metric Alert{% endblock %} {% block main %} -

    Alert Edit

    +

    + 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. +

    + +

    + 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). +

    + +

    + 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. +

    -
      -
    • -

      -

      + +
        +
      • +

        Alert

        +

        + {{ formset.management_form }} {% csrf_token %} {{ form.as_table }} {{ measuredform.as_table }}
        -

      • -

        - +

        + + {% for filter_form in formset %} +
      • +
        +

        Filter {{ forloop.counter }}

        + + {{ filter_form.as_table }} +
        +
        +
      • + {% endfor %} +
      + -
    - + + + + {% endblock %} diff --git a/rowers/views/analysisviews.py b/rowers/views/analysisviews.py index 5d099933..9d744438 100644 --- a/rowers/views/analysisviews.py +++ b/rowers/views/analysisviews.py @@ -4430,10 +4430,69 @@ def alert_edit_view(request,id=0,userid=0): r = getrequestrower(request,userid=userid) alert = Alert.objects.get(id=id) + + 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()] - - form = AlertEditForm(instance=alert) - measuredform = ConditionEditForm(instance=alert.measured) + 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) @@ -4460,6 +4519,7 @@ def alert_edit_view(request,id=0,userid=0): 'rower':r, 'form':form, 'measuredform':measuredform, + 'formset':filter_formset, }) # alert delete view From f9231f94e02f79c7b28920c6852f2130ae22ab55 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sat, 17 Aug 2019 17:38:39 +0200 Subject: [PATCH 12/17] further improvements to create/edit alerts --- rowers/alerts.py | 17 +++++++++-------- rowers/models.py | 7 ++++++- rowers/templates/alert_create.html | 6 +++--- rowers/templates/alert_edit.html | 4 ++-- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/rowers/alerts.py b/rowers/alerts.py index 5ac22e57..f5bbfec4 100644 --- a/rowers/alerts.py +++ b/rowers/alerts.py @@ -38,16 +38,17 @@ def create_alert(manager, rower, measured,period=7, emailalert=True, if 'filter' in kwargs: filters = kwargs['filter'] for f in filters: - m = Condition( - metric = f['metric'], - value1 = f['value1'], - value2 = f['value2'], - condition = f['condition'] + if f['metric'] and f['condition']: + m = Condition( + metric = f['metric'], + value1 = f['value1'], + value2 = f['value2'], + condition = f['condition'] ) + + m.save() - m.save() - - alert.filter.add(m) + alert.filter.add(m) return alert.id,'Your alert was created' diff --git a/rowers/models.py b/rowers/models.py index 06bfb562..9caf9810 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -1022,7 +1022,7 @@ class Condition(models.Model): ) metric = models.CharField(max_length=50,choices=parchoicesy1,verbose_name='Metric') value1 = models.FloatField(default=0) - value2 = 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): @@ -1030,6 +1030,11 @@ class ConditionEditForm(ModelForm): 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): diff --git a/rowers/templates/alert_create.html b/rowers/templates/alert_create.html index 138e66c0..d7b1c57a 100644 --- a/rowers/templates/alert_create.html +++ b/rowers/templates/alert_create.html @@ -41,16 +41,16 @@

    - {% for filter_form in formset %}
  • + {% for filter_form in formset %}

    Filter {{ forloop.counter }}

    {{ filter_form.as_table }}
    -
  • - {% endfor %} + {% endfor %} + diff --git a/rowers/templates/alert_edit.html b/rowers/templates/alert_edit.html index 7c15abda..081d9a36 100644 --- a/rowers/templates/alert_edit.html +++ b/rowers/templates/alert_edit.html @@ -39,16 +39,16 @@

    - {% for filter_form in formset %}
  • + {% for filter_form in formset %}

    Filter {{ forloop.counter }}

    {{ filter_form.as_table }}
    -
  • {% endfor %} + From 5015266ba88e2bf5122dcfc5ae3fdb468b3a02f1 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sun, 18 Aug 2019 15:03:21 +0200 Subject: [PATCH 13/17] rudimentary alert report page --- rowers/alerts.py | 43 ++++++++++++++-------- rowers/templates/alert_stats.html | 35 ++++++++++++++++++ rowers/templates/alerts.html | 2 +- rowers/urls.py | 3 ++ rowers/views/analysisviews.py | 59 ++++++++++++++++++++++++++++++- 5 files changed, 125 insertions(+), 17 deletions(-) create mode 100644 rowers/templates/alert_stats.html diff --git a/rowers/alerts.py b/rowers/alerts.py index f5bbfec4..76cb2c74 100644 --- a/rowers/alerts.py +++ b/rowers/alerts.py @@ -93,6 +93,7 @@ def alert_get_stats(alert,nperiod=0): ids = [w.id for w in workouts] df = getsmallrowdata_db(columns,ids=ids,doclean=True,workstrokesonly=workstrokesonly) + if df.empty: return { 'workouts':len(workouts), @@ -102,25 +103,37 @@ def alert_get_stats(alert,nperiod=0): 'nr_strokes_qualifying':0, } + # check if filters are in columns list + pdcolumns = set(df.columns) # drop strokes through filter - 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 + 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) diff --git a/rowers/templates/alert_stats.html b/rowers/templates/alert_stats.html new file mode 100644 index 00000000..713716f9 --- /dev/null +++ b/rowers/templates/alert_stats.html @@ -0,0 +1,35 @@ +{% extends "newbase.html" %} +{% load staticfiles %} + +{% block title %}Metric Alert{% endblock %} + +{% block main %} + +

    + Previous + {% if nperiod > 0 %} + Next + {% endif %} +

    + +
      + +
    • +

      Alert

      +

      {{ alert }}

      +

      This is a page under construction. Currently with minimal information

      +
    • + {% for key, value in stats.items %} +
    • +

      {{ key }}

      +

      {{ value }}

      +
    • + {% endfor %} +
    + + +{% endblock %} + +{% block sidebar %} +{% include 'menu_analytics.html' %} +{% endblock %} diff --git a/rowers/templates/alerts.html b/rowers/templates/alerts.html index 5740d620..0ea4db1c 100644 --- a/rowers/templates/alerts.html +++ b/rowers/templates/alerts.html @@ -36,7 +36,7 @@ diff --git a/rowers/urls.py b/rowers/urls.py index 14e04c6f..6e0c64ec 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -423,6 +423,9 @@ urlpatterns = [ re_path(r'^alerts/(?P\d+)/edit/user/(?P\d+)/$',views.alert_edit_view,name='alert_edit_view'), re_path(r'^alerts/(?P\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\d+)/report/user/(?P\d+)/$',views.alert_report_view,name='alert_report_view'), + re_path(r'^alerts/(?P\d+)/report/(?P\d+)/user/(?P\d+)/$',views.alert_report_view,name='alert_report_view'), + re_path(r'^alerts/(?P\d+)/report/$',views.alert_report_view,name='alert_report_view'), re_path(r'^user-boxplot/user/(?P\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'), diff --git a/rowers/views/analysisviews.py b/rowers/views/analysisviews.py index 9d744438..546fb652 100644 --- a/rowers/views/analysisviews.py +++ b/rowers/views/analysisviews.py @@ -4421,7 +4421,57 @@ def alert_create_view(request,userid=0): }) # 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", @@ -4429,7 +4479,14 @@ def alert_create_view(request,userid=0): def alert_edit_view(request,id=0,userid=0): r = getrequestrower(request,userid=userid) - alert = Alert.objects.get(id=id) + 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: From 52dff568a2cb130f5fb7f308587bc5bde896c082 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Mon, 19 Aug 2019 12:55:54 +0200 Subject: [PATCH 14/17] sending alerts in rudimentary form --- rowers/alerts.py | 22 +++++--- rowers/management/commands/processalerts.py | 58 +++++++++++++++++++++ rowers/tasks.py | 29 +++++++++++ rowers/templates/alertemail.html | 20 +++++++ 4 files changed, 121 insertions(+), 8 deletions(-) create mode 100644 rowers/management/commands/processalerts.py create mode 100644 rowers/templates/alertemail.html diff --git a/rowers/alerts.py b/rowers/alerts.py index 76cb2c74..0865d07e 100644 --- a/rowers/alerts.py +++ b/rowers/alerts.py @@ -62,16 +62,22 @@ def alert_add_filters(alert,filters): f.delete() for f in filters: - m = Condition( - metric = f['metric'], - value1 = f['value1'], - value2 = f['value2'], - condition = f['condition'] - ) + metric = f['metric'] + value1 = f['value1'] + value2 = f['value2'] + condition = f['condition'] - m.save() + if condition and metric and value1: + m = Condition( + metric = f['metric'], + value1 = f['value1'], + value2 = f['value2'], + condition = f['condition'] + ) - alert.filter.add(m) + m.save() + + alert.filter.add(m) return 1 diff --git a/rowers/management/commands/processalerts.py b/rowers/management/commands/processalerts.py new file mode 100644 index 00000000..a2180efd --- /dev/null +++ b/rowers/management/commands/processalerts.py @@ -0,0 +1,58 @@ +#!/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 + +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 + 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 = alert.next_run + 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')) diff --git a/rowers/tasks.py b/rowers/tasks.py index 065ecca4..c6f8316c 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -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 ' + + 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): diff --git a/rowers/templates/alertemail.html b/rowers/templates/alertemail.html new file mode 100644 index 00000000..bd3df931 --- /dev/null +++ b/rowers/templates/alertemail.html @@ -0,0 +1,20 @@ +{% extends "emailbase.html" %} + +{% block body %} +

    Dear {{ first_name }},

    + +

    + Here is the report for your alert on rowsandall.com. +

    + +{% for key, value in report.items() %} +

    + {{ key }}: {{ value }} +

    +{% endfor %} + +

    + Best Regards, the Rowsandall Team +

    +{% endblock %} + From 270feb4dde7b359f2ec54a27a61b4a9e8bc67667 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Mon, 19 Aug 2019 16:34:01 +0200 Subject: [PATCH 15/17] email as remote task --- rowers/management/commands/processalerts.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/rowers/management/commands/processalerts.py b/rowers/management/commands/processalerts.py index a2180efd..b9efe371 100644 --- a/rowers/management/commands/processalerts.py +++ b/rowers/management/commands/processalerts.py @@ -11,6 +11,9 @@ 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') @@ -35,20 +38,21 @@ class Command(BaseCommand): else: testing = False - todaysalerts = Alert.objects.filter(next_run = datetime.date.today(),emailalert=True) + todaysalerts = Alert.objects.filter(next_run <= datetime.date.today(),emailalert=True) for alert in todaysalerts: stats = alerts.alert_get_stats(alert) # send email - handle_send_email_alert(alert.manager.email, - alert.manager.first_name, - alert.manager.last_name, - stats,debug=True) + 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 = alert.next_run + datetime.timedelta(days=alert.period) + alert.next_run = datetime.date.today() + datetime.timedelta(days=alert.period) alert.save() if testing: From 97c156fc0a8c3ba945e22359e1038b96df30b260 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Mon, 19 Aug 2019 19:47:45 +0200 Subject: [PATCH 16/17] passed tests --- rowers/tests/testdata/testdata.csv.gz | Bin 12642 -> 12534 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/rowers/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index b7174123956e1f05b89cf6324a401c467ab1b57e..644dce606abc2edac9a17b270a537e6c35ee0a3f 100644 GIT binary patch literal 12534 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 Q0RR630LQKMsTYd?0Iw-yN&o-= From 14cdfca05d21271a8266670f805dd70d1b38fc6f Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Mon, 19 Aug 2019 21:09:25 +0200 Subject: [PATCH 17/17] tested --- rowers/tests/testdata/testdata.csv.gz | Bin 12534 -> 12642 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/rowers/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index 644dce606abc2edac9a17b270a537e6c35ee0a3f..5625c2c12af06847a3cc193485a177b65c0a0b3f 100644 GIT binary patch literal 12642 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 Q0RR630LQKMsTYd?06o=XYybcN literal 12534 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