Browse Source

timegen frequency support weekdays

you can now select on which weekdays an event should be played. The
upgrade path should be clean, with old events assumed to be "all days".

FIX #28
boyska 7 years ago
parent
commit
f2fcd075ce
3 changed files with 59 additions and 10 deletions
  1. 17 0
      larigira/tests/test_time_every.py
  2. 18 1
      larigira/timeform_frequency.py
  3. 24 9
      larigira/timegen_every.py

+ 17 - 0
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',

+ 18 - 1
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'))

+ 24 - 9
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: