Tuesday, May 26, 2015

Prevent Campaign Member Deletion

We've begun integrating email blasts from Responsys into our Salesforce org at Lakeshore[1. I'm no longer with Lakeshore. I'm at VIZIO, Inc. now.] so our sales reps have better visibility into how their contacts are being marketed to.

If it weren't for DBAmp from forceAmp, this might have been a really tough project... But, with the magic of a few SQL queries, I was able to get it working just the way we wanted it to.

But that's not what this post is about[2. I probably could write an entire post just on DBAmp. It's a totally awesome product.].

One of the things I don't like about the Salesforce Campaign/Campaign Member hierarchy is even though a user is granted Read Only permission on the Campaign object, they can still delete any Campaign Member for Leads or Contacts they have Read/Write access to, and there's no real way to configure Salesforce to prevent it.

Here's a simple hack I used to prevent Campaign Member deletion. Credit for it goes entirely to Chris Lance on the Success Community.

First, create a new Visualforce page that will be used to block Campaign Member deletion. The code below looks an awful lot like the standard Insufficient Privileges page a user gets when they're not allowed to perform an action.


<apex:page standardController="CampaignMember" standardStylesheets="true">
<apex:form >
<table cellspacing="10">
<tr>
<td><span style="font-weight: bold; font-size: 12pt;">Insufficient Privileges</span></td>
</tr>
<tr>
<td>You do not have the level of access necessary to perform the operation you requested. Please contact your administrator if access is necessary.<br/></td>
</tr>
<tr>
<td><apex:commandLink action="{!cancel}" immediate="true" value="Return to previous page."/></td>
</tr>
</table>
</apex:form>
</apex:page>


Then, in the Campaign Member object (Setup > Build > Customize > Campaigns > Campaign Members > Buttons and Links), override the "Delete" button with the Visualforce page you just created.

Campaign Member Delete Override

Finally, rejoice that Campaign Members can no longer be deleted[3. Much rejoicing was had by all in this case...].

Prevent Campaign Member Deletion

Of course, there is a limitation to this hack in that nobody can delete Campaign Members, even the System Administrator.

For our needs, this is fine since we're integrating from an outside system. If certain profiles did need to delete, I might add some logic to the Visualforce page that lets them perform the action instead of being told they don't have the privilege.

Friday, January 2, 2015

AppleScript to Compress Files and Folders

I had the need to select several folders at once in the Mac OS Finder and zip them up as individual archives. This AppleScript to compress files and folders was the solution I came up with. The script compresses each item selected into its own archive, and works with both folders and files.


tell application "Finder"
set theItems to selection
repeat with i from 1 to (count of theItems)
set theItem to (item i of theItems) as alias
set itemPath to quoted form of POSIX path of theItem
set fileName to name of theItem
set theFolder to POSIX path of (container of theItem as alias)
set zipFile to quoted form of (theFolder & fileName & ".zip")
do shell script "zip -jr " & zipFile & " " & itemPath
end repeat
end tell


As always, you'll get the best results when used with FastScripts by Red Sweater Software.

Wednesday, October 15, 2014

Passing Your Salesforce ADM201 Certification Exam

I passed the Salesforce ADM201 certification exam today. It's been a five year road to get here. From the first week I began working with Salesforce, I decided attaining certification was something I wanted to pursue.

Now that I've done it, I thought it might be good to share the things that helped me be successful with those looking to do the same.

Know Your Stuff



Your success or failure pretty much boils down to this... You've got to know your stuff or it's never going to happen.

The Salesforce ADM201 certification exam is not a walk in the park. You'll never pass it by just reading the online help, or taking online practice tests and studying flash cards -- most of which are badly outdated, or just plain wrong.

You're going to need to be familiar with just about every area of Salesforce and how it actually works -- where and how you can do something, and where and why you can't do something else.

This means getting a developer org and configuring it just like you would if you were doing it for your day job or a client. Be creative in your approach. Try doing things you've never done. Try to break it. Enter real data and learn how it flows through the system from a Campaign to a converted Lead to a resolved Case.

Learn it, live it, be it...

Do everything in your power to wrap your head around the big picture of what Salesforce does and totally understand it. That's what being a good admin is all about anyway -- knowing what can or can't be done.

Study With a Group



I can't stress this one enough.

My employer was kind enough to purchase a year of Premiere Plus Support in order to provide me the online courses to pursue certification, and I did all 60+ hours of both the ADM201 and DEV401 trainings. I learned a lot from it, but it wasn't until I joined a study group that the learning really sank in.

Collaborating with a group of your peers introduces fresh ideas, which could give you a completely different perspective about a topic. That, in turn, leads to other moments of discovery. If you're serious about earning a certification, I suggest you find others who are as committed as you to passing your exam.

I joined the Salesforce Certification Study Group in the Salesforce Success Community. Deepa Patel and the others who volunteer with the group have worked hard to develop a successful curriculum with a high passing rate for those who are committed enough to make it to the end. It doesn't cost any more than your time and effort, plus the $200 to register for your Salesforce ADM201 certification exam, which you're going to spend anyway.

Once you're accepted into the private group, watch what goes on in the Chatter feed for a while, then when signups for the next round of sessions are announced, join the one that best suits your geographical region.

Make the Time



Okay. You've been accepted into the study group. But if you aren't totally serious about earning your certification, you'll never be successful. Period. You've got to want it to happen. If you don't give studying the attention it needs, you can forget about passing, and may as well stop reading now.

Make it what you think about when you're sitting in traffic, brushing your teeth, or taking a shower. Turn off the TV for a couple hours each night and learn something new about Salesforce instead. Choose to become obsessed for a while.

If you end up joining the certification study group, make time to do the work you're supposed to do before the next session -- every single week. Commit to the 15 weeks the group is going to require from you. Be ready to demo your work for the online sessions. Know exactly what you did, why you did it, and have a reason for it. I promise you'll learn incredible amounts if you commit to working this way.

Be Real with Yourself



Be 100% honest with yourself. Ask yourself "Am I really ready to pursue certification?" If you've only been working with Salesforce a few months, it's going to be a tough row to hoe, even with the help and support of a study group -- who may not be too keen helping you along while studying themselves.

Being part of a group requires commitment. People are depending on you to have ideas that could be different than theirs. Are you really ready to commit to a group of 6-12 people for 15 weeks, and collaborate with them? Are you really ready to do 5 to 8 hours of homework each week? It's going to take serious sacrifice.

If you're not ready, then wait until you are. You have a better chance of succeeding if you do. By joining the study group without enough experience, chances are you'll fall behind fast, which is going to discourage you. Don't set yourself up to fail. Be ready.

Use Good Resources



I mentioned the online tests and flash cards earlier. Don't rely on them at all. Salesforce adds lots of new features at each release. Flash cards and tests from the Spring '11 release are full of misinformation now. If you rely on them to know your stuff, you might not know much at all.

The only other resource you need now is to watch the Dreamforce Get Started on the Certified Administrator Credential workshop video. It provides solid advice for being ready, how to take the Salesforce ADM201 certification exam, and knowing what you're walking into.

Taking Your Exam



You've scheduled your test, and now it's time to go take it.

There's only one thing to be said... Know that you know what you know. If you've worked hard in the Salesforce Certification Study Group, you're ready to pass. Walk in knowing that fact.

When you sit down to begin, take a minute to breathe and relax. Then start your exam using the techniques you learned from the workshop video. Before you know it, you'll be waking out of the testing center as a Salesforce Certified Administrator.

Friday, September 19, 2014

Sharing Parent/Child Accounts with a Single Sharing Rule

I received a request to temporarily share the child accounts under a single parent account with a second rep so he and the owner could work as a team to do a blitz on the schools within a single school district.

The parent school district had almost 300 child school sites below it which needed to be shared fast, and un-shared just as quickly once the blitz was finished.

Unfortunately, Salesforce doesn't allow sharing child accounts under a parent using a single sharing rule with Parent Account Id as the criteria. And since I didn't want to spend my entire day updating these accounts with Account Teams, I had to find a different approach because the second rep needed account access within the hour.

My solution was to add a text field on the Account object that I could load the parent account Id into, and create a criteria based sharing rule on -- kind of a faux Parent Account Id field.

Parent Account Share

Before getting to the down-and-dirty, there's a couple of gotcha's to keep in mind. Criteria based sharing rules are only available in Personal, Enterprise and Unlimited editions. This will not work for any edition below that. And since you only have 50 criteria based sharing rules available, you should use this technique sparingly.

With that out of the way, here's how I made it happen.


  1. First, I created a new "Parent Account Share" text field on the Account object, setting field security viewable by all, editable only by the Admin. I didn't add it to any page layouts.

  2. Then, I found all accounts under the parent by creating a report using the Parent Account Id as my filter, with only the Account Id and Parent Account Id columns on the report layout, and exported it in CSV format.

  3. In the exported file, I changed the "Parent Account Id" column name to "Parent Account Share" and removed all the extra report fluff at the end of the file (don't forget to do that).

  4. Using DataLoader, I updated the accounts, matching column names.

  5. Next, I created a Public Group and added both the primary and secondary rep to it. I could have gotten away with just the secondary rep, but I like to have both users in the group so I know who's sharing together.

  6. Finally, I created new criteria based sharing rule on the Account object where "Parent Account Share" equals the Id of the parent account I just inserted into the "Parent Account Share" field, sharing it with the group I created, providing the level of Account, Contact, Opportunity & Case access as needed (Read/Write, Read/Write, Private, Private). I also named the group and sharing rule similarly so I know what goes with what.

  7. To make sure it worked, I logged in as the second rep to view the child accounts.



So there you have it, a quick way for sharing parent/child accounts when there are an outrageous number of children under a single parent.

Wednesday, July 23, 2014

Salesforce Flows, Visualforce, and Record Context

I was asked to find a solution for our field reps that would simplify the process of adding a completed task to their activities when they visited one of our retail stores. We keep our retail locations in Salesforce.com as contacts under a master account, which is shared with all users using a special account sharing rule.

The solution I came up with was a simple flow that limited users to a specific set of enterable information, pre-filling the date of the activity, and marking the task complete when the activity was committed. Literally, the only thing they needed to do was select a picklist value and enter a comment about their visit. It was exactly what we were looking for.

Salesforce.com Flow

The thing I didn't like about the flow was that when a user entered it, context kind of got lost -- meaning they started out from a contact record, but didn't have a good visual cue about where they were once they got into the flow.

Sure, the flow was simple, but to me it's still poor UX not to know the context of your work at all times. So, I decided to use a Visualforce page to solve the problem:


<apex:page standardController="Contact">

<apex:sectionheader title="Add Retail Store Visit" subtitle="{!Contact.Name}"></apex:sectionheader>

<flow:interview name="Retail_Store_Visit">
<apex:param name="vContact" value="{!Contact.Id}"></apex:param>
<apex:param name="vAccount" value="{!Contact.Account.Id}"></apex:param>
</flow:interview>
</apex:page>


The trouble with this was that after the flow data was committed, the user would be returned to the beginning of the flow. That's definitely not what I wanted. I needed to go back to the contact record I started from.

So I added the finishLocation attribute to the <flow:interview></flow:interview> component tag. It would make sense that if I passed the Contact Id, the flow should return to the Contact record:


<flow:interview name="Retail_Store_Visit" finishLocation="{!URLFOR('/' + Contact.Id)}">
<apex:param name="vContact" value="{!Contact.Id}"></apex:param>
<apex:param name="vAccount" value="{!Contact.Account.Id}"></apex:param>
</flow:interview>


Nope. That didn't do it. When the flow is entered, context gets lost, so Salesforce doesn't really know where the user is anymore. Even using the vContact Apex parameter didn't work.

I searched help documentation for an answer, but it wasn't all that helpful. I searched Communities and came up empty. Then I searched the Salesforce Stack Exchange, where the only solution I could find was really convoluted, and didn't even come close to solving my problem. I was vexed.

Taking a break to get a cup of coffee, a solution dawned on me that turned out to be pretty darned simple. All I needed was an Apex variable in the page, which I could pass to the finishLocation component tag once the flow was complete. Oddly enough, this method is not documented -- at least not that I could find.

So...the final Visualforce page:


<apex:page standardController="Contact">
<apex:variable var="theContact" value="{!Contact.Id}"></apex:variable>

<apex:sectionheader title="Add Retail Store Visit" subtitle="{!Contact.Name}"></apex:sectionheader>

<flow:interview name="Retail_Store_Visit" finishLocation="{!URLFOR('/' + theContact)}">
<apex:param name="vContact" value="{!Contact.Id}"></apex:param>
<apex:param name="vAccount" value="{!Contact.Account.Id}"></apex:param>
</flow:interview>
</apex:page>


Using the Apex variable, users where returned to the contact record they started from, and could see the task they just added in the activity history related list. This was exactly what I was looking for. Everyone was happy.

If you need to return to a starting point when using flows and Visualforce pages, consider giving this solution a try.

Tuesday, September 10, 2013

Apex CASE Statement for Last Day of the Month

When using a CASE statement to find the last day of the month in Apex code, I've seen a lot of developers -- both newbie & experienced coders alike -- test for every month, when all that's really needed is to test for the shorter months, defaulting to 31 for the rest. This is a better Apex CASE statement that's easier to read:


CASE(
MONTH( TODAY() ),
2, IF( MOD( YEAR( TODAY() ), 4 ) = 0, 29, 28 ),
4, 30,
6, 30,
9, 30,
11, 30,
31 //default for months 1,3,5,7,8,10,12
)


This example is half as many lines, and since I don't think we'll be changing the Gregorian calendar any time soon, it should be safe.

Friday, October 19, 2012

Making a Field Appear Required on a Visualforce Page

I've been working on a force.com app with the requirement that a user must enter a valid email address on a Visualforce page before being able to save a record, but they must also be able to insert the related contact's email address by clicking a button instead of having to leave the edit page to go find it. That seemed simple enough.

Visual Force Page Example

In my original Visualforce page, it seemed logical that if I set the recipient email field as required, all would work as expected. However, defining the field as required prevented my custom action in my page controller from firing and entering the email address.

Original Page Controller




public class GiftCardTestController {

private ApexPages.StandardController std;
public String cEmail {get;set;}
public Gift_Card_Order__c gc {get;set;}
public GiftCardTestController(ApexPages.StandardController stdCtrl) {
std = stdCtrl;
}

//selects the email address of the related contact
//and inserts into recipient email field.
public void fillEmail() {
gc = (Gift_Card_Order__c)std.getRecord();
cEmail = [select Id, Email from Contact where Id = :gc.Contact__c].Email;
gc.Recipient_Email__c = cEmail;
}
}


Original Visualforce Page




<apex:page standardController="Gift_Card_Order__c" extensions="GiftCardTestController" title="Gift Card Test">
<apex:form>
<apex:pageblock title="Gift Card" mode="edit">

<apex:pageblockbuttons location="top">
<apex:commandbutton action="{!save}" value="Save"></apex:commandbutton>
<apex:commandbutton action="{!cancel}" value="Cancel"></apex:commandbutton>
<apex:commandbutton action="{!fillEmail}" value="Fill Email"></apex:commandbutton>
</apex:pageblockbuttons>

<apex:pageblocksection title="Email Info" columns="1">
<apex:inputfield value="{!Gift_Card_Order__c.Contact__c}"></apex:inputfield>
<apex:inputfield value="{!Gift_Card_Order__c.Recipient_Email__c}" required="true"></apex:inputfield>
</apex:pageblocksection>

</apex:pageblock>
</apex:form>
</apex:page>


In this example, the fillEmail() action should select the related contact email address, and put the value in the Recipient_Email__c field so the user can see it.

But it's not that simple it seems. When the field had the required="true" attribute set, the action would not fire because all validation is done on the client side and the page never posts back to the server -- so the controller action never gets called.

So after some digging and asking for help on the Salesforce discussion boards, the solution was to make the Recipient_Email__c appear as if it's required on the page (though it's really not), and add a new save method to my controller to handle field validation on the server side when the record gets saved.

New Page Controller




public class GiftCardTestController {

private ApexPages.StandardController std;
public String cEmail {get;set;}
public Gift_Card_Order__c gc {get;set;}
public GiftCardTestController(ApexPages.StandardController stdCtrl) {
std = stdCtrl;
}

public void fillEmail() {
gc = (Gift_Card_Order__c)std.getRecord();
cEmail = [select Id, Email from Contact where Id = :gc.Contact__c].Email;
gc.Recipient_Email__c = cEmail;
}

// add custom save method...
public pageReference save() {
gc = (Gift_Card_Order__c)std.getRecord();

// if the recipient email is null, add an error to the field
// and return null to remain on the current page...
if(gc.Recipient_Email__c == null) {
gc.Recipient_Email__c.addError('A valid email address is required.');
return null;
}

// otherwise, the field is filled, so it's okay to redirect to view page.
// standard field validation will check for valid email format.
else {
return std.save();
}
}
}


New Visualforce Page




<apex:page standardController="Gift_Card_Order__c" extensions="GiftCardTestController" title="Gift Card Test">
<apex:form>
<apex:pageblock title="Gift Card" mode="edit">
<apex:pageblockbuttons location="top">
<apex:commandbutton action="{!save}" value="Save"></apex:commandbutton>
<apex:commandbutton action="{!cancel}" value="Cancel"></apex:commandbutton>
<apex:commandbutton action="{!fillEmail}" value="Fill Email"></apex:commandbutton>
</apex:pageblockbuttons>

<apex:pageblocksection title="Email Information" columns="1">
<apex:inputfield value="{!Gift_Card_Order__c.Contact__c}"></apex:inputfield>

<!-- The updated pageblocksectionitem -->
<apex:pageblocksectionitem>
<apex:outputlabel>Email Recipient</apex:outputlabel>
<apex:outputpanel layout="block" styleClass="requiredInput">
<apex:outputpanel layout="block" styleClass="requiredBlock"></apex:outputpanel>
<apex:inputfield value="{!Gift_Card_Order__c.Recipient_Email__c}"></apex:inputfield>
</apex:outputpanel>
</apex:pageblocksectionitem>

</apex:pageblocksection>
</apex:pageblock>
</apex:form>
</apex:page>


Notice the <apex:pageblocksectionitem></apex:pageblocksectionitem> code to replace the original field. This is how we make the field appear with the "required" bar. A nifty trick that took some digging to discover. Hopefully this post saves someone else the time it took me to figure it out -- and me the time when I forget it.

For convenience, here's a Github Gist: https://gist.github.com/4595878