Wednesday, October 8, 2008
Calendar Resource Management with the Google Data API
Matt PrudenIn many enterprises, there is no piece of real estate more scarce than an unoccupied conference room. With so much importance placed on conference rooms, their rigorous management is critical to a successful Google Apps deployment.
While Google Calendar offers a flexible system for reserving conference rooms, projectors, scooters, or any other shared resource, it does not provide a documented API for creating, updating, and deleting resources. Instead, you must manually manage resources through the Google Apps control panel. Manual management may work for a small number of resources but becomes unscalable when managing thousands.
However, creative developers can find just such a Google Data (GData) API for provisioning resources. In this post, we'll explore how to create, read, update, and delete calendar resources using GData through cURL, the commonly available command line HTTP client.
Discovering Calendar Resource support in GData.
Each type of entry in Google, whether a spreadsheet row, user account, or nickname has a collection URL. In true REST fashion, a GET request to the collection URL will return a list of entries. For example, an GET request to http://www.google.com/calendar/feeds/default/private/full will return a feed of calendar event entries. Likewise, a POST to this URL will add a new event entry to a calendar. So, to retrieve and create resources, we first need to discover the collection URL for calendar resources.
A calendar resource has many of the same characteristics as a user. For example, a calendar resource can be a meeting attendee and can be browsed by clicking "check guest and resource availability" in the Calendar user interface. Also, a calendar resource isn't tied to a particular user when it is created. It is reasonable to believe that managing calendar resources through the API might closely mimic managing users through the provisioning API.
In the provisioning API, the collection URL for user accounts looks like this: https://apps-apis.google.com/a/feeds/domain/user/2.0. What if we change user to resource resulting in a URL like this: https://apps-apis.google.com/a/feeds/domain/resource/2.0? The example below uses the cURL application to send a GET request to the new URL. For details on using cURL with GData, see Google's documentation.
curl -s -k --header "Authorization: GoogleLogin auth=DQAAAH4AA" https://apps-apis.google.com/a/feeds/mydomain.com/resource/2.0 | tidy -xml -indent -quiet
<?xml version="1.0" encoding="utf-8"?> <feed xmlns="http://www.w3.org/2005/Atom" xmlns:openSearch="http://a9.com/-/spec/opensearchrss/1.0/" xmlns:gCal="http://schemas.google.com/gCal/2005" xmlns:apps="http://schemas.google.com/apps/2006" xmlns:gd="http://schemas.google.com/g/2005"> <id>https://apps-apis.google.com/a/feeds/mydomain.com/resource/2.0</id> <updated>1970-01-01T00:00:00.000Z</updated> <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/apps/2006#resource"/> <link rel="http://schemas.google.com/g/2005#feed" type="application/atom+xml" href="https://apps-apis.google.com/a/feeds/mydomain.com/resource/2.0"/> <link rel="http://schemas.google.com/g/2005#post" type="application/atom+xml" href="https://apps-apis.google.com/a/feeds/mydomain.com/resource/2.0"/> <link rel="self" type="application/atom+xml" href="https://apps-apis.google.com/a/feeds/mydomain.com/resource/2.0"/> <openSearch:startIndex>1</openSearch:startIndex> <entry> <id>https://apps-apis.google.com/a/feeds/mydomain.com/resource/2.0/-81411918824</id> <updated>1970-01-01T00:00:00.000Z</updated> <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/apps/2006#resource"/> <title type="text">Bldg 3, room 201</title> <link rel="self" type="application/atom+xml" href="https://apps-apis.google.com/a/feeds/mydomain.com/resource/2.0/-81411918824"/> <link rel="edit" type="application/atom+xml" href="https://apps-apis.google.com/a/feeds/mydomain.com/resource/2.0/-81411918824"/> <gd:who valueString="Bldg 3, room 201" email="mydomain.com_2d3831343131393138383234@resource.calendar.google.com"> <gCal:resource id="-81411918824"/> </gd:who> </entry> </feed>
We've found the collection URL for calendar resources! Now, we just need to determine the XML schema for an individual resource. A hour of trial and error results in the following schema:
<?xml version='1.0' encoding='UTF-8'?> <ns0:entry xmlns:ns0="http://www.w3.org/2005/Atom"> <ns0:category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/apps/2006#resource" /> <ns1:who valueString="long name" xmlns:ns1="http://schemas.google.com/g/2005"> <ns2:resource id="short name" xmlns:ns2="http://schemas.google.com/gCal/2005" /> </ns1:who> </ns0:entry>
Since Google already does a great job of explaining the GData API, this post will not repeat that information. Instead, you can use the collection URL and entry schema in the same fashion as the other GData APIs to create, read, update, and delete calendar resources.
Labels: Google Apps, MattPruden
Tuesday, October 7, 2008
Pricebook, or Product in Customer Portal you have most likely been met
with the dreaded "Insufficient Permissions" screen. Customer Portal hides
these standard objects for obvious reasons (you don't necessarily want external users to access your organization's most proprietary data), however, there are times when allowing read-access to these objects would facilitate certain operations. For example, it would be great to expose your product catalog (i.e. Product, Pricebook, and PricebookEntry) to your customers. How can read-access be achieved given these limitations? The workaround described below uses what I will call "proxy classes" that can stand in for these blocked standard objects.
The first step to using a proxy class is to create a custom object through Salesforce's administrative control panel. This is your opportunity to create an object that contains the fields you want from the standard object plus any additional fields that might be handy such as a formula field concatenating different values or even fields from other classes that you can get to via an object-to-object relationship. The idea here is to create an object that mimics (closely or completely) the standard object that you are otherwise not able to see in Customer Portal. When creating the proxy object the key is to establish a connection to the blocked standard object. This is done by creating a Lookup field on the proxy object that points to the ID field of the standard object. By creating this Lookup, you have now created a foreign key into the standard object. Now you can access other fields in the standard object by leveraging this relationship. In Apex, you can code RelationshipName__r.OtherField to gain access to the other fields...the Lookup you created is the gateway into the object. Remember to enable permissions on the object for Customer Portal users!
Now that your proxy class is created and it mimics the standard object you need to pump some data into it. For an initial data load, use the Apex Data Loader to 1) export data from the standard object into a CSV 2) manipulate the resulting CSV as necessary and 3) map the exported CSV back into an import for your proxy class. An alternative method would be to write an Apex class that loops through the standard object and inserts the data into the proxy class. Use whatever data loading technique you feel most comfortable with.
Armed with a data-populated proxy class you are now ready to expose this data to Customer Portal. You can use this proxy class in place of your standard object in all of your VisualForce pages, tabs, related lists, etc. You are simply using this proxy class that has permissions in Customer Portal in place of blocked standard object. The data is the same (or even customized depending on how you structured the proxy) but now you can see and work with it.
Finally, you will want to keep your proxy object populated with fresh, current data from the standard object. This can easily be done by adding a trigger to the standard object that updates the proxy. Keep in mind that triggers are not allowed on certain classes (for example, Pricebook and PricebookEntry). A creative workaround is to use a batch update as described here.
Thursday, October 2, 2008
Google Earth Integration via Visualforce
The VisualForce "contentType" page attribute makes it easy to push data from Salesforce directly to other apps. Here, we'll review an example using Google Earth. We use KML to view Salesforce Opportunities on a 3D map. Let's start with the page itself:
<apex:page controller="KMLController" cache="true" showHeader="false" contentType="application/vnd.google-earth.kml+xml">Note the following:
<kml xmlns="http://earth.google.com/kml/2.0">
<Document>
<name>Salesforce Opportunities</name>
<apex:repeat value="{!oppList}" var="o">
<Placemark>
<name>{!o.Name}</name>
<address>{!o.Account.BillingStreet} {!o.Account.BillingCity}, {!o.Account.BillingState} {!o.Account.BillingPostalCode}</address>
<description>
<![CDATA[
<p><b>Account: </b>{!o.Account.Name}
<p><b>Amount: </b>${!o.Amount}
<p><b>Close Date: </b>{!MONTH(o.CloseDate)}/{!DAY(o.CloseDate)}/{!YEAR(o.CloseDate)}
]]>
</description>
</Placemark>
</apex:repeat>
</Document>
</kml>
</apex:page>
-
The contentType="application/vnd.google-earth.kml+xml" attribute notifies the browser that the page content should be passed to Google Earth.
-
The cache="true" attribute addresses this IE security issue.
-
The meat of the page is in an <apex:repeat> block that iterates over a list of Opportunities. In this example, we're mapping the opportunity address, but you could use the Geocoding API to specify a Point with specific longitude and latitude coordinates
The page controller retrieves a List of Opportunity objects based on a comma-delimited URL parameter:
public class KMLController {
public Opportunity[] oppList {get; set;}
public KMLController() {
String sel = '';
if (null != ApexPages.currentPage().getParameters().get('sel')) {
sel = ApexPages.currentPage().getParameters().get('sel');
}
String[] idList = sel.split(',', 0);
oppList = [SELECT Id, Name, Amount, CloseDate,
Account.Name, Account.BillingStreet, Account.BillingCity,
Account.BillingState, Account.BillingPostalCode
FROM Opportunity
WHERE id IN :idList];
}
}
Finally, an Opportunity custom button is used to invoke the VisualForce page, passing a list of selected Opportunity Id's from a List View or Related List:
var sel = {!GETRECORDIDS( $ObjectType.Opportunity)};
if (!sel.length) {
alert("Please select at least one opportunity for mapping.");
} else {
var d = new Date(); // Append milliseconds to URL to avoid browser caching
url= "/apex/KMLPush?ms=" + d.getTime() + "&sel=" + {!GETRECORDIDS( $ObjectType.Opportunity)};
window.location.href=url;
}
When the button is clicked, the selected Opportunities will be displayed (via KML) in Google Earth.
If the KML file doesn't open properly, you might need to manually add the following Windows registry entries:
[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\MIME\Database\Content Type]
[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\MIME\Database\Content Type\application/vnd.google-earth.kml+xml]
"Extension"=".kml"
[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\MIME\Database\Content Type\application/vnd.google-earth.kmz]
"Extension"=".kmz"
Labels: DanGuggenheim
Tuesday, September 23, 2008
Page breaks in Visualforce PDF templates
Kyle
Roche
The Visualforce page component defines a renderAs attribute that supports certain content converters. This is extremely useful when automatically printing forms, receipts, reports, etc. Often, we're asked to create nicely formatted forms that span multiple pages. If you leave the control of page breaks to the browser unexpected things can happen. This is an easy solution to solve with some basic CSS. You can use the page-break style properties to control where the browser will insert a page break. The Force.com PDF content converter will carry that over to the PDF.
Here's the basic code to demonstrate how this works. Create a new Visualforce Page called MultiPagePDF. Add the following code to the page:
<apex:page renderas="pdf">
<div style="page-break-after:always;">
This is page one
</div>
<div style="page-break-after:always;">
This should be rendered as page two
</div>
And, finally... page three
</apex:page>
This should yield something like this illustration when rendered. Some natural extensions to this posting would be to dynamically insert these into the page. Inserting <div> tags and binding the style to an APEX property could be one way to accomplish this. You would pass back a blank style in some cases and return a string with the value of "page-break-after:always" for <div> sections where a break is needed.
As a quick side note, you can get a bit more advanced with the Page formatting via CSS. The following snippet shows you have to switch the page layout to landscape and add page numbers to your Visualforce page. This was found in the Case History Timeline example.
@page {
/* Landscape orientation */
size:landscape;
/* Put page numbers in the top right corner of each
page in the pdf document. */
@top-right {
content: "Page " counter(page);
}
}Labels: KyleRoche, Visualforce
Wednesday, September 17, 2008
Access Custom Fields on Standard Objects in Force.com IDE
I recently created a new Force.com Project in Eclipse, but was surprised to find that there was not an option to include Standard Objects in the project manifest. Upon first glance it appears that only Custom Objects can be included.
This force.com thread by JonP discusses a workaround summarized here.
First, it is worthwhile to note that you cannot include standard fields on standard objects. However, you can include CUSTOM fields on standard objects. From your existing project in Eclipse, open the package.xml file located at src->unpackaged->package.xml Locate the Custom Object section and add a new member for each standard object you would like included in your project.
This method will include all custom fields on the standard object in your project.
For example, I was able to use the following snippet to include all custom fields on the Account, Contact, and Case objects, as well as all fields from all custom objects. Simply add the code to the package.xml file and then refresh the project from the server.
<types>
<members>*</members>
<members>Account</members>
<members>Case</members>
<members>Contact</members>
<name>CustomObject</name>
</types>
Care should be taken when pushing to production, however as including a standard object will also cause field-level security settings to be pushed to profiles in the salesforce.com production environment. Should your production field-level security settings be out of sync with your sandbox, you could run into some issues.
Utilizing this method can prove invaluable if you are needing to migrate a large number of custom fields and do not want to recreate these manually in production.
Labels: KyleFreeman
Monday, September 15, 2008
Complementing Visualforce - Girafa Thumbnail Service
Kyle
Roche
There are so many technologies out there to complement your Salesforce.com projects. In a previous posting I showed you how to leverage Google's Geocoding API from within your APEX classes. In this example, let's take a look at something more useful in the UI Layer.
It's pretty typical in projects with heavy Visualforce development to create a custom search form. Recently, I was working on a project where we created a custom search form for the Account object. Instead of showing the Account's logo image, we were thinking it would be a cool enhancement to display a thumbnail to the Account's website. You could use services like Amazon's Alexa Site Thumbnail Service (as shown in the APEX Language Reference). However, most of these aren't free. After some Googling, I stumbled on the Girafa Thumbnail Service, which offers a free service to those who use less than 2000 images requests per day (200 request per day at the time of this writing. Please check girafa for updated daily limits).
Let's get started. Create a Visualforce page called GirafaThumbnail and a controller called GirafaThumbnailController. In your controller add the following APEX Property.
public string GirafaUrl
{
get
{
if (GirafaUrl == null)
{
string algorithmName = 'MD5';
string signatureKey = 'your signature key here';
string clientId = 'your client id here';
string siteUrl = 'http://www.appirio.com/';
string inputString = signatureKey + siteUrl;
Blob myDigest = Crypto.generateDigest(algorithmName, Blob.valueOf(inputString));
string myDigestString = EncodingUtil.ConvertToHex(myDigest);
integer myStartingPosition = myDigestString.length() - 16;
integer myEndingPosition = myDigestString.length();
string mySubString = myDigestString.substring(myStartingPosition, myEndingPosition);
string myUrl = 'http://scst.srv.girafa.com/srv/i?i=' + clientId + '&s=' + mySubString + '&r=' + siteUrl;
return myUrl;
}
return GirafaUrl;
}
set;
}
Let's break this down by first understanding how girafa thumbnail service works. There are a few basic steps to generating the URL for your thumbnail. Girafa's thumbnail images are embedded using the <img> tag. The call is formatted as follows:
http://scst.srv.girafa.com/srv/i?i=<client ID>&s=<signature>&r=<site URL>
client ID is your Girafa Client ID. It is supplied when you register your account. The site URL is the URL of the website for which you would like to generate a Thumbnail. In our example http://www.appirio.com
The signature authenticates your request and is generates using the following steps:
1) Concatenate your secrete key (chosen when you create your account) and the Site URL
2) Calculate the MD5 Checksum of the concatenated string.
3) The signature is the 16 least significant hexadecimal digits of the MD5 checksum.
In our method we set the variables we'll need to generate the Image call then we use the Crypto class' generateDigest() method to calculate the MD5 checksum of our concatenated string. Some basic string manipulation returns the 16 character substring and we put our URL together to return to our caller.
In your Visualforce page add an image tag to see the results. Bind the src attribute of the <img> to our APEX Property. <img src="{!GirafaUrl}"></img>. You should see something along the lines of the following illustration.
Labels: Girafa, KyleRoche, Visualforce
Sunday, September 14, 2008
Escaping Quotes in Merge Fields
S-controls, button on-click Javascript, and Visualforce pages can contain merge fields, which SFDC replaces with their record values prior to rendering. But this is a straight replacement, without any opportunity to escape characters. This leads to messy results when a merge field contains a double-quote character.
For example, consider using this merge field in on-click Javascript:
project.Name = "{!Opportunity.Name}";
That would work great, but consider what happens if the opportunity is named Acme "World Peace" Project (with the double quotes). SFDC will render the code as:
project.Name = "Acme "World Peace" Project";
Okay, now you have a huge mess. This will break the Javascript. A very clever workaround for this was created by an Appirio colleague of mine, Linda Evans. Here it is:
Step 1: Wrap the value in a hidden textarea.
Step 2: Retrieve the value by traversing the DOM.
So in our example above, first, you'd drop this HTML element into your page:
<textarea id="opportunityName" style="display:none">{!Opportunity.Name}</textarea>
Then, inside a <script> element, you'd retrieve the value:
project.Name = document.getElementById("opportunityName").value;
I told you this was clever - thanks Linda!
One final note, this won't work (directly) in a button on-click Javascript, since you can't put HTML elements in them (the entire body is considered a <script>). So you'll have to move your code into an s-control, and then tie your button instead to the s-control.
Labels: GlennWeinstein, LindaEvans



