I have been working with the MooTools framework for the past week or so. I am realyl starting to like not having to code everything by hand and am thinking it might save quite a bit of labor in the near future. A tool that extends the MooTools 1.2.1 framework is FormCheck. It makes client side validation of forms an absolute snap.
While everything worked great in IE 7, I was having serious issues getting the FormCheck code to fire when working in FireFox 3. After lots of testing and beating my head against the keyboard, I figured out that the problem was caused by Domino not generating an ID attribute for the FORM tag. I was able to finally work around it by setting the ID to be the same value as NAME before trying to validate the form.
To me, this is an extra step that shouldn't have to be done by deverlopers. I have created an ideaJam entry to have the Domino server do this automatically.
Here's a simple LS function that returns a list of ACL entries that have a specific role enabled:
Function GetEntriesByRole(rolestring As String) As String
Dim session As New NotesSession
Dim db As NotesDatabase
Dim acl As NotesACL
Dim aclentry As NotesACLEntry
On Error Goto errorproc
Set db = session.CurrentDatabase
Set acl = db.ACL
Set aclentry = acl.GetFirstEntry
While Not(aclentry Is Nothing)
If aclentry.IsRoleEnabled(rolestring) Then
If GetEntriesByRole = "" Then
GetEntriesByRole = aclentry.Name
Else
GetEntriesByRole = GetEntriesByRole & "~|~" & aclentry.Name
End If
End If
Set aclentry = acl.GetNextEntry(aclentry)
Wend
finish:
Exit Function
errorproc:
Call LogError()
Resume finish
End Function
The list of acl entries is returned in a string that uses "~|~" to separate the multiple entries. You can use the Split function to expand the list into an array. The role passed must contain the [] charaters.
I have just posted the 2.0.2 version of the ASND Export Facility. This version fixes a number of really, silly bugs. I wish I had more time to do testing the right way, but such is life when you are writing code for free.
There is one bug that is currently unresolved that I would love some help on. The crux of the problem is this: When doing sorting a collection by multiple fields, the secondary sorts are opposite what they should be. For example, if I want to sort the results by Person in ascending order and then by Date in descending order, the results are return with Person in ascending order and Date in ascending order. This did not happen when I was using a Bubble Sort, so I must have screwed up something in the Merge Sort code. If you have experience with these things and want to take a crack at solving the problem, by all means download the latest release and take a look at my spaghetti code.
ASND Designs has released the latest version of the ASND Export Facility on OpenNTF. Version 2.0.1 makes exporting on and writing reports against Domino databases easier than ever before. The new release include the following enhancements:
Update: 2.0.1 has been released to fix 2 major bugs in the sorting routines and include the image used in the web reports.
As a Notes developer, I have never been really interested in sorting algorithms. For me, I just want to sort something ascending or descending and I don't really care how it works or which sort routine is better suited for a given task. That being said, from time to time, I have had to delve into the world of sorting and have compiled a LSS file that contains 3 of the more popular sorting routines: Bubble Sort, Quick Sort, and Merge Sort. This LSS file is now available in the Code Bin on OpenNTF.org.
No more will I have to go searching through my collection of past databases to find that one sort routine that sorts DateTime values. No I can just include the LSS file and easily select which algorithm I want to use and the type of data being sorted and get my results with no muss, no fuss. Just the way I like it!
There is no better feeling for a developer than to have a user heap praise on you for something that you have built. Such is the case recently with the ASND Export Facility.
I knew I had scored big time when I got an IM in about 15 minutes from her ... "This is so freaking cool!" She had promised the reports to the user by November 30th if at all possible. With ASND Export Facility, she handed them over a day later and left everyone in awe of her technical prowess.
The really cool thing is that this quote comes from an article entitled ASND Export Facility Rocks! in LotusUserGroup.org Developer Tips December 2007. Go over and read everything that Duffbert has to say about the application.
I am in the process of adding some things to my OpenNTF applications and was wondering what designer client I should be using to do the changes. I think that I can safely move off of the Notes 6.5.x client, but is it ok to do all my coding in the Notes 8 Designer? I don't think that I will be doing anything that will cause issues, but I do wonder how other project chef's are handling the multiple supported versions of Notes.
Being from the United States, I don't run into the need to deal with Extended ASCII characters very often. In reality, I never deal with them. So I am pretty clueless as to how to solve the current issue I am having on Idea Jam.
The Idea Jam registration form is submitted via AJAX and works extremely well. However, we have run into a problem where any extended ASCII characters aren't handled correctly on by the LotusScript agent that performs the registration. The problem is that those characters are actually sent as 2 characters instead of one. For example, é is sent as ├® or encoded, using encodeURIComponent(), to %C3%A9. The code I am currently using to decode the encoded characters doesn't handle the double values.
Does anyone have any code that I can look at/borrow that translates encoded extended ascii characters and can still handle encoded spaces and ampersands? Any help would be greatly appreciated.
A task came to me recently asking for some changes to a report that we generate from a Domino site. While most of the changes were sort of minor, there was one that gave me pause. The users wanted to add a footer to the report when it is exported to Word. Now, if this was a request to be done via the Notes client, it would be a snap to add the footer via COM. That might even be a possibility if Word was installed on the server, but it's not. So the only thing I have available is standard html.
Following some hints I found on the Internet, I was able to get a footer to show up like I wanted. Unfortunately, there was one major side effect, namely that the footer printed twice on the last page, once as the footer and once at the end of the text. When I looked at the source in Word, I could see that it moved the code for the footer out into a separate HTML file, just like it would if you did a Save as HTML from Word. But for some reason, it left the footer code on the back end of the main document. Very frustrating.
Has anyone else been able to successfully get a footer on a Word form when the document is created using only HTML? Or is this simply a bug in the way Word handles HTML? Inquiring minds want to know.
Chris and I have been having some fun recently coming up with ideas for Domino sites that you aren't likely to build in your shirt and tie day job. The first of these sites has gone live and is called DatersBlueBook.com. The site itself is another in a long line of sites that allow you to take a quiz and post a badge on your site. Our quizzes are going to be based on dating and relationships and will hopefully make our users laugh a little. While the site itself isn't that novel, I would like to share some of the method we have used to create the site in Domino.
First, I have implemented the YUI CSS Foundation framework. This cross browser UI framework allows me to accurately position dynamic elements on the site as well as being able to set font sizes. The YUI Grid Builder is great for generating your basic site layout. Unless you are planning on doing something really wild, this builder will do everything you need get your stuff in the right place. YUI Fonts allows me to have consistent sizing and line-heights across all browsers. And YUI Reset sets the default styling of HTML elements to be the same across all browsers. And if that wasn't enough, the entire CSS file is hosted on a Yahoo server, so it doesn't impact my bandwidth and enables caching across multiple sites.
Something else that we implemented was the idea of using HTML generated from views to build our HTML form. For each quiz, there are a number of question documents on the back end. I use a view to sort them correctly, put them into specific categories on the page, and generate the entire HTML for the question. That is pretty straight forward, but I took it a step further. I used a ComputedText element inside a JavaScript function on the page to generate my validation routine for each field that was required. What these 2 things mean is that I never have to touch the design of the database whenever I add/change/delete quizzes.
The final little tidbit of interesting coding has to do with the way the Grade page is generated. We have all used Print statements in LotusScript agents to spit out HTML for our sites. But if you look at the way the Grade Page and Badge is generated, all you see is a SCRIPT tag with a SRC attribute pointing to an agent. What the agent does is wrap all the HTML it generates in document.write functions. Exciting, heck no! Every other platform on the web does this also. But since we rarely build code that has to be run from web pages in different domains, it's not something we often have to worry about. This is how OpenNTF is able to put a badge on your blog and how Google is able to put ads on your site.
The site is far from complete and will continue to be refined over the next month or so, but it is interesting to me to try and see how Domino can be used in a non-traditional environment. Hopefully, I will have some time to make it a bit more graphically pleasing, but I wouldn't expect the Mona Lisa any time soon.
Bruce Elgort has been using his time away from OpenNTF in ways that will keep him mostly out of trouble. His latest venture is idea exchange. The idea behind this site is to give the IBM/Lotus community the ability to put forward ideas to improve the products that they use on a daily basis. This could be an idea to improve a monster product like Notes/Domino/Webphere or an add-in product like TeamStudio's CIAO or even a specific application on OpenNTF or default template in Notes. Heck, I can even envision a future where products outside the IBM/Lotus family are discussed.
Update: Bruce clarified the spacing and naming of the site. :-)
I have run into an issue with the ASND Export Facility and Office 2007 and need a little assistance confirming the problem. If you look at this discussion response, you will see the problem that Jan is running into. What I need from a few members of the audience playing along at home is the following:
If it works like it does on my work machine (Noes 6.5.x and Office 2003), Excel should show the leading zeros in the column and flag you to let you know that it's a number stored as text. You can either send me an email/IM with your results or, better yet, leave a comment.
I know when I ask Julian and Rock about a problem I am having and they both respond "Weird!", that I am having an issue where my head might not be stuck in another orifice of my body. The worst part about this problem is that it's simple functionality that Notes provides.
The issue I am having is with soft deletions, the soft delete view in particular. After following the super simple steps to implement soft deletions in an existing database, I am able to delete documents and have them go to the trash view. The issue comes when trying to restore the documents. I have tried using both the @UndeleteDocument and @Command([EditRestore]) functions and have had mixed results with both. They both always restore the document to the other views, but it's hit or miss as to whether or not the documents get removed from the trash view. In thinking I had done something totally wrong, I have even copied the view and undelete agent from the OpenLog template, without any more success. Rebuilding the view index using Shift-F9 does nothing. I have tried it locally, on a 6.5.5 server, and on a 7.0.2 server. I have tried it on a new database created from a template and a copy of an existing db.
In searching the Internet and Notes Knowledgebase, I came up with nothing that matches my scenario.
Has anyone else come up against a problem like this in the past? How did you solve it?
In the ASND Export Facility I have on OpenNTF, I use the Fields property of the NotesForm class to generate a list of fields that you can export from a selected database. My assumption was that this property would return an array of all the names of the fields that were on the form. This worked as expected until I got a bug report recently from a user who indicated that there were fields missing when you tried to export Contacts from a Personal Address Book. For some reason, the Birthday field, along with a number of other fields, don't show up in the array returned by the Fields property. I can't figure out any reason why they fields won't show up in the list.
Does anyone have any idea as to why certain fields don't show up in the fields array? If I come up an answer, I will be sure to post it here. Any help would be greatly appreciated.
The latest version of the ASND Export Facility has been released on OpenNTF. This new version has one major new feature and one minor one. The minor feature is that you are now able to choose a dd/mm/yyyy format for date fields. This should make the application more acceptable across the pond. The major new feature is the ability to run a single report against multiple backend databases. All of the documents from all the databases are collected and then sorted as one large collection before exporting in the desired format. This is extremely helpful when reporting against multiple databases based on the same template, like Server logs or Address Books. A good example might be a manager's progress report run against multiple project management applications. Instead of having to run multiple reports and then consolidate them manually, users can now run one report in a matter of seconds and get all the information they need.
5 years old and still growing and getting stronger. Many thanks to Bruce for all his hard work in keeping the site running and promoting the hell out of it. And be sure to check out all of my ASND applications available for download.
I have released a new version of the ASND Export Facility on OpenNTF. The new version doesn't change much as far as the end results are concerned, but the code under the hood has been re-written using a couple of custom LotusScript Classes. The major benefit of the change is the fact that I am able to do multi-level sorting without relying on Excel to do it for me. That means that the sorting is available for the Delimited Text exports. When creating the export, you select the column to sort on, whether to sort ascending or descending, and the data type of the column being sorted.
I had been wanting to do this for quite a while, but had let the problem percolate in my noggin until I came up with a solution that I completely understood and felt comfortable with. The code makes extensive use of Lists and does customizable type-based sorting. I tried a couple of different ways of doing the sorting, but settled on a BubbleSort due to the way it moved list items around to get the list sorted. The BubbleSort allowed me to call the sorting subroutine for each sorted column, in reverse order. So if the export is sorted by last name and then first name, I sort it by first name and then sort it by last name.
The next release for this project should come out very shortly. I have already implemented OpenLog into new code, but want to get the web interface finished. I will probably do the web interface in 2 stages, doing simple HTML that opens in Word/Excel and then implementing some of the Office XML schema. I also need to make XML a format that is available. Are there any other report formats that users would want?
Papa Bruce gave me the keys to the caddy and I decided to get the upholstery on the drivers side redone. Since I have started to contribute more to OpenNTF, I wanted to have a way to look at of my projects at once. The My Projects page already existed, but it didn't contain enough information. So with Bruce's permission, I have updated it to make it more usable. If you have contributed to an OpenNTF project, take a look at the new design and let me know what you think. You can find a link to the My Projects page under the My Links menu on the far right after you login.
If there are any other changes you would like to see made to that page or any other part of the OpenNTF site, please let me know and I will run it by Bruce.
If you work with Domino and are frighten, yet strangely excited, by the four letter word that is AJAX, you might want to sign up for the free IBM Webcast Using AJAX with Domino Web Applications.
AJAX (Asynchronous JavaScript and XML) can improve the performance and usability of your Domino Web applications. This Software Development Platform webcast will examine how you can modify your Domino Web applications to make use of AJAX.
Bring your enterprise Domino apps to the new Web 2.0 world. Enough buzz words for ya?
Jeff Atwood has a great article talking about the difference in effort needed when modifying old code as opposed to creating a new application. The opening graphic explained my current development life. Even simple things that take just a couple of minutes to code take forever to actually get finished because I spend a large portion of time either figuring out or remembering exactly what the existing code does.
The other thing I agree with is that sometimes it's much more useful to see how an application is used as opposed to understanding how they technically did something. When I look at a new application, I am more apt to ask what it does as opposed to how does it do it. Only when I am completely stumped do I actually want to look under the covers and see how it's done.
Following my mantra of keeping things as simple as possible while still making them able to be reused, I spent the past couple of days thinking of a way to code a tabbed interface that I was tasked with building. Making the tabs wasn't such a big deal using a modified sliding doors approach, but I didn't want to take my usual approach to the JavaScript for switching between tabs. In the past, I have used a Switch-Case loop to go through the tabs and either show them or hide them. The problem with that is that I have to know what all the tabs are going to be and it's a real PITA to add or remove tabs later on.
This time I thought I would try something a little more OO. The function I wrote is only 25 lines long without much error checking. The beauty of this function is that I can reuse this function as often as I want. Here is the function:
function showTab(listname,tabname) {
var obj = document.getElementById(listname);
var obj2 = obj.getElementsByTagName('li');
var obj3;
var obj4;
for (i=0;i < obj2.length;i++) {
obj3 = document.getElementById(obj2[i].id.substring(0,obj2[i].id.length-2));
obj4 = obj2[i].getElementsByTagName('a'); if (obj2[i].id == (tabname + "li")) {
if (obj3.style.display != "block") {
obj2[i].className = "ActiveTab";
obj3.style.display = "block";
obj4[0].className = "ActiveTab";
}
} else {
if (obj3.style.display != "none") {
obj2[i].className = "InactiveTab";
obj3.style.display = "none";
obj4[0].className = "InactiveTab";
}
}
}
}The listname variable is the name of the UL that contain the tab being clicked on. The tabname is the name of the DIV that is is going to be displayed. The only naming requirement is that the LI that is the tab for a given DIV be the same name as the DIV plus 'li' on the end. For example, if the DIV is named 'personal', the LI will be named 'personalli'. This code also takes advantage of the className attribute to change the look and feel of the tabs and only changes the tabs that is being activated or deactivated, which should help with performance. Using className is so much easier than changing all of the CSS settings individually and also makes it really easy to change the UI of the page.
This is the second time in less than a year I have spent time killing myself over a bug inherent to LotusScript. Here's the scenario:
This is a known bug with the GetAllEntriesByKey method, but it is something that will cause significant hair loss, none the less. The really funny thing is that the method works fine if the first key is a multivalue column, but not on the second column.
I have been reading a number of Ajaxian articles on the different AJAX frameworks available and have been trying to figure out how they can make normal Domino applications better. At the same time, I am busy converting a number of views from Domino generated HTML or Java over to custom DHTML. While not very glamorous or extremely hard, the conversion does take quite a while to do and is labor intensive. Wouldn't it be much better if Domino served up views in a JavaScript accessible datagrid like Dojo's Sortable Table or Finetooth's Datagrid based on Prototype. By replacing the standard HTML views with AJAX based ones, IBM would be able to drastically update the UI of all Domino applications and really make strides to being able to play in the Web 2.0 space out of the box. The biggest concern for me is how to handle categorized views. Is it something that is handled with a combination of multiple widgets or something more like the way Excel does subtotaling? I do wonder if this is one of the Aces up IBM's sleeve since they are now a full contributor to Dojo. Time will tell.
There are a couple of things that I want to do, but haven't figured out how to do it. Both of these things revolve around incorporating XML-RPC into my Domino based blog.
The biggest hurdle I am running into is handling the authentication with the Agent/Domino Server. None of the examples I have seen deal with a system that is secure and most only deal with writing an agent that connects to another server via XML-RPC. Once I have this licked, everything else, even the fact that I have to used Java to write the agent, should be a snap to accomplish.
If anyone has any experience with the XML-RPC specification, any help would be greatly appreciate.
Russ Olsen has published a two part article on learning Regular Expressions.
Regular expressions are simply strings that we use to match other strings. If you give me a regular expression, perhaps ‘Ols[eo]n‘ and a string, perhaps ‘Olsen‘ or ‘Olson‘, I can tell you if the strings match the regular expression (they both do). Most computer users deal with this kind of pattern matching all the time — Windows users frequently look for Word documents by specifying ‘*.doc‘. While the ‘*.doc‘ type patterns are great for what they do, when you are doing complex pattern matching, you need a serious tool. Regular expressions are that tool.
I have never been a fan of regular expressions because I have always had a hard time reading them. These articles go along way to making things much easier for me. It will also be able to make my Javascript significantly more powerful.
Shaun Inman's article on Responsible Asynchronous Scripting goes to the heart of the problem with some of the new Web 2.0 applications, even if he doesn't spell his name correctly.
On the other hand, avoid replacing traditional navigation with XHR. Doing so breaks an inherent functionality responsible for the success of the web: Universal Resource Locators. Unless we reinvent the wheel and restore this functionality ourselves (hidden iframes and fragment identifiers anyone?), it is impossible to link directly to content included with XHR.
He goes on to point how a number of things that developers should keep in mind when deploying AJAX on a site, not the least of which is keeping the user informed that something is happening.
This article is a prime example of the good work being published on Vitamin.
Lots of free stuff: CDs, Magazines, and T-Shirts. Be sure to register by sending Jack@LeadershipByNumbers.com an e-mail.
AGENDA:
FREE CDs GIVEN TO EVERYONE WHO CONFIRMS ATTENDENCE BY SENDING AN EMAIL TO JACK@LEADERSHIPBYNUMBERS.COM
If you do not subscribe to Jeff Atwood's CodingHorror, do yourself a favor and start reading it. Here are just a few of the articles that he has written lately.
Last week's DCNUG meeting was pretty good. IBM brought the dog and pony show into town and displayed their latest addition to the Lotus family of products. This was my first real look at the IBM Workplace Forms. Prior to the meeting, I didn't quite understand what the uproar around XForms was all about, but I think I get it now. For me, I see XForms as what Notes and Domino might have grown up to be one day. The beauty of the product is that the design of the form and the data users enter into it is contained within a relatively small XML file. Although you might need the Viewer to see the "pixel perfect" form, the data in the file is nothing more than ASCII text, so it will most certainly be readable by anyone in the future. I am not sure that Adobe's Acrobat eForms or Microsoft's Infopath will be able to say the same thing. I can almost envision a day in the future where this product, or a simpler version, is a part of Domino out of the box.
Along with the paper like forms that it can create, the designer can also create front end wizards to give users a friendlier UI for entering data onto the form. The configurability of the form and the elements on it is very robust. The choices were almost dizzying as they were demonstrated, but no more than the IBM R.A.D. tool.
So, big deal. It's another XML file format with a server and design tool. Well, in my mind, this XML file is akin to a Notes document that has a stored form attached, but it's contained within a single file. The file is encrypted and able to be digitally signed (and resigned without significantly increasing the file size: See Adobe!) and can easily be transported to any other system. Any workflow or scripting is contained within the file itself, so it does not require a back-end server farm to process it. The file can then be entered into your favorite document management system or the data can be extracted and transferred to a back-end RDBMS system before being archived like any other business record. The documents are able to be viewed or edited using either the Viewer software loaded on a local machine or via a browser when being served up by the Workplace Forms Server without installing a plug-in. One of the coolest features is that a user can save an incomplete form locally to be finished later, even anonymous users. No more setting cookies or saving documents in your database in case the user might come back. And this
From what we heard, the next version, 2.6, will be compliant with the recently ratified XForms 1.0 specifications. They are also working on separating the design of the form from the underlying data and workflow so they can better serve a variety of client platforms. This sounds strikingly familiar to the way Notes and Domino works. In my mind, I have always seen Notes documents as a genetically modified XML document. It contains all the data while the form contains the schema necessary for displaying it to the user. I am not sure of the costs of an implementation of the product, but I hear it's not cheap.
Hopefully, I will be able to play with this technology sometime in the near future. Mauri ce, if you're out there, I am still waiting on that email!
Sometimes it is easy to forget that the code we write can have significant impact on other people. Jay Kimble's What We Do Matters entry reminds us that it can:
I know you say “Jay you used to work on a corporate reporting system. How could that hurt people?” Easy, my system was used to make layoff decisions. There were numbers in my app that I knew were there to help decide when people should be laid off. If one of my numbers was slightly off it could have caused a bunch of layoffs. If my numbers were too high it could bankrupt the company.
Just remember this the next time you are hurriedly throwing together a report for some big-wig right before a big meeting.
Jeffrey Palermo wonders if writing software is too easy these days.
Tools today make custom software too easy to develop. This statement may seem controversial, but I believe it. I’ve seen too many unskilled people take a programming tool and throw together something that seems to work initially. The business folks can’t tell a fraud from an expert or evaluate the work, so they use it and then feel the pain later when they actually have to maintain it.
I know that many of us in the Domino world have seen our share of really bad applications. In fact, that was the topic of our last DCNUG meeting. Jeff's points are well laid out and I agree with most of what he says. I do think that software development is part engineering, part artistry. The really good developers (Masters) will build something that blows you away and then show you how it's done, and you will be able to understand it and use the same techniques in your own code.
It's Friday at 5 PM and you've just finished adding the latest great feature to your pet project. You've spent more than a week developing it to exceed anything your users would have expected. With the enthusiasm of a proud parent, you send a note out indicating that the new feature has been tested and in now available in production. You're walking on air as you head out the door and home to a much deserved cold cervace. Fast forward a couple of weeks when you run into one of the users in the hall and ask them how they like that kewl new functionality. They sheepishly indicate that they haven't used it yet. After picking your jaw and pride off the floor, you chalk it up to a PEBCAK problem that the user had and decide to ask someone else about it. Imagine your surprise when the second, third, and fourth users haven't used it either and are still doing things the old way. After some evasive answers, they admit that they just couldn't figure out where the feature was nor how to use it.
Although not to this extreme, I am sure that every developer out there has had this happen to them. Nathan wrote recently about Lotus needing to completely rewrite the UI for Hannover because the current client has become too cumbersome for users. Jeff Atwood has written about following a simple rule when developing: If the user can't find it, the function's not there. Simplicity and obviousness is not something that is the same for all people.
I still have this button clipped to my mug boss to periodically remind me that, no matter how cool the feature may be, if users can't find it-- or understand it-- you're wasting your time. So make sure you have your priorities in order before you start: usability first, feature second.
When developing anything, it is important for the learning curve of the application to be as gradual as possible. The applications I always find most daunting as a user are the graphics apps like PhotoShop and Paint Shop Pro. If you are a noobie to photo editing, just opening these applications is enough to make you shiver in fear. You definitely need to go through the tutorials to be able to do most of what you want and, unless you use the program all the time, you will need go through them often. This is what I try not to do when I am developing my own applications.
After stiring up a nest of hornets by asking if a lack of Java skills is hurting the Notes Community, Mikkel Heisterberg has written a number of entries this week on getting us slackers in line with his Java in Notes/Domino Explained series:
Mikkle's knowledge of Java is explained in such a way that we LotusScript kiddies can easily understand it. Hopefully, he will continue to share more with us in the coming weeks. I know that I will continue to read it with great interest.
I have recently run into a problem with Firebug and XMLHTTP requests (AJAX). It seems that the current version of the extension has issues with synchronous calls rather than asynchronous calls. The difference between the 2 types is that is the same as the modality of Notes dialog boxes. Like modal boxes, synchronous calls cause the browser to wait until the request is completed. Asynchronous calls, on the other hand, make the call and continue on, not waiting for the response to come. The difference is important if you are chaining multiple AJAX calls together and a later call is dependent on a response from previous one. I will have to keep this in mind when I add the report functionality I discussed earlier.
AJAX is a great tool for building some really slick applications, but it has one limitation has made me ignore it for a number of things that I have wanted to do. After speaking with Chris Toohey and stealing getting inspiration from some code that he had, I have come up with a simple agent to allow me to get data from other domains. The agent is incredibly simple, but I have no idea what kind of impact this will have on the server, so test thoroughly before you implement it in your environment.
First, create an new LS agent and set the run-time security level to 2. Allow Restricted Operations. The agent can be as complex as you want it to be, but below is all that is necessary:
Dim s As New NotesSession Dim doc as NotesDocument Dim xml As Variant Dim temp As String Set doc = session.DocumentContext temp = StrRight(doc.Query_String_Decoded(0),"url=") Set xml = CreateObject("Microsoft.XMLHTTP") xml.Open "GET", temp , False xml.Send Print "Content-Type:text/html" Print "Cache-Control:NoCache" Print Cstr(xml.ResponseText) Set xml = nothing
Finally, you create an AJAX call on your page that calls the agent you created and pass the URL you want to retrieve as a query string parameter. That's all it takes to basically have one AJAX call access another AJAX call to get data from some other domain. Just to prove that my dogfood tastes good, I am using a version of this agent to retrieve my Bloglines Blog Roll on the right. Just click on the RSS icon to see the data retrieved via AJAX.
As I has said earlier in the week, I have been doing a lot with LotusScript classes for reports I have been generating. One of the new @Functions that Damien introduced in the R6 formula engine was @BusinessDays. What started out as trying to find a simple way to determine the number of days between 2 dates turned into the custom class you see below:
Public Class elapsedtime Private startdt As NotesDateTime Private enddt As NotesDateTime Private tstring As String Private bstring As String Private tdays As Long Private bdays As Long Private thours As Integer Private bhours As Integer Private tminutes As Integer Private bminutes As Integer Private tseconds As Integer Private bseconds As Integer Private nondays () As Integer Private nondates () As NotesDateTime Private startofwd As String Private endofwd As String Sub New(dt1 As String, dt2 As String, holidays As String) Dim tmp As Variant Dim x As Integer Set Me.startdt = New NotesDateTime(dt1) Set Me.enddt = New NotesDateTime(dt2) If dt1 = "" Or dt2 = "" Then Exit Sub Redim nondays(1) nondays(0) = 1 nondays(1) = 7 Me.startofwd = "8:00 AM" Me.endofwd = "6:00 PM" If holidays <> "" then tmp = Split(holidays,",") Redim nondates(Ubound(tmp)) For x = 0 To Ubound(tmp) Set nondates(x) = New NotesDateTime(tmp(x)) Next End If Call GetTElapsed() Call GetBElapsed() End Sub Private Sub GetTElapsed() On Error Goto gteerror Dim diff As Long, tmp As Long If Me.startdt.localtime = Me.enddt.localtime Then Me.tdays = 0 Me.thours = 0 Me.tminutes = 0 Me.tseconds = 0 Else diff = Me.enddt.TimeDifference(Me.startdt)/60 Me.tdays = Fix(diff/1440) Me.thours = Fix((diff Mod 1440)/60) Me.tminutes = Fix(diff Mod 60) Me.tseconds = Fix(Me.enddt.TimeDifference(Me.startdt) Mod 60) End If gtefinish : Exit Sub gteerror : Call LogError() Resume gtefinish End Sub Private Sub GetBElapsed() On Error Goto bteerror Dim dt1 As NotesDateTime Dim dt2 As NotesDateTime Dim dt3 As NotesDateTime Dim hdt As NotesDateTime Dim diff As Long, tmp As Long Dim stepcount As Integer Dim addit As Boolean Me.bdays = 0 If Me.startdt.localtime = Me.enddt.localtime Then Me.bhours = 0 Me.bminutes = 0 Me.bseconds = 0 Exit Sub Elseif Me.enddt.TimeDifference(Me.startdt) < 0 Then stepcount = -1 ' Based on the time of the startdt, the values are recalculated to fall within work hours If Hour(Me.enddt.TimeOnly) >= 18 Then Set dt1 = New NotesDateTime(Me.enddt.DateOnly & " " & startofwd) Call dt1.AdjustDay(stepcount) Elseif Hour(Me.enddt.TimeOnly) < 8 Then Set dt1 = New NotesDateTime(Me.enddt.DateOnly & " " & startofwd) Else Set dt1 = New NotesDateTime(Me.enddt.LocalTime) End If ' if the start date falls on a weekend or holiday, move it While CheckIfWorkDay(dt1) = False Call dt1.AdjustDay(1) Wend ' Based on the time of the enddt, the values are recalculated to fall within work hours If Hour(Me.startdt.TimeOnly) < 8 Then Set dt2 = New NotesDateTime(Me.startdt.DateOnly & " " & endofwd) Call dt1.AdjustDay(-1) Elseif Hour(Me.startdt.TimeOnly) >= 18 Then Set dt2 = New NotesDateTime(Me.startdt.DateOnly & " " & endofwd) Else Set dt2 = New NotesDateTime(Me.startdt.LocalTime) End If ' if the end date falls on a weekend or holiday, move it While CheckIfWorkDay(dt2) = False Call dt2.AdjustDay(-1) Wend Else stepcount = 1 ' Based on the time of the startdt, the values are recalculated to fall within work hours If Hour(Me.startdt.TimeOnly) >= 18 Then Set dt1 = New NotesDateTime(Me.startdt.DateOnly & " " & startofwd) Call dt1.AdjustDay(stepcount) Elseif Hour(Me.startdt.TimeOnly) < 8 Then Set dt1 = New NotesDateTime(Me.startdt.DateOnly & " " & startofwd) Else Set dt1 = New NotesDateTime(Me.startdt.LocalTime) End If ' if the start date falls on a weekend or holiday, move it While CheckIfWorkDay(dt1) = False Call dt1.AdjustDay(1) Wend ' Based on the time of the enddt, the values are recalculated to fall within work hours If Hour(Me.enddt.TimeOnly) < 8 Then Set dt2 = New NotesDateTime(Me.enddt.DateOnly & " " & endofwd) Call dt2.AdjustDay(-1) Elseif Hour(Me.enddt.TimeOnly) >= 18 Then Set dt2 = New NotesDateTime(Me.enddt.DateOnly & " " & endofwd) Else Set dt2 = New NotesDateTime(Me.enddt.LocalTime) End If ' if the end date falls on a weekend or holiday, move it While CheckIfWorkDay(dt2) = False Call dt2.AdjustDay(-1) Wend End If While dt2.dateonly <> dt1.DateOnly And dt2.TimeDifference(dt1) > 0 If CheckIfWorkDay(dt1) = False Then Me.bdays = Me.bdays + 1 End If Call dt1.AdjustDay(1) Wend If Hour(dt1.TimeOnly) > Hour(dt2.TimeOnly) Then 'If the tim of the start is later in the day then the time of the end, go back to yesterday 'and calculate the time differently and subtract a business day 'diff = (seconds from start to end of work day) + (seconds from start of workday to end) Call dt1.AdjustDay(-1) Me.bdays = Me.bdays - 1 Set dt3 = New NotesDateTime(dt1.DateOnly & " " & endofwd) diff = dt3.TimeDifference(dt1)/60 Set dt3 = New NotesDateTime(dt2.DateOnly & " " & startofwd) diff = diff + (dt2.TimeDifference(dt3)/60) Else diff = dt2.TimeDifference(dt1)/60 End If Me.bdays = Me.bdays * stepcount Me.bhours = Fix(diff/60) Me.bminutes = Fix(diff Mod 60) Me.bseconds = Fix((dt2.TimeDifference(dt1)) Mod 60) btefinish : Exit Sub bteerror : Call LogError() Resume btefinish End Sub Private Function CheckIfWorkDay(dt) As Boolean Dim i As Integer CheckIfWorkDay = True Forall x In Me.nondays If Weekday(dt.DateOnly) = x Then CheckIfWorkDay = False Exit Forall End If End Forall If CheckIfWorkDay = True Then For i = 0 To Ubound(nondates) If dt.DateOnly = Me.nondates(i).DateOnly Then CheckIfWorkDay = False i = Ubound(Me.nondates) End If Next End If End Function Public Function GetTimeString(elapsetype As String) As String On Error Goto gtserror Dim thisdays As Long, thishours As Integer, thisminutes As Integer, thisseconds As Integer Dim tmp As Integer If elapsetype = "B" Then thisdays = Me.bdays thishours = Me.bhours thisminutes = Me.bminutes thisseconds = Me.bseconds Else thisdays = Me.tdays thishours = Me.thours thisminutes = Me.tminutes thisseconds = Me.tseconds End If tmp = 0 If thisdays = 1 Then GetTimeString = "1 Day" Elseif thisdays = -1 Then GetTimeString = "-1 Day" Elseif thisdays <> 0 Then GetTimeString = Cstr(thisdays) & " Days" End If If GetTimeString = "" Then tmp = thishours Else tmp = Abs(thishours) If tmp = 1 Then GetTimeString = GetTimeString & " 1 Hour" Elseif tmp = -1 Then GetTimeString = GetTimeString & " -1 Hour" Elseif tmp <> 0 Then GetTimeString = GetTimeString & " " & Cstr(tmp) & " Hours" End If If GetTimeString = "" Then tmp = thisminutes Else tmp = Abs(thisminutes) If tmp = 1 Then GetTimeString = GetTimeString & " 1 Minute" Elseif tmp = -1 Then GetTimeString = GetTimeString & " -1 Minute" Elseif tmp <> 0 Then GetTimeString = GetTimeString & " " & Cstr(tmp) & " Minutes" End If GetTimeString = Ltrim(GetTimeString) gtsfinish : Exit Function gtserror : Call LogError() Resume gtsfinish End Function Public Function GetDays(thistype As String) As Long If thistype = "B" Then GetDays = Me.bdays Else GetDays = Me.tdays End If End Function Public Function GetHours(thistype As String) As Integer If thistype = "B" Then GetHours = Me.bhours Else GetHours = Me.thours End If End Function Public Function GetMinutes(thistype As String) As Integer If thistype = "B" Then GetMinutes = Me.bminutes Else GetMinutes = Me.tminutes End If End Function Public Function GetSeconds(thistype As String) As Integer If thistype = "B" Then GetSeconds = Me.bseconds Else GetSeconds = Me.tseconds End If End Function End Class
If you notice, none of the attributes of the class are exposed as Public variables. I did this on purpose because I wanted to make sure that they weren't monkeyed with by any external code. The New() subroutine is passed the start and end date times as strings as well as a comma separated string of holidays. The decision to make the holidays a variable that is passed in will allow this to be used internationally, where the holidays for one division of the company might be different than the others. The weekend days are automatically set to Saturday and Sunday, but they can be changed if your company works something other than a standard 5 day work week. All of the other public functions get passed a variable, B for Business and T for Total, and return the values of the private attributes. The most useful finction is the GetTimeString function, which returns the elapsed time in days, hours, and minutes. This is just the type of thing that managers love to see on their reports.
I have used custom Classes within LotusScript some in the past, but am really starting to fall in lust with them as of late. Last August, I wrote a series of articles on using Lists to write decent reports in Notes. Well, this past week, I took things a bit further by creating my own classes to replace the Lists I had used before. Well, that's a bit misleading, because my classes were really objects that contained lists of other classes, that were lists of objects. Capice? Of course, I really blame all of this on Jerry and his damn SnTT Tip.
While I don't think that the change to using classes for this agent saved me any coding over my procedural code I had before, it did allow me to control the data integrity better and hopefully improve the performance of the application. One thing I did notice while developing my classes was that I started to change the way I viewed the data and actions I wanted to perform. Everything started to look like objects and I started to see the actions I wanted to perform as subs or functions for those objects. I am not sure if it made my code any clearer, but it certainly made my mind see what I had to do much easier. It also allowed me to be able to expand this report in the future without having to change any of the underlying data structure of the classes.
The easiest way to think of using classes is to see everything in Notes as a list. A database is just a list of notes, both data notes and design notes. A view is just a list of documents. A document is just a list of fields. A field is just a list of values. And so forth. Since everything is just a list, creating classes that handle those lists should be relatively easy. Just as a NotesDocumentCollection is a list of NotesDocuments, you could create a class called CatalogItems that could contain a list of attributes for a given item. Then you could create a class called Catalog that could contain a list of CatalogItems. Once you start, the variations and options seem almost endless, just like the first time you were introduced to Notes.
The most important new keyword you need to learn when creating custom classes is Me. Me is used to reference attributes of the class that you are working in using dot annotation. Just as you would use doc.UniversalID to get the document id for a Notes document, you can use Me.attname to get or set the value of the attname attribute. If an attribute is Public, it can be read and set directly by other code using dot annotation. If it's Private, it can only be read or set by functions and subs within this class. Basically, if you want to be able to control access to the attributes of a class, set them to Private and use Public functions within the class to get or set the variables. If this seems Greek to you, don't worry, it did to me too. If you have any questions, feel free to respond here or ask more knowledgeable people like Wild Bill. H is knowledge of OO programming makes me proud to be a Scot.
So basically I created a class that was my entire results table. That class had a number of attributes, the most important of which was the one that was a List of rows. The rows were another class that had contained the values for each column as attributes. And since the values of the classes are loaded into memory, any manipulation or retrieval of the data is significantly faster.
Later on this week (think Thursday) I will release a class I wrote to calculate elapsed time, both total and business. A simple example such as that should help if you are still having trouble visualizing what I am talking about.
Being the good little CSS developer that I am, I have been trying to make sure that I set the background color of all elements for which I set a foreground color. This is something that the W3C checks when it validates your CSS. This is all well and good most of the time, but it caused me quite a headache this morning.
I was trying to use one of the SnT Thursday tips that Esther posted for hi-lighting rows on a table. I wanted to use this in a couple of reports I was creating via LS and was running into unforeseen issues. After pulling my head out of my butt and using the right JavaScript capitalization (it's always the little things that get you), I was unable to get it to work correctly in IE. Viewing it through FireFox showed me that was my code was correct, but the change wasn't being honored in IE. What's worse was that I could actually see that the change was occurring while watching the DOM using the IE Developer Toolbar.
The culprit causing the problem was the fact that I had set the background-color to transparent for all TD in the table. FireFox took this to mean that if the color of the row changed, the color of the cell must change also. IE decided that the cell color was what it started out with no matter what happened on the row below. Once I removed the background-color setting, the color changes worked as expected in both browsers. I hope this quirk is something that is fixed in IE7.
As a Notes Developer grows, we tend to move from @Functions to LotusScript to JavaScript. Since Notes is so great a handling lists, we get very attached to our @Implode and @Explode functions. So attached, that I have seen many a Evaluate({@Implode(list,",")},doc) in LotusScript routines. But fear not, young coders, there are LotusScript functions that can perform the same tasks for you.
First, I have to let the n00bies off the hook a bit. When you look at the Help document for @Implode or @Explode, it doesn't list a LotusScript function that is their equivalent. That would lead n00bies to believe that such a function doesn't exists and they either need to write their own function (Guilty!!!) or use Evaluate. Below are a few of the functions I use the most:
| @Function | LotusScript | Javascript |
| @Implode | Join | Join |
| @Explode | Split | Split |
| @Word | StrToken | Write your own |
Use of these new native functions should greatly increase the performance of your code.
Damien pointed me to this very important article for all developers.
1) More features isn't better, it's worse.
Feature overload is becoming a real issue. The last thing a customer wants is confusion-and what's more confusing than comparing technical specifications, unless you are en expert? Only nerds get a kick out of reading feature lists. (I know - I'm one of them.)
I can't tell you how many small programs I use that do just one thing well. And I can count on my fingers the number of features in Word and Excel I use on a regular basis. The KISS methodology has been sorely lost on the computer industry. I will try to keep the points in this article in mind whenever I find myself adding something cool to an application.
I am running into a problem and am wondering if anyone out there has done something similar that I might be able to steal learn from. The issue is being caused by Outlook's/Exchange's handling of long URLs. My standard notification library has the ability to add a URL link to the document at the bottom of the notification. Since these are Domino URLs, they tend to be very long. When I open up the links in Outlook, they are wrapped after a certain number of characters and the link no longer works.
I know that you can use the MimeEntity class to spit out HTML for email messages, so I looked into using it for this problem. Unfortunately, all the examples I found deal with creating the email completely from scratch. My workflow solution allows database managers to write Rich Text notification templates that are used to pull data from the document that is being processed. This works very well and it is not something that I am going to be able to easily give up. In a nutshell, here is what I want to happen:
The HTML I want to append in #4 is a simple Click Here to access the document. Nothing special, right? Well, the problem comes when I try to access the RichTextField on the document and treat it like a MimeEntity. If I try to create a new MimeEntity with the same name, it gives me an error like I would expect, but it won't let me access the existing field and create a child entity to it. Am I going down the wrong path or is there something simple I am missing?
Here is some of code in question:
If mail.AppendLink(0) = "URL" Then
Call body.AppendText(urltext)
Elseif mail.AppendLink(0) = "Doclink" Then
Call body.AppendText("Click here to access the document -->")
Call body.AppendDocLink(doc,"Link to document")
Elseif mail.AppendLink(0) = "URL Link" Then
session.ConvertMime = False
Set stream = session.CreateStream
Set mime = mail.CreateMIMEEntity
Call stream.WriteText(|
Click here to access the document|)
Call mime.SetContentFromText(stream,"text/html",ENC_NONE)
Call mail.CloseMIMEEntities(True)
End If
Any help at all would be appreciated.
If you are starting to develop AJAX enabled pages, Firebug is a must have extension for Firefox.
FireBug is a new tool that aids with debugging Javascript, DHTML, and Ajax. It is like a combination of the Javascript Console, DOM Inspector, and a command line Javascript interpreter.I have just started playing with it, but have already found it to be very useful in debugging problems.
via Jack Dausman
People offering services in this area have to be well trained and experienced ... because the product is so flexible, and there are different ways of doing things, you can make a great job or a right royal stuff-up. But that's no different than with any other software development environment; if a developer gets the constructs and underlying logic wrong in a C++ project, it won't work as to specification and the users will be most unhappy. Due to the rapid application development environment, Notes/Domino do have some redeeming factors over say C++, but nonetheless, if the underlying data model is wrong from the word go, it is expensive and time consuming to find redemption.This statement couldn't be more accurate nor more frustrating. If it's so easy to screw up other platforms and even harder to fix them when they are screwed up, then why does Notes/Domino get such a bad rap? Is it due to it's ability to adapt easily and therefore it should have been done right in the first place? Or is it because, unlike with a lot of other languages, developers of Notes/Domino applications have to be part business analyst, part systems analyst, part developer, part designer, and part trainer. I can't say as I have ever met a VB or C++ developer that does all the requirements gathering, developing, and training for a given system. They may actually do this sort of thing, but not at any of the companies that I have worked at.
In the end, it is important that you understand how essential your roles as a Notes Developer is to the success or failure of the platform at your company. While at times it might seem unfair to ask so much of just one person, think of the opportunities you have to shape the way your company does business for years to come.
via Shared Spaces Research & Consulting: The Week in Collaboration, Jan 23-27 2006
Any ways, IE has what they like to call an "intelligent caching system". That's an overstatement on the level of "you're much prettier than Adriana Lima", especially when it comes to the way it handles Domino. By default, IE's cache setting (Temporary Internet Files) is set to Automatic. This allows IE to decide when it needs to bring down a new copy of a page and when it can simply use the copy in the cache. This is all well and good when you are looking at static HTML pages, but blows monster chunks when you are trying to use a Domino application. As a Domino developer, the solution to this problem is relatively simple, but there are many pieces.
The easiest way to control caching is by creating a Web Site Rule. This article from LDD does a great job of walking you through how to create a rule. One thing to remember if you are using Quickplace is that, since Quickplace doesn't support multiple Internet Site documents yet, you will need to create a Global Web Settings document and then create the rule from there.
You can also do it on a form by form, page by page basis using either @SetHTTPHeader or META tags in the HTMLHead. Most of the time, this is very easy to do and can allow you to granularly control which pages and forms are cachable. Check your Administration Help database for more information on how to use @SetHTTPHeader and its counter-part @GetHTTPHeader.
Neither of these ways will work for Quickplace, however. To control caching of Quickplace data, you need to modify the qpconfig.xml file and either modify or add the following lines:
Cache control is even more important when it comes to AJAX. I have been working a bunch with Domino Agents returning data via XMLHTTP calls. While I knew that the first Print statement had to be "Content-type: text/xml" (although "Content-type: text/plain" also seems to work), I was unaware of the issues with caching. To prevent your AJAX calls from being cached, the second Print statement has to be "Cache-Control: no-cache". Otherwise, agents that are called using the same URL will not be access on the server and will instead be access in the cache, regardless of the data that is actually returned. This can be very important if the data on the back end changes without the URL changing.
Following the tips above should allow your site to work correctly, regardless of how your user's browser settings are configured.
var xmlhttp=false;
/*@cc_on @*/
/*@if (@_jscript_version >= 5)
try {
xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
} catch (e) {
try {
xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
} catch (E) {
xmlhttp = false;
}
}
@end @*/
if (!xmlhttp && typeof XMLHttpRequest != 'undefined') {
xmlhttp = new XMLHttpRequest();
}
var dbloc = getHomeFolder('34');
xmlhttp.open("GET", "/PlaceCatalog.nsf/QPLocalGroup?OpenAgent&qp=" + escape(dbloc) + "&user=" + escape(haiku.loginName));
xmlhttp.onreadystatechange = function() {
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
try {
' Display everything that is hidden by default
}
catch (err) {
document.getElementById('column_left').style.display = '';
}
}
}
xmlhttp.send(null);
The getHomeFolder function returns the directory for the current Quickplace. The code for QPLocalGroup is very simple agent that I placed in the Place Catalog database.
Sub Initialize
Dim session As New NotesSession
Dim db As NotesDatabase
Dim view As NotesView
Dim collection As NotesDocumentCollection
Dim agentdoc As NotesDocument
Dim doc As NotesDocument
Dim parameters List As String
Dim nname As NotesName
Dim outbuf As String
Set agentdoc = session.DocumentContext
Print "Content-type: text/plain"
Call PutQSIntoList(parameters,agentdoc.Query_String_Decoded(0))
Set db = New NotesDatabase("",parameters("qp") & "Contacts1.nsf")
Set view = db.GetView("h_PeopleByGroup")
Set nname = New NotesName(parameters("user"))
Set collection = view.GetAllDocumentsByKey(nname.Canonical,True)
Set doc = collection.GetFirstDocument
outbuf = "None"
While Not(doc Is Nothing)
If outbuf = "None" Then
outbuf = doc.GetItemValue("h_Name")(0)
Else
outbuf = outbuf & "," & doc.GetItemValue("h_Name")(0)
End If
Set doc = collection.GetNextDocument(doc)
Wend
Print outbuf
End Sub
The agent returns the list of groups separated by commas, which is easy enough to step through using JavaScript. The PutQSIntoList function puts the parameters of the QueryString into a List object for easy access. The displayContent function can be placed in either the layout HTML pages or in individual imported pages, but not both. Here is an example of what the function might look like:
function displayContent(groups) {
var tmp = new Array();
tmp = groups.split(",");
for (var i=1;i
// snipped
}
}
}
I had searched for a similar solution on the LDD Forums, but found nothing, so I hope this might help someone else out there. As far as speed is concerned, there is no discernible delay due to the AJAX call.
Sub ConnectionDocument(formalname As String,connectiontype As String, tcpip As String,location as string)
Dim collection As NotesDocumentCollection
Dim doc As NotesDocument
Dim servername As NotesName
Set servername = New NotesName(formalname)
Set collection = localdb.Search(|Type = "Connection" & @LowerCase(@Name([CN];Destination)) = @LowerCase("| & connectionname & |")|,Nothing,0)
If collection.count = 0 Then
Set doc = localdb.CreateDocument()
doc.Form = "Connection"
doc.Type = "Connection"
Else
Set doc = collection.GetFirstDocument
End If
Select Case connectiontype
Case Is = "LAN" :
doc.ConnectionType = "0"
doc.LanPortName = "TCPIP"
Case Is = "Passthru" :
doc.ConnectionType = "2"
End Select
doc.Destination = servername.Canonical
If location <> "" Then doc.ConnectionLocation = "*" Else doc.ConnectionLocation = location
doc.Source = "*"
doc.OptionalNetworkAddress = tcpip
Call doc.ComputeWithForm(True,False)
Call doc.save(True,False,True)
End Sub
Computers are supposed to make our lives easier, not more difficult. As usability-conscious designers, we can make our users’ lives easier by thinking about the way people interact with our websites, providing clear direction, and then putting the burden of sorting out the details in the hands of the computers—not the users.
The problem in question had to do with the Shared Actions in Notes 6.5. Somehow, the Shared Actions on one of the replicas of the database I was working on didn't look like it should. I tried all the normal things to get the changes in the design to update. I cleared the replication history and replicated from every other replica I could find. I created a template of the design, replaced the design of the database with a different template, and then replaced the design again with the template I had just created. Finally, after banging on it for over an hour, I deleted the replica in question and replicated a new copy from one of the other servers. All better, right? WRONG!
Lo and behold, the same problem existed in the brand new replica I just created. It seems that nothing I did to the Shared Actions on the other servers had any impact on the server that was having the problem. My initial theory as to the cause of the problem was a Save and Replica Conflict of the Shared Actions design element. I had seen this happen to other design elements in the past, namely views and forms. Those were easy enough to handle through the Designer UI since they were listed in a view like fashion, but there was no easy way for me to see the Shared Actions conflict. Or was there???
The solution to my problem was to change the $FormulaClass property of a view to allow the view to display design elements in a view. Yes, you read that right, you can display design elements as if they were documents in a database. For me, the beauty of Notes has always been that data and design have always been kept separate. A field on a document doesn't ever have to exist on a form nor does a document ever have to show up in a view index. What I didn't really grasp until I had this problem is that all Notes Design elements are just Notes in the database. If you can get a handle on it with a NoteID or NotesURL, you can do almost anything to it that you can do with a regular document. A 1999 DominoPower article by Dan Velasco and more recent blog entries by Vince Schuurman and Julian (of course) gave me all the information I needed to create the view I needed.
Dim session As New NotesSession
Dim db As NotesDatabase
Dim view As NotesView
Dim doc As NotesDocument
Set db = session.CurrentDatabase
Set view= db.GetView("Admin\Design")
Set doc = db.GetDocumentByUNID(view.UniversalID)
Call doc.ReplaceItemValue("$FormulaClass", "32766") ' Shows all non-data notes
Call doc.Save(True, True,True)
Now, I could see that there were indeed 2 Shared Action ($ACTION) design elements. So, I thought, I'll just select the one I don't want and hit delete to remove it forever. WRONG! Doing that through the Client UI gave me an oh-so-helpful error, #02:70. So, after chatting my issue up with Rocky, I created a LotusScript agent that allowed me to enter the NotesURL of the bad $ACTIONS Note and delete it. Plus, I now have a kewl view that I can add to all of my databases to get a look at all of the design elements in one view. I think I will have to do a huge @Replace for the $Flags column to get a readable description of what the element is.
It seems that whenever I tell a Lotus Notes programmer about this site, their reaction is consistently, "you must have a lot of Lotus Notes code posted !" Of course, this is not to say that Lotus Notes is a bad language (I've never used it), but it is odd that I keep hearing this.
Even Damien took a couple of seconds to add to the conversation. I just wish that some of the other responders were a little more informed and had something more to say than "Notes sucks".
The 2 new properties are more like 2 faces to the same thing. The HttpURL and NotesURL are the improved siblings of the UniversalID property. They both contain all the information you need to get to a particular document or design element, but the use different protocols to get there, namely http:// and notes://, respectively. The URLs generated for both properties use UNIDs instead of database and view names. The easiest way to get an idea of what they look like is to bring up the properties dialog for a document and click on the Meta tab. In case you don't know which one that is (I didn't), it's the one that is a plus sign (+) inside of angled brackets. The Identifier field at the bottom of the dialog is the NotesURL for that document.
Now, I know what you are thinking. What's the big deal if you already have hooks into the database and the view. The big deal is when these properties are used with the Resolve method of the NotesSession class. notessession.Resolve(NotesURL) will return a handle to whaever the NotesURL points to, whether it's a design element or a document. Imagine the generic subs and functions you can write if all you need the one little string to get a handle on whatever you need.
For me, this was important when returning a list of agents from a database. I didn't want to use the name of the agent, cause that could change over time. It was less likely that the NotesURL would change short of the agent getting cut and pasted back into the database. I have a feeling I will be using it these new features more and more in the near future.
function getHomeFolder(returnValues) {
var temp = new Array();
temp = document.location.href.split('/');
var rv = "";
var protocol = temp[0];
var server = temp[2];
var qp = temp[3];
var sub = temp[4];
var db = temp[5];
var view = temp[6];
var doc = temp[7];
if (returnValues.indexOf('1') != -1) { rv = rv + protocol + '//';}
if (returnValues.indexOf('2') != -1) { rv = rv + server + '/';}
if (returnValues.indexOf('3') != -1) { rv = rv + qp + '/';}
if (returnValues.indexOf('4') != -1) { rv = rv + sub + '/';}
if (returnValues.indexOf('5') != -1) { rv = rv + db + '/';}
if (returnValues.indexOf('6') != -1) { rv = rv + view + '/';}
if (returnValues.indexOf('7') != -1) { rv = rv + doc;}
return rv;
}
The above function could be easily generalized to be able to be used in any database on a Domino server or any regular web page. It's much easier than looping through the href using indexOf() or substring(). In fact, you can use split to create a JS version of Rocky's favorite @Function, @Word.
function atWord(fullstring, sep, wordno) {
var temp = new Array();
temp = fullstring.split(sep);
if (temp.length > wordno) {
return temp[wordno - 1];
} else {
return false;
}
}
A CSS hack should (or MUST in the RFC2119 sense if you prefer):Tantek goes on to correctly state that IE7 will NOT remove the need for such hacks. What is most interesting about the article is to see how the hacks and the need for them evolved over time.
- Be valid. Invalid hacks are unacceptable. Back in the heyday of Web 1.0 (i.e. the late 1990s), the The Web Standards Project and numerous others were already spreading the message of better coding through validity, and thus hacks themselves had to validate as well. (Nevermind that many/most so-called "Web 2.0" sites can't be bothered to validate. See above about the real professionals having to repeat the message).
- Target ONLY older/frozen/abandoned versions of user agents / browsers. When the Box Model Hack was introduced, we were already playing with betas of IE6/Windows (back when we expected there would be an IE6/Mac), and so we knew how to make sure it wasn't affected. And now we're playing with betas of IE7.
- Be ugly. It's actually a good thing that a hack be visually ugly from a coding aesthetic point of view in the hopes that the ugliness will be a reminder that the hack is a hack, and should incite a tendency for people to a) minimize it's usage, and b) remove it's usage over time. At it's core, browser switching is one of those things you really shouldn't, but must, do to get your job done. Hacks' ugliness are like the equivalent of persistent warning tags, a reminder to dispose of them when no longer necessary.
What really amazes me is that browser programmers and email vendors continue not to fully support the CSS2(.1) specification. The lack of support is more troubling with the emergence of Firefox as a true, and some would say better, alternative to IE. With the death of Netscape earlier this decade, most developers began to be able to develop for the IE platform only. While this may not have been a perfect platform to work with, it's flaws were well known and you could easily work within those limitations to accomplish what you wanted to. Now, you have to code to the standard and then hack the CSS to get it to work in IE. This can be extremely frustrating, especially when things look so much better in Firefox.
via Scobleizer
I've been talking to countless Notes customers. One thing I tell them always surprises everyone in the room. "What you need is a Domino 7 server and Domino Designer 7." More that once the Microsoft sales folks got big eyes or chocked. So why would I say this? Its simple, most Notes customers have a large investment in Notes applications and data. The quickest way to write cool .NET applications that are relevant for end users is to include functionality in existing systems. And the easiest way to include functionality from one system to another is with web services.Even though his bosses would rather it be different, he knows the power of the Domino platform and ability that it has to adapt and play well with other applications. Domino 7's ability to easily build web services may turn out to be a killer feature that changes Domino's fortunes for a number corporations.
Eclipse is an open source community whose projects are focused on providing an extensible development platform and application frameworks for building software. Eclipse provides extensible tools and frameworks that span the software development lifecycle, including support for modeling, language development environments for Java, C/C++ and others, testing and performance, business intelligence, rich client applications and embedded development.The guides include are quite varied and should help you learn more about Eclipse, whether you've never even heard of it or you've been working in it for years.
In case you have never used the Execute command, it is used to compile and run a text expression at run time. In other words, things like variable names and subroutine calls can be determined at run time. This was important to me because the names of 2 sets of 13 Lists I had were almost identical except for a number. So here is the code as it would have been written using normal Switch-Case statements:
Select Case monthnumber
Case 1 :
If Iselement(month1orders(ccode)) Then month1orders(ccode) = month1orders(ccode) + oqty Else month1orders(ccode) = oqty
If Iselement(month1total(ccode)) Then month1total(ccode) = month1total(ccode) + ocost Else month1total(ccode) = ocost
Case 2 :
If Iselement(month2orders(ccode)) Then month2orders(ccode) = month2orders(ccode) + oqty Else month2orders(ccode) = oqty
If Iselement(month2total(ccode)) Then month2total(ccode) = month2total(ccode) + ocost Else month2total(ccode) = ocost
Case 3 :
If Iselement(month3orders(ccode)) Then month3orders(ccode) = month3orders(ccode) + oqty Else month3orders(ccode) = oqty
If Iselement(month3total(ccode)) Then month3total(ccode) = month3total(ccode) + ocost Else month3total(ccode) = ocost
Case 4 :
If Iselement(month4orders(ccode)) Then month4orders(ccode) = month4orders(ccode) + oqty Else month4orders(ccode) = oqty
If Iselement(month4total(ccode)) Then month4total(ccode) = month4total(ccode) + ocost Else month4total(ccode) = ocost
Case 5 :
If Iselement(month5orders(ccode)) Then month5orders(ccode) = month5orders(ccode) + oqty Else month5orders(ccode) = oqty
If Iselement(month5total(ccode)) Then month5total(ccode) = month5total(ccode) + ocost Else month5total(ccode) = ocost
Case 6 :
If Iselement(month6orders(ccode)) Then month6orders(ccode) = month6orders(ccode) + oqty Else month6orders(ccode) = oqty
If Iselement(month6total(ccode)) Then month6total(ccode) = month6total(ccode) + ocost Else month6total(ccode) = ocost
Case 7 :
If Iselement(month7orders(ccode)) Then month7orders(ccode) = month7orders(ccode) + oqty Else month7orders(ccode) = oqty
If Iselement(month7total(ccode)) Then month7total(ccode) = month7total(ccode) + ocost Else month7total(ccode) = ocost
Case 8 :
If Iselement(month8orders(ccode)) Then month8orders(ccode) = month8orders(ccode) + oqty Else month8orders(ccode) = oqty
If Iselement(month8total(ccode)) Then month8total(ccode) = month8total(ccode) + ocost Else month8total(ccode) = ocost
Case 9 :
If Iselement(month9orders(ccode)) Then month9orders(ccode) = month9orders(ccode) + oqty Else month9orders(ccode) = oqty
If Iselement(month9total(ccode)) Then month9total(ccode) = month9total(ccode) + ocost Else month9total(ccode) = ocost
Case 10 :
If Iselement(month10orders(ccode)) Then month10orders(ccode) = month10orders(ccode) + oqty Else month10orders(ccode) = oqty
If Iselement(month10total(ccode)) Then month10total(ccode) = month10total(ccode) + ocost Else month10total(ccode) = ocost
Case 11 :
If Iselement(month11orders(ccode)) Then month11orders(ccode) = month11orders(ccode) + oqty Else month11orders(ccode) = oqty
If Iselement(month11total(ccode)) Then month11total(ccode) = month11total(ccode) + ocost Else month11total(ccode) = ocost
Case 12 :
If Iselement(month12orders(ccode)) Then month12orders(ccode) = month12orders(ccode) + oqty Else month12orders(ccode) = oqty
If Iselement(month12total(ccode)) Then month12total(ccode) = month12total(ccode) + ocost Else month12total(ccode) = ocost
Case 13 :
If Iselement(month13orders(ccode)) Then month13orders(ccode) = month13orders(ccode) + oqty Else month13orders(ccode) = oqty
If Iselement(month13total(ccode)) Then month13total(ccode) = month13total(ccode) + ocost Else month13total(ccode) = ocost
End Select
Even though it's valid script, that's a lot of code that is almost identical. And if the formula I am using changes, it will take quite a while to modify all those lines of code. And I don't even think what a pain it would be to add another list to the mix or try to compile more than 13 months worth of data. Contrast the code above to the elegant 2 lines listed below:
Execute |If Iselement(month| & monthnumber & |orders("| & ccode & |")) Then month| & monthnumber & |orders("| & ccode & |") = month| & monthnumber & |orders("| & ccode & |") + | & oqty & | Else month| & monthnumber & |orders("| & ccode & |") = | & oqty
Execute |If Iselement(month| & monthnumber & |total("| & ccode & |")) Then month| & monthnumber & |total("| & ccode & |") = month| & monthnumber & |total("| & ccode & |") + CDbl(| & ocost & |) Else month| & monthnumber & |total("| & ccode & |") = CDbl(| & ocost & |)|
A couple of things to notice about the code above. You can't just reference any of the local variables. You have to convert them to text and use the value of the variables to created the text expression. However, you can change the value of global variables that are referenced in the text expression. I also used the Execute command with a For loop to write some code that manipulates an Excel spreadsheet.
For x = 4 To 29
Execute |xlSheet.cells(| & thisrow & |,| & x & |).FormulaR1C1 = "=SUM(R| & startrow & |C| & x & |:R| & endrow & |C| & x & |)"|
Next
This is where using the R1C1 formulas really come in handy. Instead of writing 26 lines of code that each write a single formula to a single cell in a spreadsheet, I was able to write 3 lines of code that can do the same thing. I love it when a plan comes together!!! One word of caution before you go Execute crazy. I have no idea what kind of performance impact will be paid for the increased manageability of your code. Leave a comment if you have some actual numbers.
Color Scheme Generator 2 from WellStyled - Very easy to use color scheme maker. Allows you to either select a color from a visual palette or enter the RGB for a color. Returns complimentary and contrasting colors and allows you to select only web-safe colors.
Color Palette Creator from slayeroffice - It will create 10 shades of the base color, located top-left, at varying degrees of opacity. Along with giving you some nice color combinations, the author also provides the JavaScript and CSS source for easy dissection.
I think that any tool that can help us Domino developers create better looking applications is manta from Heaven.
For those of us who bleed yellow and black, you can think of SSIs as the subforms of the HTML world. Although SSIs do have a performance cost, the productivity gains will be similar to what you have seen with subforms. They can either be a static or computed link to a separate HTML file, like a footer that you want on all pages, or they can be a formula for showing information like the last time the file was modified. The first thing you will notice about sites that use SSIs is that their pages end in either a .SHTM or .SHTML extension. These extensions tell the web server to look for SSI code in the HTML. Once you have the extension correct, just add a line like to your code. The above line inserts the contents of the footer.html file at that point in the HTML. On the CGC Builders site, I have used SSIs for the header, navigation, and footer.
It is important to note that the files that are included are not complete HTML files, but rather snippets of HTML. They do not have HTML, HEAD, or BODY tags in them, since that would cause problems if they were included in other HTML pages. There are a number of web resources out there for learning about SSIs. Here are a couple of the pages I used:
If you find yourself doing plain jane web development, SSIs are a nice way to maintain sanity in a sea of HTML files.
While on the musical front, the lunchtime DJ on a local Rock station asked people to write in with their All-time Greatest Concert lineup. Basically, you choose 3 bands that you would like to see in a single concert. The bands do not have to have similar music or don't even have to have existed at the same time. After some hard thinking, here is my list:
A request that recently came to me was to put a reminder for an upcoming IT outage on the calendar for a specific list of users. To me, this seemed like a natural extension of the Company Communications Database. The form I used for setting up the reminder was just a modified mass email form that was already built. All the user has to do is select who gets the reminder, select when the reminder should go off, and enter the subject and body of the reminder. Below is the subroutine that actually puts the reminders on the user's calendar:
Sub SendReminder(person As NotesDocument, doc As NotesDocument)
Dim mailfile As NotesDatabase
Dim collection As NotesDocumentCollection
Dim reminder As NotesDocument
Dim item As NotesItem
Dim body As NotesRichTextItem
Dim oldbody As NotesRichTextItem
Dim datetime As NotesDateTime
Set mailfile = New NotesDatabase(person.MailServer(0),person.MailFile(0))
If Not(mailfile.IsOpen) Then Exit Sub
Set datetime = New NotesDateTime(doc.GetItemValue("calendardatetime")(0))
Set collection = mailfile.Search(|Form = "Appointment" & ApptUNID = "| & doc.UniversalID & |"|,Nothing,0)
If collection.Count = 0 Then
Set reminder = mailfile.CreateDocument()
reminder.Form = "Appointment"
Set item = reminder.ReplaceItemValue("$Alarm",1)
Set item = reminder.ReplaceItemValue("$AlarmOffset",0)
' Set item = reminder.ReplaceItemValue("$NoPurge",doc.GetItemValue("calendardatetime"))
Set item = reminder.ReplaceItemValue("$PublicAccess","1")
Set item = reminder.ReplaceItemValue("$CSVersion","2")
Set item = reminder.ReplaceItemValue("_ViewIcon",10)
Set item = reminder.ReplaceItemValue("Alarms","1")
Set item = reminder.ReplaceItemValue("AppointmentType","4")
Set item = reminder.ReplaceItemValue("ApptUNID",doc.UniversalID)
Set item = reminder.ReplaceItemValue("Categories","Company Reminders")
Set item = reminder.ReplaceItemValue("Chair","CN=Generic ID/O=Company")
Set item = reminder.ReplaceItemValue("ExcludeFromView","D")
Call item.AppendToTextList("S")
Set item = reminder.ReplaceItemValue("OrgTable","C0")
Set item = reminder.ReplaceItemValue("Principal","CN=Clark Construction/O=Clark")
Set item = reminder.ReplaceItemValue("SequenceNum",1)
Set item = reminder.ReplaceItemValue("UpdateSeq",1)
Set item = reminder.ReplaceItemValue("WebDateTimeInit","1")
Else
Set reminder = collection.GetFirstDocument
Set item = reminder.ReplaceItemValue("SequenceNum",reminder.SequenceNum(0) + 1)
Set item = reminder.ReplaceItemValue("UpdateSeq",reminder.UpdateSeq(0) + 1)
Call reminder.RemoveItem("Body")
End If
Set item = reminder.ReplaceItemValue("CalendarDateTime",doc.GetItemValue("calendardatetime")(0))
Set item = reminder.ReplaceItemValue("EndDate",Datevalue(datetime.DateOnly))
Set item = reminder.ReplaceItemValue("EndDateTime",doc.GetItemValue("calendardatetime")(0))
Set item = reminder.ReplaceItemValue("EndTime",Timevalue(datetime.TimeOnly))
Set item = reminder.ReplaceItemValue("StartDate",Datevalue(datetime.DateOnly))
Set item = reminder.ReplaceItemValue("StartDateTime",doc.GetItemValue("calendardatetime")(0))
Set item = reminder.ReplaceItemValue("StartTime",Timevalue(datetime.TimeOnly))
Set item = reminder.ReplaceItemValue("Subject",doc.subject(0))
Set oldbody = doc.GetFirstItem("Body")
Set body = reminder.CreateRichTextItem("Body")
Call body.AppendRTItem(oldbody)
Call reminder.ComputeWithForm(True,False)
Call reminder.Save(True,False,True)
End Sub
A couple of things that you should notice about the subroutine. First, a couple of NotesDocument objects are passed in, one pointing to the user's person document in the address book and the other pointing to the reminder document. Second, the agent is built to be able to update reminders that have already been created. The ApptUNID field will contain the UniversalID of the reminder document so that if the reminder document gets updated, the reminder on the user's calendar will also be updated, not duplicated. Finally, I only set the static calendar fields when the document is created. I think that I got all the required fields, but I might have missed one. Please drop me a line if you think I did.
Now, since we have used lists to gather the data, writing the data out to the Excel spreadsheet doesn't contain a single loop!! How cool is that?!? First, let me explain what some of the other subroutines that you have seen do:
Once we have the sheets setup like we want them, we use the AddStatstoReport sub to populate the sheet. Here is a portion of that sub:
Sub AddStatstoReport
Call AddLine(3,"Total")
Call AddPercentLine(4)
Call AddLine(6,"Company 1")
Call AddLine(7,"Company 2")
Call AddLine(8,"Company 3")
Call AddLine(10,"Mid-Atlantic")
Call AddLine(11,"Midwest")
Call AddLine(12,"Northest")
Call AddLine(13,"Northern California")
Call AddLine(14,"Southeast")
Call AddLine(15,"Southwest")
Call AddLine(16,"West")
Each row is populated by calling the AddLine sub and passing the row number and the identifier. For the second sheet of this report, calling this Sub is done using a ForAll loop and a counter to calculate identifier and row number. Here is what the AddLine sub looks like:
Sub AddLine(row As Integer, listname As String)
If Iselement(statint(listname)) Then xlSheet.cells(row,2).Value = statint(listname) Else xlSheet.cells(row,2).Value = 0
If Iselement(statpass(listname)) Then xlSheet.cells(row,3).Value = statpass(listname) Else xlSheet.cells(row,3).Value = 0
If Iselement(statoffer(listname)) Then xlSheet.cells(row,4).Value = statoffer(listname) Else xlSheet.cells(row,4).Value = 0
If Iselement(statreject(listname)) Then xlSheet.cells(row,5).Value = statreject(listname) Else xlSheet.cells(row,5).Value = 0
If Iselement(stataccept(listname)) Then xlSheet.cells(row,6).Value = stataccept(listname) Else xlSheet.cells(row,6).Value = 0
xlSheet.cells(row,7).FormulaR1C1 = "=if(R" & row & "C2 <> 0,R" & row & "C6/R" & row & "C2,0)"
xlSheet.cells(row,7).NumberFormat = getNumberFormat("Percent")
xlSheet.cells(row,8).FormulaR1C1= "=if(R" & row & "C4,R" & row & "C6/R" & row & "C4,0)"
xlSheet.cells(row,8).NumberFormat = getNumberFormat("Percent")
End Sub
Again, the IsElement function is used to see if the identified exists for each List and, if it does, the value for that identified is retrieved. Since a couple of the cells contain formulas that evaluate out to percentages, we also set the NumberFormat for those cells here. If you have never seen the FormulaR1C1 syntax before, I would suggest getting to know it since it is the easiest way to write Excel formulas from LotusScript.
So there you have it. With a little bit of ingenuity and some efficient coding, you can use Lists and Excel to create some really nice reports that anyone in your company can run. I have posted the code for the Agent in it's entirety for you to dissect at your leisure. Just be nice in your comment as I bruise easily.
So, now let's take a look at the RecruitStats() sub and what do you know, it calls another sub to actually populate the lists.
Sub RecruitStats()
Dim collection As NotesDocumentCollection
Dim child As NotesDocument
Dim intern As Boolean
Set collection = recruit.Responses
Set child = collection.GetFirstDocument
intern = False
Set child = collection.GetFirstDocument
While Not(child Is Nothing)
If child.form(0) = "Education" Then
Call AddtoLists(recruit.offer_status(0),child.school(0))
Call AddtoLists(recruit.offer_status(0),child.degree_type(0))
Call AddtoLists(recruit.offer_status(0),child.degree(0))
If child.degree_type(0) = "Engineering" Then Call AddtoLists(recruit.offer_status(0),child.engineering_type(0))
End If
If child.form(0) = "Internship" Then intern = True
Set child = collection.GetNextDocument(child)
Wend
Call AddtoLists(recruit.offer_status(0),"Total")
Call AddtoLists(recruit.offer_status(0),recruit.company(0))
Call AddtoLists(recruit.offer_status(0),recruit.region(0))
Call AddtoLists(recruit.offer_status(0),recruit.gender(0))
Call AddtoLists(recruit.offer_status(0),recruit.ethnicity(0))
Call AddtoLists(recruit.offer_status(0),recruit.contact_type(0))
Call AddtoLists(recruit.offer_status(0),recruit.candidate_type(0))
If recruit.International(0) = "Yes" Then Call AddtoLists(recruit.offer_status(0),"International")
If intern = True Then Call AddtoLists(recruit.offer_status(0),"Intern")
End Sub
Again, because I didn't want to write more than I had to, I decided to put the actual population of this lists in a different sub called AddtoLists(). This sub is passed 2 variables, the current status of the recruit and the List identifier to use. This separate sub allowed me to consolidate all of my logic in one place and not have to copy and paste it all over the place. See, I told you I was lazy. So let's take a look at the AddtoLists() sub:
Sub AddtoLists(status As String, listname As String)
Dim addint As Boolean
If status = "Rejected Offer" Then
addint = True
Forall x In recruit.GetItemValue("reason")
If x = "Prior to Interview" Then addint = False
End Forall
If addint = True Then
If Iselement(statint(listname)) Then statint(listname) = statint(listname) + 1 Else statint(listname) = 1
End If
Else
If Iselement(statint(listname)) Then statint(listname) = statint(listname) + 1 Else statint(listname) = 1
End If
Select Case status
Case "Passed" :
If Iselement(statpass(listname)) Then statpass(listname) = statpass(listname) + 1 Else statpass(listname) = 1
Case "Offer Made" :
If Iselement(statoffer(listname)) Then statoffer(listname) = statoffer(listname) + 1 Else statoffer(listname) = 1
Case "Accepted Offer" :
If Iselement(statoffer(listname)) Then statoffer(listname) = statoffer(listname) + 1 Else statoffer(listname) = 1
If Iselement(stataccept(listname)) Then stataccept(listname) = stataccept(listname) + 1 Else stataccept(listname) = 1
Case "Rejected Offer" :
If addint = True Then
If Iselement(statoffer(listname)) Then statoffer(listname) = statoffer(listname) + 1 Else statoffer(listname) = 1
End If
If Iselement(statreject(listname)) Then statreject(listname) = statreject(listname) + 1 Else statreject(listname) = 1
End Select
End Sub
The first thing this code does every time we want to add sometime to the list is to check if the identifier in question already exists in the list. This is accomplished by the calling the Iselement function. If it doesn't exist, then we set the List(identifier) equal to 1, otherwise add 1 to the value stored in List(identifier). IMHO, this is so much better than looping through arrays to find where a given identifier exists.
So basically, I gather a list of recruits and, for each recruit, add 1 to each statistical category that it corresponds to. Once this is done, it's time to put the statistics onto the finished Excel spreadsheet.
We recently deployed a database to track our college recruiting activities. Along with a calendar on which college recruiting fair we will be attending and contact information on the college career offices, the database also tracks all recruits that we invite for an interview in the office. For the recruits, we track information such as gender, ethnicity, contact information, school, major, graduation date, and recruiting status. There is also an area to add an copy of their resume, cover letter, and a picture. So naturally, the HR group would love to get some pretty reports with statistics showing how we are doing in hiring at different schools, across different ethnic groups, and if we are getting the recruits that we really want. Here is the recruiting dashboard report that they developed for me to populate with data.
While there are many ways to skin a cat, being the lazy programmer that I am, I always try to find the simplest solution to any problem I have. When thinking about gathering statistics on documents in a database, a large number of arrays is what comes to mind. In fact, the image of dealing with a number of multidimensional arrays invades my psyche and causes me to shiver uncontrollably. Well, maybe it just causes me to eat more chocolate. In any event, I have found that List elements sooth my mind and allow me to elegantly write code to do what the customer wants.
So, after looking at the report they wanted, I came up with a number of lists that I thought would be necessary. I made each of the statistical columns a List and used the row titles as List identifiers. In the end, I needed just 5 Lists in Declarations:
Dim statint List As Integer
Dim statpass List As Integer
Dim statoffer List As Integer
Dim statreject List As Integer
Dim stataccept List As Integer
These Lists match up to Total Inteviewed, Passed, Made Offer, Accepted Offer, and Rejected Offer. Now that I had my Lists, I needed to populate them. Here is the first part of the Initialize subroutine:
Sub Initialize
Dim w As New NotesUIWorkspace
Dim schools As Variant
Dim flag As Variant
On Error Goto olecleanup
Set session = New NotesSession
Set db = session.currentdatabase
Set tmp = db.CreateDocument()
flag = w.DialogBox("DashboardDialog",True,True,False,False,False,False,"Limit Results",tmp,True)
If flag = False Then Exit Sub
Set recruits = db.Search(tmp.searchstring(0),Nothing,0)
If recruits.Count = 0 Then Exit Sub
Set recruit = recruits.GetFirstDocument
Set xlApp = CreateObject("Excel.application")
xlApp.Visible = False
xlApp.Workbooks.Add
Set xlSheet = xlapp.workbooks(1).worksheets(1)
Call SetExcelOptions
Call SetWorksheetTitleRows
While Not(recruit Is Nothing)
Call RecruitStats()
Set recruit = recruits.GetNextDocument(recruit)
Wend
Call AddStatstoReport
Set xlRange = xlSheet.Range(xlSheet.Columns(1),xlSheet.Columns(6))
Call xlRange.AutoFit
You can ignore most of the code above for now, but basically I retrieve a collection of recruits and then capture the stats for each recruit using the RecruitStats() subroutine. Note that recruits and recruit were also declared as Global Variables. It is in the RecruitStats() subroutine that the Lists are populated. Check back in my next installment to see what goes on in that sub.
While it may not be perfect, I have found Excel to be a poor man's replacement for such a product. Over the years, I have built a set of "Export to Excel" design elements that I shove into every database I have. This allows me to setup an export that users can run as often as they need to without having to know anything about the design of the database. The form that sets up the export can be seen here. This allows users to export their data to a familiar tool and then slice it and dice it however they please. However, this does nothing to give the executives the pretty reports they yearn for on a daily basis.
For that, I have learned to turn to LotusScript agents and specifically to the use of List elements. If you have never heard of Lists before, here is the definition from the Designer Help file:
A list is a one-dimensional collection of elements of the same data type. You can change the size of a list at any time while the application is running and LotusScript does not allocate any storage space at compile time for the elements of a list. Lists automatically shrink or grow when elements are deleted from or added to them. You access each element in a list by a unique String value, called a list tag.Basically, it's an array that works the way arrays should work. You don't have to specify the number of elements in the array and you access the values of the array with an identifier instead of a number. If this doesn't make complete sense to you, come back for part 2 of this entry and see an agent I recently wrote that makes extensive use of Lists.