Lunchforce is an employee networking app built on the Salesforce Platform. Using Apex Triggers on CollaborationGroupMember (new as of Spring '13), it matches people in a Chatter Group who have never met before, so they can meet to have lunch and network. Join us to see how you can combine Apex Triggers, Scheduled Apex, and Junction Objects to analyze Chatter connections, adding intelligence to any Chatter Group.
Strategize a Smooth Tenant-to-tenant Migration and Copilot Takeoff
Coding Apex Triggers on Chatter Group Members
1. Coding Apex Triggers on Chatter
Group Members
A case study in coding for collaboration
Carolyn Grabill
Salesforce.com
Software Development Engineer
@CarolynCodes
2. Safe harbor
Safe harbor statement under the Private Securities Litigation Reform Act of 1995:
This presentation may contain forward-looking statements that involve risks, uncertainties, and assumptions. If any such uncertainties
materialize or if any of the assumptions proves incorrect, the results of salesforce.com, inc. could differ materially from the results
expressed or implied by the forward-looking statements we make. All statements other than statements of historical fact could be
deemed forward-looking, including any projections of product or service availability, subscriber growth, earnings, revenues, or other
financial items and any statements regarding strategies or plans of management for future operations, statements of belief, any
statements concerning new, planned, or upgraded services or technology developments and customer contracts or use of our services.
The risks and uncertainties referred to above include – but are not limited to – risks associated with developing and delivering new
functionality for our service, new products and services, our new business model, our past operating losses, possible fluctuations in our
operating results and rate of growth, interruptions or delays in our Web hosting, breach of our security measures, the outcome of any
litigation, risks associated with completed and any possible mergers and acquisitions, the immature market in which we operate, our
relatively limited operating history, our ability to expand, retain, and motivate our employees and manage our growth, new releases of our
service and successful customer deployment, our limited history reselling non-salesforce.com products, and utilization and selling to
larger enterprise customers. Further information on potential factors that could affect the financial results of salesforce.com, inc. is
included in our annual report on Form 10-K for the most recent fiscal year and in our quarterly report on Form 10-Q for the most recent
fiscal quarter. These documents and others containing important disclosures are available on the SEC Filings section of the Investor
Information section of our Web site.
Any unreleased services or features referenced in this or other presentations, press releases or public statements are not currently
available and may not be delivered on time or at all. Customers who purchase our services should make the purchase decisions
based upon features that are currently available. Salesforce.com, inc. assumes no obligation and does not intend to update these
forward-looking statements.
3. Lunchforce Technologies
▪ Using custom junction objects
▪ Triggering on Chatter Group Members
▪ Implementing an interface in Apex
• Schedulable
• Comparable
▪ Posting to Chatter from Apex
4. Lunchforce brings the power
of Chatter to the real world.
Get the source: http://bit.ly/DF13Lunch
5. Lunchforce’s moving parts
User joins Lunchforce
Chatter group
Chatter Group
Member Trigger fires
Luncher__c record is
created for that User
Admin schedules
LunchMatch Apex
Scheduled Apex runs
matching algorithm,
posts results to
Chatter
Users see Chatter
posts, meet up for
lunch together
6. Lunchforce Technologies
▪ Using custom junction objects
▪ Triggering on Chatter Group Members
▪ Implementing an interface in Apex
• Schedulable
• Comparable
▪ Posting to Chatter from Apex
8. Records created by matching process
Luncher__c
Location Picker
Luncher__c
Time Picker
Lookup
Lunch_Match__c
Match Record
Lookup
9. Lunchforce Technologies
▪ Using custom junction objects
▪ Triggering on Chatter Group Members
▪ Implementing an interface in Apex
• Schedulable
• Comparable
▪ Posting to Chatter from Apex
11. CollaborationGroupMember fields
▪ CollaborationGroupId
• The Id of the Chatter group joined
▪ CollaborationRole
• Whether the User is Standard or Admin group member
▪ MemberId
• Id of the User joining the group
▪ NotificationFrequency
• Picklist – Daily, Weekly, Never, Post
13. Lunchforce Technologies
▪ Using custom junction objects
▪ Triggering on Chatter Group Members
▪ Implementing an interface in Apex
• Schedulable
• Comparable
▪ Posting to Chatter from Apex
14. Lunchforce’s moving parts
User joins Lunchforce
Chatter group
Collaboration Group
Member Trigger fires
Luncher__c record is
created for that User
Admin schedules
LunchMatch Apex
Scheduled Apex runs
matching algorithm,
posts results to
Chatter
Users see Chatter
posts, meet up for
lunch together
15. Interfaces
▪ A template that defines methods, but does not implement them
▪ Apex class implements an interface by implementing its methods
▪ Schedulable is an interface built in to Apex
▪ Example:
global class scheduledLunchMatch implements
Schedulable {
global void execute(SchedulableContext ctx) {
goMatchGo();
}
}
18. Lunchforce Technologies
▪ Using custom junction objects
▪ Triggering on Chatter Group Members
▪ Implementing an interface in Apex
• Schedulable
• Comparable
▪ Posting to Chatter from Apex
19. Problem: how do you maximize
new matches in a group of people,
when some of them have already
met?
21. Demo
▪ Example Comparable Class
• LuncherWithHistory
▪ Sorting a list of LuncherWithHistory records
• LunchMatcher
22. Lunchforce Technologies
▪ Using custom junction objects
▪ Triggering on Chatter Group Members
▪ Implementing an interface in Apex
• Schedulable
• Comparable
▪ Posting to Chatter from Apex
23. Posting to Chatter from Apex using FeedItem
▪ Represents an entry in the feed, such as changes in a record feed,
including text posts, link posts, and content posts.
▪ Example:
FeedItem fi = new FeedItem();
fi.ParentId = [object id]; //user, account, etc.
fi.Body = “Body of the post”;
insert fi;
24. Demo
▪ Example FeedItem creation
• LuncherWithHistory
▪ Example weekly lunch matchup
▪ Mobile lunch match results
28. Trigger on CollaborationGroupMember
trigger addNewLunchers on CollaborationGroupMember
(after insert, after delete)
{
if (Trigger.isInsert) {
// When a user joins the group, create a
// Luncher__c record for them
LunchChatter.newLuncherOnGroupJoin(Trigger.new);
} else if (Trigger.isDelete) {
// When a user leaves the group, mark the
// Luncher__c record inactive
LunchChatter.makeLuncherInactiveOnGroupLeave(Trigger.old);
}
}
29. //create a new luncher when someone joins the lunch chatter group
public static void newLuncherOnGroupJoin(List<CollaborationGroupMember> allNewMembers) {
//get the members that joined the lunch match chatter group
List<CollaborationGroupMember> members = new List<CollaborationGroupMember>();
for (CollaborationGroupMember cgm : allNewMembers) {
if (cgm.CollaborationGroupId == LunchChatter.lunchGroupId) {
members.add(cgm);
}
}
if (!members.isEmpty()) {
//for each member, figure out if we need to create a new luncher or update an existing one
for (CollaborationGroupMember cgm : members) {
if (keySet.contains(cgm.MemberId)) {
userIdsForLunchersToActivate.add(cgm.MemberId);
} else {
userIdsForNewLunchers.add(cgm.MemberId);
}
}
30. //create new lunchers and add them to the upsert list
if (!userIdsForNewLunchers.isEmpty()) {
for (User u : [SELECT Id, Name FROM User WHERE Id IN :userIdsForNewLunchers]) {
Luncher__c newLuncher = new Luncher__c(
Name = u.Name,
Status__c = 'Active',
User__c = u.Id);
lunchersToUpsert.add(newLuncher);
}
}
//get existing lunchers to update and add them to the upsert list
if (!userIdsForLunchersToActivate.isEmpty()) {
for (Luncher__c l : [SELECT Id, Status__c, User__c FROM Luncher__c where User__c
IN :userIdsForLunchersToActivate]) {
l.Status__c = 'Active';
lunchersToUpsert.add(l);
}
}
//insert new lunchers, if any were created
if (!lunchersToUpsert.isEmpty()) {
upsert lunchersToUpsert;
}
}
}
31. //mark a luncher inactive when someone leaves the lunch chatter group
public static void makeLuncherInactiveOnGroupLeave(List<CollaborationGroupMember> allDeleted) {
//get the members that are part of the chatter group you actually care about
List<Id> delUserIds = new List<Id>();
for (CollaborationGroupMember cgm : allDeleted) {
if (cgm.CollaborationGroupId == LunchChatter.lunchGroupId) {
delUserIds.add(cgm.MemberId);
}
}
//set status inactive for lunchers with delUserIds
if (!delUserIds.isEmpty()) {
List<Luncher__c> lunchersToUpdate = new List<Luncher__c>();
for (Luncher__c l : [SELECT Id, Status__c, User__c FROM Luncher__c WHERE User__c
IN :delUserIds]) {
l.Status__c = 'Inactive';
lunchersToUpdate.add(l);
}
if (!lunchersToUpdate.isEmpty()) {
update lunchersToUpdate;
}
}
}
32. global class scheduledLunchMatch implements Schedulable {
global void execute(SchedulableContext ctx) {
goMatchGo();
}
//shortcut for demos, to perform a match on command
public void goMatchGo() {
datetime lunchTime = datetime.now();
LunchMatcher lm = new LunchMatcher();
boolean matchSucceeded = lm.performScheduledMatch(lunchTime);
System.assertEquals(true, matchSucceeded);
}
}
33. public boolean performScheduledMatch(DateTime lunchDate) {
//get all the active lunchers
List<Luncher__c> activeLunchers = [SELECT Id, Name from Luncher__c WHERE Status__c = 'Active'];
Map<Id, Luncher__c> luncherMap = new Map<Id, Luncher__c>();
//get all the inactive lunchers
List<Luncher__c> inactiveLunchers = [SELECT Id FROM Luncher__c WHERE Status__c = 'Inactive'];
//get all the previous matches
List<Lunch_Match__c> prevMatches = [SELECT Id, Location_Picker__c, Time_Picker__c from
Lunch_Match__c];
//construct a list of lunchers with history that holds all the previous matches and lunchers
// with no matches
Map<Id, Set<Id>> prevMatchMap = getPrevMatchMap(prevMatches, activeLunchers, inactiveLunchers);
List<LuncherWithHistory> lunchersWithHistory = LuncherWithHistory.convertFromMap(prevMatchMap);
//do the matching
List<Id> thirdWheelIds = new List<Id>(); //holds any unmatched lunchers
List<Lunch_Match__c> newLunchMatches = matchup(lunchersWithHistory, lunchDate, thirdWheelIds);
insert newLunchMatches;
//handle the thirdwheels
handleThirdWheels(thirdWheelids, newLunchMatches);
}
34. /**
* A container class for convenience, representing a single Luncher__c,
* and the set of other Luncher__c's that they have already been matched with,
* based on existing Lunch_Match__c records.
*/
global class LuncherWithHistory implements Comparable {
public Id luncherId;
//id of this luncher
private Set<Id> historySet; //set of ids of other lunchers this luncher has already
//been matched with
//constructor for luncher that does have match history
public LuncherWithHistory(Id luncherId, Set<Id> historySet) {
this.luncherId = luncherId;
this.historySet = historySet;
}
// CompareTo() will return 0 if ids are equal, else, will return 1 if this
// LuncherWithHistory's group size is larger
global Integer compareTo(Object compareTo) {
LuncherWithHistory compareToLuncher = (LuncherWithHistory)compareTo;
if (luncherId == compareToLuncher.luncherId) return 0;
if (historySet.size() > compareToLuncher.historySet.size()) return 1;
return -1;
}
}
35. //given a list of new Lunch Matches, post to each picker's chatter wall
public static void notifyLunchers(List<Lunch_Match__c> matches) {
//construct maps of match to time picker and match to loc picker
for (Lunch_Match__c lm : matches) {
matchToLuncherLoc.put(lm.Id, lm.Location_Picker__c);
matchToLuncherTime.put(lm.Id, lm.Time_Picker__c);
}
//construct a map of luncher id to user id
Map<Id, Luncher__c> luncherIdToLuncherMap = getLunchersFromMatches(matches);
//build the chatter messages
List<FeedItem> feedItems = new List<FeedItem>();
for (Lunch_Match__c match : matches) {
Luncher__c locPicker = luncherIdToLuncherMap.get(matchToLuncherLoc.get(match.Id));
if (locPicker.Receive_Chatter_Notifications__c) {
FeedItem locFeedItem = buildFeedItem(locPicker);
feedItems.add(locFeedItem);
}
//repeat for time picker
}
insert feedItems;
}