Introduction
I have been using John Rankin's WikiCalendar on many wikis for some time, and I know (now) that Chris Cox has developped PmCalendar for PmWiki.
After playing with the 3KWA skin to my satisfaction, I have had problems integrating John's calendar (I think something messy happened during dev because the i18n file was corrupted). Both recipes seem to have grown into something too big and tricky for my requirement. I must admit that adding AJAX support in version 0.5 has drastically complicated my solution ... but it is worth it ;).
My solution is inspired greatly by John's approach to the problem:
- one document per day which title is the date in an intelligible format YYYYMMDD
- all days in a group defined by $LogbookGroup in config.php (default to Logbook)
- access using a one month calendar display suitable for side bars with browsing feature
- use of (:pagelist:) in the holding group home page to list ... (see pagelist section bellow).
Code (logbook.php)
I have heavily documented the code for it to be read and understood. The only thing that I think can remain obscure is the reason behind the encapsulation of the returned table in <div id='logbook'></div>, it has to do with CSS and namespace or scope (see next section).
/* Logbook v 0.6 by EuGeNe Van den Bulke (licence ... I guess MIT).
A simple weblog like calendar browser for side bars, see http://www.3kwa.com/Tutorial/Logbook for more information. Special thanks to Ian Barton for his feedback and assistance, and to Daniel Friedmann for his requests and interest. */
/* Change in v 0.5 (playing with AJAX)
The file cookbook/logbook.php must be accessible by a web browser for it to be called by a XmlHttpRequest. The first thing we do is catch such a direct call as opposed to an include by PmWiki, and do the required variable assignation based on what is saved when PmWiki is calling. */
@session_start();
if (defined('PmWiki')) {
$_SESSION['PubDirUrl']=$PubDirUrl;
$_SESSION['WorkDir']=$WorkDir;
$_SESSION['pagename']=$pagename;
$_SESSION['LogbookGroup']=$LogbookGroup;
$_SESSION['LogbookNewPost']=$LogbookNewPost;
$_SESSION['LogbookDays']=$LogbookDays;
$_SESSION['LogbookMonths']=$LogbookMonths;
$_SESSION['LogbookEnableAJAX']=$LogbookEnableAJAX;
} else {
$PubDirUrl=$_SESSION['PubDirUrl'];
$WorkDir=$_SESSION['WorkDir'];
$pagename=$_SESSION['pagename'];
$LogbookGroup=$_SESSION['LogbookGroup'];
$LogbookNewPost=$_SESSION['LogbookNewPost'];
$LogbookDays=$_SESSION['LogbookDays'];
$LogbookMonths=$_SESSION['LogbookMonths'];
$LogbookEnableAJAX=$_SESSION['LogbookEnableAJAX'];
echo ajax();
exit();
}
/* Set default value for $LogbookGroup using PmWiki defined SDV function which will only set the value if it hasn't been already defined. For example if the user has defined $LogbookGroup in local/config.php.
$LogbookGroup tells Logbook which WikiGroup is holding the daily entries.
$LogbookNewPost if set to True allow link on date who do not exist for faster editing of new entrie.
$LogbookEnableAJAX if set to True enables AJAX support ... the default is to False because the installation is a bit tricky, and the code in version 0.5 requires some tweaking to get it working. */
SDV($LogbookGroup,'Logbook');
SDV($LogbookNewPost,False);
SDV($LogbookEnableAJAX,False);
/*Change in v 0.4 to allow for wiki admin to specify how days and months are named (mostly for language purposes) */
SDV($LogbookDays,array('Mon','Tue','Wed','Thu','Fri','Sat','Sun'));
SDV($LogbookMonths,array('Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep',
'Oct','Nov','Dec'));
/* Define Markup allowing for arguments even if not used (yet) the callback function is logbook() which outputs plain HTML that shouldn't processed by PmWiki as markup henceforth PmWiki defined Keep() is used to tell PmWiki not to parse the result of logbook(). The single quote around the $1 are critical. */
Markup(
'logbook',
'directives',
'/\\(:logbook(\\s+.*?)?:\\)/ei',
"Keep(logbook('$1'))"
);
/* logbook() is the function called when the markup (:logbook [arg]:) is parsed by PmWiki. It returns a one month calendar for the current month or a specified month (log_month in the URL), in the form of an HTML table which appearance is governed by a CSS style sheet located in pub/css. */
function logbook($args) {
/* logbook() needs to know where the CSS is so must know $PubDirUrl of the wiki installation (maybe it should be $FarmPubDir). It also needs to know which is the WikiGroup used to store the daily entries ($LogbookGroup), and where the text files containing the daily entries are located ($WorkDir). A call to PHP defined global tells logbook to get the values from outside its scope. */
global $PubDirUrl,$WorkDir,$pagename,
$LogbookGroup,$LogbookDays,$LogbookMonths,$LogbookNewPost,
$LogbookEnableAJAX;
/* We want to be able to display the current month or a specified month. The URL argument log_month in the form YYYYMM is read by logbook() thanks to the $_GET array which contains all posted args (keys are arg name). If log_month is not defined then the default month is the current month. $time (a timestamp) is defined because we use the PHP date() function all over the place for the month displayed so we must have a timestamp refering to it. */
/* Change in version 0.2, uses sessions to keep track of which month should be displayed in the sidebar. Priority goes to what is directly specified by the user i.e. log_month in the URL through the $_GET variable which is then assigned to the $_SESSION var. Then comes the session lookup for log_month, and if all fails, we'll display the current month. */
@session_start();
if ($log_month=$_GET['log_month']) {
$time=strtotime($log_month."01");
$_SESSION['log_month']=$log_month;
} elseif ($log_month=$_SESSION['log_month']) {
$time=strtotime($log_month."01");
} else {
$time=time();
$log_month=date("Ym");
}
/* In order to be able to browse through the calendar we must have links to the previous and next month based on displayed month (where $time comes handy). strtotime() is an extremely useful and smart PHP function/ */
$prev=date("Ym",strtotime("-1 month",$time));
$next=date("Ym",strtotime("+1 month",$time));
/* We must find out which is the first day of the month in order to start the numbering at the right place in the calendar table. Changed in v 0.4 */
/* $Days=array('Mon','Tue','Wed','Thu','Fri','Sat','Sun');
$first_day=date("D",strtotime("1 ".date("F Y",$time)));
$offset=array_search($first_day,$Days); */
$offset=date("w",strtotime("1 ".date("F Y",$time)))-1;
/* change in v 0.6 to fix mis-management of Sundays (bug) */
if ($offset<0) $offset=6;
/* We must know which is the last day of the month 28,29,30 or 31 ... */
$max_day=date("t",$time);
/* $acc is the accumulator for the numbering of days in the calendar. It is offset-ed so the numbering starts under the correct day, the proper column, the first day of the month. */
$acc=-$offset+1;
/* $return is the variable holding the html output returned by logbook(). First we return the cosmetic (CSS) and navigation material (prev and next month). \n is added everywhere so if one wants to look at the source it can be read easily. */
/* Change in v 0.3, hopefuly finaly dealing with non Clean Url setups. */
$return="<link rel='stylesheet' href='$PubDirUrl/css/logbook.css' type='text/css' />\n";
/* Change in v 0.5 let's go AJAX loading the excellent Mochikit and logbook.js if $LogbookEnbaleAJAX (set in local/config.php)*/
if ($LogbookEnableAJAX) {
$return.="<SCRIPT type='text/javascript' src='$PubDirUrl/javascript/lib/MochiKit/MochiKit.js'>
</SCRIPT>\n
<SCRIPT type='text/javascript' src='$PubDirUrl/javascript/logbook.js'>
</SCRIPT>\n";
}
$return.="<DIV ID='logbook'>\n<TABLE>\n";
/* Change in v 0.4 to allow for language support in naming months*/
$month=$LogbookMonths[date("n",$time)-1]." ".date("y",$time);
/* Chaneg in v 0.5 $fmt in MakeLink to include class='async' in A tag */
$return.="<CAPTION>".
MakeLink($pagename,
$pagename,
'[-]','',
$fmt="<A class='async' href='\$PageUrl?log_month=$prev'>\$LinkText</A>").
"<span class='month'>$month</span>".
MakeLink($pagename,
$pagename,
'[+]','',
$fmt="<A class='async' href='\$PageUrl?log_month=$next'>\$LinkText</A>"
);
/* The first row contains the days of the week as showing in $Days. foreach is a very useful function, this is one of the 2 ways of using it. */
$return.="<TR>\n";
foreach($LogbookDays as $day) {
$return.="<TH>$day</TH>\n";
}
$return.="</TR>\n";
/* Generation of the calendar table as such. $week is used to keep track of how many weeks are covered by the month displayed and know when to change line (row). */
$week=1;
/* while loop increasing $week by 1 as long as $acc is lower than the number of days in the displayed month. */
/* <= bug fix in 0.6 */
while($acc<=$max_day) {
$return.="<TR>\n";
/* There are 7 days in a week. If the accumulator is positive or null i.e. we are at least on the first day of the month, and less or equal to the number of days in the month i.e. not past the last day, we display $cell (the accumulator with a leading 0 when required) otherwise and empty cell. For every day in the displayed month check if an entry exist. Entries are stored in $WorkDir under the name $LogbookGroup.YYYYMMDD where YYYYMMDD is $log_month$acc. */
for($day=1;$day<=7;$day++) {
if ($acc>0 and $acc<=$max_day) {
$class="";
/* Change in version 0.2 to pad days bellow 10 with a 0 for date format consistency (YYYYMMDD). */
$cell=$acc;
if ($acc<10) {
$cell="0$acc";
}
/* Change in version 0.2 to deal with non CleanUrl installation using PmWiki pre-defined function MakeLink which automatically generate the appropriate html code for a given installation. */
$file="$WorkDir/$LogbookGroup.$log_month$cell";
if (file_exists($file)) {
$class="exists ";
$cell=MakeLink($pagename,
"$LogbookGroup/$log_month$cell",
$cell);
} else if ($LogbookNewPost) {
/* Change in version 0.3 if $LogbookNewPost is set to True in local/config.php links to create a new document appear in the calendar. */
$cell=MakeLink($pagename,
"$LogbookGroup/$log_month$cell",
$cell,"",
$fmt="<a class='newpost' href='\$PageUrl?action=edit'>\$LinkText</a>"
);
}
/* Change in version 0.4 to make todays day more visible following Daniel's Request */
if ($acc==date("d")&&($log_month==date("Ym"))) {
$class.="today ";
}
$return.="<TD class='$class'>$cell</TD>\n";
} else {
$return.="<TD> </TD>\n";
}
$acc++;
}
$return.="</TR>\n";
$week++;
}
/* We are done, closing the table tag and the div tag (which is CSS related). */
$return.="</TABLE>\n</DIV>\n";
return $return;
}
function ajax() {
/* Change in v 0.5. Basically a repeat of logbook() but without all the PmWiki helpers that are unfortunately unavailable when logbook.php is called outside of PmWiki i.e. not as an include. This function requires some minor user tweaking to get efficient AJAX support. */
global $PubDirUrl,$WorkDir,$pagename,
$LogbookGroup,$LogbookDays,$LogbookMonths,$LogbookNewPost,
$LogbookEnableAJAX;
if ($log_month=$_GET['log_month']) {
$time=strtotime($log_month."01");
$_SESSION['log_month']=$log_month;
} elseif ($log_month=$_SESSION['log_month']) {
$time=strtotime($log_month."01");
} else {
$time=time();
$log_month=date("Ym");
}
$prev=date("Ym",strtotime("-1 month",$time));
$next=date("Ym",strtotime("+1 month",$time));
$offset=date("w",strtotime("1 ".date("F Y",$time)))-1;
if ($offset<0) $offset=6;
$max_day=date("t",$time);
$acc=-$offset+1;
$month=$LogbookMonths[date("n",$time)-1]." ".date("y",$time);
$return="<TABLE>\n";
$return.="<CAPTION><A class='async' href='javascript:logbookShow(\"$prev\")'>[-]</A>".
"<SPAN class='month'>$month</SPAN>".
"<A class='async' href='javascript:logbookShow(\"$next\")'>[+]</A></CAPTION>";
$return.="<TR>\n";
foreach($LogbookDays as $day) {
$return.="<TH>$day</TH>\n";
}
$return.="</TR>\n";
$week=1;
while($acc<=$max_day) {
$return.="<TR>\n";
for($day=1;$day<=7;$day++) {
if ($acc>0 and $acc<=$max_day) {
$class="";
$cell=$acc;
if ($acc<10) {
$cell="0$acc";
}
/* TWEAK check the path from cookbook/logbook.php to your $WorkDir and set $file accordingly. Usually ../ before $WorkDir. */
$file="../$WorkDir/$LogbookGroup.$log_month$cell";
if (file_exists($file)) {
$class="exists ";
/* TWEAK we can't use MakeLink so you must know what your URLs look like and modify the href accordingly (usually ?n=$LogbookGroup.$log_month$cell or for CleanUrl /$LogbookGroup/$log_month$cell */
$cell="<A href='/$LogbookGroup/$log_month$cell'>$cell</A>";
} else if ($LogbookNewPost) {
/* TWEAK we can't use MakeLink so you must know what your URLs look like and modify the href accordingly (usually ?n=$LogbookGroup.$log_month$cell&action=edit or for CleanUrl /$LogbookGroup/$log_month$cell?action=edit */
$cell="<A href='/$LogbookGroup/$log_month$cell?action=edit'>$cell</A>";
}
if ($acc==date("d")&&($log_month==date("Ym"))) {
$class.="today ";
}
$return.="<TD class='$class'>$cell</TD>\n";
} else {
$return.="<TD> </TD>\n";
}
$acc++;
}
$return.="</TR>\n";
$week++;
}
$return.="</TABLE>\n</DIV>\n";
return $return;
}
?>
Look (logbook.css)
The output of (:logbook:) is included in a <div> tag which id is logbook.
All style elements specific to (:logbook:) are specified in a CSS style sheet preceded by #logbook so the styles only affect the elements in the logbook div. It is a way to have namespaces in CSS or restrict the scope of a style definition.
text-align:center;
}
#logbook table {
font-family: courier new,mono;
font-size:12px;
border:2px #CCC dashed;
width:180px;
margin-right:auto;
margin-left:auto;
}
#logbook caption {
font-size:120%;
font-variant:small-caps;
font-weight:bold;
margin-bottom:10px;
text-align:center;
margin-left:auto;
margin-right:auto;
}
#logbook .exists {
background-color:#CCC;
}
#logbook td {
text-align:center;
}
#logbook th {
font-size:80%
}
#logbook .month {
padding-left:30px;
padding-right:30px;
}
#logbook .newpost {
text-decoration:none;
color:#333;
}
#logbook .today {
border:1px black solid;
}
The only IE vs Firefox problem I encountered was with the text-align clause that works with IE and not with Firefox (for tables in a div) the reciprocity being that margin:auto works with Firefox and fails miserably with IE. All in all I am happy with this simple look, maximizing the information/pixel ratio.
AJAX (logbook.js)
Why AJAX? Why not refresh the calendar without reloading the whole page?
warning: I am not a Javascript expert, but playing with AJAX made me want to learn more about it!
The code bellow comes from the MochiKit wiki (Trac not PmWiki but ...) HowToSimpleAjax. I have tweaked it to make it work and do what I want in the context of (:logbook:). If the visitor's browser do not support Javascript it will work as expected in a normal browser. If it does ... let's AJAX using Mochikit!
var pathToPmWikiRoot='/';
var clicklink = function (url) {
return function (evt) {
if (evt && evt.preventDefault) {
evt.preventDefault();
evt.stopPropagation();
} else if (typeof(event) != 'undefined') {
event.cancelBubble = false;
event.returnValue = false;
}
var doReplace = function (req) {
$('logbook').innerHTML = req.responseText;
};
var doReplaceError = function () {
$('logbook').innerHTML = '(:logbook:) bug';
};
var res = MochiKit.Async.doSimpleXMLHttpRequest(url);
res.addCallbacks(doReplace,doReplaceError);
}
};
var convertA = function (linkelement) {
var link=linkelement.toString();
var index=link.indexOf('log_month=');
var log_month=link.substr(index,link.length);
var href=pathToPmWikiRoot+'cookbook/logbook.php?'+log_month;
MochiKit.DOM.addToCallStack(linkelement,
'onclick',
clicklink(href)
);
};
var initpage = function () {
MochiKit.Base.map(convertA,
MochiKit.DOM.getElementsByTagAndClassName('a','async')
);
};
MochiKit.DOM.addLoadEvent(initpage);
function logbookShow(what) {
var res=MochiKit.Async.doSimpleXMLHttpRequest(
pathToPmWikiRoot+'cookbook/logbook.php?log_month='+what
);
res.addCallback(
function (req) {
$('logbook').innerHTML = req.responseText;
}
);
}
Using AJAX means updating the logbook div with an xmlhttprequest call to a url that would generate a calendar for the target month.
By aiming at making (:logbook:) work on most installation (i.e. non Clean Url ones to please Ian :P) using PmWiki defined functions such as MakeLink, I made the code so dependent on pmwiki.php being loaded that, making the script called by the xmlhttprequest requires a lot of work, redefining SDV (easy), MakeLink (a lot trickier) transforming all the required global variable into session variable and getting all the dependencies right.
I decided to code a tuned down version of (:logbook:) to start with, which supports AJAX but requires some user tweaking in the PHP code to get it working (that's why by default $LogbookEnableAJAX is set to False).
Usage
Files
In order to use (:logbook:) in your wiki you need to:
- create/edit/save logbook.php in your cookbook/ folder.
- create/edit/save logbook.css in your pub/css/ folder (you may need to create the folder).
- create/edit/save logbook.js in your pub/javascript/ folder.
- if you want AJAX support copy the lib/ folder of the Mochikit distribution in your pub/javascript folder you end up with a folder pub/javascript/lib/MochiKit/ containing many .js files.
pmwiki.php (or index.php)
pub/
css/
logbook.css
javascript/
logbook.js
lib/
MochiKit/
(all the .js files in the MochiKit distribution)
cookbook/
logbook.php
wiki.d/ (that's $WorkDir)
Configuration
- make sure the cookbook/logbook.php is accessible by a browser (you may have to change the htaccess file).
- edit local/config.php and add include(cookbook/logbook.php); and define:
- $LogbookGroup='MyLogbookGroup' if you don't want it to be Logbook
- $LogbookNewPost to True if you want an easy way to create a new entry for a day in the calendar (I don't).
- $LogbookDays and $LogbookMonths to whatever you want the days and months to be called (see example bellow)
- $LogbookEnableAJAX to True if you have installed Mochikit as mentioned above.
- if you want AJAX support TWEAK the ajax() function definition in logbook.php and adjust the var pathToPmWikiRoot in logbook.js to reflect where is PmWiki in your url (for example if it lives in http://your.domain.name/pmwiki/, var pathToPmWikiRoot='/pmwiki/';
- anywhere in your wiki (it is designed with side bars in mind) type (:logbook:) and hopefully see the magic.
Example of a config.php file:
(:pagelist:)
With version 2.1 of PmWiki (:pagelist:) has grown into a killer tool to easily turn the wiki into a bliki.
For 3KWA I have created a fmt entry in Site.PageListTemplates for #logbook (derived from the #include entry adding the page name to have a date reference).
Then in the homepage of my logbook group (Logbook.HomePage) I just added ...
... to display the latest entry. Most off the shelf blog software display the last 5 by default. Guess ... change count to 5 and you are set ;)
Todo
It is operational but not complete yet, listed bellow things I want to do:
use sessions to store log_month in order not to go back to the current month when displaying a new document without uglyfying the URL.Done in v0.2.Test and adjust for WikiFarms.should work v 0.3 if not it is a $PubDirUrl vs $FarmPubDirUrl issue easily fixed.AJAX with decent degraded behaviour using MochiKit.started in v 0.5I am now thinking of adding blog like features but I am not sure it is worth it considering Pm is working on something it seems. What I have in mind is something along the line of (:logbook display=latest:) or (:logbook display=first count=5:) which would display the entries (in a blog manner). pagelist may be doing that already ...pagelist does it all it seems, now I have to figure out how to use it :D.- CODE REFACTORING REQUIRED
Changelog
- v 0.1 inital release
- v 0.2 following feedback from Ian Barton and noticing a bug in the date format:
- now uses MakeLink to deal with non CleanUrl install.
- pads day numbers bellow 10 with a 0 to fix date format bug in file lookup.
- uses session to save which month is currently displayed to ensure consistent user experience when navigating after browsing through the calendar.
- v 0.3
- finish dealing with non clean url setup (hopefully) and CSS tweaking
- $LogbookNewPost can be set to True to make it easier to create a new entry in the Logbook
- v 0.4 answer to Daniel's requests
- better multilingual management through $LogbookMonths and $LogbookDays
- look & feel make today more visible
- v 0.5 AJAX support using Mochikit
- v 0.6 bug fix (cf. 20060520)


