I have a SuiteCRM alert being shown 3 times when it is supposed to be shown once after a background task is completed. If you can see something I am doing wrong, please let me know.
Sorry for the long post; I was going to just post edited (shortened) versions of the code, but thought that would cause more confusion than the full code. If that is wrong, I can re-do with shortened code.
SuiteCRm v7.13 on Debian 11
I am sending a job to the background scheduler. The job could (and often does) affect over 1,000 records so I want the user freed up so they can continue working with the system while the job runs.
One the job is finished, I use
BeanFactory::newBean('Alerts');
to let the user know the job is complete.
Problem is, the alert is sent 3 times. It is sent 3 times whether there are 2 or 1,000 records affected.
The job sucessfully runs and the records are properly updated. It is just the multiple alerts being generated that I would like to resolve.
If it helps, this is for a customized version of SuiteCRM v7 used by my Rotary to track donations for our various fund-raising efforts. We have annual fund-raising projects (Golf tournament, evening gala with Lobster dinner and dancing, …) and we want the Accounts and Contacts who have supported us to have a flag set, identifying what annual projects they have supported. I added checkboxes to each Account and Contact record for each project. Yes, I know we can get the information using Reports, but the person we have dong the data entry likes this way and I want to keep her happy.
I am sure I am doing something wrong in my sequence or call to the background scheduler, but I have tried and cannot see the error. I have noticed that when you put javascript in a file and source it from within the custom html (used to create a new menu), the javascript file sometimes gets called more than once. This does not happen if you embed the javascipt in tags in the html but I am not sure of that is the issue here.
If anyone has suggestions, please let me know.
Code shown below
Any suggestions would be appreciated.
The background task is triggered from a custom controller at
custom/modules/FP_events/controller.php
<?php
if(!defined('sugarEntry') || !sugarEntry) {
die('Not A Valid Entry Point');
}
class CustomFP_eventsController extends SugarController
{
public function action_getmatchingids() {
$sourceKey = filter_var( $_COOKIE['source_key'] , FILTER_SANITIZE_SPECIAL_CHARS , FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK );
// print "The Source is ". $sourceKey . "<br/>";
$eventIdList = filter_var( $_REQUEST['uid'] , FILTER_SANITIZE_SPECIAL_CHARS , FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK );
if ( !empty( $eventIdList )) {
// This system will be storing thousands of records
// and this logic hook will be looping through them one at a time
// to calculate the totals for all associated records
// Rather than run the code real-time inside this logic hook
// which would take time and impact the user interaction
// this logic hook creates a background job (Scheduled Task)
// so the process works in the background
// allowing the user interaction to proceed unaffected
// while the code executes
require_once 'include/SugarQueue/SugarJobQueue.php';
$setSourceForLinkedRecords = new SchedulersJob();
// Give it a name that will be clear in the Scheduler list what it is for
$setSourceForLinkedRecords->name = "Set Source for Accounts and Contacts linked to this Event";
// Jobs need an assigned user in order to run
// I like to use the id of the person requesting the job
global $current_user;
$setSourceForLinkedRecords->assigned_user_id = $current_user->id;
// Pass the information that our job will need
$sourceKey = filter_var( $_COOKIE['source_key'] , FILTER_SANITIZE_SPECIAL_CHARS , FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK );
$eventIdList = filter_var( $_REQUEST['uid'] , FILTER_SANITIZE_SPECIAL_CHARS , FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK );
$setSourceForLinkedRecords->data = json_encode(array(
'module' => 'FP_events',
'id' => $eventIdList,
'sourcekey' => $sourceKey,
)
);
// Tell the scheduler what class to use
$setSourceForLinkedRecords->target = "class::SetSourceForLinkedRecords";
// Mark this job for 2 retries
// This is optional and can be left out if not needed
// I leave it in
$setSourceForLinkedRecords->requeue = true;
$setSourceForLinkedRecords->retry_count = 2;
$jobQueue = new SugarJobQueue();
$jobQueue->submitJob($setSourceForLinkedRecords);
}
SugarApplication::redirect('index.php?module=FP_events');
// sugar_die('');
return true;
}
}
The scheduler is setup at
custom/Extension/modules/Schedulers/Ext/ScheduledTasks/SetSourceForLinkedRecords.php
<?php
if (!defined('sugarEntry') || !sugarEntry) {
die('Not A Valid Entry Point');
}
class SetSourceForLinkedRecords implements RunnableSchedulerJob {
public function run($queueFromEvent) {
global $app_list_strings;
// The only different part of the job code
// compared to what it would look like in a controller file
// is this part at the head of the file
// and the code at the end of this file
// to Submit the Job and Notify the user the job has finished
// Grab the bean information using the supplied arguments.
$jsonData = stripslashes(html_entity_decode($queueFromEvent));
$eventDataArray = json_decode("$jsonData",1);
// Unpack the data to identify the Bean
$moduleName = $eventDataArray['module'];
$eventIdList = $eventDataArray['id'];
$sourceKey = $eventDataArray['sourcekey'];
$sourceName = $app_list_strings['funds_source_names_list'][$sourceKey];
// After this, with the exception of the code at the end of this class
// the additional function below
// setJob(SchedulersJob $job)
// and the generation of the alert message
// the rest of this code is the same as it would have been
// had the code been directly executed from the logic hook
// instead of submitted as a job queue
if ( !empty( $eventIdList )) {
$recordIds = explode(',',$eventIdList);
foreach ( $recordIds as $recordId ) {
$eventBean = BeanFactory::getBean($moduleName, $recordId);
$eventName = $eventBean->name;
//print "Record ID {$eventBean->id}, contains <br/>";
//var_dump($eventBean);
$eventBean->load_relationship('fp_events_ayu_funds_1');
$listOfFundRaiseIDs = $eventBean->fp_events_ayu_funds_1->get();
//print "The list of linked records is <br/>";
//var_dump($listOfFundRaiseIDs);
//print "<br/>For Event " . $recordId . "<br/>";
$numChanged = 0;
$numTotal = 0;
foreach( $listOfFundRaiseIDs as $key => $fundRaiseID ) {
$numTotal = $numTotal + 1;
$fundRaiseBean = BeanFactory::getBean('AYU_Funds', $fundRaiseID);
if( $fundRaiseBean->deleted == 0 ) {
$numChanged = $numChanged + 1;
//print "<br/><br/>The FundRaise name is " . $fundRaiseBean->name. "<br/>";
//print "The full information for this linked record is <br/>";
//var_dump($fundRaiseBean);
if ( $fundRaiseBean->parent_type == "Accounts" ) {
//print"This is an Account"<br/>";
$accountBean = BeanFactory::getBean('Accounts', $fundRaiseBean->parent_id);
if ( $accountBean->$sourceKey == 0 ) {
// print $accountBean->name." was not set<br/>";
$accountBean->$sourceKey = 1;
$accountBean->save();
}
} else {
//print"This is a Contact"<br/>";
$contactBean = BeanFactory::getBean('Contacts', $fundRaiseBean->parent_id);
if ( $contactBean->$sourceKey == 0 ) {
// print $contactBean->first_name." ".$contactBean->last_name." was not set<br/>";
$contactBean->$sourceKey = 1;
$contactBean->save();
}
}
}
}
}
if ( $numChanged > 0 ) {
// Create a message to advise the user the job has run
global $current_user;
$alertMessage = "Of ".$numTotal." Accounts and Contacts linked to the ".$eventName." event, ".$numChanged." had to be changed to have Source: ".$sourceName." set in the Accounts and Contacts";
$receiptAlert = BeanFactory::newBean('Alerts');
//$receiptAlert->name = "Source set for Accounts and Contacts";
$receiptAlert->description = $alertMessage;
$receiptAlert->assigned_user_id = $current_user->id;
$receiptAlert->is_read = 0;
$receiptAlert->type = "info";
$receiptAlert->target_module = "FP_events";
$receiptAlert->url_redirect = "index.php";
$receiptAlert->save();
$numChanged = 0;
}
}
}
// Submit the job
// which will run in the background, not impeding user interactions
public function setJob(SchedulersJob $job) {
$this->job = $job;
}
}
Required language files have been added.
The system does work; it is just the multiple Alerts that I cannot figure out.
The controller is initiated from a javascript function which is triggered by a custom menu added to the Bulk Action dropdown menu in FP_events.
the controller file is at
custom/modules/FP_events/controller.php
<?php
if(!defined('sugarEntry') || !sugarEntry) {
die('Not A Valid Entry Point');
}
class CustomFP_eventsController extends SugarController
{
public function action_getmatchingids() {
$sourceKey = filter_var( $_COOKIE['source_key'] , FILTER_SANITIZE_SPECIAL_CHARS , FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK );
// print "The Source is ". $sourceKey . "<br/>";
$eventIdList = filter_var( $_REQUEST['uid'] , FILTER_SANITIZE_SPECIAL_CHARS , FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK );
if ( !empty( $eventIdList )) {
// This system will be storing thousands of records
// and this logic hook will be looping through them one at a time
// to calculate the totals for all associated records
// Rather than run the code real-time inside this logic hook
// which would take time and impact the user interaction
// this logic hook creates a background job (Scheduled Task)
// so the process works in the background
// allowing the user interaction to proceed unaffected
// while the code executes
require_once 'include/SugarQueue/SugarJobQueue.php';
$setSourceForLinkedRecords = new SchedulersJob();
// Give it a name that will be clear in the Scheduler list what it is for
$setSourceForLinkedRecords->name = "Set Source for Accounts and Contacts linked to this Event";
// Jobs need an assigned user in order to run
// I like to use the id of the person requesting the job
global $current_user;
$setSourceForLinkedRecords->assigned_user_id = $current_user->id;
// Pass the information that our job will need
$sourceKey = filter_var( $_COOKIE['source_key'] , FILTER_SANITIZE_SPECIAL_CHARS , FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK );
$eventIdList = filter_var( $_REQUEST['uid'] , FILTER_SANITIZE_SPECIAL_CHARS , FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK );
$setSourceForLinkedRecords->data = json_encode(array(
'module' => 'FP_events',
'id' => $eventIdList,
'sourcekey' => $sourceKey,
)
);
// Tell the scheduler what class to use
$setSourceForLinkedRecords->target = "class::SetSourceForLinkedRecords";
// Mark this job for 2 retries
// This is optional and can be left out if not needed
// I leave it in
$setSourceForLinkedRecords->requeue = true;
$setSourceForLinkedRecords->retry_count = 2;
$jobQueue = new SugarJobQueue();
$jobQueue->submitJob($setSourceForLinkedRecords);
}
SugarApplication::redirect('index.php?module=FP_events');
// sugar_die('');
return true;
}
}
and the javascript file (called with an onclick in the html used for teh new menu) is at
custom/include/ayu_scripts/ayu_modal.js
/*
* This script pops op a modal dialog window
* to confirm that the user actually wants to do
* what they have selected from the custom Set Source To: action menu choice
* This extra strep of confirmation is utilized
* since, unlike most menu selections which just present database contents
* this menu selection will change the database records
* Thanks to John Mertic and pgrod for their posts on this functionality
*/
var sugarListView;
var eventKey;
var eventName;
function displayDialog(args) {
eventKey = args.eventKey;
eventName = args.eventName;
/* console.log('The Event Name is: ' + eventName); */
let userClicked = confirm("Set " + eventName + " as a Source for all records of this event?");
if(userClicked) {
/* console.log("The user clicked OK"); */
setSource(eventKey);
sugarListView.get_checks();
/* Object.keys(this).forEach((prop)=> console.log(prop)); */
if(sugarListView.get_checks_count() < 1) {
alert("{$app_strings['LBL_LISTVIEW_NO_SELECTED']}");
return false;
}
document.MassUpdate.action.value='getmatchingids';
document.MassUpdate.submit();
return true;
} else {
/* console.log("The user clicked Cancel"); */
return false;
}
}
/*
* This enables the javascript to pass data to the php script
* This could have been done using AJAX but this is simple
* and it keeps the interaction local
*/
function setSource(sourceKey) {
document.cookie = 'source_key=' + sourceKey + '; SameSite="None;Secure"' + ' ; path=/';
/* console.log("Set the source to: " + eventName); */
return true;
}
The custom menu is added via a custom view.list file at
custom/modules/FP_events/views/view.list.php
<?php
if(!defined('sugarEntry') || !sugarEntry) {
die('Not A Valid Entry Point');
}
require_once('include/ListView/ListViewSmarty.php');
require_once('include/MVC/View/views/view.list.php');
class CustomFP_EventsViewList extends ViewList
{
/**
* @see ViewList::preDisplay()
*/
public function preDisplay()
{
parent::preDisplay();
$this->lv = new ListViewSmarty();
if( ACLController::checkAccess('FP_events', 'edit', true) ) {
// Add a menu item
$this->lv->actionsMenuExtraItems[] = $this->buildMyMenuItem();
}
}
/**
* @return string HTML
*/
protected function buildMyMenuItem()
{
global $mod_strings;
global $app_list_strings;
// Be VERY careful in the use of nested quotes
$htmlContent = <<<MENUCODE1
<div
id='confirm-set-source'
onmouseover='document.getElementById("sourceSubMenu").style.display = "block";'
onmouseout='document.getElementById("sourceSubMenu").style.display = "none";'>
<a class='menuItem'
style='width: 150px;'
href='#'
onmouseover='hiliteItem(this,"yes");'
onmouseout='unhiliteItem(this);'
>{$mod_strings['LBL_RC_NEWMENU']}
</a>
<ul
id='sourceSubMenu'
style='display: none; text-align: right;'>
MENUCODE1;
foreach($app_list_strings['funds_source_names_list'] as $sourceKey => $sourceName){
// The first entry is blank to enable no selection, so skip it
if( $sourceName != "" ){
$htmlContent .= " <li style='color: black;'\n";
$htmlContent .= " onmouseover='style=\"color: white;\";'\n";
$htmlContent .= " onmouseout='style=\"color: black;\";'\n";
$htmlContent .= " var eventKeyName = {'eventKey': \"{$sourceKey}\", 'eventName': \"{$sourceName}\"};\n";
$htmlContent .= " onclick='displayDialog( {\"eventKey\": \"{$sourceKey}\", \"eventName\": \"{$sourceName}\"} )'\n";
$htmlContent .= " >{$sourceName} \n";
$htmlContent .= " </li>\n";
}
}
$htmlContent .= <<<MENUCODE2
</ul>
</div>
<script src="custom/include/ayu_scripts/ayu_modal.js"></script>
MENUCODE2;
return $htmlContent;
}
}
Any ideas?