Wednesday, September 29, 2004

Office Pools As Learning Tools

As we approach the United Way silly season, I can't help thinking that most of the really useful Domino web tricks I've come up with were not so much aimed at solving business problems, but at solving various charitable fundraising problems. Eventually, all of these methods made their way into one line-of-business support application or another, but the fact remains that the tricks came about because I needed new and compelling ways to separate people from their money. And the best part of doing it on Domino is that authenticated users can't deny participating when the collector comes around to their desks.

Games and pools need to be immediately usable. If the user has to ask, they will move on without actually asking. Most of these deals run on a fifty-fifty basis (the winners can claim half of the funds taken in, the charity gets the other half, and the administrators and developers can bask in the glow of a good deed well done), and a user can tell at a glance whether or not the potential winnings are worth the price of admission. After all, if you're the only one dumb enough to play, then all you can get is half of your money back. You might as well have just given the money to charity to begin with — and the reluctance to do that is why we're building the games in the first place.

What follows is a necessary disclaimer designed to cover my posterior like a pair of corduroy Dockers: I am not suggesting, even for a moment, that anyone develop or deploy any lottery or game of chance not fully licensed and approved by the appropriate federal, provincial, state and/or municipal authorities. We now return to our regularly-scheduled broadcast.

Take a look at any of the duly-authorized and completely above-board draws, bingos, auctions and/or pools that may be run in your organisation today, and think about how you might move them to the technological cutting edge using nothing more than your wits and your favorite collaboration platform. You'll probably find, as I did, that truly compelling game UIs are web creatures. It's not that you can't create a compelling UI in the Notes client; but the business-oriented Notes client isn't very good at the kind of Vegas-inspired UI effects needed to strip the very food from your co-workers' children's dinner plates. For charity, of course. If you do it right, though, you can bring some of what you've done back into your business apps and make it permanent.

Tuesday, September 28, 2004

Still Kicking ...

... but somewhat busy. We will rejoin our regularly-scheduled programming shortly. Please stand by.

Being Publicly Private

Warning: Morality Tale

As Ed has noted, in the first two weeks of blogging I seem to have exposed quite a bit of myself. I've done that for a couple of reasons, and I hope to explain them here.

I really believe that's it's hard to know someone without knowing where they came from. Obviously, I am going to hold some opinions that may be hard for the normies out there to understand. I will react to some things in quite a different way from folks who haven't been where I've been. I have made some eFriends (a couple-three-four of whom I believe I could just call friends if all of this electronic nonsense were removed), and those people should know why I believe the things I believe, and why I react the way I do.

Apart from that, though, I want to put aside the idea that things like recovery from addiction and homelessness are things that people should be ashamed of or afraid of talking about. I was a horrible drunk — more than nineteen years ago. I am sure that my friends and family didn't speak of it proudly. My co-workers and supervisors probably didn't brag about having the highest-flying, least-reliable SOB who ever lived on their crew (and YOU can't have him, nah nah nah nah nah nah). When I entered recovery, I sure didn't want anyone knowing who I was. Oddly, I had no problem demonstrating addictive and impaired behaviour in public; it was sobriety that was a problem, I guess.

I am an alcoholic and a multiple-drug addict. I will be that until the day I die. I am not ashamed of my condition, any more than I would be ashamed of having leukemia that was in long-term remission. No-one can hold my deep, dark secret against me unless I try to keep it secret.

At least the first few steps on the road to addiction were voluntary. I can see how some people might be inclined to attach a moral turpitude tag to that aspect of my past. People who don't drink can never be alcoholic; those who don't do drugs will never become addicted to them. (I exclude here those who have become addicted to narcotics due to poor medical practice.) What I can't wrap my tiny little mind around is how people can attach the same attitude to something like poverty; yet I can say that I was treated with more scorn and disdain as a sober homeless man who spent every penny he could scrape up at Kinko's printing resumes than I was as a raging, but employed, everything-oholic.

Something about that strikes me as wrong. Yes, we're all aware of the folks who are left on the streets because of ridiculous libertarian attitudes towards people with severe mental illness, and there is that segment of the population, particularly among youth, who ain't gonna kowtow to The Man no matter what the personal consequences may be — at least until they've had a proper chance to evaluate those consequences. There are those, though, who have just had a string of bad luck.

There are a few of you out there, I suspect, who believe (as I do) that I have at least a budding aptitude for this line of work, yet I spent nearly five years shining shoes (and losing tech currency with each passing day) before I was able to land a zero-expectations, probationary, entry-level job. I'm not the only useful person who has ever been dealt a few bad blows in life — I met few folks on my journey who should have been doing something more than looking for their next meal. What this all boils down to is a plea that you try to judge people on the basis of who they are as individuals. You never know who you might find if you can look past the shabby clothes for a second or two.

Wednesday, September 22, 2004

We Know Why You Fly

Does anyone else out there find the new American Airlines commercial a little, um, disturbing? Data mining might get them when, where, how often and with whom, but how are they getting why? Time for the tin foil hat, I guess.

History Bites

I don't know if this Canadian television show is available outside of Canada, but if you get a chance to check it out, you should. You may me able to find it on your local public broadcaster or on the non-Canadian version of the History Channel.

The show is the brainchild of Rick Green, whom Americans might know as the hapless title character of the "Adventures with Bill" segments of The Red Green Show. Green is actually a capable and accredited educator, but he's been involved in ensemble comedy in Canada for about as long as I can remember. A few years ago, he took advantage of his "star" power to propose a project that would combine the two.

History Bites is premised on one question: "What if television had been there?" Each half-hour show has the viewer flipping through the channels on his or her television, offering glimpses of critical events in the past from news clips on CNN (or whatever it might have been called in that region at the time), interviews with key figures by Larry King and Barbara Walters, period lifestyle clues from episodes of Seinfeld or All in the Family as well as "science" programming, kids' shows, David Letterman, Dennis Miller, the cable company's TV Guide channel, and so on. The ensemble cast, featuring Ron Pardo's incredible mimicry, does a creditable job of capturing the flavour of the shows they parody. As with most educational programming, there is evidence that "no bank balances were harmed in the making of this motion picture", but this is educational programming that doesn't feel especially educational. You (and your kids) will love it, even if you don't always get the local Toronto commercial references. Don't worry, the history is not all (or even much) Canadian, just the local commercials.

If you know anyone who doesn't like reading about a bunch of dead white guys (and, let's face it, most history text books are drier than the begats in the Torah), treat them to this. It might spark enough interest to make them want to learn more.

Tuesday, September 21, 2004

Vroon! Weeee-laaah!

A few days back, I was the victim of an attempted drive-by trolling. The young fellow who posted the comment had put on his best Zaphod Beeblebrox persona in what I assume was an attempt to impress with sheer hoopiness. Oh, the irony!

Your Humble Narrator has been clean and sober since July 10, 1985. In the six or seven years prior to that date, I would have made old Zaphod look like Arthur Dent on the wagon. I have imbibed, inhaled and ingested (but never injected — I hate needles) more than any human can reasonably expect to survive; in fact, without frequent intervention, I would not have lived through the period. There were a lot of days when I had at least two heads, and they both (or all) hurt like hell. And I have experienced the sudden appearance of a molten landscape with penguins without benefit of an Infinite Improbability Drive. Yeah, I really knew where my towel was.

I was a two-four-a-day maintenance drinker. (That's 24 bottles of proper 5% Canadian beer, Newkie Brown, Guinness and/or various high-test, 14%+ homebrews for those whose experience is with three-two Bud.) There were times, though, when I needed to get drunk, so I might throw a forty of Navy rum (or, when I needed to feel high-class, a bottle of Chivas Regal Royal Salute or maybe an eighteen-year-old Islay — not that I could tell any of it from peat moss and rubbing alcohol in the state I was usually in) onto the fire. If I could find that Ol' Janx Spirit, I'd have guzzled gallons and damn the side effects. I downed eight successive 48-oz pitchers of draught in twenty minutes to win a beer-drinking contest once, after having gotten enough of a buzz on to rise to the challenge (one of the few things I actually recall is that my "crew" was already at the point of singing the three or four lines were knew of The Black Velvet Band over and over again when the temporary-duty-trip grunts challenged us Noble Aircraftsmen to a match of military skills). I had to rupture my abdominal wall to accomodate the volume, but this was for pride of service. I was quite adept at getting scrips for whatever uppers or downers I wanted (Seconal was a particular favorite), and you just know there are times a fellow needs to, um, get mellow. One also needs an occasional face-to-face with the deity of one's choice, and let's not forget about the poppy juice — everyone needs a break now and then, and it really helped with the never-quite-healed-properly-broken-neck pain. My favorite game was Morning Jeopardy!, and the correct questions were always "what the hell time is it, Alex", "where the hell am I, Alex", and "who the hell is this Alex you think you're talking to?"

The surprising part of all this is not that someone can voluntarily do that much damage to his body and his psyche, it's that he can do the vast majority of that damage while in the military, maintaining critical avionics systems that, if things go wrong, can try to force a Sea King helicopter to maintain a hover precisely forty feet underwater. When I was on my game, you see (that is, when I wasn't in a falling-down stupor), I was a hell of a tech. People were not afraid to express their disapproval to my face, but they always covered my ass. Nothing ever hit paper and stuck. At the time, I thought that was a Good Thing. I may have had to do extra duties now and again, but I stayed out of jail and (this is the important part) I was never administratively referred to rehab. Once you get referred, you have to stay dry for a year or face discharge.

Then I woke up one morning completely blind. I'd been "dead" before and revived; that wasn't a problem. This time, though, I might have to live with the consequences, in the dark, and that was scary. After what seemed like days, but was probably only a couple of hours, the light came back, and I wished it hadn't. I'd never felt that level of pain before (not even from a 1981-vintage cranial arteriogram; ask anyone who's had one what that's like). It took me more than two hours to button up my uniform shirt (timed it; hell, I was already beyond late and was trying to compute the AWOL punishment against the out-of-uniform penalty, and shaving was out of the question that day since my hands were doing pretty much what they pleased without consulting me). I managed to half-stagger, half-crawl to the hangar. Even looking and smelling like I did, and after arriving several hours late for work, it took an unbelievable amount of time and interviews with superiors to finally get my request for a voluntary medical referral to an alcohol rehab clinic approved. Losing my diagnostic skills for twenty-eight days and an afternoon a week for a year was, apparently, worse than watching me kill myself that way.

That was July 10, 1985. Nineteen years and a bit later, I still live with the damage I did. My heart and liver are largely scar tissue. The neck I broke playing Rugby drunk still causes me pain and occasional partial paralysis of my left side. (Five-ten and fifteen stone is small for a tighthead prop, even if you can push a ruddy ton uphill. Sober people that size play the wing three-quarter or, if they're smart as well as sober, sit singing bawdy songs in the stands while the monsters on the pitch get on with the carnage.) The back I injured falling (drunk, of course) from a Sea King flares up now and again, and needs traction for relief. I am prone to paranoid depression, and worst of all I can't find the little travel bag with the tin of olive oil in it. When I'm not stepping out of the shower, I really don't give a flying [censored] where my towel is anymore. I've dealt with much scarier things than the Ravenous Bugblatter Beast of Traal and lived to tell the tale.

To sum up: if you want to try to impress me with your drinking prowess or tales of chemical adventures, you can't. Whatever you've done, I've done more and couldn't be less proud of it. And if you're covering the ass of someone who has a problem like mine, the little trouble they may get into now is nothing compared to the big trouble that's coming. Stop it.

Why code here?

A big part of what I wanted to do with this site from the beginning is captured in the web calendar articles. It's not about putting code out there so much as allowing you to follow my thought processes as I put some of my ideas together. I am not a particularly brilliant coder, although I do score the occasional three-pointer. Heck, I'm just now getting to a level that lets me see the humour on The Daily WTF. If I have one indispensable quality, it's that I refuse to be locked into the obvious. I am willing to explore a few of my wilder ideas, even if some of them lead to dead ends. They're not all winners. Not by a long shot. With the calendar, I went through a number of decreasingly-poor implementations before polishing this version publicly. I would not have used the calendar here at all except for the recent rash of requests I've received, precisely because it was pretty much a done deal already.

I'm not trying to create any how-tos. There are a lot of people who would be better hosts of "This Old Database" (or is that "The New Yankee NSF"?). I want to try to explore the creative process. If you were to ask me which of the calendar articles I thought was the most important, it would have to be The Principles (Part I). As I said, any idjit can code the thing once he (or she) knows where he (or she) is going. It may take some longer than others to put the code together, but all it takes is a bit of Formula Language, a bit of basic HTML, and an even littler bit of JavaScript and CSS to make it work. None of this really requires grokking Notes or the web; the "aha!" for me was simply realizing that when a view is set to "Treat view contents as HTML", the actual output from the view didn't necessarily have to be HTML.

The hard part of a development project is rarely the actual coding (assuming a basic knowledge of the language and environment), it's in evaluating the starting point, determining the destination, and developing the roadmap between those points. The rest is really just filling in the blanks. Yes, there are good ways and bad ways to fill in those blanks, but in the end it's a mechanical process. When the destination doesn't appear to be something that came on the CD, you need to let your imagination loose. (Outline views won't create displays for dates that don't have an entry, and Calendar views don't let you categorize. Now what, smart guy?)

The next time you see code here, I hope that it is a fresh problem for me as well as for you, and that it is once again something that Notes "doesn't do".

Sunday, September 19, 2004

Lazy Nogoodniks may now download That Damned Calendar

The code has been uploaded to the OpenNTF.org CodeBin as "Open WebCalendar". It's an .ns5 to make things easier all-round; ND6 users can make the few changes required to make it an all-6 solution if they want. In keeping with the spirit of OpenNTF.org, please repost any improvements you make to the code. Since this is not a fully-realised application, it has gone into the CodeBin instead of being thrown out as a Project. I will periodically review the entry and make any fixes/improvements as may be necessary.

Friday, September 17, 2004

That Damned Calendar: Errata

While preparing the database for OpenNTF.org, I ran across a couple of errors in the code and a couple of things that I could have done a bit better. I have no qualms about editing postings like those ("articles" as opposed to "opinion and observation"), so the original articles will be corrected as necessary. Those who have been following this since the beginning, though, will need to be told which bits have been changed and why. Let's do the bugs first, shall we?

First, the goToDate() funtion is missing a set of parentheses. As it stood, if there were no categories in use, you would always go to the redirect page, and then to the current month. The problem is this line:

window.location.search = 'OpenView&RestrictToCategory=' + (cat=='')?'':(cat + '~') + year + '-' + month;

If "cat" is an empty string, then year and month are ignored as well. The line should have read:

window.location.search = 'OpenView&RestrictToCategory=' + ((cat=='')?'':(cat + '~')) + year + '-' + month;

As well, the line checking the Category field is going to cause fires and explosions if there are no categories in use in your application. The Categories field does not exist, and when you try to examine its properties, the script errors out. This line:

var cat = f.Category.options[f.Category.selectedIndex].value;

needs to be changed to:

var cat = (f.Category)? f.Category.options[f.Category.selectedIndex].value: '';

Next, the formulas for the Previous and next links are missing "?OpenView&RestrictToCategory=". The formula given for the Previous link should have read:

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=\"" + @ViewTitle + "?OpenView&RestrictToCategory=" + @If(SelectedCategory = "";"";@URLEncode("Domino";SelectedCategory) + "~") + @Right("000" + @Text(year);4) + "-" + @Right("0" + @Text(month);2) + "\">" + textMonth + " " + @Text(year) + "</a>"

Note, too, that I have put "@ViewTitle" in where ViewName used to be. I don't know quite what I was thinking when I typed the original posting; the application has always used @ViewTitle, even in its earliest incarnation. Sorry. And I have incorporated the enhancement discussed below.

On to the refinements, then. I had been storing a URL-encoded version of the current category in the SelectedCategory field. It occurred to me that it's easier to @URLEncode that value where it needs to be encoded than it is to try to decode it when needed for R5 use, like for the window title and page heading displays. Change the formula for the SelectedCategory field to:

@Left(Category; "~")

The formula for the category picker already has taken this change into account. Again, there may be typos remaining. The demo database is clean and works, but getting its bits and pieces up here as HTML has induced the odd problem. If you find anything, please let me know.

Addendum

A change to keep Internet Explorer happy: in the two spacer fields, insert a &nbsp;into both the "spacetop" and "spacebootom" cells. That will force the borders (if any) to display in those cells.

For Mozilla: add "height: 58px;" to the DIV.cellcontent CSS entry, or Mozilla will extend the <div> content outside of the table cell.

Thursday, September 16, 2004

The band will be rockin' when I get there

There aren't many Ramones left on this terrestrial plane. Johnny has joined Joey and Dee Dee on stage at the great CBGB in the sky. Unconfirmed reports have St. Peter greeting him at the gates with, "Gabba, gabba, we accept you, one of us."

Johnny Ramone
Rockuiescat in pacem

About that hockey lockout....

Fans of the current game should probably avert their gaze. I don't hold opinions, I speak eternal truths, and sometimes the truth hurts. You are free to disagree with me, of course, and I will defend to the death your right to disagree and voice that disagreement — but you'll have to do so with the certain knowledge that you will be wrong.

The mere fact of my Canadian birth and life-long residence in my Home and Native Land makes me eminently qualified to comment authoritatively and in depth on the state of the game of professional hockey. It is my stereotype, my heritage and my birthright. And now, with news of a lockout of the NHL players eclipsing coverage of events of genuine social, political and economic import throughout the Canadian media, it is my wont.

The current NHL game blows buffalo-bladder bagpipes. Badly.

I am just old enough to remember the Original Six, but even the Gump had given in and started wearing a mask in net by the time hockey began to rule my life. (That Age of Ascension was once an inevitability for Canadian lads.) Even though there were very few players who were not Canadian-born playing in the NHL, at eighteen suited players per team and only six teams, you can be sure that the players who made it to the NHL were better than just good. There were guys that could handle themselves in a confrontation (even Bobby Orr, the Gretsky of his day, could go ten rounds with the baddest cat on the other team), but there was no Designated Goon (the player whose only purpose is to cause injury to an opposition target). Helmets? Unthinkable, unless (like Stan Makita) the player had suffered an injury of such a kind as to make continued play without a helmet suicidal. No, children, there were no armoured players in those days. The pads that were worn were made of stiffened leather over a thin layer of felt. The game was chippy, but because the guy doing the chipping was as likely to get hurt as the guy being chipped, there was nothing like the level of violence you see today. Yes, there were abominations, such as the Two Handed Clubbing, of which you can read in the Saga of Maurice the Rocket, but I'm talking about the day-to-day style of play.

At the same time, the team/player relationship was such that a player would likely be with a team for his entire career. More often than not, that player had spent his entire junior career in the team's farm system. Sure, some of them may have had to work at normal jobs in the off-season; after all, free agency had not been invented yet. I'm not saying that professional players should be making the equivalent of eight bucks an hour in this day and age. The old, bad system meant that fans knew their players (hereinafter referred to as The Good Guys), and could hold some real enmity for the players on the other teams (Them Bums). The change to free agency and the player movement that it brings has pretty much killed the Habs. Les Canadiens were not just a team in Montréal, they were the team of Montréal and of all of Québec. My team was the Leafs, and by Leafs I mean Davey Keon, Tim Horton, Norm Ullman, Ronny Ellis, Johnny Bower, et al. None of them was picked up on a short-term contract for a playoff run.

Then came The Expansion. Twelve teams. The game seemed to survive it. At least I found it not only watchable, but exciting. A few years later, Buffalo and Vancouver were allowed in to bring the total to fourteen teams, and the roster size was increased. At about the same time, I began to lose interest in the game. I still watched it, but I didn't enjoy it nearly as much. Given my age at the time, I put a lot of that disinterest down to the inability to easily collect a full-league deck of hockey cards and memorize everyone's stats.

The horrible truth was that the game was changing, and not for the better. I finally realised that there was a problem during the historic Canada/Russia series in '72. It wasn't just that the Canadian team looked out of shape and sluggish compared to the Russians, it was that Bobby Clarke had to deliberately cripple Valeri Kharlamov for life in order for the Canadian team to even appear competitive. I could not celebrate the Holy Goal of St. Paul of Henderson; I was in the can puking.

I demanded emancipation from hockey's oppression the day I saw Dave Schultz of the Philadelphia Flyers in a playoff game skating with one leg actually thrown over the back of an opponent, as if to ride him like a pony. Something about that just told me that this wasn't My Game anymore. The Howie Meeker game was gone, the Don Cherry game had come to take its place.

I have watched the odd game since then, and I can honestly say that I have seen nothing to improve relations between me and The Game. Checking is no longer about puck control, it's all about inflicting injury. More armour equals more injuries. Hearing rabid Leafs fans cheer as Brian McCabe makes yet another deliberate and obvious dive at an opponent's knees arouses such a depth of anger in me as to endanger innocents around me. This may be what they want to see in Phoenix and San Jose, but it ain't Hockey, and I ain't a fan.

So the NHL team owners have spent themselves into a hole, and now they want to take a mulligan on those player contracts. As much as I am sickened by the idea that some kid who can hardly spell his own name (because he was playing major junior when he should have been going to high school) can make seven million U.S. a year doing something that, in the final analysis, makes absolutely no contribution to the betterment of mankind, I am nauseated more thoroughly by people who, in the course of operating their businesses, have wilfully and knowingly spent more than they can reasonably hope to take in, and then expect other people to pay the price for their mistakes. Neither side is particularly right in this dispute; the owners seem more wrong, but I'll have to go with "C: Let's start all over again", Regis. Final answer.

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');
newLink.setAttribute('href',urlLocation);
newLink.setAttribute('title',titleText);
var newText = document.createTextNode(displayText);
newLink.appendChild(newText);
cell.appendChild(newlink);
}
catch(e){
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";
@Trim(withCategories: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]
*/
</script>

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 OpenNTF.org (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.

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.

What's with IT people?

Okay, I may "do" computers for a living, but I'm absolutely convinced that I will never understand IT people. Today, I spent most of my day disconnected, not (as you might imagine) because I was at a remote office or otherwise prevented from plugging in, but because the networking gnomes decided to reduce the number of available IP addresses in the DHCP pool to a "musical chairs" number. Not accidentally, they did this by design. Pardon my ignorance, but if you have fewer IP addresses than you have people who need them, how can you expect them to get anything done?

I wonder if we set things up for our clients this way too...

Tuesday, September 14, 2004

That Damned Calendar: The Table (Part II)

I've already described the table you'll need, and seeing as we're trying to display a view, why not let's make the form that table appears on a $$ViewTemplate for something-or-other. (While you're at it, create and save a something-or-other view to go with the $$ViewTemplate. Don't worry about the view design yet, you'll just need a view with the right name saved so you can look at the $$ViewTemplate.)

The question is, how do you make it? Well you're going to need three hidden fields at the top before you go any further. The first is StartDate, which represents the date of the first day of the month you want to display. (It's also the likely name of the StartDate field in your calendar entry forms/documents, so it won't add another field name to the database list. I like keeping things tidy when I can.) Like all of the fields on the $$ViewTemplate form, it is computed-for-display, and for the moment you can make it the first of this month:

@Adjust(@Today;0;0;1-@Day(@Today);0;0;0)

That formula will get a bit bigger when I discuss safe methods for switching dates and categories. It will be enough for you to see what the table's a-gonna look like, though, and you shall see that it is indeed pretty.

The second field is NumberOfDays, which (oddly enough) is a number representing the number of days in the month you're looking at. It is a computed-for-display number field, and its formula is:

@Day(@Adjust(@Adjust(StartDate;0;1;0;0;0;0);0;0;-1;0;0;0))

For those of you who don't follow, start with the inner @Adjust. We adjust the "first-of-the-month" date forward one month, which gives us the first of next month. We then @Adjust that date back one day, which gives us the last day of this month. @Day gives us the number of that day which, by sheer coincidence, is also the number of days in the month.

The third is a little number I like to call VarName. It's just:

@Text(@Year(StartDate)) + "_" + @Text(@Month(StartDate)) + "-"

That little ditty will be used in the table cells below to create the id values.

Personally, I like to set this form to "Treat contents as HTML". You can set it to "Treat as Notes" and mark everything as passthru HTML, but I'm going to tell you here and now that there won't be much Notes-native stuff that you can leave unmarked. The table we're creating absolutely has to be in HTML, and the view itself is going to be marked "Treat as HTML", but its content is actually going to be JavaScript. Save yourself some torment, and mark the form as "Treat content as HTML". Just do it. Stop whining! I don't care how you usually work, just do as I say! There'll be no dessert if you don't.

Make a new Designer paragraph, and make sure it's not hidden. After all, you do want something to be sent to the browser. Just to take a peek at the table, then you'll need to do some work. Start with a standard HTML heading (NOTE: the addEntry function has been edited since the original posting):

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Final//EN">
<html>
<head>
<style type="text/css">
BODY {font-family: verdana; font-size: 12px; color: black}
H1 {font-size: 22px; font-style: normal; font-weight: bold; line-height: 22px; color: #ff0000}
TH {font-family: verdana; font-size: 12px; font-weight: bold; text-align: center; background-color: #ccccff; border-bottom: solid black 1px}
TD {font-family: verdana}
TABLE.outer {border: solid black 1px}
TD.spacetop {font-size: 10px; font-weight: bold; height: 15px; text-align: center; background-color: #999999; border: solid black 1px}
TD.spacebottom {font-size: 10px; font-weight: bold; height: 60px; text-align: center; background-color: #cccccc; border: solid black 1px}
TD.weekendtop {font-size: 10px; font-weight: bold; height: 15px; text-align: center; background-color: #ffcccc; border: solid black 1px}
TD.weekendbottom {font-size: 10px; font-weight: normal; height: 60px; text-align: center; background-color: #fff0f0; border: solid black 1px}
TD.weekdaytop {font-size: 10px; font-weight: bold; height: 15px; text-align: center; background-color: #ccccff; border: solid black 1px}
TD.weekdaybottom {font-size: 10px; font-weight: normal; height: 60px; text-align: center; background-color: #ffffff; border: solid black 1px}
DIV.cellcontent {height: 58px; overflow: auto;}
DIV.cellcontent A {display: block;}
</style>
<script language="javascript" type="text/javascript">
var error;
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');
newLink.setAttribute('href',urlLocation);
newLink.setAttribute('title',titleText);
var newText = document.createTextNode(displayText);
newLink.appendChild(newText);
cell.appendChild(newlink);
}
catch(e){
error += e.message;
}
}
}
</script>
</head>
<body>

Didja catch the try-catch? That'll be right some frikkin' important later, my son. (That phrase carries a lot more import in Newfoundland than, say, nota bene, so if you can say it to yourself in Newfanese you'll really get the gist of what I'm trying to get across here. By the way, for those of you who are from away and have never been screeched in, the name of the island is pronounced noof'n'LAND. Da little accent's on da "noof" and da big accent's on da "LAND". The "ound" is properly reduced to a barely-audible schwa-en combination. The typical foreign pronunciation, in which the word "found" can clearly be heard, is just plain wrong, and something of an insult. You may be invited in for dinner, but you'll not get more than two servings — three at the very most.) I'm not introducing the view at this point, but all it's going to do is call the addEntry function a few times.

You'll probably want to do something about the stylesheet later, particularly the colours. That I'll leave in your capable hands. In case you're wondering, setting the links inside the cellcontent divisions to display: block means that you never have to worry about creating line breaks between entries, and putting the div inside the lower part of the table cells means that the days become individually scrollable while the overall look of the calendar remains, well, calendar-looking. The multitude of sizes and shapes displayed in the native Domino calendar is a real turn-off. The calendar may be functional, but let's face it, it looks like hell.

Now, throw in the title block for your calendar. Any old HTML stuff you want to display is fine by me. After that comes our calendar table:

<table class="outer" width="735">
<thead>
<tr height="20">
<th width="105">Sunday</th>
<th width="105">Monday</th>
<th width="105">Tuesday</th>
<th width="105">Wednesday</th>
<th width="105">Thursday</th>
<th width="105">Friday</th>
<th width="105">Saturday</th>
</tr>
</thead>

That gets us all the way to the first row of the meat of the calendar. Now we need to figure out how many spacers we need before the first day of the month. Add a computed-for-display text field with the following formula:

spacer:="<td valign=\"top\"><table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\"><tr><td class=\"spacetop\">&nbsp;</td></tr><tr><td class=\"spacebottom\">&nbsp;</td></tr></table></td>";
@If(@Weekday(StartDate)=1;"";"<tr>" + @Repeat(spacer;@Weekday(StartDate)-1))

(The downloadable database NS5 version contains a fix for the @Repeat overflow error in R5.) Now for the "meat" of the table. You'll need to create 31 nearly-identical fields, all computed-for-display text fields. The first twenty-eight look like this:

dayNumber := 1;
thisdate:=@Adjust(StartDate;0;0;dayNumber-1;0;0;0);
thisday:=@Weekday(thisdate);
isWeekend := @If(thisday = 1:7;@True;@False);
@If(thisday=1;"<tr>";"") + "<td><table cellpadding=\"0\" cellspacing=\"0\" width=\"100%\"><tr><td class=\"" + @If(isWeekend;"weekendtop";"weekdaytop") + "\">"+@Text(dayNumber)+"</td></tr><tr><td class=\"" + @If(isWeekend;"weekendbottom";"weekdaybottom") + "\"><div id=\"" + VarName + @Text(dayNumber) + " class=\"cellcontent\"></div></td></tr></table></td>"+@If(thisday=7;"</tr>";"")

The only thing that changes is the first line. In the first field, daynumber is 1, in the second it's 2, and so on. I did that on purpose. Copy-and-paste beats the crap out of type-and-type-and-type-and-type-and-type. The twenty-ninth through thirty-first are only slightly different:

dayNumber := 29;
thisdate:=@Adjust(StartDate;0;0;dayNumber-1;0;0;0);
@If(!(@Month(thisdate) = @Month(StartDate));@Return("");"");
thisday:=@Weekday(thisdate);
isWeekend := @If(thisday = 1:7;@True;@False);
@If(thisday=1;"<tr>";"") + "<td><table cellpadding=\"0\" cellspacing=\"0\" width=\"100%\"><tr><td class=\"" + @If(isWeekend;"weekendtop";"weekdaytop") + "\">"+@Text(dayNumber)+"</td></tr><tr><td class=\"" + @If(isWeekend;"weekendbottom";"weekdaybottom") + "\"><div id=\"" + VarName + @Text(dayNumber) + " class=\"cellcontent\"></div></td></tr></table></td>"+@If(thisday=7;"</tr>";"")

Again, remember to change the daynumber value in the first line of each field. The added @Return line stops the output when needed in short months. All that remains now is the closing spacer computed-for-display text field:

spacer:="<td valign=\"top\"><table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\"><tr><td class=\"spacetop\"> </td></tr><tr><td class=\"spacebottom\"> </td></tr></table></td>";
enddate := @Adjust(StartDate;0;0;NumberOfDays-1;0;0;0);
allSpaces := @Repeat(spacer;7-@Weekday(enddate));
@If(allSpaces="";"";allSpaces + "</tr>")

and closing off the HTML document:

</table>
</body>
</html>

Now all that's left to do is save the $$ViewTemplate form and preview your view in the browser to check your work. The view isn't contributing anything but its name at this point, but if you don't have a pretty calendar table, the rest is meaningless.

Tune in tomorrow, same Bat-time, same Bat-channel, for the next installation.

That Damned Calendar: The Principles (Part I)

Before ND6 and the wonderful @SetViewInfo, there was a single, huge, and to all intents and purposes, fatal flaw with the Notes calendar view, and just about every developer I know found out about it the same way — when a rather rude messagebox let them know in no uncertain terms that the first column had to be a date-time and the second had to be a duration, and that Designer didn't want to hear any of their whining and/or complaints. You simply cannot categorize a calendar view, at least not in any way that would allow you to use RestrictToCategory. That means that you needed to create separate views to cover any "categories", and that a new keyword in the database meant that somebody needed to create a new view and link to same.

Like most, I found that to be good reason for complaint, and I was fixin' to let somebody at Lotus have an earful. Then (as happens far too often) I tried to understand the reason why things were the way they were. I occurred to me (as it should occur to you if you give it some thought) that the calendar you look at in the client has absolutely nothing to do with the view you create in Designer, at least at the most basic level. The calendar is just a table, nothing more, nothing less, generated by a relatively dumb perpetual calendar generating routine. The content of that table (after the dates and format have been determined by the "dumb" routine) are taken from the view you created. By lookup, if you hadn't guessed, with the date and duration acting together as lookup keys. That's why you can't sneak another column into the most valuable real estate.

"Two can play at that game," he muttered.

What I needed was a way to generate a table that looked like a calendar, and a way to populate that table with data. The first part isn't all that difficult; after looking at several completely different months on a broad spectrum of commercially-available and private, highly-classified intelligence and military calendars, I came to the inescapable conclusion that what would be required would be a table that was seven cells wide. The first row of that table would have between zero and six "blank" cells, apparently representing days that are, for whatever reason, not to be included in the current month. That would be followed by between twenty-eight and thirty-one numbered cells, arranged so that if one of those cells were in the rightmost of seven cells, the following numbered cell would appear in the leftmost cell of the row below. In the case where the last numbered cell of that month did not appear in the rightmost cell of its row, there should be precisely enough blank cells added to that row as to complete the row. Any idjit can do that using Formula Language to create HTML. And any idjit did, and he saw the calendar, that it was pretty and regular and everything a calendar should be except fast.

What made me an idjit is that I took the lookup idea too far. Did you now that 31 @DbLookups on a single form makes the form slow? So did I, but I did it anyway. Why? Well, how else are you supposed to get the information into the calendar table before you send it out to the browser? Besides, the calendar I made was just so much prettier than what Domino gives you out of the box.

The answer, it turns out, is that you don't populate the calendar before serving it. You create the table, and you send it along with all of the information that should wind up in the table to the browser, and let the browser do the rest of the work. As long as the table cells are properly identified in HTML and the calendar entries know what dates they should be displayed in, it takes but a wee smidgen of JavaScript to fill in the blanks.

That Damned Calendar: A Rant

What seems like forever ago, I made the mistake of mentioning in one or the other of the developerWorks forums that I had a way to create a pretty-looking categorized calendar on the web. I say "mistake" because the requests for the code have reached the level that they actually swamp out the spam in my inbox (proving once and for all that Domino, at least, is not dead yet). I've sent out demo databases in response, and they have helped a few people. Many, many more people have just turned around and outright demanded that I do the work it would take to integrate the calendar into their applications. For free, mind you.

When did that kind of behaviour become acceptable? I really hate to unfairly categorize these requests, but the vast majority have come from the same people and organisations that are trying to put me out of work by claiming to be able to do my job for less money. (I should point out here that I make a whole lot less than most Americans and Europeans in the Notes development game would make as the consequences of being Canadian, of how I got this job, and of the vagaries of corporate M&A.) Now, I've been beyond broke, and I can understand beggars, but I never imagined that I would regularly encounter the equivalent of streetcorner beggars who are fully employed and ask not for spare change, but for passers-by to go to the office and do their work for them, gratis, so that they can collect their salaries.

I'm quite well aware that there are a lot (and I do mean a lot) more people working in places like Bangalore and Mumbai than have ever requested code from me, but there still seems to be a disproportionate number of people from the major outsourcing centres who feel entitled to demand that I do their work for them. I'm not talking about "borrowing" code here — anyone who has embarked on a developer's career without peeking at other people's code can probably be fairly classified as an idiot. Nor am I talking about people who are merely trying to learn the sort of stuff you don't get in the "Introduction to Domino Designer" course; I generally have a high tolerance for newbies who actually seem to want to learn. I did, after all, offer the code base, and I have no problem working with people who have made an effort to understand what I've done and may have a lingering question or two. I'm just wondering what kind of cultural climate would persuade anyone, anywhere, to believe that it's perfectly okay to demand that I (or anyone else, for that matter) do work for free so that they can be paid for "work" they got by undercutting my wages.

A warning, then. When I post code here, I do so on the understanding that you (whoever you may be) will put at least as much effort into understanding and adapting the ideas as I did developing them. I am more than willing to discuss what I've done with other interested developers. I'll explain why I've done what I've done if it's not clear enough, and I am always ready to accept criticism and modifications from my readers. I will not, however, countenance demands for free development. Don't ask — I've a taste for a malenky bit of the horrorshow red krovvy these days, droogs, and I've a skorry britva.

Anonymous cowards may now comment.

Sorry about that, folks. I'm still getting the hang of the template. You may choose to log in or merely put your name in plain text now.

Monday, September 13, 2004

Alkyds are bad,m'kay?

One of the few constants in my life has been painting. Not houses so much. You know, pictures. That was one of the activities that I found difficult to keep up with while I was homeless (which explains the credit card, but we'll get around to that a bit later, okay?), and during the last five years or so I've spent what little time and money I've had on things like computers, software and books. (The skills I'd amassed working with Z80s and 6502s did not translate well to Notes and Domino, and the web skills I had were — well, lets just say that the analogy that the guy who hired me was given was a little over-exaggerated.) I've had to teach myself a lot, and that has taken most of my resources. Well, folks, the time has finally arrived when I can start to indulge my other passions.

I've worked in most media that can be described as "painting", including watercolours, egg tempera, encaustic, pastels (I used to roll my own, since nobody seems to want to manufacture good, soft, really dark darks; Talens/Rembrandt uses black in their mix, which is a "colour" I never, ever use), acrylics and oils. Each of these media forces its own personality on me. Watercolours make me a very Victorian flora-and-fauna illustrator; pastels awaken my inner Impressionist. My favorite medium, though, is my first: oils. Oils were the only medium I've ever been schooled in, and my technique is very much anchored in the "old masters". (I somehow doubt that children today would be afforded the training I received as a seven-year-old, what with all of the lead, mercury, cadmium, cobalt, selenium, arsenic and cyanide used in the really good pigments.) Nothing else quite matches the tactile sensuousness of buttery oils on a well-worn #12 hog filbert. Mmmmmm, buttered filberts.....

If there is a problem with oils, it's that some colours can take weeks or even months to dry to the point that you can paint over them. Oils "dry" by polymerization through oxidation. Some pigments catylize the process, others retard it. There have been drying mediums (additives) available for hundreds of years to hurry the process along, notably copal and japan drier, but they almost universally have the deleterious effect of causing the paint layer to shrink and become brittle, guaranteeing cracks. Sometime in the early '80s, I discovered Winsor & Newton's Liquin medium, an oil-modified alkyd resin. It tightened the apparent drying time enough that I could usually overpaint in a week, but left the paint "open" so I could work into the same layer for a couple of days. Best of all, it left the paint layer as flexible as linseed oil would make it and obeyed the fat-over-lean rules. (And I'm just anal enough to have carried out my own accellerated aging tests before using it in any of my serious work.)

Well, I made the mistake of thinking that alkyd-based paints from that same reputable maker would behave in more-or-less the same way. Wrong. The open time can be measured in minutes before the paint begins to tighten (the brush begins to pull off more paint than it puts down), and there is still a week before you can overpaint without harming the underlayer. The worst of both oils (long waits for overpainting) and acrylics (short wet-into-wet time frame). Alkyds may be useful to those who paint alla prima, but that ain't me. Even at a third of the cost of good oils, it was an expensive lesson.

Oh, well, I guess its back to buying real oils. Ouch. Have you priced a tube of real vermilion or cobalt violet light lately? For those of you who don't dabble, it's not unheard of for a one-ounce tube of some pigments to exceed a hundred dollars Canadian (about $US75.00), and most good-quality pigments hover at around $35. And no, the bargain brands are no bargain; they're essentially thinnned down with excess linseed oil and aluminum stearate, and mix horribly with other colours, making weak darks at best.

I'll try to get some of my pictures up here along the way. I promise they'll be worth the wait.

The next question is, "Why?"

Let's be absoltely clear about this from the very beginning: this is a vanity site. I hope to do a little bit of code dissemination here, but the real point of the excercise is to give a few eAquaintances a chance to virtually know me better. Oh, and I need to justify my attendance at any Domino community bloggers' get-togethers I may attend in the future. Why not run the site on Domino? Frankly, I don't have a credit card, and even the "free" Domino hosting options available require a credit card. I'm not sure about the reasoning behind that. That being said, I am blogging now, and those who prodded me into this (particularly Jess and Chris) have only themselves to blame.

I've been wondering about this whole blogging idea. (That would be breaking the rules if it weren't for the current climate of philosophising about the social implications of the blog.) I mean, those of you who cyberknow me have certainly found out by now that I'm rarely at a loss for words — heck, I've been known to prattle on for days at a time. Trouble is, all of that prattling is usually in response to something someone else has said. I can play the counselor, confidant, consultant or critic, but I'm not so sure that I can maintain anything like a consistent solo output. I'm like that in real (or is that "solid") life as well, you know. If you were to come visiting and expected me to start the conversation, we'd probably spend the whole time whittlin' on the front porch. If I had a front porch, that is.

You start a conversation,
you can't even finish it.
You're talking a lot,
but you're not saying anything.
When I have nothing to say,
my lips are sealed.
Say something once,
why say it again?
Talking Heads - Psycho Killer

That pretty much sums up my personal philosophy on yapping. Come to think of it, none of you knows for sure that the title of the song I've quoted doesn't make a fitting label for me. Maybe starting this adventure out with a twenty-one gun salute to the old ultra-violence was a giveaway? I'll let you decide.

Friday, September 10, 2004

What's it going to be, then?

Welcome to the world. There was me, that is Stan, and I keep here my messels and raskazzes, but you will find no folk warbles here, my droogs, for I am not that Stan Rogers. No, but I AM he that smots at Forms and Views and viddies lovely 'script. Real dobby 'script. Horrorshow veshches, the likes of which you will not find in all your books, O my brothers. So sit with like me a while, my droogs, and have a nice chasha of moloko while you sloosh to my raskazzes and viddy my webbish Domino eegras.

Like a lot of folks in the Domino world, I am an accidental programmer. I guess that's a common enough thing with people my age, since computing was not a game for Everyman when I was a kid. When I graduated high school, the Commodore PET and the original Radio Shack TRS-80 were about the extent of personal computing. There was the Altair as well (great for hobbyists, not for users), and Apple was just getting started with the Apple II about to come to the mass market. Apart from the few people who dreamed of lab coats and mainframes, programming for us old fogies was mostly a quick stab at 10 PRINT "HELLO WORLD" 20 END and a bit of PEEKing and POKEing. We understood well enough, though, and when macros came along we could write them. I had some adventures with Ada and assembly language for embedded systems while I was in the military, and took a turn at Lisp/Lingo. I created HTML for Lynx browsers to hold some online manuals when the web began. All of that was as adjuncts to my "real job".

Eventually, along came Notes and Formula Language. By the time I started using LotusScript and Java in anger, I had become an accidental programmer -- a real programmer who cared about what he was doing and why, but who had never actually planned to become one. No-one is more aware than I that I have much left to learn, but I'm getting there.

If you've heard of me, it's because I have something of a web presence. It started with the Notes.Net forum (now the developerWorks R4 & R5 Forum), where I skulked to learn, and eventually answered the few questions I could as payback. There are my comments in the Domino blogs, too, and that began with stealing code from Jake Howlett and sort of grew from there by following links. I'm probably a lot better known than I deserve to be -- there are a lot of folks who quietly get on with the business of creating code that is well beyond what I can do. If it matters to you at all, that's why I share what I know. I almost have to believe that the few really cool tricks I've figured out were meant to be someone else's aha! experience, and I'm hoping they find their rightful owners.

If you do find anything useful here, you're welcome to use it. One uses what one has to use, based on the resources available to him, though; you may notice that this blog is not running on Domino. That means that all you're going to get is snippets, and maybe a link to stuff stored elsewhere now and then. But you're going to have to wade through my other rantings as well. There is, after all, a price to everything in this life.