Building a PmWiki Calendar
Over the last few months, I’ve been building a calendar/event system on top of PmWiki. For our impro group, we needed a central system to store all events and internal deadlines.
At first,I hesitated to implement it in PmWiki syntax. Since PmWiki is a flat-file wiki system, no database would be involved. A lot of the stuff I would write would get pretty hard to maintain afterwards. Last summer, I already worked quite a lot on our PmWiki install, and it always striked me as a delicate balance between total syntactic freedom (there’s little rules on what you can and can’t do on a wiki page) and controlling pages with templates and tricks (in order to keep things indexable and uniform).
More behind the cut.
Why on earth would you do that?
I felt that the latter was needed because not everyone is an experienced wiki user. Our organisation has a constant influx of new members. I thought it should be possible to do all the basic stuff (subscribing to events, commenting on pages, …) without ever seeing any page edits in order to promote the wiki as a tool for everyone, not just for the 2 or 3 computer geeks in the attic.
I went on with it implementing the Calendar in PmWiki-syntax anyway, because
- Flexibility: as long as the page has the required fields, it is indexable, but the page itself could be of any form.
- Easy backups: Since Pmwiki is a flat-file wiki system, all it takes to backup is copying the whole folder elsewhere. Cronjob-a-looza.
- Achievement: The twisted sense of achievement if I could pull this off.
Required
- Pmwiki 2.2.x
- Fox
- Powertools
- (optional) Commentbox
- (optional) DeleteAction
Calendar Events Templating
I started by reading up on PTV’s (Page Text Variables). Using a specific syntax, one could add invisible “fields” to a page, containing any information you’d like. Like object variables, but without all the fancy type-checking or visibility controls. After that, I found out I could manipulate PTV’s with Fox (A form processor from the cookbook). So I could create instances with specific fields and update them? Me likey.
I made a list of fields every Calendar item should have:
- Type
- Year/Month/Day/Hour/Minute (last ones can be left zero, and then hidden)
- Title
A template for a calendar item could look like this:
%comment% Required Fields
(:Type: Show:)
(:Year: {$$year}:)
(:Month: {$$month}:)
(:Day: {$$day}:)
(:Hour: {$$hour}:)
(:Minutes: {$$minutes}:)
(:title: Show - {$$info}:)
%comment% Type-specific fields
(:location: {$$location}:)
...
The type-specific fields could be anything you like, but they will not be used in event listings without checking that the event is in fact of the right type. (In this case: the field location cannot be used without first checking that the type is a Show). This is functionality we’ll have to implement ourselves, of course.
Elsewhere in the template you can display the PTV’s by using the syntax:
{$:PTVName}
(for example:)
{$:Year}
This way, you can dress up the rest of the page the way you want, everything is filled with PTV content anyway. I defined a standard commentbox (cookbook recipe here) in the template too, because it’s nice if people can discuss and post small messages for an event.
Page Naming
In order to create events like this, we have to be able to use this template, fill in the fields and create a new page. This brings us to the next issue: how should we name the pages? (Note that this is different from defining the (:title:) PTV – a page name is how it is stored on the filesystem). First, make sure, you have a seperate group for the Calendar (here: Calendar). In my example, I’ve saved the Templates (ShowTemplate, BirthdayTemplate,…) in the same group too, but ideally, you should put them in a seperate group too … saves one exclusion later on in our story.
I opted for this structure: YYYMMDDHHMM-serial
- Using this page naming scheme, the pages will be ordered by ascending date when they are ordered alphabetically. 200910221200 is < 200910231200. This will prove to be vital for listing the Calendar items.
- The serial is needed to prevent pagename collision when two events happen on the same date (when HH and MM are 00, with birthdays, for example). You can add auto-generated incrementing serials to pages using the Serial functionality of the Powertools recipe.
- You could consider adding the title field to the name too, but you risk of running into character limits. I do not recommend that. Keep in mind that ideally, these page names will never be visible to anyone.
Add Event Form
Now, we have to design the FOX form to create an event. FOX basicly takes form input and updates template variables with it. You can also use it to directly update PTV’s, but I’m using the simple “fill-in-the-variables” approach here. For every type of event, you’ll have to create an add form. Maybe there is a way to exploit the common fields in a general form, but I haven’t figured that out yet.
Again, with our event type Show as an example:
(:fox addshow template=Calendar.ShowTemplate target='{$$(serialname Calendar{$$year}{$$month}{$$day}{$$hour}{$$minutes}-)}' pagecheck=1 redirect=1:)
'''Title:'''
(:input text Title:)
'''Date'''
Day (:input text size=2 maxlength=2 day:)
Month (:input select name=month value=01 label=jan:)
(:input select name=month value=02 label=feb:)
...
(:input select name=month value=12 label=dec:)
Year (:input text size=4 value={(ftime fmt="%Y")} maxlength=4 year:)
'''Hour,Minutes:'''
(:input text maxlength=2 size=2 hour:)(:input text maxlength=2 size=2 minutes:)
'''Location:'''
(:input text value='' location:)
(:input submit post Add!:)
(:foxcheck day regex='^\d{2}$' msg='Day missing, not the right format':)
(:foxcheck jaar regex='^\d{4}$' msg='Year missing, not the right format':)
(:foxcheck title msg='No title added':)
(:input hidden csum value='added show':)
(:foxend addshow:)
Some remarks should be made:
- For every field in the template, we have an input. Make sure you use the exact same template variables.
- Since the “day” field should contain 09 instead of simply “9”, we must check the field with regexp‘s. We do the same to check the year field and to make sure the title field is not empty.
- I’m not checking the hours/minutes fields, because 20091202 is the same as 200912020000 – the alphabetical/chronological ordering stays the same. In an ideal world, one should check this too, or at least initialize the fields on 00, 00 – my bad.
- The hidden csum value gets used to display the changes in the LastAction pages. In this case: (Author) (Pagename) added show
- To unclutter the code I’ve left them out here, but you can display per-field warnings using (:foxmessage variablename:).
- And just a usibility tip: make sure you mark which fields are required in your form, by using a *.
Listing Calendar Events
This is by far the most essential part of the Calendar system. We’ve got a wiki group (Calendar) full of events now. We want to display them in a date-ordered list now, preferably only showing dates that aren’t in the past yet. PmWiki has a built-in pagelist functionality, which allows to list pages in a group, exclude pages, … Read up about it in the Pm wiki documentation, it’s an essential tool.
This is the command we’ll use to display our calendar. The custom format we will define later on. You can place this anywhere you want to display a calendar.
(:pagelist fmt=#calendar group=Calendar name=-*Template $:Type=Show,Birthday if="date {(ftime fmt="%Y%m%d")}..({{=$FullName}$:Jaar}{{=$FullName}$:Maand}{{=$FullName}$:Dag})":)
Let’s examine this part by part. Make sure you understand what this pagelist does, since it will be the primary tool to generate all kinds of lists.
- fmt: If you don’t specify this, the list will be rendered in the standard style, which is just a listing of Page names. We don’t want that (because our pagenames are ugly), we want to have a title-based list, with some extra info and good date formatting. We’ll implement our own calendar format (#calendar) later on.
- group: We only want to list pages from the Calendar group.
- (optional) name:- We don’t want to display any pages ending in Template. Only necessary if you placed your Templates in the same group as the events themselves.
- (optional) $Type: Only display the types listed. This is handy if you only want to display certain types
-
if=”: This is the date filtering. Between the “”, you can place any if-test that pmwiki accepts. In this case, we use the date test to see whether or not the current time (using ftime to display it in YYYYMMDD format) is before the YYYYMMDD of the page. Check the syntax to retrieve PTV info from a page:
- {=$Fullname}: The item of the pagelist: you can see this as the proverbial “i” in the loop.
- $:PTVname: The PTV info you want to retreive
- Curly brackets all around required
Note that you can do the date filtering in the custom format we’re about to define in the next paragraph too, but you’ll miss out on optimizations like pagelist caching, and it is an overall inferior, uglier method.
Custom Pagelist Format
In the standard PmWiki distribution, new pagelist formats are defined on the Site.LocalTemplates page. We’ll dive straight in:
(:if1 false:)
[[#calendar]]
(:table width="90%":)
(:cell width=14% style="background:#e5e5ff;padding-left:5px":)'''[[{=$FullName} | {(ftime fmt="%a" when="{{=$FullName}$:Year}{{=$FullName}$:Month}{{=$FullName}$:Day}")} {{=$FullName}$:Day}/{{=$FullName}$:Month}/{{=$FullName}$:Year}]]'''
(:cell width=65% style="background:#e5e5ff;padding-left:5px":)[[{=$FullName} | {{=$FullName}$:title}]]
(:if2 authgroup @admin:)(:cell width=3% style="background:#d4d4ff;padding-left:5px":)'''%red%[[{=$FullName}?action=delete|delete.png"remove this item"]]%%'''(:if2end:)
(:tableend:)
[[#calendarend]]
(:if1end:)
Let’s pick it apart. Note that I’m using nested-if functionality here, which is only available in the most recent versions of PmWiki.
- The first column contains the verbose name of the day, which I retrieve using ftime (format “%a”) and the European date style DD/MM/YY. I’m collecting those using PTV lookups.
- Second column contains the item title.
- Both columns link to the calendar item itself.
- Last column only gets shown to an admin group, who have the ability to delete pages. To do this, I use the DeleteAction recipe.
Further Optimization: Pagelist Cache
At this point, you’ve got the basics for building a full-featured Calendar:
- You can make several kinds of Calendar event types
- Make forms for them
- Make customized list to display all of them, a selection, …
As the amount of events grow, building the Pagelist can take increasingly longer, because more pages have to be searched. I strongly recommend to enable the pagelist cache feature. It allows Pmwiki to save pagelists. As long as no pages are edited/added to Pmwiki, the cache stays valid. This is a huge performance boost.
You can enable the pagelist cache by adding
$EnablePageIndex = 0;
To your local/config.php file. You can check whether or not it’s working by enabling PmWiki diagnostic mode:
$EnableDiag = 1;
$EnableStopWatch = 1;
If you load a page with a pagelist and you see a line like this:
00.32 PageListCache begin save key=998469267ca22a743cdb636ead92e518
It means your pagelist just has been saved to the cache. Any following pageload should display something like this:
00.10 PageListCache begin load key=998469267ca22a743cdb636ead92e518
There are caveats. In my PmWiki installation, I had two scripts (UserLastAction and ActionLog), which wrote something to a page at every page load. This interferes with the Pagelist cache, resulting in the wiki.d/lastmod file being rewritten every time. The Pagelist Cache system uses this file to determine a cache miss.
In order to disable the overwriting of the lastmod file by a recipe, you should add similar code like this to the recipe:
$lmf = $LastModFile; $LastModFile = ''; # disable file mod tracking
...
(recipe code)
...
$LastModFile = $lmf; # restore file mod tracking
Conclusion
It vorks! If you’ve got any remarks, I’d like to hear them in the comments :)