diff --git a/larigira/tests/test_time_every.py b/larigira/tests/test_time_every.py index 965b0c6..d0a70e4 100644 --- a/larigira/tests/test_time_every.py +++ b/larigira/tests/test_time_every.py @@ -30,6 +30,15 @@ def onehour(now, request): 'interval': intervals[request.param], 'end': now + days(1)}) +@pytest.fixture(params=[1, '1']) +def onehour_monday(request): + monday = request.param + yield FrequencyAlarm({ + 'interval': 3600*12, + 'weekdays': [monday], # mondays only + 'start': 0 + }) + @pytest.fixture(params=['seconds', 'human', 'coloned']) def tenseconds(now, request): @@ -111,6 +120,14 @@ def test_freq_ring(now, onehour): eq_(len(allr), 49, pprint(allr)) +def test_weekday_skip(onehour_monday): + t = datetime.fromtimestamp(0) + for _ in range(20): # 20 is an arbitrary number + t = onehour_monday.next_ring(t) + print(_, t, t.isoweekday()) + assert t.isoweekday() == 1 # monday; don't get confused by .weekday() + + def test_single_registered(): timegenerate({ 'kind': 'single', diff --git a/larigira/timeform_frequency.py b/larigira/timeform_frequency.py index 43adad9..f0dad75 100644 --- a/larigira/timeform_frequency.py +++ b/larigira/timeform_frequency.py @@ -2,7 +2,7 @@ from datetime import datetime from pytimeparse.timeparse import timeparse from flask_wtf import Form from wtforms import StringField, DateTimeField, validators, \ - SubmitField, ValidationError + SubmitField, ValidationError, SelectMultipleField class FrequencyAlarmForm(Form): @@ -22,6 +22,18 @@ class FrequencyAlarmForm(Form): description='Date after which no alarm will ring, ' 'expressed as YYYY-MM-DD HH:MM:SS; if omitted, the ' 'alarm will always ring') + weekdays = SelectMultipleField('Days on which the alarm should be played', + choices=[('1', 'Monday'), + ('2', 'Tuesday'), + ('3', 'Wednesday'), + ('4', 'Thursday'), + ('5', 'Friday'), + ('6', 'Saturday'), + ('7', 'Sunday')], + default=list('1234567'), + validators=[validators.required()], + description='The alarm will ring only on ' + 'selected weekdays') submit = SubmitField('Submit') def populate_from_timespec(self, timespec): @@ -31,6 +43,10 @@ class FrequencyAlarmForm(Form): self.start.data = datetime.fromtimestamp(timespec['start']) if 'end' in timespec: self.end.data = datetime.fromtimestamp(timespec['end']) + if 'weekdays' in timespec: + self.weekdays.data = timespec['weekdays'] + else: + self.weekdays.data = list('1234567') self.interval.data = timespec['interval'] def validate_interval(form, field): @@ -48,6 +64,7 @@ def frequencyalarm_receive(form): 'kind': 'frequency', 'nick': form.nick.data, 'interval': form.interval.data, + 'weekdays': form.weekdays.data, } if form.start.data: obj['start'] = int(form.start.data.strftime('%s')) diff --git a/larigira/timegen_every.py b/larigira/timegen_every.py index df28d64..724e668 100644 --- a/larigira/timegen_every.py +++ b/larigira/timegen_every.py @@ -24,6 +24,7 @@ class Alarm(object): raise NotImplementedError() def has_ring(self, time=None): + '''returns True IFF the alarm will ring exactly at ``time``''' raise NotImplementedError() def all_rings(self, current_time=None): @@ -74,6 +75,8 @@ class FrequencyAlarm(Alarm): self.interval = timeparse(obj['interval']) assert type(self.interval) is int self.end = getdate(obj['end']) if 'end' in obj else None + self.weekdays = [int(x) for x in obj['weekdays']] if \ + 'weekdays' in obj else None def next_ring(self, current_time=None): '''if current_time is None, it is now()''' @@ -87,15 +90,27 @@ class FrequencyAlarm(Alarm): assert self.start <= current_time <= self.end else: assert self.start <= current_time - n_interval = ( - (current_time - self.start).total_seconds() // self.interval - ) + 1 - ring = self.start + timedelta(seconds=self.interval * n_interval) - if ring == current_time: - ring += timedelta(seconds=self.interval) - if self.end is not None and ring > self.end: - return None - return ring + # this "infinite" loop is required by the weekday exclusion: in + # fact, it is necessary to retry until a valid event/weekday is + # found. a "while True" might have been more elegant (and maybe + # fast), but this gives a clear upper bound to the cycle. + for _ in range(60*60*24*7 // self.interval): + n_interval = ( + (current_time - self.start).total_seconds() // self.interval + ) + 1 + ring = self.start + timedelta(seconds=self.interval * n_interval) + if ring == current_time: + ring += timedelta(seconds=self.interval) + if self.end is not None and ring > self.end: + return None + if self.weekdays is not None \ + and ring.isoweekday() not in self.weekdays: + current_time = ring + continue + return ring + log.warning("Can't find a valid time for event; " + "something went wrong") + return None def has_ring(self, current_time=None): if current_time is None: