[PEAK] Modelling a calendar application with the Trellis

Phillip J. Eby pje at telecommunity.com
Thu Jul 19 16:34:46 EDT 2007


Building on Grant's wx experiments, suppose you wanted to create a 
simple calendar application.  Perhaps you have some views like day, 
week, and month, and perhaps you have a smaller month view that 
highlights how busy you are on different days, and serves as a 
selection mechanism to decide what day or week to view in the larger display.

And, let's also suppose that we are timezone and recurrence aware, so 
that you can be viewing the calendar from any timezone (and change 
which one it is at any time), and that events in the calendar can 
recur a potentially infinite number of times.  How would we model all 
that in the Trellis, such that our UI is only a thin layer bridged to 
the model (like my rewrite of Grant's temperature converter)?

The central component, I think, would be a Day object:

     class Day(trellis.Component):
         trellis.values(
             date = None,
             items = (),
         )

         @trellis.rule
         def busyPercent(self):
             # code to loop over self.items and figure out how
             # tightly-booked the day is, returning a percentage

Calendar widgets (or bridges thereto) would have a read rule that 
loops over a Day's items in order to render the day.  Summary 
calendars would draw themselves using the Day's busyPercent.

Of course, these components need some way to *get* Day instances for 
a given date.  For that, we need a Calendar object of some sort, e.g.::

     class Calendar(trellis.Component):
         trellis.values(
             events = (),
             timezone = "America/New York",
         )
         trellis.rules(
             cache = lambda self: WeakValueDictionary()
         )

         def __getitem__(self, date):
             try:
                 return self.cache[date]
             except KeyError:
                 d = self.cache[date] = Day(
                     date = date,
                     items = Cell(lambda: list(self.itemsForDate(date))))
                 )
                 return d

         def itemsForDate(self, date):
             zone = self.timezone
             for event in self.events:
                 for occurrence in event.occurrencesForDay(date, zone):
                     yield occurrence

So, here we assume that the "events" attribute can contains recurring 
events as well as individual events, and that there's some way to get 
the occurrences for a particular day (if any).  We're using a very 
naive (i.e. possibly very slow) algorithm here, in that we're 
scanning every event, including ones that can't possibly be related 
to a given day.  We could improve on that by introducing a Month 
object, though:

     class Month(trellis.Component):
         trellis.values(
             items = ()
         )

         @trellis.rule
         def itemsByDay(self):
             month = {}
             for item in self.items:
                 for day in item.startDate, item.endDate:
                     month.setdefault(day, set()).add(item)
             return month

         def __getitem__(self, date):
             return Day(
                 date=date,
                 items=Cell(lambda: self.itemsByDay.get(date, ()))
             )

     class Calendar(trellis.Component):
         trellis.values(
             events = (),
             timezone = "America/New York",
         )
         trellis.rules(
             cache = lambda self: WeakValueDictionary()
         )

         def __getitem__(self, date):
             try:
                 return self.cache[date]
             except KeyError:
                 month_id = date.year, date.month
                 try:
                     month = self.cache[month_id]
                 except KeyError:
                     month = self.cache[month_id] = Month(
                         items = Cell(lambda: 
set(self.itemsForMonth(month_id)))
                     )
                 d = self.cache[date] = month[date]
                 return d

         def itemsForMonth(self, (year, month)):
             zone = self.timezone
             for event in self.events:
                 for item in event.occurrencesForMonth(year, month, zone):
                     yield item

Now, there is only one loop over the events per month being displayed 
at the time of a change in events or timezone.  Individual Day 
objects don't change or recalculate their busy-ness unless the set of 
events for that particular day changes.

Notice that so far, we don't even need a database or any indexing, as 
long as the total time to loop over all events and generate 
occurrences for a given month is less than say, 1/10th of a second, 
less screen refresh time.  Also notice that changing either the 
Calendar.timezone or Calendar.events will automatically refresh 
everything correctly!

For that matter, we could add this to the Calendar class:

         trellis.values(
             filename = None
         )

         @trellis.rule
         def events(self):
             if filename:
                 # code here to load event objects from a file, perhaps
                 # an .ics file using vObject or some such, and return
                 # a sequence of events
             else:
                 return []

And now any time we change the 'filename' attribute, a new calendar 
will be loaded, and all views will be refreshed.  Woohoo!

To make this an editable calendar, we'd want the event objects 
themselves to be trellis.Components, with cells describing the start 
time, duration, etc.  For a pure read-only calendar, it doesn't 
matter because any change to Calendar.events triggers the recalc of 
all currently-cached views.  But for an editable calendar, we want 
recalcs to be triggered by changes to the events themselves as well.

Of course, once there are enough events that we don't want to scan 
the entire list of them every time there's a change, things may have 
to get a bit more sophisticated, using the "fact base" model to do 
many-to-many indexing, as described in my previous posts.

The idea is that an event may map to any number of Days, and 
conversely, a Day can refer to any number of Events.  In order to 
prevent re-scanning an entire database worth of Events and 
recalcuating for every Month or Day, we could use a hub-and-spokes 
model to update only the Days with changed Events.

The rough concept is that you set up events so that when you modify 
them, you are adding them to a list of changes to the Calendar that 
includes before-and-after information about the changes.  The 
Calendar then uses this information inside an "update" rule that 
figures out the set of before and after occurrences of the changed 
events, within the scope of the currently-cached months (i.e., the 
months being viewed), and directly notifies Day objects of the 
changes (by setting the Day.items to an updated set), rather than 
causing a database rescan for all months and days.

Essentially, the Cell() objects created by Calendar and Month in the 
above sketches would be replaced with Spoke() objects (once those 
exist!) that are tied to the Calendar's "update" cell.  The update 
cell rule would read the changed events info, and update the cached 
Spokes.  Thus, changes to individual events or recurrence rules would 
not require a pass over the entire database, although it would still 
be required for displaying new months.

Of course, there are many other ways to optimize the "database" to 
avoid unnecessary processing, especially for non-recurring events, 
and events with sufficiently less-than-infinite repetitions.  For 
that matter, one could always replace the idea of a Month with the 
idea of a Year -- the processing time for recalculation wouldn't 
change substantially (at least for anything that isn't a daily 
event), but you wouldn't have to do it as many times.

Finally, note that none of this stuff is tied to UI code in any way; 
you could implement separate platform-native UI's over it if you 
wanted to, or have a command-line or curses UI!  You also can easily 
unit test the whole thing, because again, there's no GUI code.

For views that only display events or busy-shading, all they need to 
be able to do is ask a Calendar for a Day (via a cell rule, so they 
get refreshed when things change).  For views that allow editing or 
direct manipulation of events, you of course need some code that sets 
cells on the Event objects, ala the EditBridge stuff.

You may also want some other trellis components to manage things like 
the idea of "what month/week/day am I viewing" and implement commands 
like "move forward/backward a day/week/month" in the 
display.  Components like these can also be written independently of 
the UI, and thus unit tested as well.




More information about the PEAK mailing list