Direkt zum Hauptbereich

Zugriff auf "private" Daten mit Visualforce ermöglichen

Die Organisationsübergreifenden Sicherheitseinstellungen können den allgemeinen Zugriff auf den Datenbestand verhindern. Ist der Standardzugriff auf "Privat" gesetzt (s. Screenshot), hat nur der Eigentümer inklusive seines Vorgesetzten den Zugriff auf die Daten, wo er als Inhaber hinterlegt ist.
Insbesondere Vertriebsmitarbeiter arbeiten intensiv mit Account-Informationen. Hier werden unter anderem sensible Daten erfasst. In diesem Zusammenhang entstehen Fragen folgender Art:
Wie soll ich meine Salesforce-Organisation konfigurieren, wenn ich nur bestimmte Daten trotz "Privat" Einstellung allen Vertriebsmitarbeiter zur Verfügung stellen möchte?
Eine potenzielle Lösung ist die Entwicklung einer Visualforce-Seite, die relevante Daten darstellt. Basierend auf dem Beitrag von Jeff Douglas habe ich ein Suchformular gebaut. Damit lassen sich alle Accounts, unabhängig von den "Freigabeeinstellungen" durchsuchen.
Im folgenden Video ist die Funktionalität dargestellt.

Vorteile:
- der Zugriff auf die neue Seite und somit auf alle Daten ist konfigurierbar
- die Darstellung der Account-Felder lässt sich über ein Feldset steuern (keine Programmierung notwendig)
- schnelle Umsetzung
- flexibel für Anpassungen

Bausteine der Visualforce-Seite:
- CSS: was fürs Auge
- JavaScript:
   * Übergabe der Suchparameter an den Controller
   * Identifikation der Enter-Taste mit Ausführung der Suchfunktion
- ActionStatus: visuelle Darstellung für die Ausführung der Suchfunktion

Jetzt wird's technisch ;-)

------------------------------- VUSUALFORCE SEITE -------------------------------
<apex:page controller="f42_Search_Controller" sidebar="false">
 <style>
  .linkSearchResult{
   text-decoration:none !important;
   color:#015ba7 !important;
  }
  .loadingContainer{
   width:84%;
   height:88%;
   border:6px solid #CCCCCC;
   border-radius: 2px;
   z-index:1000;
   position:absolute;
   background-color: #efefef;
   filter: alpha(opacity=90);
   opacity: 0.8;
   -moz-opacity:0.8;
   -khtml-opacity:0.8;
  }
  .btn:hover {
   box-shadow: 0px 15px 20px -7px #fff inset, 0px 0px 5px -1px #333 !important;
  }
  .btn {
   display: block !important;
   padding: 5px 10px !important;
   background: #3ba9cd !important;
   border-radius: 5px !important;
   box-shadow: 0px 15px 20px -10px #fff inset, 0px 0px 5px -1px #333 !important;
   border: solid 2px #fff !important;
   margin: 5px !important;
   color: #fff !important;
   cursor: pointer !important;
   text-shadow: 0px 0px 2px #185375 !important;
   width:95%;
  }
  .resultsContainer{
   border-radius: 2px;
   box-shadow: 0px 10px 10px 0px #efefef inset;
   padding:4px;
   margin-top:10px;
   margin-left:2px;
  }
  .labelCol{
   text-align:left !important;
  }
  .buttonSection{
   border-top:1px solid #E0E0E0;
  }
  INPUT{
   width:100%;
  }
  </style>
  <script type="text/javascript">
   /************************
   doSearch
   *************************/
  function doSearch() {
   doSearchApex(
    document.getElementById("Name").value,
    document.getElementById("BillingCity").value
   );
  }

  /************************
  initSearchOnEnter
  *************************/
  function initSearchOnEnter(e){
   if(window.event){
    key = window.event.keyCode;
   }else{
    key = e.which; //firefox
   }
   if(key == 13) {
    var ele=document.getElementById("search-button");
    ele.click();
    return false;
   }else{
    return true;
   }
  }
 </script>
 <apex:form >
<!-- -------------------------------------------------------------------- -->
<!-- APEX FUNCTIONS -->
<!-- Getting the Daschboard ID find the corresponding Monthly KAM Review -->
 <apex:actionFunction name="doSearchApex" action="{!runSearch}" rerender="results,debug,errors,noSearchResults" status="ajaxLoading">
  <apex:param name="Name" value="" />
  <apex:param name="BillingCity" value="" />
 </apex:actionFunction>
<!-- -------------------------------------------------------------------- -->
 <table width="100%" border="0">
  <tr>
<!-- SEARCH SETTINGS -->
<!-- ---------------- -->
   <td width="200" valign="top">
    <apex:pageBlock title="{!$Label.f42_Search_Parameter}" mode="edit" id="criteria">
     <table cellpadding="2" cellspacing="2" width="100%">
<!-- ACCOUNT NAME -->
      <tr>
       <th class="labelCol">
        <apex:outputLabel value="{!$ObjectType.Account.fields.Name.label}" /><br />
        <input type="text" id="Name" onkeypress="return initSearchOnEnter(event);" />
       </th>
      </tr>
<!-- CITY -->
      <tr>
       <th class="labelCol">
        <apex:outputLabel value="{!$ObjectType.Account.fields.BillingCity.label}" /><br/>
        <input type="text" id="BillingCity" onkeypress="return initSearchOnEnter(event);" />
       </th>
      </tr>
     </table>
    </apex:pageBlock>
<!-- SEARCH BUTTON -->
    <input type="button" id="search-button" class="btn" name="btnSearch" value="{!$LABEL.f42_Search_Button}" onclick="doSearch();" />
<!-- NUMBER OF SEARCH RESULTS -->
    <apex:outputPanel id="noSearchResults">
     <div class="resultsContainer">
      <apex:outputLabel styleclass="labelCol" value="{!$Label.f42_Search_NumberOfResults} {!numberSearchResults}" />
     </div>
    </apex:outputPanel>
   </td>
   <td valign="top">
    <apex:pageMessages id="errors" />
<!-- ********** AJAX LOADING STATUS ************-->
     <apex:outputPanel >
      <apex:actionStatus id="ajaxLoading">
       <apex:facet name="start">
        <div class="loadingContainer">
         <div style="text-align:center; padding-top:50px;"><img src="/img/loading32.gif" /> </div>
        </div>
       </apex:facet>
      </apex:actionStatus>
     </apex:outputPanel>
<!-- ----------------------- -->
       <apex:pageBlock mode="edit" id="results">
<!-- LIST with ACCOUNTS -->
<!------------------------>
       <apex:PageBlockTable value="{!lstAccounts}" var="obj">
<!-- Link View Account -->
<!-- ----------------- -->
        <apex:column headerValue="">
         <apex:outputLink styleClass="linkSearchResult" target="_top" value="/{!obj.ID}?retURL=%2F{!obj.id}" rendered="{!obj.OwnerId = idCurrentUser}">{!$Label.f42_Search_View}</apex:outputLink>
        </apex:column>
<!-- Get fields for displaying from FieldSet -->
<!------------------------>
        <apex:repeat value="{!$ObjectType.Account.FieldSets.SearchForm}" var="item">
         <apex:column >
          <apex:facet name="header">
           <apex:commandLink value="{!$ObjectType.Account.fields[item].label}" action="{!toggleSort}" rerender="results" status="ajaxLoading">
            <apex:param name="sortField" value="{!item}" assignTo="{!sortField}"/>
           </apex:commandLink>
          </apex:facet>
          <apex:outputField value="{!obj[item]}" />
         </apex:column>
        </apex:repeat>
       </apex:PageBlockTable>
      </apex:pageBlock>
     </td>
    </tr>
   </table>
   <apex:pageBlock title="Debug - SOQL" id="debug" rendered="false">
    <apex:outputText value="{!debugSoql}" /> 
  </apex:pageBlock>
 </apex:form>

</apex:page>

------------------------------- CONTROLLER -------------------------------

public without sharing class f42_Search_Controller {
public List<Account> lstAccounts {get;set;} // the collection of accounts to display
public String idCurrentUser{get; set;}
public Integer numberSearchResults{get; set;}
// PRIVATE --------------------------------------------------
private String API_NAME_ACCOUNT = 'Account';
private String STANDARD_ACCOUNT_SORTFIELD = 'Name';
//-----------------------------------------------------------
private String soqlInit;
private String soql; // the soql without the order and limit
private map<String, Schema.SObjectField> mapFieldnamesAccount;
//-----------------------------------------------------------
public Boolean runIntoException = false; // used by Test Class in order to run into exceptions and reach more coverage
/************************
CONSTRUCTOR
***/
// init the controller and display some sample data when the page loads
public f42_Search_Controller() {
idCurrentUser = UserInfo.getUserId();
// get all field names and build a query
mapFieldnamesAccount = f42_Helper.getFieldMapFromObject(API_NAME_ACCOUNT);
soqlInit = 'SELECT ' + f42_Helper.getFieldsAsStringFromObject(mapFieldnamesAccount) + ' ' +
'FROM ' + API_NAME_ACCOUNT + ' ' +
'WHERE IsDeleted=false';
soql = soqlInit;
}


/***********************
sortDir
***/
// the current sort direction. defaults to asc
public String sortDir {
get{
if(sortDir == null)
sortDir = 'asc';
return sortDir;
}set;
}
/***********************
sortField
***/
// the current field to sort by. defaults to Account Name
public String sortField {
get{
if(sortField == null)
sortField = STANDARD_ACCOUNT_SORTFIELD;
return sortField;
}set;
}
/***********************
debugSoql
***/
// format the soql for display on the visualforce page
public String debugSoql {
get{return soql + ' order by ' + sortField + ' ' + sortDir;}
set;
}
/***********************
toggleSort
***/
// toggles the sorting of query from asc<-->desc
public void toggleSort() {
// simply toggle the direction
sortDir = sortDir.equals('asc') ? 'desc' : 'asc';
// run the query again
runQuery();
}
/***********************
runSearch
***/
// build search string with parameters passed via Javascript
public PageReference runSearch() {
soql = soqlInit;
// URL Parameters
Map<String, String> mapParam = Apexpages.currentPage().getParameters();
if(mapParam != null){
for(String param : mapParam.keySet()){
// find the parameter in Account Fields Map
if(mapFieldnamesAccount.containsKey(param.toLowerCase())){
String val = mapParam.get(param);
if(!val.equals(''))
soql += ' AND ' + param + ' LIKE \'' + String.escapeSingleQuotes(val) + '%\'';
}
}
}
// run the query again
runQuery();
return null;
}
/***********************
runQuery
***/
// runs the actual query
private void runQuery() {
try {
if(runIntoException)
soql += 'invalid soql param';
lstAccounts = Database.query(soql + ' order by ' + sortField + ' ' + sortDir);
numberSearchResults = lstAccounts.size();
if(numberSearchResults >= f42_Helper.MAX_RECORDS_DISPLAY){
list<Account> lstTemp = new list<Account>();
for(Account acc : lstAccounts){
if(lstTemp.size() >= f42_Helper.MAX_RECORDS_DISPLAY)
break;
else
lstTemp.add(acc);
}
lstAccounts.clear();
lstAccounts.addAll(lstTemp);
ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.Info, LABEL.f42_Search_ToManyResult));
}
}catch (Exception e) {
ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR, 'ERROR: ' + e.getMessage()));
}
}

}


------------------------------- TEST CLASS -------------------------------

@isTest
private class f42_Test_Search_Controller {
private static Integer maxAccount = f42_Helper.MAX_RECORDS_DISPLAY + 1;
static testMethod void testSearchForm() {
// List with accounts (more than allowed for the search)
List<Account> lstAccounts = new list<Account>();
for(Integer i=0; i<=maxAccount; i++ ){
Account a = f42_TestDataGenerator.createTestAccount(i, false);
lstAccounts.add(a);
}
insert lstAccounts;
test.startTest();
//******* CHECK
system.assertnotequals(null, lstAccounts[0].id);
// init controller
f42_Search_Controller cnt = new f42_Search_Controller();
// load the search page
Pagereference pg = Page.f42_Search;
Test.setCurrentPage(pg);
// test with to many accounts
cnt.runSearch();
//******* CHECK
for(Apexpages.Message msg : ApexPages.getMessages()){
system.assertequals(Label.f42_Search_ToManyResult, msg.getDetail());
}
// Number of displayed accounts is reduced to the defined max number
system.assertEquals(f42_Helper.MAX_RECORDS_DISPLAY, cnt.lstAccounts.size());
// search for just one account
pg.getParameters().put('Name', lstAccounts[0].Name);
cnt.runSearch();
//******* CHECK
system.assertEquals(1, cnt.lstAccounts.size());
// run other methods to reach more test coverage
cnt.toggleSort();
String s = cnt.debugSoql;
cnt.runIntoException = true;
cnt.runSearch();
test.stopTest();
}
}

------------------------------- CLASS f42_TestDataGenerator -------------------------------

public with sharing class f42_TestDataGenerator {
// Account
public static Account createTestAccount(Integer i, Boolean insertObject){
Account acc = new Account();
acc.Name = 'TestAccount' + i;
if(insertObject) insert acc;
return acc;
}
}

------------------------------- CLASS f42_Helper -------------------------------
public with sharing class f42_Helper {
public static Integer MAX_RECORDS_DISPLAY = 250; // max number of search results for the page "f42_Search"
/********************
getFieldMapFromObject
***/
public static Map<String, Schema.SObjectField> getFieldMapFromObject(String objName){
return Schema.getGlobalDescribe().get(objName).getDescribe().fields.getMap();
}
/********************
getFieldListFromObject
***/
public static String getFieldsAsStringFromObject(Map<String, Schema.SObjectField> mapFields){
String strFields='';
for ( String f : mapFields.keySet() ){
strFields += f;
strFields += ', ';
}
strFields = strFields.subString(0,strFields.Length() -2);
return strFields;
}
}

Kommentare

Beliebte Posts aus diesem Blog

Salesforce.com Certified Advanced Administrator (WI15)

Im Vergleich zu den beiden Consultants Zertifizierungen war die Advanced Admin ein Spaziergang!
Allerdings ganz ohne Vorbereitung schafft man die Prüfung auch nicht.


Hier sind einige Fragen/Antworten aus der aktuellen Prüfung.

1. Capabilities of collaborative forecasting?
- Rename categories
- Forecast using opportunity splits
- Overlay Quota
- Add categories
- Select a default forecast currency setting

2. We have four Sales Regions. Each of the region's VPs wants to have a dashboard emailed every Monday.
- Create a separate dashboard for each VP
- Create one dashboard using Visualforce
- Create one dashboard that includes a filter for each region
- Create a reporting snapshot

3. User is trying to access content in a library, but receiving an insufficent privileges message.
How to troubleshoot?
- has the user's profile the CRM content permission enabled
- user has been granted permission "Viewer" to the library
- user's record has the CRM content permisson enabled
- …

Salesforce - Datensätze sperren (Umsetzung)

Record Lock - nun, die Umsetzung.Wir haben bereits mehrere VisualForce Pages im Einsatz. Aus dem Grund ist das zuvor beschriebene Konzept darauf zugeschnitten. Selbstverständlich lässt sich der Code auch für Standard und Custom Objects verwenden, ohne den Einsatz von VisualForce Pages. Na ja, mindestens eine VF Page brauchen wir eigentlich schon - diejenige, die von dem dem "Edit" Button aufgerufen wird.

Ich zeige jetzt die wichtigsten Bausteine aus der Umsetzung.
1) Diese VF Page wird aufgerufen sobald man den "Edit" Button betätigt

<apex:pagestandardController="myObject"title="{!myObject.Name}"extensions="LockController"action="{!SwitchToPage}">
<!-- READ MODE --> <apex:includepageName="myObjectRead"rendered="{!isEditMode == false}"/>
<!-- EDIT MODE --> <apex:includepageName="myObjectEdit"rendered="{!isEditMode == true}"/>
</apex:page>

Folgendes passiert …

Emails mit APEX | SINGLE_EMAIL_LIMIT_EXCEEDED

Email-Versand mit Visualforce und Apex kann schon manchmal Kopfschmerzen bereiten....

Entweder ist die Email zu groß, oder zu viele Attachments oder der HeapSize "beschwert sich" oder, wenn letztendlich alles läuft,  man wird von den Salesforce Limits zurück zum Ausgangspunkt katapultiert.

Das kann doch nicht so schwer sein!
Ist es auch nicht, wenn man das schon einmal gemacht hat ;-)

Habe eine Visualforce Maske zum Versenden von HTML Emails programmiert.
Diese werden als SingleEmailMessage gesendet:
Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
Sobald die Anzahl der pro Tag gesendeten Emails den Salesforce Limit "knackt", wird dem User die entsprechende Meldung eingeblendet.



Die auf die Org bezogene Limitierung lässt sich mit LIMITS.getLimitEmailInvocations() berechnen.

Hier ein Auszug aus den "Execution Governors and Limits"
https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_gov_limits.htm

A Dev Edition org ha…