Cognos Prompt API

Well, it’s been a week since I’ve returned from the IOD, and I’m almost fully recovered from the jet-lag. I had an absolute blast, and am looking forward to going back next year.

I promised more details on the various booths that I went to, and I do have the various papers that they gave me. I will eventually get to them! But before I do, this is what I started writing while I was at the IOD, but only now am I getting around to actually publishing it.

One of the most exciting new features in Cognos 10.2 is the introduction of a standardized prompt api. No longer will developers have to scramble to try to figure out how to get validate prompts at run time. Or how to repopulate them as needed through other interactions.

IBM has released a standard set of functions, and has even released a few samples containing them. The goal in this post is to learn how to use the new API and maybe to redo a few challenges I’ve had previously.

Just as previous version required a bit of JavaScript to prepare your page for playing with the prompt objects, this version is no different. What is different, is that instead of having to memorize the entire fW formWarpRequest nonsense, now it’s a much simpler:

var acme = {};
acme.getControl = function(promptName)
{
  var ocr = cognos.Report.getReport("_THIS_");
  return ocr.prompt.getControlByName(promptName);
};

The acme namespace is to prevent function conflicts with existing Cognos functions, just preface all functions with that and you’ll be good. The “_THIS_” will automatically be replaced with the correct Cognos Namespace, so your JavaScript should still work if you’re running the report from Report Studio or the Cognos Connection. This never ever needs to change, so just copy and paste the above (although employees of Acme Inc may want to change the fictious namespace). This solution was shamelessly ripped from Rick Blackwell’s wonderful session at the IOD. All credit for anything and everything related to Cognos and JavaScript should obviously go to him now.

To begin with, let’s use Rick’s standard example for Postal Code validation.

 	/*
	 *
	 * This function will associate a validation function with the prompt control called promptPostalCode
	 *
	 */
	asdf.assignValidator = function ( ) {
		// Create the report object
		oCR = cognos.Report.getReport( "_THIS_" );

		// Create an array of prompt controls
		// This object is used in both functions in this script.
		asdf.promptControlPostalCode = oCR.prompt.getControlByName("promptPostalCode");

		// Make sure we have a valid Postal Code in the format "ana nan" such as K3F 5F4
		asdf.promptControlPostalCode.setValidator(asdf.postalCodeValidator);

	};

	/*
	 *
	 * This function is called for each character entered by the user.
	 * Ensure the value is a valid Postal Code (ie. A1A 1A1 with spaces) using RegEx.
	 *
	 */
	asdf.postalCodeValidator = function(promptValue) {
		//promptValue is an array of 1 value

		// If a value has been entered by the user ensure it matches the pattern
		if (promptValue.length > 0) {
			var rePostalCodeFormat = new RegExp("[a-z][0-9][a-z] [0-9][a-z][0-9]", "gi" );
			if ( rePostalCodeFormat.test(promptValue[0].use) ) {
				return true;
			} else {
			    return false;
			}
		} else {
		 	// If the prompt contains no value it is valid.
			// This is important as the prompt and filter are optional
			return true;
		}
	};

His example is a bit complicated, but it’s good to demonstrate the capabilities.

The first function finds the promptPostalCode prompt, and assigns the validator. Now when a user enters a value, it will trigger the validator function, passing the entered value.
The second function will take the entered value compare it against the regex, and return a true/false, telling Cognos whether the finish button should be active, and if there should be a red underline in the prompt object.

Of course, this is not the only thing that can be done with the validator function. The validator gives us an easy to use onchange function for the prompts.

A few of my clients want tree prompts which automatically hides after a user has selects a member. Previously, there were a few was this could be accomplished, each of them more difficult than the last. But now, instead of using the JS to validate, I can use the validate function to hide the prompt after a few seconds.

First I create an HTML item containing span with an ID:

<span id="treeLabel" onclick="acme.toggleTree()">Tree Prompt</span>

Next, I wrap the tree prompt inside a span with boxtype set to none:

<span id="treePrompt" style="display:none">

</span>

First a function to directly toggle the display of the tree prompt:

acme.toggleTree = function()
{
document.getElementById('treeprompt').style.display=document.getElementById('treeprompt').style.display=='none'?'block':'none';
}

Next, the actual validation function:

/*
 * function hideTree. Paul Mendelson - 2012-10-22
 * Whenever a tree node is clicked, it will wait {acme.hideTimer} seconds before hiding
 * the tree. If another selection is made, it will restart the counter.
 */
acme.hideTree= (function () {
  var timer;
  var elm;
  return function (){
    clearTimeout(timer);
  var promptName = this.getName();

    //standard way of determing source of click.
    var targ;
    if (!e) var e = window.event;
    if(!e) return false;
    if (e.target) targ = e.target;
    else if (e.srcElement) targ = e.srcElement;
    if (targ.nodeType == 3) // defeat Safari bug
      targ = targ.parentNode;

    timer = window.setTimeout(function() {
      document.getElementById('treePrompt').style.display='none';
      document.getElementById('treeLabel').innerHTML = acme.getControl('Time').getValues()[0].display;
    },acme.hideTimer);
    return true;
    };
})();

Notice that it’s not actually doing any validation; it’s simply returning true. Notice that it’s also sending the label of the selected node to the tree label. Ultimately the new Prompt API makes it significantly easier to build complex reports.

Example XML:

<report xmlns="http://developer.cognos.com/schemas/report/9.0/" useStyleVersion="10" expressionLocale="en-us">
				<modelPath>/content/folder[@name='Samples']/folder[@name='Cubes']/package[@name='Sales and Marketing (cube)']/model[@name='2008-07-25T15:28:38.072Z']</modelPath>
				<drillBehavior modelBasedDrillThru="true"/>
				<layouts>
					<layout>
						<reportPages>
							<page name="Page1">
								<style>
									<defaultStyles>
										<defaultStyle refStyle="pg"/>
									</defaultStyles>
								</style>
								<pageBody>
									<style>
										<defaultStyles>
											<defaultStyle refStyle="pb"/>
										</defaultStyles>
									</style>
									<contents><textBox parameter="Parameter1" name="textBox" multiSelect="true"/><HTMLItem description="Label">
			<dataSource>
				<staticValue>&lt;span id="treeLabel" onclick="acme.toggleTree()"&gt;Tree Prompt&lt;/span&gt;
&lt;span id="treePrompt" style="display:none"&gt;</staticValue>
			</dataSource>
		</HTMLItem><selectWithTree parameter="Time" refQuery="Time" name="Time"><selectWithTreeItem refDataItem="Time"/></selectWithTree><HTMLItem description="scripts">
			<dataSource>
				<staticValue>&lt;/span&gt;

&lt;script&gt;

/*
 * Fictious Namespace and getter. Don't change these unless IBM says so or you know what you're doing.
 * acme is now the official standard fictious namespace (thanks Rick), so make sure all functions sit there.
 * This is based on the samples, so I'm giving all credit to Rick Blackwell of IBM.
 */
var acme = {};
acme.getControl = function(promptName)
{
  var ocr = cognos.Report.getReport("_THIS_");
  return ocr.prompt.getControlByName(promptName);
};

/*
 * Global Params
 * Paul Mendelson 2012-28-10
 * These control the behaviour of the functions below.
 */

//Time in miliseconds to wait before prompt disappears.
acme.hideTimer = 1000;

/*
 *  This will return any available methods or functions or whatever available on the referenced object.
 */
function getMethods(myObject)
{
  var funcs=[]
  for(var name in myObject)
  {
    funcs.push(name)
  }
  return  funcs.join(', ');
}

/*
 * Hide the prompt when validating
 */

/*
 * function hideTree. Paul Mendelson - 2012-10-22
 * Whenever a tree node is clicked, it will wait {acme.hideTimer} seconds before hiding
 * the tree. If another selection is made, it will restart the counter.
 */
acme.hideTree= (function () {
  var timer;
  var elm;
  return function (){
    clearTimeout(timer);
  var promptName = this.getName();

    //standard way of determing source of click.
    var targ;
    if (!e) var e = window.event;
    if(!e) return false;
    if (e.target) targ = e.target;
    else if (e.srcElement) targ = e.srcElement;
    if (targ.nodeType == 3) // defeat Safari bug
      targ = targ.parentNode;

    timer = window.setTimeout(function() {
      document.getElementById('treePrompt').style.display='none';
      document.getElementById('treeLabel').innerHTML = acme.getControl('Time').getValues()[0].display;
    },acme.hideTimer);
    return true;
    };
})();

acme.toggleTree = function()
{
  document.getElementById('treeprompt').style.display=document.getElementById('treeprompt').style.display=='none'?'block':'none'
}

/*acme.checkPostalCode = function()
{
  var rePostalCodeFormat = new RegExp( "[a-z][0-9][a-z] [0-9][a-z][0-9]", "gi" );
  if(rePostalCodeFormat.test(this.getValue())) return true;
  return false;
}
*/
acme.checkPostalCode = function()
{
  var rePostalCodeFormat = new RegExp( '^[0-9]{0,3}$', "gi" );
  if(rePostalCodeFormat.test(this.getValue())) return true;
  return false;
}
acme.getControl('textBox').setValidator(acme.checkPostalCode);
acme.getControl('Time').setValidator(acme.hideTree);
&lt;/script&gt;

</staticValue>
			</dataSource>
		</HTMLItem></contents>
								</pageBody>
							</page>
						</reportPages>
					</layout>
				</layouts>
			<XMLAttributes><XMLAttribute name="RS_CreateExtendedDataItems" value="false" output="no"/><XMLAttribute name="listSeparator" value="," output="no"/><XMLAttribute name="RS_modelModificationTime" value="2008-07-25T15:28:38.133Z" output="no"/></XMLAttributes><queries><query name="Time"><source><model/></source><selection><dataItem name="Time" aggregate="none"><expression>rootMembers([sales_and_marketing].[Time].[Time])</expression></dataItem></selection></query></queries><reportName>Paul - Tree prompt</reportName></report>

Cognos 10.1.1 bug: Charts breaking when switching between tabs

An annoying bug has been reported by several of my clients.

Any portal tab containing a chart will occasionally break when the user clicks away then returns:

If the user were to click on Public Folders, then return to the portal tab, suddenly the chart images are broken.

While I can’t explain why this is happening, I’m guessing that Cognos is flushing the image from the cache when the user leaves the page. When the user returns the image is no longer in the cache, so it’s returns a broken image.

Fortunately the refreshing the report will return the chart so this gives us a partial solution. If we can write a script that detects the state of the image (if it’s broken or not) we can have the portlet refresh.

First the report should be wrapped in a div with the display set to none. This will prevent users from seeing the broken charts. Second the chart itself should be wrapped in a div to give the function a place to start looking for broken images.

The function itself is fairly simple:

function checkImg()
{
var reportWrapper = document.getElementById('reportWrapper');
var chartWrapper = document.getElementById('chartWrapper');
var message = document.getElementById('message');
var img = chartWrapper.getElementsByTagName('IMG');
message.innerHTML = "checking image";
if (img[0].readyState == 'uninitialized') {refreshReport();message.innerHTML = "refreshing report";}
else {reportWrapper.style.display="";message.innerHTML = "";}

}

It checks the image’s readyState attribute. If it’s unitialized, the image is broken and the report should be refreshed, otherwise everything is fine and show the report.

The refresh function is from one of the IBM knowledge base articles.

function refreshReport()
{
var intval;
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;
if(intval!="")
{

    self.clearInterval(intval);
    intval="";

}
self["RunReportInterval"] = self.setInterval( nameSpace + ".getRV().RunReport()",'0' );
intval = self["RunReportInterval"];
}

Finally the checkImg function needs to be called. It can’t be called as soon as the page is loaded, because the chart image itself takes a moment to load. If the function is called before the chart is loaded, it will refresh the page in an infinite loop. Instead it’s best to wait a moment. I find that 1 second works well.

setTimeout("checkImg()",1000);

Now when the page loads it will wait 1 second and check the chart. If the image is broken it will refresh the page:

It’s worth mentioning that this is a band-aid solution (and not a great one either). The true fix needs to come from IBM. There is an APAR open for this issue, but so far no hotfix. Slower computers may take longer to load the image, and may result in the user being stuck in an infinite loop. Additionally the report will only display after the function has a chance to check the image, effectively increasing the load time of the report by however long is set in the setTimeout.

Report XML: (Remember to create a page and add this to a cognos viewer)

<report xmlns="http://developer.cognos.com/schemas/report/8.0/" useStyleVersion="10" expressionLocale="en-us">
				<modelPath>/content/folder[@name='Samples']/folder[@name='Models']/package[@name='GO Data Warehouse (query)']/model[@name='model']</modelPath>
				<drillBehavior modelBasedDrillThru="true"/>
				<queries>
					<query name="Query1">
						<source>
							<model/>
						</source>
						<selection><dataItem name="Gross margin" aggregate="automatic"><expression>[Sales (query)].[Gross margin]</expression><XMLAttributes><XMLAttribute name="RS_dataUsage" value="fact" output="no"/></XMLAttributes></dataItem><dataItem name="Product line" aggregate="none" rollupAggregate="none"><expression>[Sales (query)].[Products].[Product line]</expression><XMLAttributes><XMLAttribute name="RS_dataType" value="3" output="no"/><XMLAttribute name="RS_dataUsage" value="attribute" output="no"/></XMLAttributes></dataItem></selection>
					</query>
				</queries>
				<layouts>
					<layout>
						<reportPages>
							<page name="Page1"><style><defaultStyles><defaultStyle refStyle="pg"/></defaultStyles></style>
								<pageBody><style><defaultStyles><defaultStyle refStyle="pb"/></defaultStyles></style>
									<contents>
										<HTMLItem description="report Display None">
			<dataSource>
				<staticValue>&lt;div id="reportWrapper" style="display:none"&gt;</staticValue>
			</dataSource>
		</HTMLItem><HTMLItem description="Chart Wrapper">
			<dataSource>
				<staticValue>&lt;div id="chartWrapper"&gt;</staticValue>
			</dataSource>
		</HTMLItem><combinationChart showTooltips="true" maxHotspots="10000" refQuery="Query1" name="Combination Chart1">
								<legend>
									<legendPosition>
										<relativePosition/>
									</legendPosition>
									<legendTitle refQuery="Query1">
										<style>
											<defaultStyles>
												<defaultStyle refStyle="lx"/>
											</defaultStyles>
										</style>
									</legendTitle>
									<style>
										<defaultStyles>
											<defaultStyle refStyle="lg"/>
										</defaultStyles>
									</style>
								</legend>
								<ordinalAxis>
									<axisTitle refQuery="Query1">
										<style>
											<defaultStyles>
												<defaultStyle refStyle="at"/>
											</defaultStyles>
										</style>
									</axisTitle>
									<axisLine color="black"/>
									<style>
										<defaultStyles>
											<defaultStyle refStyle="al"/>
										</defaultStyles>
									</style>
								</ordinalAxis>
								<numericalAxisY1>
									<axisTitle refQuery="Query1">
										<style>
											<defaultStyles>
												<defaultStyle refStyle="at"/>
											</defaultStyles>
										</style>
									</axisTitle>
									<gridlines color="#cccccc"/>
									<axisLine color="black"/>
									<style>
										<defaultStyles>
											<defaultStyle refStyle="al"/>
										</defaultStyles>
									</style>
								</numericalAxisY1>
								<combinationChartTypes>
									<bar/>
								</combinationChartTypes>
								<style>
									<defaultStyles>
										<defaultStyle refStyle="ch"/>
									</defaultStyles>
								</style>
							<commonClusters><chartNodes><chartNode><chartNodeMembers><chartNodeMember refDataItem="Product line"><chartContents><chartTextItem><dataSource><memberCaption/></dataSource></chartTextItem></chartContents></chartNodeMember></chartNodeMembers></chartNode></chartNodes></commonClusters><defaultChartMeasure refDataItem="Gross margin"/></combinationChart>
									<HTMLItem description="closing divs and script">
			<dataSource>
				<staticValue>&lt;/div&gt;
&lt;/div&gt;
&lt;div id="message"&gt;
&lt;/div&gt;
&lt;script&gt;

function checkImg()
{
var reportWrapper = document.getElementById('reportWrapper');
var chartWrapper = document.getElementById('chartWrapper');
var message = document.getElementById('message');
var img = chartWrapper.getElementsByTagName('IMG');
message.innerHTML = "checking image";
if (img[0].readyState == 'uninitialized') {refreshReport();message.innerHTML = "refreshing report";}
else {reportWrapper.style.display="";message.innerHTML = "";}

}

function refreshReport()
{
var intval;
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;
if(intval!="")
{

    self.clearInterval(intval);
    intval="";

}
self["RunReportInterval"] = self.setInterval( nameSpace + ".getRV().RunReport()",'0' );
intval = self["RunReportInterval"];
}

setTimeout("checkImg()",1000);
&lt;/script&gt;
</staticValue>
			</dataSource>
		</HTMLItem></contents>
								</pageBody>
								<pageHeader>
									<contents>
										<block><style><defaultStyles><defaultStyle refStyle="ta"/></defaultStyles></style>
											<contents>
												<textItem><style><defaultStyles><defaultStyle refStyle="tt"/></defaultStyles></style>
													<dataSource>
														<staticValue>This works</staticValue>
													</dataSource>
												</textItem>
											</contents>
										</block>
									</contents>
									<style>
										<defaultStyles>
											<defaultStyle refStyle="ph"/>
										</defaultStyles>
										<CSS value="padding-bottom:10px"/>
									</style>
								</pageHeader>
								<pageFooter>
									<contents>
										<table>
											<tableRows>
												<tableRow>
													<tableCells>
														<tableCell>
															<contents>
																<date>
																	<style>
																		<dataFormat>
																			<dateFormat/>
																		</dataFormat>
																	</style>
																</date>
															</contents>
															<style>
																<CSS value="vertical-align:top;text-align:left;width:25%"/>
															</style>
														</tableCell>
														<tableCell>
															<contents>
																<pageNumber/>
															</contents>
															<style>
																<CSS value="vertical-align:top;text-align:center;width:50%"/>
															</style>
														</tableCell>
														<tableCell>
															<contents>
																<time>
																	<style>
																		<dataFormat>
																			<timeFormat/>
																		</dataFormat>
																	</style>
																</time>
															</contents>
															<style>
																<CSS value="vertical-align:top;text-align:right;width:25%"/>
															</style>
														</tableCell>
													</tableCells>
												</tableRow>
											</tableRows>
											<style>
												<defaultStyles><defaultStyle refStyle="tb"/></defaultStyles>
												<CSS value="border-collapse:collapse;width:100%"/>
											</style>
										</table>
									</contents>
									<style>
										<defaultStyles>
											<defaultStyle refStyle="pf"/>
										</defaultStyles>
										<CSS value="padding-top:10px"/>
									</style>
								</pageFooter>
							</page>
						</reportPages>
					</layout>
				</layouts>
			<XMLAttributes><XMLAttribute name="RS_CreateExtendedDataItems" value="true" output="no"/><XMLAttribute name="listSeparator" value="," output="no"/><XMLAttribute name="RS_modelModificationTime" value="2011-06-09T13:50:33.233Z" output="no"/></XMLAttributes><reportName>Test 1</reportName></report>

Quickie – Dynamic select and search size

Generally the select box of the Select and Search prompt is wide enough to display all the information you need. But there are times where you need it to show everything.

Is the first option there Chaun Yang Sport Equipment Company? Org? IBM does have a way to fix this issue, but it involves modifying one of the internals, specifically the webcontentpromptingproperties.js file. In several of my clients, modifying any internal files is expressly forbidden. So I’ve written a quick and somewhat dirty javascript to handle it.

Drag an HTML item to the left of the prompt:

<div id="selectSearch">

Drag an HTML item to the right of the prompt:

</div>
<script>
  var e=document.getElementById('selectSearch');
  var myselect = e.getElementsByTagName('select')[0];
  if(myselect.childNodes.length>0){myselect.style.width=''}
</script>

This code will find the selectSearch div you wrapped the prompt with, find all of the select tags, and if there are any options the select will expand or shrink to fit.

And finally, as usual (when I remember) the report XML:

<report xmlns="http://developer.cognos.com/schemas/report/8.0/" useStyleVersion="10" expressionLocale="en-us">
				<modelPath>/content/folder[@name='Samples']/folder[@name='Models']/package[@name='GO Data Warehouse (query)']/model[@name='model']</modelPath>
				<drillBehavior modelBasedDrillThru="true"/>
				<layouts>
					<layout>
						<reportPages>
							<page name="Page1">
								<style>
									<defaultStyles>
										<defaultStyle refStyle="pg"/>
									</defaultStyles>
								</style>
								<pageBody>
									<style>
										<defaultStyles>
											<defaultStyle refStyle="pb"/>
										</defaultStyles>
									</style>
									<contents><HTMLItem>
			<dataSource>
				<staticValue>&lt;div id="selectSearch"&gt;</staticValue>
			</dataSource>
		</HTMLItem><selectWithSearch parameter="Parameter1" refQuery="Query1"><useItem refDataItem="Retailer"/></selectWithSearch><HTMLItem>
			<dataSource>
				<staticValue>&lt;/div&gt;
&lt;script&gt;
  var e=document.getElementById('selectSearch');
  var myselect = e.getElementsByTagName('select')[0];
  if(myselect.childNodes.length&gt;0){myselect.style.width=''}
&lt;/script&gt;</staticValue>
			</dataSource>
		</HTMLItem></contents>
								</pageBody>
							</page>
						</reportPages>
					</layout>
				</layouts>
			<XMLAttributes><XMLAttribute name="RS_CreateExtendedDataItems" value="false" output="no"/><XMLAttribute name="listSeparator" value="," output="no"/><XMLAttribute name="RS_modelModificationTime" value="2011-06-09T13:50:33.233Z" output="no"/></XMLAttributes><queries><query name="Query1"><source><model/></source><selection><dataItem name="Retailer" aggregate="none"><expression>[Sales (query)].[Retailers].[Retailer]</expression></dataItem></selection></query></queries></report>

The XML is for Cognos 10, but this code was tested successfully on all the flavors of Cognos 8.4.