Thursday, April 3, 2008

Creating approval levels for records through dynamic forms

This was done on the advisorpage website. I had so much trouble figuring how to do this, but I finally got it done. Before I show you how to do it, I want to give the situation this was applied. There a bunch of companies that needed to be approved. There were separate approval levels: approved, conditionally approved, and not approved. Rather than have the user have to go into the company detail page and select approve, disapprove, and stuff. The client wanted the ability to change the approval level in list view. The default list view of the companies was changed and the list of different approved options to change the approval level was displayed in the list view.

This was done through the actions.ini file and assigning actions to the category=record_actions. The list view displays any actions under this category in a specific location specified in the template. So there are several key things to note here:


  1. All the companies information was stored in a companies table

  2. The details of approval were stored in a separate table called dealers_companies. The reason this was done was because there were different dealers for the website, and each dealer wanted the ability to either approval or not approve the company. The net result is that each approval level is specific to a certain dealer. So that there "customers" will only see what companies THEIR dealer approved of and not others.
So here are the steps I followed to get it done:

This was done using actions. In the application's actions.ini file, there was three separate actions called bd_approve, bd_approve_record and bd_conditional_approve_record. Each of these actions corresponds to a separate thing that can be done to the company record. The content of one of these actions:


[bd_approve_record]
;; This action is available to Broker Dealers to change the approval level
;; of the current record to "Approved"
label="Approve"
category=record_actions
condition="$record and $record->_table->tablename == 'companies' and isBrokerDealer() and $record->val('approval_level') < url="">getId()}');"
url_condition="$record"
permission=bd_approve
icon="images/confirm_icon.gif"
description="Approve this company and all its news to be viewed by your agents."


  1. The label attribute was used to display the text of the action

  2. category was used to group this action under the record_actions category which is used by the template to show actions under this category at a location specified

  3. condition is when this action appears. Here we say that there must be a record, the tablename must be companies, the user logged in must a broker dealer and the approval level of the record must be less than 2.

    Note that the companies doesn't include an approval level, this is actually found in a separate table called called "dealer_companies" This issue will be addressed later.

  4. url_condition is like the condition the url has to satisfy in order for this action to be present. The reason why this is needed is because the condition attribute relies on like a record...so unless there is a record it won't be able to like apply the conditions. The url condition first checks to make sure that the url has a record...something along those lines.

  5. permission is taken from the permissions.ini file. Here the user who is currently logged on must have the bd_approve permission for this action to appear.

  6. icon is the image that appears beside the label

  7. description is the text that appears when the label is mouseovered..



Here the url attribute is assigned to a javascript function called WLS_ApproveCompany(). This function is found in the script.js file in the folder js and it was included into the website in the ApplicationDelegate class by the function block__custom_javascripts():


function block__custom_javascripts(){
echo '<script src="%27.DATAFACE_SITE_URL.%27/js/scripts.js" type="text/javascript" language="javascript"></script>';
echo '<script src="%27.DATAFACE_SITE_URL.%27/js/menu.js" type="text/javascript" language="javascript"></script>';
}


If you to the script.js and find that function:


function WLS_ApproveCompany(level, id){
var form = document.getElementById('companyApprovalForm');
form.elements['--selected-ids'].value = id;
form.elements['--level'].value = level;
form.submit();
}


Here we are grabbing the html form companyApprovalForm (more on this later) assigning the elements --selected-ids and --level values to the function inputs.

The --selected-ids variable has to exactly named this way. This is because we use a Dataface function called df_get_records() which looks specify for the --selected-ids variable.

And this submitting the form using javascript. The form is located in the templates folder called companyApprovalForm.html:



{foreach from=$ENV.QUERY key=name item=val}
{if $name != '-action' and $name != '-table' and $name != '--level' and '--selected-ids'}
<input name="{$name}" value="{$val|escape:'html'}" type="hidden">
{/if}
{/foreach}
<input name="-action" value="bd_approve" type="hidden">
<input name="-table" value="companies" type="hidden">
<input name="--level" value="" type="hidden">
<input name="-from" value="{$ENV.action}" type="hidden">
<input name="--selected-ids" value="" type="hidden">
</form>


Here we are just including all the values in the query string into this form so that when we submit it they are included in the post request and can be used again. Note the --level and --selected-id elements. We changed this in the javascript function. Notice how the action of the form is itself, but there is an additional element called -action and with a value called "bd_approve." This action is in the actions folder and called bd_approve.php. It is called when the page is get reloaded because the query string will have ?-action=bd_approve.php will tells it to run this.

The key thing to note is that if something isn't in the query string it won't be put into the form. You have to include the selected_ids and level in the $name not equal because it will continue to add these hidden fields to the form when you refresh it

So I was trying to extend this to include a separate form that handled trust values. The problem was I wasn't including the approval level in the query string and so when I tried to modify the database, I was just overriding the approval level (you will see why in a bit). We need to include this form on the page of our interest. So we can use the particular delegate class of our interest, and insert this code:


function block__before_header(){
df_display(array(), 'abstract_approval_form.html');


The bd_approve.php class

class actions_bd_approve {

function handle(&$params){
$app =& Dataface_Application::getInstance();
$query =& $app->getQuery();
$records = df_get_selected_records($query);
//echo count($records).' records selected.';


$dealerid = getDealerID();


if ( !isset($dealerid) ){
return PEAR::raiseError("No dealer id set", DATAFACE_E_ERROR);
}

if ( !isBrokerDealer() ){
return PEAR::raiseError("Sorry, you must be a BD Administrator to approve companies for your BD.", DATAFACE_E_ERROR);

}

if ( !isset($_REQUEST['--level']) ){
$level = 0;
} else {
$level = $_REQUEST['--level'];
}

$sql = array();
$sql[] = "replace into dealer_companies (company_id,dealer_id,approval_level) values ";
$vals = array();

foreach ($records as $rec){
if ( $rec->_table->tablename != 'companies' ){
// we only approve companies
continue;
}

$vals[] = "('".$rec->val('company_id')."','$dealerid', '$level')";

}

if (count($vals) == 0 ) return PEAR::raiseError("No records were selected to be approved", DATAFACE_E_ERROR);

$sql[] = implode(',', $vals);
$sql = implode(' ',$sql);
$res = mysql_query($sql, df_db());
if ( !$res ) return PEAR::raiseError("A mysql error occurred while trying to approve companies: ".mysql_error(df_db()), DATAFACE_E_ERROR);

if ( isset($query['-from']) ){
$query['-action'] = $query['-from'];
} else {
$query['-action'] = 'list';
}
$url = $app->url($query).'&--msg='.urlencode('The selected companies have been succesfully approved for viewing by your representatives. '.count($vals).' companies approved.');
header("Location: $url");
exit;


}
}


The important line is the one with the sql statement

"$sql[] = "replace into dealer_companies (company_id,dealer_id,approval_level) values ";"


And then we include the values. So we've now changed the approval level. It is important to notice that any columns in that record that don't have a value will be set to their default...meaning it will get overwritten. For example, say the dealer_companies had another column called trust_level. If I just included the replace statement without specifying the trust_level, it will always set that value to the default which could be like 0.

So you have to include the trust value even if you are only modifying the approval level. The way to make the changes are:


$sql[] = "replace into dealer_companies (company_id,dealer_id, approval_level) values ";

BECOMES

$sql[] = "replace into dealer_companies (company_id,dealer_id, approval_level, trust_level) values ";

Have to add this in the foreach loop to grab the trust level of the corresponding record:

$trust_level = $rec->val('trust_level');

And finally:

$vals[] = "('$company_id','$dealerid', '$level')";
BECOMES
$vals[] = "('$company_id','$dealerid', '$level')";




The next step is getting that to be reflected in the actions. Remember in our actions.ini file, for each action there was an attribute called condition which had this:

record->val('approval_level')

function init(&$table)
This is a function that is called each time the companies table is run. Note that the parameter $table refers to the dataface company table (like a table class that dataface represents). This allows us to create like an extended version of the companies table in the sense that we are adding new columns.

What is done here is:


$app =& Dataface_Application::getInstance();

$sql = array();
$sql[] = "create temporary table MyCompanyPermissions (
company_id int(11) not null,
role varchar(32) not null,
approval_level int(5) default 0,
trust_level int(1) default 0,
Primary Key (company_id))";


We create a temporary table which grabs columns from other tables. We need to write the sql code to do this like this:


$sql[] = "replace into MyCompanyPermissions (company_id,role,approval_level)
select c.company_id, '$defaultRole' as role, ct.approval_level from companies c inner join company_types ct on c.resource_type=ct.id where ct.approval_level > '".APPROVAL_LEVEL_NOT_APPROVED."'";


I am not going to show all the code, but the idea is to store the sql queries in an array of sql statements. And then:


foreach ($sql as $query){
$res = mysql_query($query, df_db());
if ( !$res ){
trigger_error(mysql_error(df_db()).' while executing SQL query "'.$query.'"', E_USER_ERROR);
}
}


This will execute the sql statements for use. And then the MyCompanyPermissions table will be available for use each time the companies table is executed.

__sql__ function
This function is the function that is called to override the usual sql statement which grabs rows from the companies table (or in delegate class). By default it will grab all rows using the statement "SELECT * FROM table_name". But we are overriding it here. We are still grabbing all rows, but now we are inner joining it with the MyCompanyPermission table that we just created:


function __sql__(){

return "select c.*, mcp.role as MyRole, mcp.approval_level, mcp.trust_level from companies c left join MyCompanyPermissions mcp on c.company_id=mcp.company_id";



It is important to understand that the __sql__ function must still grab all the rows of the original table. It can only add new columns.

Note the use of the left join here. We can't just normally do the SELECT column1, column2, FROM A, B WHERE (A.a = B.b). Because we need to grab all companies. Some companies might not actually have an approval level...so if we just did that they would not be returned. But if we did a left join, it will always return a row from the companies table (or which ever table was on the left of the join...hence the name LEFT JOIN) regardless of whether there was a row in the right table.

So our companies table is actually expanded by some additionally columns, and thus we have access to MyRole, approval_level, and trust_level columns. Note the trust level column was actually something I was adding on to the system.

No comments: