From d0a8abc85b332c4013457e84f088f7dae4371719 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 7 Aug 2019 11:51:46 +0200 Subject: [PATCH 01/13] 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/13] 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 04fcdf6d6d6429e88929500a55e390e602237b6a Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 14 Aug 2019 21:42:30 +0200 Subject: [PATCH 03/13] 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 04/13] 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 05/13] 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 06/13] 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 07/13] 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 08/13] 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 09/13] 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 10/13] 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 11/13] 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 12/13] 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 13/13] 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: