Wednesday, September 15, 2004

That Damned Calendar: Finishing the Template (Part III)

Yesterday, we took the $$ViewTemplate to the point where we had a beautiful calendar-looking table (or, rather, something that only needs a touch of make-up, not craniofacial reconstructive surgery). Today, we need to make the $$ViewTemplate morph from month to month, allow for categorization, and provide a bit of navigation. Because we're adding functionality, there is going to be a bit of hopping around on the template. I'd rather add blocks of functionality and explain what's going on as I go than do a neat top-to-bottom progression and create mysteries as I go.

First, I should let you know what the URL for the view is going to look like in use. Keep in mind that we've already determined that we can't use a Notes calendar view, so the view needs to be categorized not only on the (optional) single-category value, but on the month to display as well. I've found that this URL works well for me:

http://server/path/db.nsf/view?OpenView&RestrictToCategory=category~2004-09

I had a wee bit of trouble trying to make the original design work with different regional settings. A big part of that is that changing from month to month relies on a parameter passed in the URL. I don't know how your servers work, but on mine the URL is text. If you remember from part II, the fields that govern the layout of the calendar table are date fields. Text-to-date conversions are scary, and they immediately lock you into a particular machine setting. Obviously, that is going to be a problem if, say, you have one server running with the American mm/dd/yyyy setting in New York and another running with yyyy-mm-dd in Frankfurt, both running replicas of the same database, or even different databases based on the same template. There's only one way to make sure the conversion always happens correctly, and that's to avoid the conversion altogether.

In the hidden fields group at the top of the $$ViewTemplate, somewhere before the StartDate field, put another date field called BaseDate. This field needs to be a hard-coded date, namely January 1, AD1 (or 1CE, as befits your taste). Do not use text (@TextToTime) to create the field value; use a date literal (the square-bracket construct). On my workstation, I'd enter:

[01/01/0001]

The value you enter will be immediately converted to a binary date-time value according to your settings. If someone else using different settings were to view the design, they'd see the value in whatever format they use on their machine. Clearly document (using always-hidden text) the value expected in this field; it is absolutely vital to the working of the database. Test the value to make sure it's right (never make assumptions about your date-time settings, especially on Windows — there are too many ways to change the settings for you to ever really be sure). I'd suggest adding a simple LotusScript action/button to messagebox Format$(document.GetItemValue("BaseDate","dd mmmm yyyy")) and previewing the $$ViewTemplate in Notes. Having done that, we now have a known base date to work from, and can @Adjust that value with strings that we know represent years and months taken from the URL.

About getting the URL parameters: how you do this depends on whether you are using ND6+ exclusively or have to mix with (or settle for) R5 servers. Add another hidden CFD text field called Category between BaseDate and StartDate. We are after the RestrictToCategory parameter if it exists. In ND6+, all we need to use is:

@URLQueryString("RestrictToCategory")

Almost magically, we get a URL-decoded, parsed, gutted and deboned parameter, or an empty string if the parameter doesn't exist in the URL. In R5, you need to create yet another computed-for-display field called Query_String_Decoded with the value Query_String_Decoded, then parse the value you really want out of that:

rawCat := @Right(Query_String_Decoded;"ategory=");
@If(@Contains(rawCat;"&");@Left(rawCat;"&");rawCat)

"ategory"???? It's okay: we can't be sure whether a user-entered URL will use "restricttocategory" or "RestrictToCategory" (both are valid), and we don't want to change the case of the whole category string just to extract it. If you've somehow managed to create another URL parameter that contains the string "ategory", I'd like to know about it; I may not know every word in every language, but I can't think of anything that would cause a conflict. What if the user enters all upper-case? Screw 'em, I say -- they can wait for you to upgrade to ND6, where @URLQueryString eliminates the problem.

Remember, I said that the formula you have now in StartDate was just a temporary that would allow you to preview and verify the table. Now we need to make it "real":

dateCat := @If(@Contains(Category;"~");@Right(Category;"~");Category);
yearString := @Left(dateCat;"-");
monthString := @RightBack(dateCat;"-");
year := @TextToNumber(yearString);
month := @TextToNumber(monthString);
@If(@IsError(year)|@IsError(month); @Adjust(@Today; 0; 0; 2-@Day(@Today); 0; 0; 0); @Adjust(BaseDate; 1-year; 1-month; 0; 0; 0; 0))

Let's walk through that one. If the URL is what we expect, then we'll be able to extract a year value and a month value from it. In order to create the calendar table, StartDate needs to be the first day of the month being displayed. If there is an error with either the month or the day, I am deliberately setting StartDate to the second day of the current month. Why? Simple enough: This $$ViewTemplate is going to contain two complete web pages, one hidden when @Day(StartDate)=1 (everything working as expacted), and the other hidden when !(@Day(StartDate)=1) (the problem condition). So add a couple of carriage returns above the HTML you now have, then select the whole deal and add a hide-when formula:

!(@Day(StartDate=1))

Now add the following HTML between the motly collection of hidden fields and the existing web page:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Final//EN">
<html>
<head>
<title>(your title goes here)</title>
</head>
<body>
Finding current month...
<script type="text/javascript" language="javascript">window.location.search = '<Computed Value>';</script>
</body>
</html>

Feel free to pretty that up a bit, but don't make it something that's going to take forever to load. The computed text in that JavaScript is going to point the user to the current month, optionally with the "default category" for your view, if there is one. Should you decide to use a default category, add a field called DefaultCategory to the pile of hidden fields and do your lookup there. The formula for the computed text would look like this:

@If(DefaultCategory=""; "";@URLEncode("Domino"; DefaultCategory) + "~") + @Text(@Year(@Today)) + "-" + @Right("0" + @Text(@Month(@Today)); 2)

Now select that whole redirection web page, and give it this hide-when:

@Day(StartDate)=1

Now, if users somehow winds up at your view with an invalid or incomplete URL, they will be quickly redirected to the current month, optionally with a default restricted category for the user or database.

That handles incoming URLs, but now we have to set about creating the navigation. Before we can worry about "getting there from here", we need to know where "here" is. We already have StartDate to figure out which month the user is viewing, but we need a way to determine which category the user is looking at, if any. Back to the hidden fields collections at the top then; we need to create another one. Call this one SelectedCategory. Its formula is:

@URLEncode("Domino"; @Left(Category; "~"))

Now we can create the simplest navigation links, the Previous and Next controls. How you wat to create them is up to you; I simply use text links. My "previous" link is computed text that looks like this:

lastMonth := @Adjust(StartDate;0;-1;0;0;0;0)
year := @Year(lastMonth);
month := @Month(lastMonth);
textMonth := @Select(month; "January"; "February"; "March"; "April"; "May"; "June"; "July"; "August"; "September"; "October"; "November"; "December");
"<a href=\"ViewName?OpenView&RestrictToCategory=" + @If(SelectedCategory = "";"";SelectedCategory + "~") + @Right("000" + @Text(year);4) + "-" + @Right("0" + @Text(month);2) + "\">" + textMonth + " " + @Text(year) + "</a>"

The "next" link is pretty much the same, but with the StartDate value adjusted one month ahead instead of one month back. I generally make these two links side-by-side, separated by a couple of bars (||), centred above the calendar table.

I guess that just leaves us with the quick link selectors. We'll need to create three <select> fields in passthru HTML for the users to play with: one to select the category; one to select the year; and one to select the month. While we're at it, we'll need to create an HTML form for those fields to live in. Find a suitable place on the template to insert your form, and create something like this:

<form name="nav" method="get" action="#" onsubmit="return false">

</form>

The category selector is optional, but that doesn't mean you need to leave it off of the $$ViewTemplate. Like all code-y bits, you want to make reuse as easy and painless as possible, and copying from one database into another is about as easy and painless as it gets. If you initially create the $$ViewTemplate in a database that doesn't use categories and later move it to one that does, you'll have some re-coding to do. If you create the category selector using a computed-for-display field, you can have that field return an empty string, which means that there will not be a category selector for the user to interact with, so you get the field when you need it and don't when you don't. The easiest way to get the categories is to use a separate lookup view, categorized on the category value of your calendar entries, and use an @DbColumn against the categorized column.

allCats := @DbColumn(""; ""; "(CalendarCategories)"; 1);
@If(@IsError(allCats); @Return(""); allCats = ""; @Return(""); "");
allOption := "<option value=\"\">All</option>";
options := "<option value=\"" + @URLEncode("Domino"; allCats) + "\">" + allCats + "</option>";
"Select Category: <select name=\"Category\">" + @Implode(allOption:options; "") + "</select>"

The year selector should always be there, as should the month selector. The years listed in the selector need to be able to slide around a bit, so you can hard-code the label and <select> tag on the $$ViewTemplate, but the option list needs to be computed. If I could be sure that ND6+ was being used, I'd use an @For loop to create the options, but since there may be R5 servers left out there, I have to hard-code a range. I figure five years before and after the currently-displayed date should be enough for most purposes, so the CFD field for the year selector would look like this:

startYear := @Year(@Adjust(StartDate;-5;0;0;0;0;0));
yearList := startYear:(startYear + 1):(startYear + 2):(startYear + 3):(startYear + 4):(startYear + 5):(startYear + 6):(startYear + 7):(startYear + 8):(startYear + 9):(startYear + 10);
@Implode("<option value=\"" + @Text(yearList) + "\">" + @Text(yearList) + "</option>"; "")

The month selector can be completely hard-coded unless you have language issues to deal with. In English, it would look like this:

Select Month: <select name="Month">
<option value="01">January</option>
<option value="02">February</option>
<option value="03">March</option>
<option value="04">April</option>
<option value="05">May</option>
<option value="06">June</option>
<option value="07">July</option>
<option value="08">August</option>
<option value="09">September</option>
<option value="10">October</option>
<option value="11">November</option>
<option value="12">December</option>
</select>

That leaves the "go" button. Now, it is possible to do everything that's required directly in the onclick event of the "go" button, but that would be ugly and would offend my sensibilities. Instead, I add a function to the <script> tag in the <head> of the display web page (not the redirection job). It looks like this:

function goToDate() {
var f = document.forms['nav'];
var cat = f.Category.options[f.Category.selectedIndex].value;
var year = f.Year.options[f.Year.selectedIndex].value;
var month = f.Month.options[f.Month.selectedIndex].value;
window.location.search = 'OpenView&RestrictToCategory=' + (cat=='')?'':(cat + '~') + year + '-' + month;
}

That means that all the "go" button has to do is call goToDate():

<button id="go_button" onclick="goToDate()">Go!</button>

In tomorrow's installment, we'll actually design and add the view. That is, if I don't have to spend the whole day on the sneakernet again.

No comments: