[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