Thursday, September 16, 2004

That Damned Calendar: The Rest of the Story (Part IV)

So far, we've set up a calendar table that can represent any month of any year from January, AD1 to December, AD9999. (By the way, Notes and Domino is not Y10K-compliant, so there should be developer and admin jobs available for at least the next eight thousand years, with an expected increase around fiscal 9997-9998.) The calendar will respond and adjust to URLs, and we've provided user navigation. Lovely, if all we needed was the ability to print blank calendars. If that's all you need, you can quit coding now and work on the CSS — you're done. The rest of us have to display entries in the calendar.

Ultimately, we need to create view output that will call the addEntry() function on the $$ViewTemplate. If you have been following along from the time this series was originally posted, you need to know that I have changed the addEntry function on the $$ViewTemplate to make the view output smaller. The function should now look like this:

function addEntry(cellId,displayText,urlLocation,titleText) {
var cellArray = cellId.split(';');
for (i=0; i<cellArray.length;i++) {
try {
var cell = document.getElementById(cellArray[i]);
var newLink = document.createElement('a');
var newText = document.createTextNode(displayText);
error += e.message;

The change was made so that entries that span multiple days only need to occupy a single row in the view. (I did warn you that this was a work in progress, right? This doesn't represent a "fix" as such — call it a refinement.)

Given the function, we would like a event entry covering October 1st and 2nd of 2004, titled "2 Day Soiree", and running 8:00 to midnight each day at the Community Centre to make this call:

addEntry('2004_10_01;2004_10_02','2 Day Soiree','EventCalendar/B40F7437715FC15F85256ED30041D17A','8:00 PM - 12:00 -- Community Centre -- Public Welcome');

Of course, that would just be the setup of this particular hypothetical Events database. The title attribute of a link is a valuable asset to an application like this one, since the individual calendar table cells are by their nature too small to include much information in the link text. Don't be afraid to load the title to the gunwhales.

There's not a lot to creating the output, but there is some calculatin' to do. If at all possible, it would be far better to do the caculations on the calendar entry form, but if you're adding the functionality to, say, a Lotus template or some third-party design, you'll need to do the cyphering in the view. It's no big deal to mark a single view and the $$ViewTemplate as "Prohibit design refresh or replace to modify", so you can use this view and still take advantage of template upgrades. If it's a custom design, then do your server a favour and do the calculations on the form. In any case, make sure the view is set to "Treat view content as HTML"; this is code, after all, and we need literal character, not HTML tables or Java applets.

The first thing that needs to be computed is a date list that runs from the start date to the end date. If your design only allows for single-day entries, you can skip this step. (A heads-up here: if you are working exclusively with single-day entries, you can put a hidden sort column in the view, sorted by start time, to force multiple entries on the same day to appear in the correct order. That's not an option for possibly-overlapping multi-day entries, at least not yet. I haven't wrapped my head around the JavaScript to do that bit yet, although I am getting closer.) The formula for that is:

@TextToTime(@Text(@Explode(@TextToTime("[" + @Text(StartDate) + "-" + @Text(EndDate) + "]"))))

That's an R5 formula, I know. For ND6, replace the @TextToTime with @ToTime, and eliminate the @Text around the @Explode. Somewhere in the R5 code stream, the behaviour for exploding a date range changed on me. Don't you just love it when an admin upgrades the server without telling you and you come to work to find eleventy-nine hundred trouble tickets in your inbox?) Since I had seen the results returned as a date list in some versions and a text list of date-like strings in others, I had to use @Text followed by @TextToTime in order to ensure that the final result was a genuine date list. If you do the computation on the form, call the computed multi-value date field IncludedDates. If you have to do it in the view, make this column the first column in the view, hide the column, make sure it is not sorted, and give the column the programmatic name IncludedDates. (Since this value needs to be available to calculate both the category and the cell ids, it has to be the first column. If you were to sort the column, it would interfere with the RestrictToCategory call.)

The next thing to compute is the view's category value. Again, you can do the computation on the form and just use the field name in the view, or do the calculation in the view itself. In either case, we need to derive the year and month values from the dates to get them into the format "yyyy-mm". Both the four-digit year and the two-digit month are required; if you were using the view to display historical information and the year was to be earlier than AD1000 for any entry, Domino would misinterpret the year, and if the calling URL ended in "-1" and there were no January entries, Domino will happily serve the October entries as a partial match ("-10"). Both of those are Bad Things. Okay, the year thing is being anal. Read the entry titled "Alkyds Are Bad, M'Kay"; details like this are what my life runs on.

We also need to allow for categorization at this point. If there is a Category field, it will be prepended to the year-month string, and separated from that string by a single tilde. The different separator makes it easier to extract the SelectedCategory value on the $$ViewTemplate. When we do that concatenation, it needs to be permuted so that every Category value is added to every year-month value. We also need to have the year-month values sitting there solo, since [no category value] is our "All" category. The formula, then, is:

webMonths := @Unique(@Right("000" + @Text(@Year(IncludedDates)); 4) + "-" + @Right("0" + @Text(@Month(IncludedDates));2));
withCategories := @If(Category = ""; ""; Category *+ "~" *+ webMonths);
REM "Remember: the ALL category is just webMonths";

The next column is the actual function call. In the formula below, the webCells variable is the list of cell ids to write to. Again, we'll call on IncludedDates, but this time we don't need to worry about maintaining four-digit years and two-digit months/days. JavaScript does not suffer from any ambiguities when addressing elements by id. (Besides, I didn't include the padding code in the 31 nearly-identical table cell fields you've already put on the $$ViewTemplate, and it's a hell of a lot easier to minimize the importance of padded values in this article than it would be to persuade any of you to go back to the template and edit 31 fields.) The rest of the formula is fairly straightforward; we just need to create a relative link to the document, include the event title or entry subject (as the case may be), and add whatever text we want to appear on mouseover (if any):

webCells := @Implode(@Text(@Year(IncludedDates)) + "_" + @Text(@Month(IncludedDates)) + "_" + @Text(@Day(IncludedDates));";");
linkValue := @URLEncode("Domino" @ViewTitle) + "/" + @Text(@DocumentUniqueID);
mouseOverString := (Whatever you want it to be, it's just a string);
"addEntry('" + webCells + "','" + EventTitle + "','" + linkValue + "','" + mouseOverString + "');"

Again, if you compute that value on the form, it will create less of a load on the server, but it's only practical to do so if you are using a custom database design.

That just about covers everything — in a perfect, error-free world where all months and all categories contain documents, and no entry ever starts in one month and ends in another. Let's take the second case first: an entry that starts in one month and ends in another. As you know, a document either belongs to a category or it doesn't; you can't compute column values based on he category a document is currently appearing in. If you are looking at this month (September 2004), the cells in the table will have ids like 2004_9_1, 2004_9_2, 2004_9_3, and so forth. If there is an entry that starts this month but ends in October, it will try to write to 2004_10_1, which doesn't appear anywhere on the page. Error! No probs — that's why there's a try-catch in the addEntry() function. If an error occurs (and we actually expect errors on a regular basis), it will generate an Exception, which is handled in the catch block. (The catch block is set to accumulate error messages into a variable called error, which you can use for debugging if you want.)

The other problem, though, isn't something that can be handled with try-catch. Did you know that "<h2>No Documents Found</h2>" is not valid JavaScript?. If your users ever come to a month and category combination that contains no documents, the page blows up and you get a trouble ticket. Not something any of us want to design into a database, right? The answer to that is actually fairly simple: block comments. That's why I've waited until now to have you embed the view on the $$ViewTemplate. And do embed it, will you — $$ViewBody is an R4.x thing, and I can't think of a single good reason to use it, ever, in later versions. So, go to the $$ViewTemplate, after the calendar table, and enter:

<script language="javascript" type="text/javascript">
[EMBED VIEW HERE using Create->Embedded Element->View]

In the Embedded View properties, select "Display using view's display property", then go to the second tab and make sure that column headers are not being displayed. You can allow quite a few lines to display — the output for each document is very small and there's no good way to do ViewNextPage or even to let the user know that there are more documents to see in a given month. I usually leave it set at zero.

Have you noticed the remaining problem? The comments prevent errors when the no documents message is returned, but now any JavaScript that is written to the document lives inside JS comments, and so cannot run. That means we need to make one minor change to that last formula to add a "close comment" at the beginning and an "open comment" at the end. It'll look weird if anyone does a View->Source of your calendar, but sometimes leaving mysteries out there is good for the geeks. You can add an explanation in the comments if you're worried about turning up on The Daily WTF.

webCells := @Implode(@Text(@Year(IncludedDates)) + "_" + @Text(@Month(IncludedDates)) + "_" + @Text(@Day(IncludedDates));";");
linkValue := @URLEncode("Domino" @ViewTitle) + "/" + @Text(@DocumentUniqueID);
mouseOverString := (Whatever you want it to be, it's just a string);
"*/ addEntry('" + webCells + "','" + EventTitle + "','" + linkValue + "','" + mouseOverString + "'); /*"

As for other errors, well, it's really up to you to handle things like missing start or end dates on the calendar entry documents. What I have provided is working code, not a heat-and-serve, boil-in-the-bag application. It's not meant to be a complete solution to any business problem; it's only a workaround for the shortcomings in Domino's built-in calendar view. You have enough, now, to apply the same principles to a one-week DayTimer/Organizer-style view

Folks, that's all there is to it. It may not be obvious in the sense that it would be the first thing that jumps into your head when you think about prettying up Domino's calendar, but you can see that it's just a matter of AHA! There's no really esoteric code or anything, you can't brute-force it. It was there all along, waiting for someone to not look at it in just the right way (I believe deeply in the reality of SEP fields, and I believe that there was one around the first version of this calendar.) All I've done is notice it and tell other people about it.

I have a working model now, and when I get it nice and pretty I'm going to take the good folks at (are you a cook, and if not, why not?) up on their offer to host the sample db for download. Once it's up, I'll add a link to it and these articles in my sidebar.

No comments: