Validation in before_save

Hello.

I have a custom module and I want to perform some validation (duplicate checking) when a record is saved and if the validation fails, the user should remain on the edit view with an error message.

I have tried using the before_save hook, but the results have been less than what I would have hoped for. The issue is that the SugarBean save() method that calls the hook, simply carries on:

$this->call_custom_logic("before_save", $custom_logic_arguments);

This means that I have to do something unpleasant in my hook code, either throwing an exception or executing a die(). However, in the first case, I just get a blank screen and in the second case I get a screen containing just the message from the die() command. Neither of which are acceptable.

I am considering adding some client side Javascript to perform the validation, but that strikes me as insecure.

Does anyone have any suggestions?

Thanks,

Carl

You can use

unset($bean); 

to clean the variables

Then use the redirect method:

SugarApplication::redirect("index.php?module=zz_vat_codes&action=EditView)

Hello.

Thanks for the reply. I will have a look at that. However, I don’t think that will work in all cases. For example, my custom module has a many-to-one relationship with Account. This means that when I am viewing an Account, I can create one of my custom records in a subpanel. Looking at the code in SubPanelTiles.js, method inlineSave, it always seems to ignore the result of the save operation (unless it is returning a dupl status). The means that even if I do the redirect, the “create” subpanel will close, with no indication that there was a problem.

Regards,

Carl

The logic hook will be called every time the module is called. Doesn’t mater if it is from a panel of a view. It will run even if you call the module from another logic hook.

Hello.

Yes, the hook will be called and the record won’t be created, but it wouldn’t exactly be the best user experience, since there will be no message to say why the record wasn’t created. The user will just be taken back to the list of records and they may not notice that it wasn’t created.

The trouble is that I am having a hard time seeing how I can make this work without junking a big chunk of SuiteCRM code. How do other people deal with the issue of validating? It can’t be all done client-side, since that is insecure.

Regards,

Carl

You can use this method to present a message to the user:


SugarApplication::appendErrorMessage(' ERROR my custom message!!!!');

Hello.

Unfortunately, the changes you have suggested don’t appear to have any effect. My hook code is:


<?php

if (!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');

class WFAU1_ValidateBeforeSave {

    function validateBeforeSave ($bean, $event, $arguments) {
        $GLOBALS['log']->debug('WFAU1_ValidateBeforeSave: Start'); 

        if (is_null($bean->id)) {
            $beanList = $bean->get_list(
                                'wfau1_wmsaccount.name', // order
                                "wfau1_wmsaccount.name = '" . $bean->name ."'",
                                0, // offset
                                1 // max items
            );
        } else {
            $beanList = $bean->get_list(
                                'wfau1_wmsaccount.name', // order
                                "wfau1_wmsaccount.name = '" . $bean->name ."' and wfau1_wmsaccount.id != '" . $bean->id . "'",
                                0, // offset
                                1 // max items
            );
        }

        if ($beanList['row_count']>0) {
            $GLOBALS['log']->debug('WFAU1_ValidateBeforeSave: Duplicate found'); 
            SugarApplication::appendErrorMessage('Duplicate account id');
            SugarApplication::redirect("index.php?module=&action=EditView");
        }

        $GLOBALS['log']->debug('WFAU1_ValidateBeforeSave: End'); 

    }
}

I have attached pictures showing what I am doing. In step 1, I am viewing an Account record and have gone to the sub-panel for my custom module.

In step 2, I have clicked on the “Create” button to bring up the quick create form.

If I enter a duplicate account id, and click “Save”, I get taken back to the same sub-panel as in step 1, showing only a single record but no error message.

Regards,

Carl

I see an error on the redirect. It needs to include the module name:

SugarApplication::redirect(“index.php?module=YOURMODULE&action=EditView”);

Hello.

Yes, I noticed that and changed it, but still no difference. As I said, I believe the fundamental problem is that when you click “Save”, it makes an AJAX call and then does exactly the same thing regardless of whether the call succeeds or fails:


    inlineSave: function (theForm, buttonName) {
      var saveButton = document.getElementsByName(buttonName);
      for (var i = 0; i < saveButton.length; i++) {
        saveButton[i].disabled = true;
      }
      ajaxStatus.showStatus(SUGAR.language.get('app_strings', 'LBL_SAVING'));
      var success = function (data) {
        var module = get_module_name();
        var id = get_record_id();
        var layout_def_key = get_layout_def_key();
        try {
          SUGAR.util.globalEval('result = ' + data.responseText);
        } catch (err) {
        }
        if (typeof(result) != 'undefined' && result != null && result['status'] == 'dupe') {
          document.location.href = "index.php?" + result['get'].replace(/&amp;/gi, '&').replace(/&lt;/gi, '<').replace(/&gt;/gi, '>').replace(/&#039;/gi, '\'').replace(/&quot;/gi, '"').replace(/\r\n/gi, '\n');
          for (var i = 0; i < saveButton.length; i++) {
            saveButton[i].disabled = false;
          }
          return;
        } else {
          SUGAR.subpanelUtils.cancelCreate(buttonName);
          // parse edit form name in order to get the name of
          // module which saved item belongs to
          var parts = theForm.split('_');
          var savedModule = '';
          var subPanels = [];
          for (var i = parts.length - 1; i >= 0; i--) {
            if (parts[i] == '') {
              continue;
            }
            if (savedModule != '') {
              savedModule = '_' + savedModule;
            }
            savedModule = parts[i] + savedModule;
            if (window.ModuleSubPanels && window.ModuleSubPanels[savedModule]) {
              subPanels = subPanels.concat(window.ModuleSubPanels[savedModule]);
            }
          }
          for (var i = 0; i < subPanels.length; i++) {
            showSubPanel(subPanels[i], null, true);
          }
          ajaxStatus.showStatus(SUGAR.language.get('app_strings', 'LBL_SAVED'));
          window.setTimeout('ajaxStatus.hideStatus()', 1000);
          for (var i = 0; i < saveButton.length; i++) {
            saveButton[i].disabled = false;
          }
        }
      }

      YAHOO.util.Connect.setForm(theForm, true, true);
      var cObj = YAHOO.util.Connect.asyncRequest('POST', 'index.php', {
        success: success,
        failure: success,
        upload: success
      });
      return false;
    },

I think the only way around this is to disable the quick create screen and get the other edit screen to work correctly.

Regards,

Carl

Hello.

I have managed to create a work-around that works for the full form, but not for the quick create form. I created a custom controller and overrode the save() method:


class CustomWFAU1_WMSAccountController extends SugarController
{

    public function action_save()
    {
        try {
            parent::action_save();
        } catch (Exception $ex) {
            $this->action = 'EditView';
            $this->view = 'edit';
            $_REQUEST['action'] = 'EditView';
            SugarApplication::appendErrorMessage($ex->getMessage());
        }
    }

}

When an exception is thrown in the parent save() method (due to my before_save logic hook), it displays the edit screen with the fields populated and the error message at the top of the page.

It seems to work in the limited tests I have done.

Regards,

Carl

I’m glad you make it work. Thanks for sharing.

Hello.

Unfortunately I have uncovered a fatal flaw in my approach. The problem is that the “before_save” hook is called after any relation records have been inserted. This means that after the exception has been thrown, although the actual record is not saved, there are still orphan relation records left behind.

As such I have had to take a different approach. The first set of steps apply to the application as a whole, so need to be put somewhere under custom/Extension/application

  1. Create a new class that extends Exception so that the field name can be specified:

<?php
if (!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');

class WF1_FieldValidationException extends Exception {

    private $fieldName;

    public function __construct($fieldName, $message, $code = 0, Exception $previous = null) {
        parent::__construct($message, $code, $previous);

        $this->fieldName = $fieldName;
    }

    public function getFieldName() {
        return $this->fieldName;
    }
}
  1. Create a class that extends Basic that overrides the save() method to call a new hook, called “validate_before_save”, then run the parent save() method:

<?php
if (!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');

class WF1_Basic extends Basic {

        public function save($check_notify = false) {
            $this->call_custom_logic("validate_before_save");
            parent::save($check_notify);
        }
}
  1. Create a class that extends SugarController and overrides the action_save() method to handle exceptions, causing the edit view to be displayed. It also stores the name of the field that failed validation in the request, for later use:

<?php
if(!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');

require_once('.../WF1_FieldValidationException.php');

class WF1_Controller extends SugarController
{

    public function action_save()
    {
        try {
            parent::action_save();
        } catch (WF1_FieldValidationException $fvex) {
            $this->action = 'EditView';
            $this->view = 'edit';
            $_REQUEST['action'] = 'EditView';
            $_REQUEST[$fvex->getFieldName() . 'Error'] = $fvex->getMessage();

        } catch (Exception $ex) {
            $this->action = 'EditView';
            $this->view = 'edit';
            $_REQUEST['action'] = 'EditView';
            SugarApplication::appendErrorMessage($ex->getMessage());
        }
    }
}

The following steps must be implemented for each module that you want to do server-side validation with:

  1. Change the module bean to extend the custom Basic bean created above, e.g. modules/<module_name>/<module_name>.php, change:

class <module_name> extends Basic

to


require_once('.../WF1_Basic.php');
class <module_name> extends WF1_Basic
  1. Implement your validation logic hook, throwing a WF1_FieldValidationException on a validation error:

    .
    .
    .
    throw new WF1_FieldValidationException("<field_name>", "<error_message>");
    .
    .
    .
  1. Register the logic hook, in custom/Extension/modules/<module_name>/Ext/LogicHooks:

<?php
$hook_version = 1;
$hook_array = Array();
$hook_array['validate_before_save'][] = Array(
    1,	
    '<operation_name>',
    '.../<logic_hook_file_name>',
    '<logic_hook_class_name>',
    '<logic_hook_method_name>'
);

Note that the hook name is ‘validate_before_save’.

  1. Create a custom controller that extends the WF1_Controller class, called custom/modules/<module_name>/controller.php:

<?php
if(!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
require_once('.../WF1_Controller.php');
class Custom<module_name>Controller extends WF1_Controller
{
}

Note that this is deliberately empty, since the parent class overrides the action_save() method

  1. Display the error message by adding custom code to the modules/<module_name>/metadata/editviewdefs.php, changing:

    .
    .
    .
        array (
          0 => 'name',
    .
    .
    .

to


    .
    .
    .
        0 => array(
          '<field_name>' => '<field_name>',
          'customCode' => '{if !empty($smarty.request.<field_name>Error)}{literal}<script type="text/javascript">$(window).bind("load", function() {       add_error_style("EditView", "<field_name>", "{/literal}{$smarty.request.<field_name>Error}{literal}");})</script>{/literal}{/if}',
        'customCodeRenderField' => true,
        ),
    .
    .
    .
  1. Disable the “quick create” screen" in custom/Extension/modules/<parent_module>/Ext/Layoutdefs/<module_name>_<parent_module_name>.php by changing:

.
.
.
'top_buttons' =>
array (
  0 =>
  array (
    'widget_class' => 'SubPanelTopButtonQuickCreate',
  ),
.
.
.

to


.
.
.
'top_buttons' =>
array (
  0 =>
  array (
    'widget_class' => 'SubPanelTopCreateButton',
  ),
.
.
.

The upshot of all this, is that when you edit your record and submit it, if the record fails validation, you will be taken to the edit form, with the field that failed validation highlighted and the error message shown below it, just like client-side validation.

Hopefully this helps someone else in the future.

Regards,

Carl

1 Like

Hello, awesome!
I implemented the solution, I have a problem, can you help me?

When the page is redirected, the time attributes are not saved, but are displayed on the screen. How do I resolve this?