Plone & Salesforce.com: Using Salesforce.com as a Content Catalog
For those craving a bit more of a hands on update than my earlier background post on making Plone play well with Salesforce.com provided, hopefully this takes a step in the right direction. I'm going to be demoing the display of Salesforce.com content within the portlet of a Plone site. Credit to Jesse for helping me realize how dirt simple this is. Writing the code to do this took less than 10 minutes and that included asking a colleague a few questions about how exactly Salesforce.com works :)
The Use Case
Recently, the Executive Director of ONE/Northwest, Gideon, sent an email to staff emphasizing the use of quotes as a way to increase our credibility by allowing partners to tell our story in new potential partner circles. Being a nonprofit, think of a trusted grants manager quoted in a proposal to a new foundation or a donor quoted in our annual report. This also includes a quote about a completed web project appearing in our technology solutions section or a scope of work.
Even though we've had a Quote content type in our oft-internally-used, but not widely publicized Pigeonhole product, I thought I'd try an alternate implementation, which I see as very much complimentary, rather than competitive. For us, it makes sense to have our quotes coupled with Salesforce.com contacts. In other cases the ease and speed of doing Plone catalog lookups for related Quotes based on keywords is superior, especially as implemented in Pigeonhole as a lightweight content relation engine.
So, my colleague Steve had done some behind the scenes Salesforce.com fiddling allowing for the addition of a quote that's tied to a contact. I just added a new quote to my own contact record. See image below:

But, how would we display this and other quotes in a portlet on a local copy of onenw.org...
Beatbox Setup
If you're reading this post, I'm assuming you've got a working Plone 2.5.x instance lying around, which means you've got Python 2.4 as well. You may even have EasyInstall. If not, start here. Once you've met the basic requirements, you can install beatbox from the CheeseShop with:
easy_install-2.4 beatboxJust to prove this is working, fire up the interactive Python used by your Plone instance and type:
>>> import beatbox
Salesforce Base Connector Setup
Next you need to get salesforcebaseconnector
from the Plone Collective (hopefully we'll have a 1.0 alpha 1 release in the Plone Software Center
soon). Execute the following from your Zope instance's Products folder:
svn co https://svn.plone.org/svn/collective/salesforcebaseconnector/trunk/ ./salesforcebaseconnector
Upon restarting Zope, head to the ZMI and the Plone site where you'd like to talk to the Salesforce.com API.
Add a Salesforce Base Connector:

Select the radio button and hit "Add"


Enter information for a valid Salesforce.com account. This could be a free developer account.

The Code
I chose to do this in a Zope 3 view within the DIYPloneStyle-generated product, ONENWSkin, we use at onenw.org, but as mentioned in the previous post, an explicit goal for salesforcebaseconnector was to enable integrators to easily do the same in ZPT or a Python skin script that's not necessarily filesystem-based.
After the obligatory ZCML wiring, I created a file, browser.py, at the root of my product. The following import statements and the
"RandomQuotePortletView" class were all that was necessary:
from Products import Five
from Products.CMFCore import utils as cmf_utils
import random
class RandomQuotePortletView(Five.BrowserView):
"""Find recent job listings for portlet display
"""
def __init__(self, context, request):
Five.BrowserView.__init__(self, context, request)
self.sfbase = cmf_utils.getToolByName(self.context, 'portal_salesforcebaseconnector')
def getRandomQuote(self):
"""Returns a random quote from Salesforce.com in a dictionary
"""
res = self.sfbase.query(['Name','Quote_Text__c'],'Quote__c',"Usage_Guidelines__c!='Need to get permission'")
# res looks like a dictionary with a 'records' key
return random.choice(res['records'])
Of note, is the "query" method on our "portal_salesforcebaseconnector" object created at the site root in the setup above. We pass the query method a list of which fields ('Name' and 'Quote_Text__c' -- The '__c' convention signifies a custom field/object) we'd like to retrieve. The second parameter is our desired SObject, the custom Quote object, and finally we have an additional where clause which is based on our internal business rules of never showing any quotes where we don't yet have permission.
That's it! It's basically 2 lines of code (that could be collapsed into 1) within the contents of the "getRandomQuote" method and we're able to retrieve any and everything we might ever want from our Salesforce.com account.
Finally, we setup the following portlet, which defines the view, calls "getRandomQuote" on the view, and then displays the quote text for our randomly collected quote:
<html xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal"
i18n:domain="plone">
<body>
<div metal:define-macro="portlet"
tal:define="view context/@@random_quote_portlet;
qDict view/getRandomQuote">
<dl class="portlet" id="portlet-quote">
<dt class="portletHeader">
<span class="portletTopLeft"></span>
Random Quote
<span class="portletTopRight"></span>
</dt>
<dd class="portletItem odd">
<span tal:replace="qDict/Quote_Text__c" />
</dd>
<dd class="portletFooter">
<a href=""
tal:attributes="href string:$portal_url/all-quotes"
>
More quotes…
</a>
<span class="portletBottomLeft"></span>
<span class="portletBottomRight"></span>
</dd>
</dl>
</div>
</body>
</html>
And after a few page reloads, to bypass the other quotes in our Salesforce.com instance, we see the following:

Conclusion
While I only demoed the use of the "query" call to the Salesforce.com API, you can skim the methods of the SalesforceBaseConnector class or read up on the API via Salesforce.com's APEX developer wiki for a better picture of what's possible. Note: Beatbox currently talks to the 7.0 version of the Salesforce.com API (several major version behind), so everything described in the docs may not be supported.For my money, this piece of the Plone and Salesforce.com integration landscape is probably the most ready for prime-time right now, due to the relative feature-completeness of Salesforce Base Connector. That is, if you ignore the lack of an official release :)
In the next few days, I plan to show off some work done to make PloneFormGen post directly to a Salesforce.com instance.
Cool Beans