Code

Code

Sunday, March 8, 2015

Custom Contact Roles - Part 2

So if you remember from Part 1, we created a hierarchical custom setting to allow us to control who can create/edit/delete certain contact roles.  We made it flexible enough to allow for an admin to make changes on the fly without a developer needing to adjust code.

There is however, a use case that we did not account for that would require a coding change.  In the code in part 1, if there was a requirement to allow a new profile access to one or more of the existing roles, all you would need to do is add a new value to our hierarchy custom setting for the new profile.  Quick and easy, but no coding required.

What about though if the business decided there was a new role that needed to be added?  Now we not only have a Buyer, Technical, and Executive role, but now there is a 'Influencer' role that needed to be available?

Since we have our roles hard-coded into our trigger, and they rely on a field on the custom setting, the only way to accomplish this wold be to add a new custom field to the custom setting and to update ur code.

if(
    (r.Role__c == 'Buyer' && !perm.Buyer__c) ||
    (r.Role__c == 'Technical' && !perm.Technical__c) ||
    (r.Role__c == 'Executive' && !perm.Executive__c)
)

....not as flexible as we had hoped.  BUMMER.


So how can we adjust our custom setting and code to account for this flexibility?  What if instead of a custom setting field for each role, we just have a single text field that contains the text name of the role that a profile has permission to.  This way, you never have to hardcode a role into your code, you can just check that the text field in the custom setting contains the role name.


So here would be our new custom setting.  Its still a hierarchy custom setting, but now there is only a single field.  This field will hold all the roles the profile has permission to.


The custom setting we created last time.  You see there is a field for each custom role



Now you can see our new custom setting just has a single field that contains all the roles that the profile or user has permission to


So now we have to adjust our code to use this new custom setting.  As you can see our code gets much simpler as we don't have to check each custom setting field against the role.  Its just a single check now.

public with sharing class CustomContactRoleTriggerHelper {

    public void CustomRoleBeforeInsert(List<Custom_Contact_Role__c> newRoles){
        checkPermissions(newRoles, null, 'create');
    }

    public void CustomRoleBeforeUpdate(List<Custom_Contact_Role__c> newRoles, List<Custom_Contact_Role__c> oldRoles, map<Id,Custom_Contact_Role__c> newMap, map<Id,Custom_Contact_Role__c> oldMap){
        checkPermissions(newRoles, oldMap, 'update');
    }

    public void CustomRoleBeforeDelete(List<Custom_Contact_Role__c> newRoles, List<Custom_Contact_Role__c> oldRoles, map<Id,Custom_Contact_Role__c> newMap, map<Id,Custom_Contact_Role__c> oldMap){
        checkPermissions(oldRoles, oldMap, 'delete');
    }

    private void checkPermissions(List<Custom_Contact_Role__c> rolesToCheck, map<Id,Custom_Contact_Role__c> oldMap, string actionDone){

        Contact_Role_Permissions_2__c perm = Contact_Role_Permissions_2__c.getInstance(UserInfo.getUserId());
        string roleString = (perm.Permission_to_Roles__c == null) ? '' : perm.Permission_to_Roles__c;
        set<String> roleSet = new set<String>(roleString.split(','));
        
        for(Custom_Contact_Role__c r : rolesToCheck){
            
            if(actionDone == 'create'){

                if(!roleSet.contains(r.Role__c)){
                    r.Role__c.addError('You do not have permission to ' + actionDone + ' contact roles with the role of  \'' + r.Role__c + '\'');
                }
            
            }else If(actionDone == 'update'){

                if(!roleSet.contains(oldMap.get(r.Id).Role__c)){
                    r.Role__c.addError('You do not have permission to ' + actionDone + ' contact roles with the role of  \'' + oldMap.get(r.Id).Role__c + '\'');
                }else if(!roleSet.contains(r.Role__c)){
                    r.Role__c.addError('You do not have permission to ' + actionDone + ' contact roles to the role of  \'' + r.Role__c + '\'');
                }

            }else If(actionDone == 'delete'){

                if(!roleSet.contains(oldMap.get(r.Id).Role__c)){
                    r.Role__c.addError('You do not have permission to ' + actionDone + ' contact roles with the role of  \'' + oldMap.get(r.Id).Role__c + '\'');
                }
            }
        }
    }
}

So let's try to break down how I use the custom setting now.  

Contact_Role_Permissions_2__c perm = Contact_Role_Permissions_2__c.getInstance(UserInfo.getUserId());
string roleString = (perm.Permission_to_Roles__c == null) ? '' : perm.Permission_to_Roles__c;
set<String> roleList = new set<String>(roleString.split(','));


Line 1 - I first get the text field from the custom setting as you see in line
Line 2 - Here we are accounting for the edge case that the field is null.  We don't want the code to throw an exception, so we set it to a blank string or ''.
Line 3 - Here we are using the string split method.  The split method returns a list, and we want to leverage the contains method, which is a set method so we add the list returned from the set method to a set.

Now that we have a set of strings that contains the roles the current user has permission to, we just need to check that the set contains the role they are trying to use.

if(!roleSet.contains(r.Role__c)){
     r.Role__c.addError('You do not have permission to ' + actionDone + ' contact roles with the role of  \'' + r.Role__c + '\'');
}

And that's it.  We now have made our code even more flexible and scalable.  We now can add any new role to the custom contact role picklist and it does not require any recoding of our trigger.

That's all I have this time.  Next time I will go through writing the test code for this helper class.

Sunday, February 22, 2015

Custom Contact Roles - Part 1

I have been wanting to start blogging about Salesforce and my experiences in Salesforce for quite a long time.  I always would get caught up trying to figure out what my first post would be, thinking it had to be something HUGE and EPIC.  I finally came to the realization I just needed to go for it and stop waiting.  So here it is.

Quite a few times I have been asked to set up Account Contact Roles within an org.  More than once I have been asked if I could restrict by profile who can create/edit/delete certain roles.  How many times have you heard complaints from one department that someone from another department changed a role that they 'had no business' changing.

The scenario seems simple enough.

***This scenario is 100% arbitrary for the example, no promises the roles/profiles make sense

Let's say I have 3 Account Contact Roles,
  • Buyer Role
  • Technical Role
  • Executive Role
Lets assume that the business rules require that anyone can view these roles, but only certain profiles can create, edit, or delete these roles.  Lets say I have the following 3 profiles
  • Sales User
  • Support User
  • Marketing User

The rules say that
  • Only Sales User profile can Create/Edit/Delete Buyer Roles
  • Only Support User profile can Create/Edit/Delete Technical Roles
  • Only Marketing User profile can Create/Edit/Delete Executive Roles

This seems like a fairly reasonable request, but with a little research and playing around you realize that this can't be done with standard Account Contact Roles.  Wait, I can write some triggers to control this functionality.  Wait, WHAT???   Contact roles don't support triggers either.  This is hopeless.......What to do????

I want to premise this solution by saying that I in no way think this is the only or the best solution to this problem.  In fact, there are likely 10+ other ways to solve this problem.  This just happens to be the way that I solved it.  In fact, I will show you a very elegant way that I stumbled across and think its a pretty slick solution.

http://www.youtube.com/watch?v=7QAWIPw5VIk&feature=youtu.be

The solution I came up with is a bit different.  I decided to create a custom object to emulate the Standard Contact Role Object.  By doing this, I can use triggers to control permissions.

I created a very simple custom object, Custom Contact Role.  I kept it very simple, it has only 3 fields.
  1. Account - Master-Detail to Account
  2. Contact - Master-Detial to Contact
  3. Role - Pick-list
Now that I have my Custom object, I can write a trigger that controls the permissions on each Role.  So I could just write a trigger that checks which role is being created, and which profile the current user has, and throw an error if its not he correct profile.  

trigger CustomContactRoleTrigger on Custom_Contact_Role__c (before insert) {

     map<Id,Profile> profileMap = new map<Id,Profile>([Select Id, Name From Profile]);
     for(Custom_Contact_Role__c cRole : trigger.new){
          if(cRole.Role__c == 'Buyer' && profileMap.get(Userinfo.getProfileId()).Name != 'Sales User'){
               cRole.Role__c.addError('You do not have permission to create this Role');
          }
     }
}




BOOM!  Great it works.  Now what if the business rules change?  What if in a few weeks they decide that another profile can create the Buyer role?  That change would require you (or another developer type) to go in an update the code.  What if the business wants an admin to be able to make these changes on the fly?  This isn't possible with the current construct as any change in the business rules would require a developer to update the code.  BUMMER.

So how do we fix that?  How do we make it a bit more flexible to allow an admin to maintain this?

I thought using a hierarchy custom setting would be perfect for this.  So I create a new hierarchy Custom setting.  You can do this by navigating to

Setup -> Develop -> Custom Settings.

I won't get into custom settings and why they are awesome, but if you have never used them before or are curious as to why they are so awesome, here are 2 excellent posts on the 2 kinds of custom settings from Jeff Douglas.  They are older, but still extremely relevant.  I highly recommend following his blog as well, it's truly excellent.

http://blog.jeffdouglas.com/2010/01/07/using-list-custom-settings-in-salesforce-com/

http://blog.jeffdouglas.com/2010/02/08/using-hierarchy-custom-settings-in-salesforce-com/

I call my new hierarchical custom setting Contact Role Permissions.  I create a custom checkbox field on the setting for each role in my Role pick-list.  So in this example I would need 3 custom fields, which match exactly to the roles in my pick-list.

  • Buyer
  • Technical
  • Executive
Here's what my new custom setting looks like



Now that I have this configured, I can start adding new data sets to it by clicking the manage button.  The first one I want to add is for the Org Wide permissions.  We want to set all fields to false for the org wide values.  This way, the default is to not allow anyone to update custom contact roles.  We can add permissions to those who need it.



Now is where we can start adding profile specific permissions.  Lets create new data sets for the 3 profiles we mentioned above.  Lets also add a set for system administrators as we want them to be able to have permissions for all roles.

Here I create the System Admin record with access to change all three roles



So then we create the records for all three profiles, giving them the specific permission the business is asking for.  



So now that we have all the custom settings as we need them, we just need to make some changes to my trigger.  First I move the logic from the trigger itself and put it in a helper class.


public with sharing class CustomContactRoleTriggerHelper {

     public void CustomRoleBeforeInsert(List<Custom_Contact_Role__c> newRoles){
          checkPermissions(newRoles, null, 'create');
     }

     public void CustomRoleBeforeUpdate(List<Custom_Contact_Role__c> newRoles, List<Custom_Contact_Role__c> oldRoles, map<Id,Custom_Contact_Role__c> newMap, map<Id,Custom_Contact_Role__c> oldMap){
         checkPermissions(newRoles, oldMap, 'update');
     }

     public void CustomRoleBeforeDelete(List<Custom_Contact_Role__c> newRoles, List<Custom_Contact_Role__c> oldRoles, map<Id,Custom_Contact_Role__c> newMap, map<Id,Custom_Contact_Role__c> oldMap){
         checkPermissions(oldRoles, oldMap, 'delete');
     }

     private void checkPermissions(List<Custom_Contact_Role__c> rolesToCheck, map<Id,Custom_Contact_Role__c> oldMap, string actionDone){

          Contact_Role_Permissions__c perm = Contact_Role_Permissions__c.getInstance(UserInfo.getUserId());

          for(Custom_Contact_Role__c r : rolesToCheck){

               if(actionDone == 'create'){

                    if(r.Role__c == 'Buyer' && !perm.Buyer__c || r.Role__c == 'Technical' && !perm.Technical__c || r.Role__c == 'Executive' && !perm.Executive__c){
                         r.Role__c.addError('You do not have permission to ' + actionDone + ' contact roles with the role of \'' + r.Role__c + '\'');
                    }

               }else If(actionDone == 'update'){

                    if(oldMap.get(r.Id).Role__c == 'Buyer' && !perm.Buyer__c || oldMap.get(r.Id).Role__c == 'Technical' && !perm.Technical__c || oldMap.get(r.Id).Role__c == 'Executive' && !perm.Executive__c){
                         r.Role__c.addError('You do not have permission to ' + actionDone + ' contact roles with the role of \'' + oldMap.get(r.Id).Role__c + '\'');
                    }else if(r.Role__c == 'Buyer' && !perm.Buyer__c) || r.Role__c == 'Technical' && !perm.Technical__c) || r.Role__c == 'Executive' && !perm.Executive__c){
                         r.Role__c.addError('You do not have permission to ' + actionDone + ' contact roles to the role of \'' + r.Role__c + '\'');
                    }

               }else If(actionDone == 'delete'){

                    if(oldMap.get(r.Id).Role__c == 'Buyer' && !perm.Buyer__c) || oldMap.get(r.Id).Role__c == 'Technical' && !perm.Technical__c) || oldMap.get(r.Id).Role__c == 'Executive' && !perm.Executive__c){
                         r.Role__c.addError('You do not have permission to ' + actionDone + ' contact roles with the role of \'' + oldMap.get(r.Id).Role__c + '\'');
                    }
               }
          }
     }
}



So now we have a trigger, that actually uses our custom setting to check if the current user has permission to make the change to the contact role they are attempting.


This code grabs our custom setting record using the current users user Id.
Contact_Role_Permissions__c perm = Contact_Role_Permissions__c.getInstance(UserInfo.getUserId());


If the user is trying to create or insert a new custom contact role, we just need to check that the role they are trying to create, is a role they have permission to work with. If they do, no action, basically let the record be created without intervening. If they do not have permission, we add an error to the page.

if(actionDone == 'create'){

     if(r.Role__c == 'Buyer' && !perm.Buyer__c || r.Role__c == 'Technical' && !perm.Technical__c || r.Role__c == 'Executive' && !perm.Executive__c){
          r.Role__c.addError('You do not have permission to ' + actionDone + ' contact roles with the role of \'' + r.Role__c + '\'');
     }
}


Here is an example if I am a Marketing User and try to create a new contact role with the role of 'Buyer'.



We do basically the same thing with updates, but for updates you have to check two scenarios. You need to check that you have the permission on the role you are changing the record to, but also the role that you are changing it from. For instance, I may have permission on the 'Technical' role, but does that mean I can change an existing record with a role of 'Technical' to 'Buyer'? I think not.

else If(actionDone == 'update'){

    if(oldMap.get(r.Id).Role__c == 'Buyer' && !perm.Buyer__c || oldMap.get(r.Id).Role__c == 'Technical' && !perm.Technical__c || oldMap.get(r.Id).Role__c == 'Executive' && !perm.Executive__c){
          r.Role__c.addError('You do not have permission to ' + actionDone + ' contact roles with the role of \'' + oldMap.get(r.Id).Role__c + '\'');
     
    }else if(r.Role__c == 'Buyer' && !perm.Buyer__c) || r.Role__c == 'Technical' && !perm.Technical__c) || r.Role__c == 'Executive' && !perm.Executive__c){
          r.Role__c.addError('You do not have permission to ' + actionDone + ' contact roles to the role of \'' + r.Role__c + '\'');
    }
}


We do this by first using the map of old values and checking that the user has permissions to the role we are changing the record from.  Then similar to insert we make sure the user has permission to the role we are changing it to.

Here is an example of a user having permission to the 'Buyer' role that we are changing it to, but doesn't have permission to the 'Technical' role we are changing it from.



For the delete, you obviously can only check the old values, as there are no new values.

else If(actionDone == 'delete'){

     if(oldMap.get(r.Id).Role__c == 'Buyer' && !perm.Buyer__c) || oldMap.get(r.Id).Role__c == 'Technical' && !perm.Technical__c) || oldMap.get(r.Id).Role__c == 'Executive' && !perm.Executive__c){
          r.Role__c.addError('You do not have permission to ' + actionDone + ' contact roles with the role of \'' + oldMap.get(r.Id).Role__c + '\'');
     }
}


The only caveat with the delete is how the error is rendered on the page. For the insert and update, the error is the usual Salesforce red 'Error: Invalid Data' at the top of the page. When you sue addError() with delete in a trigger though, the error does not render this way. It is rendered on a separate page. Below is an example of a user that does not have permission on the 'Technical' role trying to delete a contact role of 'Technical'.



Its obvious a bit different, but the only way around it is overriding the standard page with Visualforce, and I really didn't think it was all that necessary.

So now we have it.  We have a custom contact role that we can control who can create, edit, and delete.  We also made it a bit more flexible.  Remember how our first trigger required recoding if we decided there was another profile that we wanted to allow access to a certain role?  That is no longer a problem.  An admin can simply add a new value to our hierarchical custom setting.  Below is me adding a new record to allow the Customer Advocate profile to have permission on the 'Buyer' and 'Technical' Role.



And that's all that needs to be done.  Now the Customer Advocate profile can create, edit, and delete custom contact roles with the role of 'Buyer' and 'Technical'.

We now are able to give the business this functionality they have been asking for.

There is actually a few further steps we can take this to make it even more flexible and dynamic, but I think I will take that step in my next post.

Hope you all found this useful and helpful.