1. Alfredo PUMEX
Pluggable Users Module Additions for SugarCRM
Introduction / The SugarCRM way
When developing new modules for SugarCRM, a frequent requirement is to provide per-user
customizations for the new module, e.g. login credentials for some external enterprise resource.
However, customizing SugarCRMs Users module turns out to be painful, simply because the Users
module was not designed to be customization friendly!
An approach chosen by some module writers (most notably YAAI, where we @ abcona e.K. were
somewhat involved, see http://www.sugarforge.org/projects/yaai/ is to OVERWRITE some core
files in SugarCRMs Users module, providing logic for displaying and editing custom fields.
For instance, in YAAI we used to overwrite DetailView.{php,html},EditView.{php,html} and
Save.php. Obviously, this solution is rather ugly, as it involves creating a new patch release for
every SugarCRM update1, and it doesn't scale well: have two modules using this approach and you
are busted. (The YAAI manual has some elaborations on that topic).
Rumours are there will be a rewritten Users module for SugarCRM 6, but we needed a more
modular solution for SugarCRM 5.2 NOW!
To the rescue / The Alfredo way
To provide an plug-in mechanism for the Users module, we make extensive use of Alfredo Patch
and jQuery. By using jQuerys magic powers, we dynamically extend the Users module functionality
WITHOUT ALTERING A SINGLE FILE in that module.
A basic understanding of jQuery and Alfredo Patch is thus helpful to understand the following
walkthrough.
We'll outline only the basic control flow and the most important statements, so you should use
PUMEX source file as reference.
custom/modules/Users/Ext/javascript/Alfredo-ext.php
This is the entry point into the extension mechanism and invoked from Alfredo Patch. Alfredo-
ext.php takes care of including the required jQuery libraries, and then includes, depending on the
current request's action parameter, either
- DetailView-ext.js or
- EditView-ext.js
1 And one good thing about SugarCRM is that the hard-working guys at SugarCRM, Inc. provide maintenance
releases on a frequent schedule.
2. custom/modules/Users/Ext/Alfredo/DetailView-ext.js
This is the snippet of jQuery code that extends the Users module DetailView page. Essentially, it
uses jQuery magic to insert some <div> into the DOM tree and then uses an AJAX call to fetch the
extended DetailView.
Typically for jQuery, this is very compact code:
jQuery(document).ready(function() {
// Extract user id from form data
var userId = jQuery(':input[name=record]').val();
jQuery('#subpanel_list').before('<div id="Alfredo_Extension_Point"></div>');
jQuery('#Alfredo_Extension_Point').load('index.php?
module=Users&entryPoint=extend_detail_view&record=' + userId);
});
Please note the use of SugarCRM's entryPoint mechanism; the actual mapping from entryPoints to
executed scripts is defined in entry_point_registry.php.
custom/modules/Users/Ext/Alfredo/ajax/ExtendDetailView.php
This is the server-side AJAX script which creates the additional HTML to display custom
parameters in the DetailView.
Basically, it sets up a SugarSmarty template and then renders an arbitrary number of DetailView.tpl
templates found at a well-known location:
$sugar_smarty = new Sugar_Smarty();
$sugar_smarty->assign('MOD', $mod_strings);
$sugar_smarty->assign('APP', $app_strings);
$sugar_smarty->assign('APP_LIST', $app_list_strings);
$sugar_smarty->assign('USER', $cUser);
foreach (glob("custom/modules/Users/Ext/Alfredo.Ext.Dir/*") as $extDir) {
if (is_dir($extDir)) {
$GLOBALS['log']->fatal("Using extensions from $extDir");
$sugar_smarty->display($extDir . '/DetailView.tpl');
}
}
Please note that all the custom properties for User are automagically set up by the SugarCRM
framework, and thus available through the $cUser variable (resp. {$USER->....} in the
templates).
3. custom/modules/Users/Ext/Alfredo/EditView-ext.js
This is somewhat similar to the DetailView case, again we use jQuery magic to modify the DOM
tree:
jQuery(document).ready(function() {
// Insert extension point into main form...
var mainForm = jQuery('#EditView');
var userId = jQuery(':input[name=record]', mainForm).val();
mainForm.find("div[id]:last").after('<div id="Alfredo_Extension_Point"></div>');
// Then use AJAX wizardry to load extended settings
jQuery('#Alfredo_Extension_Point').load('index.php?
module=Users&entryPoint=extend_edit_view&record=' + userId);
//
// Finally, hook into sumbit handler
// Intercept standard 'Save' action with own
//
mainForm.submit(function(e) {
var currentAction = jQuery(':input[name=action]', this);
if (currentAction.val() == 'Save') {
currentAction.val('AlfredoDispatch');
}
var action = jQuery(':input[name=action]', this).val();
return true;
});
});
However, this time we also changed the forms action from the customary Save into
AlfredoDispatch! By using this trick, we may safely intercept the EditView's save action without
actually touching Save.php.
custom/modules/Users/AlfredoDispatch.php
This is the modified save action for the Users module EditView form. To perform its duties, we first
do a loopback call into Save.php, using PHPs curl library:
$curlHandle = curl_init($loopbackURL);
…
curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $encodedArgs);
…
$curlResult = curl_exec($curlHandle);
4. $httpRC = curl_getinfo($curlHandle, CURLINFO_HTTP_CODE);
…
On success of the save action, an instance of the User bean is instantiated and passed to all custom
save actions, using the same globbing mechanism we have seen before:
$focus = new User();
$id = isset ($_POST['record']) ? $_POST['record'] : $continuationArgs['record'];
$focus->retrieve($id);
$log->fatal('_________________ @ ' . __FILE__);
$log->fatal(" Retrieved User bean for id=$id");
//
// Gather extended save actions
//
foreach (glob("custom/modules/Users/Ext/Alfredo.Ext.Dir/*") as $extDir) {
if (is_dir($extDir)) {
$extSaveFile = $extDir . "/save_params.php";
if (file_exists($extSaveFile)) {
$log->fatal(" Using extended save action: $extSaveFile");
$rc = include($extSaveFile);
$log->fatal('_________________ @ ' . __FILE__);
$log->fatal(" rc:$rc");
}
}
}
$focus->save();
Finally, the updated bean is again persisted to save our changes.
5. Tutorial: Alfredofying the YAAI module
As mentioned in the introduction, YAAI used to use the ill-fated approach of overwriting
SugarCRMs core files for customization of the Users module. Now, with PUMEX in our toolchest,
it should be easy to cure that! Let's get started:
Creating the extension folder
Remember? We need a folder, containing exactly 3 files, that will become our PUMEX extension.
We'll gonna create them at the root level of my modules source folder:
We'll leave the content of the files empty now, they are filled with live soon....
Surely I dont forget to add the new files to manifest.php:
6. Fixing the DetailView
To augment the DetailView in the Users module, we previously had to overwrite DetailView.php
and DetailView.html, where DetailView.html is really a Xtemplate template (sic).
DetailView.php takes care of setting the templates parameters from the User bean, thus we had to
copy&paste this snippet of code somewhere inside DetailView.php:
Likewise, we had to extend DetailView.html to show YAAI's custom parameters:
With Alfredo PUMEX, we can neatly replace that cut&paste approach by putting the template code
in DetailView.tpl. Unfortunately, we cannot simply copy this code, as the Users module uses the
legacy Xtemplate library, while Alfredo uses the newer Smarty templates, also favored by
7. SugarCRM.
Thus, our new DetailView.tpl will finally look like this:
<p>
<!-- Asterisk refactored -->
<table width="100%" border="0" cellspacing="0" cellpadding="0" class="tabDetailView">
<tr>
<th align="left" class="tabDetailViewDL" colspan="3" width="100%">
<h4 class="tabDetailViewDL">{$MOD.LBL_ASTERISK_OPTIONS_TITLE}</h4>
</th>
</tr>
<tr>
<td class="tabDetailViewDL" style="width: 15%">{$MOD.LBL_ASTERISK_EXT}</td>
<td class="tabDetailViewDF" style="width: 15%">{$USER->asterisk_ext_c}</td>
<td class="tabDetailViewDF">{$MOD.LBL_ASTERISK_EXT_DESC}</td>
</tr>
<tr>
<td class="tabDetailViewDL">{$MOD.LBL_ASTERISK_INBOUND}</td>
<td class="tabDetailViewDF"><input class="checkbox" type="checkbox" disabled {if
$USER->asterisk_inbound_c}checked{/if}/></td>
<td class="tabDetailViewDF">{$MOD.LBL_ASTERISK_INBOUND_DESC}</td>
</tr>
<tr>
<td class="tabDetailViewDL">{$MOD.LBL_ASTERISK_OUTBOUND}</td>
<td class="tabDetailViewDF"><input class="checkbox" type="checkbox" disabled {if
$USER->asterisk_outbound_c}checked{/if}/></td>
<td class="tabDetailViewDF">{$MOD.LBL_ASTERISK_OUTBOUND_DESC}</td>
</tr>
</table>
</p>
There are a few points worth noting:
• There is no counterpart for the olde DetailView.php!
The setup of template variables is automagically done by PUMEX; notably we have the
standard variables in the templates scope:
$USER Focused user
$MOD Module strings
$APP Application strings
Also the necessary translation from config values (0,1) to HTML 'checked' attribute is no
done in the template.
• Note the different Syntax for $USER (An User Object) and $MOD/$APP (plain arrays).
Fixing the EditView
This is very similar to the DetailView case, however this time we have to write a little snippet of
8. code to save our custom settings.
Again, let's start with the template code we have in EditView.html:
Translated in Smartyspeak this becomes
<div id="asterisk">
<table width="100%" border="0" cellspacing="0" cellpadding="0" class="tabForm">
<tr>
<th align="left" class="dataLabel" colspan="3">
<h4 class="dataLabel">{$MOD.LBL_ASTERISK_OPTIONS_TITLE}</h4>
</th>
</tr>
<tr>
<td width="15%" class="dataLabel" valign="top"><slot>{$MOD.LBL_ASTERISK_EXT}</slot></td>
<td width="15%" class="dataField"><slot><input type="text" tabindex='3'
name="asterisk_ext_c" value="{$USER->asterisk_ext_c}" size="3"></slot></td>
<td class="dataField"><slot>{$MOD.LBL_ASTERISK_EXT_DESC}</slot></td>
</tr>
<tr>
<td class="dataLabel" valign="top"><slot>{$MOD.LBL_ASTERISK_INBOUND}</slot></td>
<td class="dataField"><slot><input type="checkbox" tabindex='3'
name="asterisk_inbound_c" value="1" {if $USER->asterisk_inbound_c}checked{/if}></slot></td>
<td class="dataField"><slot>{$MOD.LBL_ASTERISK_INBOUND_DESC}</slot></td>
</tr>
<tr>
<td class="dataLabel" valign="top"><slot>{$MOD.LBL_ASTERISK_OUTBOUND}</slot></td>
<td class="dataField"><slot><input type="checkbox" tabindex='3'
name="asterisk_outbound_c" value="1" {if $USER->asterisk_outbound_c}checked{/if}></slot></td>
<td class="dataField"><slot>{$MOD.LBL_ASTERISK_OUTBOUND_DESC}</slot></td>
</tr>
</table>
</div>
No big surprises here, the most important difference is that the checkboxes are now enabled...
9. We need to make a final move to deal with the checkboxes, though.
Put this in save_params.php to translate the values passed for the checkboxes into {0,1} values:
<?php
global $log;
$log->fatal('_________________ @ ' . __FILE__);
$log->fatal('Working with User bean:' . $focus->id);
$focus->asterisk_inbound_c = (isset($_REQUEST['asterisk_inbound_c']) &&
$_REQUEST['asterisk_inbound_c']) ? 1 : 0;
$focus->asterisk_outbound_c = (isset($_REQUEST['asterisk_outbound_c']) &&
$_REQUEST['asterisk_outbound_c']) ? 1 : 0;
return 0;
?>
Replacing the logic hook
YAAI uses a logic hook to inject Javascript code into all of SugarCRMs generated pages.
Here is the hooks' code which conditionally emits some Javascript:
function echoJavaScript($event,$arguments){
// asterisk hack: include ajax callbacks in every sugar page except ajax requests:
if(empty($_REQUEST["to_pdf"])){
if(isset($GLOBALS['current_user']->asterisk_ext_c) && ($GLOBALS['current_user']-
>asterisk_ext_c != '') && (($GLOBALS['current_user']->asterisk_inbound_c == '1') ||
($GLOBALS['current_user']->asterisk_outbound_c == '1'))){
echo '<script type="text/javascript"
src="include/javascript/jquery/jquery.pack.js"></script>';
echo '<link rel="stylesheet" type="text/css" media="all"
href="modules/Asterisk/include/asterisk.css">';
if($GLOBALS['current_user']->asterisk_inbound_c == '1')
echo '<script type="text/javascript"
src="modules/Asterisk/include/javascript/dialin.js"></script>';
if($GLOBALS['current_user']->asterisk_outbound_c == '1')
echo '<script type="text/javascript"
src="modules/Asterisk/include/javascript/dialout.js"></script>';
}
}
}
Of course we want to use Alfredo Patch for that! So we need to drop an include file for Alfredo
Patch into its global extension folder at custom/application/Ext/javascript/ . Lets call it YAAI-
ext.php:
10. And its content is as simple as this:
<?php
require_once 'custom/include/javascript/jquery/1.3/include-core.php';
echo "<link rel='stylesheet' type='text/css' media='all'
href='modules/Asterisk/include/asterisk.css'>n";
?>
Remember to use require_once(), if at all possible to avoid duplicate inclusion of Javascript code!
Again, we add this new file to manifest.php:
And we're done!