Splendiferous News! And Saving Prompts to a Local File

First, the news:

For the past 4 years I’ve been working as a freelancer. Fixing issues at client sites, finding novel solutions to unusual problems, and generally having a ball. The BI market where I live is fairly saturated, I can think of a dozen BI companies covering a country smaller than New Jersey. My high price (I just can’t bring myself to undersell my expertise) tends to put me out of the running for many clients. So, at this point, I’m looking for an American company to adopt me. I’m not focusing on any specific location, although my wife would prefer to avoid Alaska.

What can I offer your company? All of the solutions I’ve published here, and quite a few that I haven’t, are at your disposal. I’ve been involved in almost all phases of BI projects, from planning and architecture at the beginning, to the hand-off training at the end. My main selling point has been my talent for finding solutions to those impossible insane fascinating requests clients have had. I am willing to travel on occasion, and I am willing to teach everything I know to your company employees.

Want to hear more? Drop me a line at cognospaul@gmail.com.

Now for the fun bit: Saving Prompts to a Local File

Every now and then clients request a report with many prompts. I’ve seen one report with 60 of them. Usually these reports show row level data, and the people viewing the reports are only interested in a small subset. In cases like these it’s best to train the users on how to create a report view, and to save the prompt settings in the properties of the report.

But what happens if your users can’t create a report view? This might be the case in OEM implementations, or in cases where users simply aren’t trained in Cognos. In these cases I’ve found it better to simply give the users a way to save their prompt selections. I actually have three solutions for this.

The first (this one) has a button to download a text file containing the prompt selections. Another button selects the text file and loops through the selections.

The second is based on the first, but instead of a button, it automatically saves the prompt selections into a table with a stored procedure. I’ll write about that in the future.

Finally we have a solution that Rick Blackwell wrote that saves the prompts into a cookie. This solution can be found in the Prompt API samples. I dislike this solution as browsers have a finite number of cookies that can be saved per domain. If you use this solution extensively, it will cause problems with Cognos sessions. Cookies also have a finite max size of (roughly) 4k, and are sent in all browser requests, eating a lot of bandwith. Lots of big cookies will give a server indigestion.

Rick’s solution is a great starting point though. He wrote a nice script that loops through the prompts on the page and saves them as JSON. We’ll simply copy that, with some minor modifications, for use in our own script.


/*
 * function paulScripts.JSONEncode - Rick Blackwell - ?
 * Modified by Paul Mendelson - 2012-11-20
 * Why reinvent the wheel? Rick wrote this to populate a cookie (as opposed to a file) with the prompt values. 
 * Modified to escape the strings.
 */
paulScripts.JSONEncode = function(promptControl) {
  // Create an empty variable to contain the JSON encoded prompt values
  var JSONData = " ";

  // Create an array to hold the selected values for the prompt control passed to this function (promptControl)
  aPromptValues = promptControl.getValues();

  // If there are no selected values, then the array length is zero
  if (aPromptValues.length == 0) {
    JSONData = "[]";
  } else {
    JSONData = "[";

      // Loop through the prompt values
      for (var j=0; j< aPromptValues.length; j++) {
        var promptValue =  aPromptValues[j];
        if (promptValue.use) {
          // Non Range value 
          if (j == 0) {
            JSONData = JSONData + " {'use' : '" + escape(promptValue.use) + "'" +", 'display' : '" + escape(promptValue.display) + "' }";
          } else {
            JSONData = JSONData + ",  {'use' : '" + escape(promptValue.use) + "'" +", 'display' : '" + escape(promptValue.display) + "' }";
          }
        } else {
          // Range value
          var rangeStart = promptValue.start;
          var rangeEnd = promptValue.end;
          if (rangeStart && rangeEnd) { 
            if (j == 0) {
              JSONData = JSONData + " {'start' : {'use' : '" + escape(rangeStart.use) + "'";
            } else {
              JSONData = JSONData + ", {'start' : {'use' : '" + escape(rangeStart.use) + "'";
            }
            if (rangeStart.display) {
              JSONData = JSONData +  ", 'display' : '" + escape(rangeStart.display) + "'}"
            } else {
              JSONData = JSONData +  "}"
            }
            JSONData = JSONData + ", 'end' : {'use' : '" + escape(rangeEnd.use) + "'";
            if (rangeEnd.display) {
              JSONData = JSONData +  ", 'display' : '" + escape(rangeEnd.display) + "'}  }"
            } else {
              JSONData = JSONData +  "}  }"
            }
          } else if (rangeStart && !rangeEnd) {
            if (j == 0) {
              JSONData = JSONData + " {'start' : {'use' : '" + escape(rangeStart.use) + "'";
            } else {
              JSONData = JSONData + ", {'start' : {'use' : '" + escape(rangeStart.use) + "'";
            }
            if (rangeStart.display) {
              JSONData = JSONData +  ", 'display' : '" + escape(rangeStart.display) + "'} }"
            } else {
              JSONData = JSONData +  "} }"
            }
          } else if (!rangeStart && rangeEnd) {
            if (j == 0) {
              JSONData = JSONData + " {'end' : {'use' : '" + escape(rangeEnd.use) + "'";
            } else {
              JSONData = JSONData + ", {'end' : {'use' : '" + escape(rangeEnd.use) + "'";
            }
            if (rangeEnd.display) {
              JSONData = JSONData +  ", 'display' : '" + escape(rangeEnd.display) + "'} }"
            } else {
              JSONData = JSONData +  "} }"
            }
          } else {
            alert ("Range not set.");
          } // end if
        } // end if
      } // end for 
    JSONData = JSONData + " ]";
  } //end if

  return JSONData;
};  // end JSONEncode function

This will convert the selected prompt values into a string, which we can then store to reuse later.

How do we store that string? Initially I tried using the Data URI method, but unfortunately Internet Explorer does not support Data URIs correctly. So I had to shop around for another solution. The best solution I found was to use a flash widget called “OpenSave”. You can download that here: OpenSave.

The Save button will run a function to pull the prompt values:

/*
 * Function createPrompts. Paul Mendelson 2012-11-12
 * This will pass a string containing all of the prompts and currently selected values to the OpenSave save button.
 */ 
paulScripts.createPrompts = function()
{
//find all of the controls and stick them in aPromptControls. 
  var aPromptControls = oCR.prompt.getControls( );
  var names = [];
  var choices = names

//loop through the controls - finding the name and values and sticking them into the names array
  for(i=0;i<aPromptControls.length;i++)
  {
    var ctrl = aPromptControls[i];
    var values = ctrl.getValues();
    names[i] = ctrl.getName();

//if the prompt doesn't have any selected values, then skip it and move on.
    if(values.length == 0) continue;

//append the JSON encoded values to the prompt name.
    names[i] = ctrl.getName() + '||' + paulScripts.JSONEncode(ctrl);

  }
names.unshift('Cognos Prompts');
return  names.join('\r\n');
}

On the flip side, when we pull the file we use the following function to populate the prompts.

/* 
 * function paulScripts.getFile - Paul Mendelson 2012-11-20
 * This function will load the file and populate the prompts accordingly. If the prompt file has unmatched prompt
 * names, it will dump those into an array and alert the users at the end.
 */ 
paulScripts.getFile = function(base)
{
  var base = opensave.Base64_decode(base.data64);
  if(!base||base.length===0) {alert('File is empty');return false};
  var params=base.split('\r\n');
  var notFound = new Array();

  //is this a prompts file? If not, STOP EVERYTHING.
  if(params[0] != 'Cognos Prompts') {alert('File is not formatted correctly.');return false;};
  for (i=1;i<params.length;i++)
  {
    if(params[i].length===0) continue;
    //any params with values will have '||' separating the param name from the value
    //on second thought, if the parameter exists on the page, but has no value selected, it should simply clear the parameter. 
    //if(params[i].indexOf('||')<0) continue;
    var sepPos = params[i].indexOf('||');	

    //if the sep isn't found, then the promptname is the entire string
    var promptName = sepPos<0?params[i]:params[i].substring(0,sepPos);

    var promptObject = paulScripts.getControl(promptName);
    //sanity check - does the prompt exit? If not, push it into the notFound array 
    if(!promptObject) {notFound.push(promptName);continue}

    //clear the prompt before applying the new values
    promptObject.clearValues();

    //now set the value, if there is a value to set
    if(sepPos>0) promptObject.setValues(eval(unescape(params[i].substring(sepPos+2))));
    promptObject.checkData();
  }

  //Are there any unmatched prompt names? Oh no! Quick, tell the user!
  if(notFound.length>0) alert('Could not find the following prompts: ' + notFound.join(', '));
}

The save button should be an HTML item set to report expression. This way we can automatically have it pull the report name:

'<div id="saveTextFieldButton"> Save Text Field Button Target Div</div>

<script>

function myOpenSaveHandler(buttonID){
   var bi = opensave.getButtonInfo(buttonID);
   var retObj = new Object();
     retObj.filename = "' +
case when  ReportName () ='<%ReportName()%>' then 'Unsaved_Report' else URLEncode (ReportName ()) end
+'.prompts";
      retObj.data = acme.createPrompts();
   return retObj;
}

opensave.make({
  width: 105,
  height: 24,
  buttonDiv: "saveTextFieldButton",
  handler:  myOpenSaveHandler}
);
</script>'

On the other side, the button to open the prompts file can be a simple HTML item:

<div id="openTextFieldButton"> Open Text Field Button Target Div</div>

<script>

opensave.make({ 
  kind: "open", 
  filename: "report.prompts", 
  width: 105,
  height: 24,
  buttonDiv: "openTextFieldButton",
  handler: paulScripts.getFile
}
);
rsa1
</script>

Finally, if everything works, it should look at bit like the following:
saving prompts with openSave

Because we’re using the Prompt API, this will only work in versions 10.2+, it could be adapted to previous versions with a bit of work. I’ve chosen to put the OpenSave folder under webcontents/JS/OpenSave. If you don’t save it there, make sure to change the pointer in the HTML item at the top of the page. It’s also worth mentioning that these make good candidates for custom toolbox items; add these to the toolbox controls and report developers can simply drag them into their reports.

openSave-report.txt (743 downloads)

Quickie: Resizing a value prompt list

Another problem from a reader. Unlike checkbox or radio prompts, list prompts don’t expand and collapse to fit the number of options available to them.
This prompt is taking far too much room.

In the above image, we have a prompt that has too much space. Let’s shrink that down.
resizing lists

Now it shrinks to the number of rows displayed, or expands to a maximum of 15 items.

It’s actually a very easy script. The first step is to identify the prompt. We’ll use the old fW script (so this should work in every since 8.4).

var fW = (typeof getFormWarpRequest == "function" ? getFormWarpRequest() : document.forms["formWarpRequest"]);
if ( !fW || fW == undefined)
{
  fW = ( formWarpRequest_THIS_ ? formWarpRequest_THIS_ : formWarpRequest_NS_ );
}
var preFix = "";
if (fW.elements["cv.id"])
{
  preFix = fW.elements["cv.id"].value;
}
var nameSpace = "oCV" + preFix;

Next, we need to identify the prompt object:

var pr = fW._oLstChoicesPromptName;

The prompt itself is a standard SELECT element. We can set the size attribute to easily set the height.

This gives us:

if  (pr.options.length>15)
{
  pr.size=15
}
else 
{
  pr.size=pr.options.length
}

The following report XML is based on 10.2.1, but you can downgrade it easily by changing the schema number on the first line.
Resizing-Value-Prompt-List.txt (1076 downloads)

Freezing headers in Cognos 10.2

Starting in Cognos 10.2, IBM released a way to freeze crosstab headers and rows. This method works in all major browsers, and doesn’t rely on dirty CSS hacks. To get it working, simply right-click on the list or crosstab, and select Freeze Headers. The report will even remember the state of the crosstab after the page refreshes (from a prompt, possibly).

right click action

While this method works well, there are many times where we’d want to have the lists or crosstab start off frozen; or maybe we want to give the user a button to freeze one or all of the crosstabs in one go. Somewhat surprisingly, the JavaScript is very easy to use.

First, let’s define the initial variables:

var paulScripts = {}
  , win=window['oCV'+'_THIS_'];

paulScripts.oCV = win.getRV().getCV();

Reports run from Report Studio need to use window[‘oCVRS’] while reports from the connection need window[‘oCV_NS_’]. Cognos will automatically replace _THIS_ to the correct fragment. Most of the Cognos functions we need are found inside win.getRV().getCV(), so we’re aliasing that into “paulScripts.oCV”.

The first function I’ll define is:

paulScripts.freezeContainer = function(objectName){
  var pFMngr = paulScripts.oCV.getPinFreezeManager();
  pFMngr.freezeContainer(objectName,true,true);
  window.onResizeViewerEvent()
};

All of the freezing functions are found inside getPinFreezeManager. The function getPinFreezeManager().freezeContainer() takes the object name (“Crosstab1”), and a Boolean to freeze/unfreeze the headers, and another Boolean to freeze/unfreeze the rows.

The freezing mechanism seems to have a small bug in the way it handles resizing. In my tests, it always resizes to a much smaller window than I need. The window.onResizeViewerEvent() function tricks the browser into thinking the window has been resized, and Cognos will then correctly recalculate the size of the frozen crosstab.

The unfreezing function is simple:

paulScripts.unfreezeContainer = function(objectName){
  var pFMngr = paulScripts.oCV.getPinFreezeManager();
  pFMngr.freezeContainer(objectName,false,false);
};

These two functions will let us automatically freeze a crosstab as soon as the page loads. But say we want to toggle it on and off.

/* paulScripts.toggleContainer 
 * Paul Mendelson - 2014-03-25
 * This wil check if a container has frozen headers. If so, it unfreezes it, if not, it freezes the container. 
 */
paulScripts.toggleAll = function(objectName) {
  var pFMngr = paulScripts.oCV.getPinFreezeManager();
  if(pFMngr.hasFrozenColumnHeadings(objectName)) {
    paulScripts.unfreezeContainer (objectName);
  }
  else {
    paulScripts.freezeContainer (objectName);
    window.onResizeViewerEvent()
  }
}

The hasFrozenColumnHeadings function returns a Boolean, true or false, on the state of the column headers.

And finally let’s say that we want to freeze all the crosstabs on the page in one go.

/* paulScripts.toggleAll 
 * Paul Mendelson - 2014-03-25
 * in: type {string - crosstab or list}
 * This wil loop through every "type" on the page, and freezing or unfreezing, depending if the column header is frozen. 
 */

paulScripts.toggleAll = function(type) {
  setTimeout(function(){
    var xts = win._getContainers(type)
      , xtLen = xts.length
      , pFMngr = paulScripts.oCV.getPinFreezeManager();
      for(var i =0;i<xtLen;++i){
        var lid = pFMngr.removeNamespace(xts[i].getAttribute('lid'));
        if(i<xtLen-1 && lid==pFMngr.removeNamespace(xts[i+1].getAttribute('lid'))) continue; //when the panes are frozen the crosstab is split into four elements, all with the same lid. Without this hack, the xtab would toggle four times!
        if(pFMngr.hasFrozenColumnHeadings(lid)) {
          pFMngr.freezeContainer(lid,false,false);
        }
        else {
          pFMngr.freezeContainer(lid,true,true);
          window.onResizeViewerEvent()
        }
      }
    }
  ,200);
}

That was a little bit more complex than before. We can use the _getContainers functions to get an array of all the lists or crosstabs on the page. The lid of the object is the name, plus the namespace. We can use the removeNamespace function to get it back to the name the freezeContainer functions expect.

The toggleAll function should only be used in a button. As Cognos stores the state of the object; if the headers are locked, they will remain locked after refreshing the page. Toggling them will cause them to unlock. Instead, it’s best to use a freezeAll function when loading the page:

paulScripts.freezeAll = function(type){
  var xts = win._getContainers(type)
  , xtLen = xts.length
  , pFMngr = paulScripts.oCV.getPinFreezeManager();
  for(var i =0;i<xtLen;++i){
    var lid = pFMngr.removeNamespace(xts[i].getAttribute('lid'));
    if(pFMngr.hasFrozenColumnHeadings(lid)) continue; 
    paulScripts.freezeContainer (lid);
  }
};

Once everything is working, we can see how it works.
Freezing panes

The example report in using 10.2.1, against the sales and marketing cube.
Freezing Panes report XML (1961 downloads)