Location: PHPKode > projects > CHASERS > chasers_gpl_snapshot1a/Unduplication_Engine.php
<?php

/*
<LICENSE>
This file is part of CHASERS.
CHASERS is Copyright (c) 2003-2008 Downtown Emergency Service Center (DESC).
All rights reserved.
For more information, about DESC, see http://www.desc.org/.
For more information about CHASERS, see http://chasers.desc.org/.

CHASERS is free software: you can redistribute it and/or modify
it under the terms of version 3 of the GNU General Public License 
as published by the Free Software Foundation.

CHASERS is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with CHASERS.  If not, see <http://www.gnu.org/licenses/>.

</LICENSE>
*/

//------------------------Process() Explained--------------------------//
//
// This takes the user through a series of steps, in the following order:
//
// 1) new - (or invalid) - an input form is displayed
//
// 2) pre-confirm - for client unduplication only, the user is prompted to verify the
//    unduplication. Records are displayed side by side.
//
// 3) pre-process - a record is added to the appropriate unduplication table
//     for clients only, record is marked as not approved, user is allowed to enter a comment
//     this ends the cycle, and the user is returned to
//     step (1) discribed above
//
// 4) for staff, and DB client unduplication. Records are displayed side-by-side with
//     radio buttons to choose the desired value for the record merge.
//
// 5) process - a) child tables are unduplicated
//              b) merged record is inserted into the valid ID
//              c) duplicate ID is marked as deleted
//
//---------------------------------------------------------------------//

class Unduplication_Engine {

	var $permissions = array('staff'=>'admin',  
					 CG_MAIN_OBJECT_DB=>'admin'); //client undup should probably get their own permission type

	var $control = array('object'=>CG_MAIN_OBJECT_DB, //for now...normally default would be client
				   'step'=>'new',
				   'action'=>'regular'); //other actions include DB unduplication & Backlog unduplication (both for client only)

	var $unduplication_table = array('staff'=>'tbl_duplication_staff',
						   CG_MAIN_OBJECT_DB=>'tbl_duplication');

 	var $special_tables = array( //tables requiring special unduplication action
					    'user_option'=>'', //FIX ME has a unique constraint, not worth fiddling with at the moment (for clients, this won't suffice)
 					    'log'=>'unduplicate_log',
					    'charge'=>'', //charges cannot be changed
					    'staff_password'=>''
 					    );
	var $ignore_fields = array(
					   'staff' => array('username_unix','staff_email'),
					   CG_MAIN_OBJECT_DB=> array()
					   );

	var $title = null;
	var $mesg = array();
	var $errors = array();
	var $output = null;

	function Unduplication_Engine() {

		global $engine;

		$this->control=array_merge($this->control,$_SESSION['undupControl'],$_REQUEST['undupControl']);
		$this->object = $this->control['object'];
		$this->title = ucwords($this->object.' unduplication');
		$this->IDs = orr($_REQUEST['undup_ids'],$_SESSION['undup_ids']);
	}

	function process() {

		if (!$this->perms()) {
			array_push($this->mesg,'You don\'t have proper permissions to unduplicate '.$this->object.' records.');
			return;
		}
		switch ($this->control['step']) {
		case 'preprocess':
			$this->add_record_table(); //adds a record to the appropriate table
			if ($this->object==CG_MAIN_OBJECT_DB) {
				$this->output=$this->form();
				$this->IDs = null;
				break;
			}
		case 'process':
			$this->merged_record = $_REQUEST['merged'];
			$this->undup_db();
			$this->update_record_table(); //for clients, flags the unduplication as approved
			$this->control['step'] = 'new';
			$this->IDs = null;
			$this->output .= $this->form();
			break;
		case 'confirm':
			if ($this->valid_submission()) {
				$_SESSION['undup_ids'] = $this->IDs;
				$this->records = $this->get_records();
				$this->output .= $this->merge_form();
				break;
			} 
		case 'new':
		default:
			$_SESSION['undup_ids']= null; //reset session vars
			$this->output = $this->form();
		}
		$_SESSION['undupControl']=$this->control;
	}

	function display() {

		outline(bigger(bold($this->title)),2);
		$this->send_errors();
		$this->send_mesg();
		out($this->output);
	}

	function perms() {
		return has_perm($this->permissions[$this->object],'W');
	}

	function N_record_form($records,$object) {
		//takes an arbitrary number of records, embedded in an array, and returns a form for merging
		//offers user the option to merge two records field by field
		//returns a form which will return a $merged variable when submitted
		//which contains the merged record
		global $engine;
		
		$def = $engine[$object];
		$count = count($records);
		$i=1;
		if ($count < 1 || !is_array($records)) {
			return false;
		}
		$checked=array();
		$out=array();
		foreach ($records as $rec_name => $rec) {
			foreach ($rec as $key => $value) {
				if ($i==1) { //first time around
					$checked[$key]=true;
					$out[$key]=array();
				}
				$check = !is_null($value) ? $checked[$key] : false;  //if it hasn't been checked yet, check it
				if ($i==$count) { //last time around
					$check=$checked[$key]; //check any that have yet to be checked
				}
				$checked[$key] = $check ? false : $checked[$key];
				array_push($out[$key],cell(formradio('merged['.$key.']',$value,$check)).cell(value_generic($value,$def,$key,'view')));
			}
			$i++;
		}

		$rec_titles = array_keys($records);
		for ($i=0;$i<$count;$i++) {
			$header .= cell(bold($rec_titles[$i]), 'colspan="2"');
		}
		$output .= row(cell().$header);
		foreach ($out as $key => $rec_array) {
			$output .= row(cell(label_generic($key,$def,'view')).implode(' ',$rec_array));
		}
		return $output;
	}

	function valid_submission() {
		$VALID=false;
		if (!empty($this->IDs)) {
			$valid=true;
			foreach ($this->IDs as $which => $id) {
				$tmp[]=$id;
				if (!is_numeric($id) || $id < 1) {
					$valid=false;
					array_push($this->errors,'Please enter a numeric ID for the '.ucfirst($this->object). ' you wish to '.$which);
				} elseif (!call_user_func('is_'.$this->object,$id)) {
					$valid=false;
					array_push($this->errors,ucfirst($this->object). ' ID '.$id.' was not found.');
				} else {
				}
			}
			if ($tmp[0]==$tmp[1]) { //entered the same ID
				array_push($this->errors,'Can\'t unduplicate the same '.$this->object.'!');
				$valid=false;
			}
			$VALID = $valid ? true : false;
			return $VALID;
		}
		array_push($this->errors,'ID numbers are required for the '.$this->object.' you wish to unduplicate.');
		return $VALID;
	}

	function form() {

		global $chasers_home_url;
		$cancel_url = $chasers_home_url;
		$cancel_button = button_link($cancel_url,'Cancel');

		$form = tablestart('','border="1" cellpadding="3"')
			. formto()
			. rowrlcell('Valid '.ucfirst($this->object).' ID:',formvartext('undup_ids[keep]',$this->IDs['keep']))
			. rowrlcell('Duplicate '.ucfirst($this->object).' ID:',formvartext('undup_ids[unduplicate]',$this->IDs['unduplicate']))
			. hiddenvar('undupControl[step]','confirm')
			. row(cell(button('Submit'))
				. formend()
				. cell($cancel_button))
			. tableend();
		return $form;
	}
	
	function send_errors() {
		foreach ($this->errors as $error) {
			outline(red($error));
		}
		$this->errors=array();
	}

	function send_mesg() {
		foreach ($this->mesg as $message) {
			outline($message);
		}
		$this->mesg=array();
	}

	function get_records() {
		
		global $engine;
		$records = array();
		foreach ($this->IDs as $which => $id) {
			$filter = array($this->object.'_id'=>$id);
			$res = get_generic($filter,'','',$engine[$this->object]);
			$records[$which] = sql_fetch_assoc($res);
			foreach ($this->ignore_fields[$this->object] as $field) {
				unset ($records[$which][$field]);
			}
		}
		return $records;
	}

	function merge_form() {
		
		$keepID = $this->IDs['keep'];
		$undupID = $this->IDs['unduplicate'];

		return 
			tablestart() . formto()  
			. row( cell('photos') 
				 . cell(formradio("undup_photo",$keepID,true)).cell(call_user_func($this->object.'_photo',$keepID)) 
				 . cell(formradio("undup_photo",$undupID,false)).cell(call_user_func($this->object.'_photo',$undupID))) 
			. $this->N_record_form($this->records,$this->object)
			. row(cell(button('Submit'),' rowspan="2"'))
			. hiddenvar('undupControl[step]','preprocess')
			. formend() 					
			. row(cell(cancel_button($_SERVER['PHP_SELF'].'?undupControl[step]=new','Cancel')))
				 . tableend();
	}

	function get_tables() {

		global $engine, $CG_ENGINE_TABLES;

		// create an array of all tables and staff/client fields
		$TABLES = array();
		foreach ($CG_ENGINE_TABLES as $obj) {
			if (in_array($obj,array_keys($this->special_tables))  //these will be handled elsewhere
			    || is_view($engine[$obj]['table_post'])  //don't care about views
			    || $obj == $this->object ){  //the parent table requires special handling
				if (function_exists($this->special_tables[$obj].'_'.$this->object)) { //this should go somewhere else but it works fine here for now.
					call_user_func($this->special_tables[$obj].'_'.$this->object,$this->IDs['unduplicate'],$this->IDs['keep']);
				}
				continue;
			}
			$table = $engine[$obj]['table_post'];
			$TABLES[$table]=array();
			$fields =& orr($engine[$obj]['fields'],array());
			foreach ($fields as $field_name => $info) {
				//there must surely be a better way to do this via array searching... (not as of PHP 4 or 5...)
				$type = $info['data_type'];
				if ($type == $this->object 
				    && !$info['view_field_only']
				    && !in_array($field_name,$this->ignore_fields[$this->object]) ) {
					array_push($TABLES[$table],$field_name);
				}
			}
		}
		return array_filter($TABLES);
	}


	function undup_db()
	{
		global $engine, $UID;
		
		$newid=$this->IDs['keep'];
		$oldid=$this->IDs['unduplicate'];
		
		$tables = $this->get_tables();
		
		outline(bigger(bold('Unduplicating '.$this->object.' '
					  .call_user_func($this->object.'_link',$oldid).' into '
					  .call_user_func($this->object.'_link',$newid))));
		$res=sql_query('BEGIN');
		foreach($tables as $table => $field_list)
		{
			$this->undup_table($table,$field_list);
			$this->send_errors();
			$this->send_mesg();
		}

		//PHOTO STUFF
		if ($this->object == CG_MAIN_OBJECT_DB ) {
// 			$use_old_photo = ($_REQUEST['undup_photo']==$oldid);
			
// 			$photo_res=client_photo_transfer( $newid, $oldid , $use_old_photo);
 			outline($photo_res 
 				  ? "Transfered all photos for ".CG_MAIN_OBJECT." $oldid to ".CG_MAIN_OBJECT." $newid"
 				  : "Failed to transfer photos for ".CG_MAIN_OBJECT." $oldid to ".CG_MAIN_OBJECT." $newid");
		} else {
			$use_old_photo = ($_REQUEST['undup_photo']==$oldid);
			$photo_res=$this->staff_photo_transfer($newid,$oldid,$use_old_photo);
		}

		//Mark old ID as deleted
		$table = $engine[$this->object]['table_post'];
		$filter=array($this->object.'_id' => $oldid);
		$result = desc_query(sql_delete($table,$filter,"MARK"));
		if ($result) {
			array_push($this->mesg, ucfirst($this->object)." ID $oldid succesfully marked as deleted in $table.");
		} else {
			array_push($this->errors,"Failed to mark ".$this->object." $oldid as deleted in $table.");
		}
		//set sys log in old record to be deleted
		$sys_log_mesg = ucfirst($this->object)." is a duplicate of $newid --- '||CURRENT_DATE||' by staff ID: $UID\n"; 
		$sys = sql_query("UPDATE $table
                                       SET sys_log = COALESCE(sys_log,'') || '$sys_log_mesg',
                                           deleted_comment='deleted for unduplication of {$this->object} $newid\n'
                                       WHERE {$this->object}_id = $oldid");
		if (!$sys) {
			array_push($this->error,"Failed to update sys_log for {$this->object} $oldid");
		}
		//set syslog in merged record
		$this->merged_record['sys_log'] = $this->merged_record['sys_log']
			."Data for a duplicate {$this->object} ($oldid) was merged into this record --- ".dateof('NOW','SQL')." by staff ID: $UID\n"; 
		//merge records
		$result = desc_query(sql_update($table,$this->merged_record,array($this->object.'_id'=>$newid)));
		if ($result) {
			array_push($this->mesg,"Merged records for {$this->object} ($oldid) and {$this->object} ($newid).");
		} else {
			array_push($this->errors, "Failed to merge records for {$this->object} ($oldid) and {$this->object} ($newid).");
		}
		$res=sql_query('END');
		$this->send_errors();
		$this->send_mesg();
		return;
	}

	function undup_table($table,$field_list) {

		global $UID;
		$keepID = $this->IDs['keep'];
		$undupID = $this->IDs['unduplicate'];

		$message = 'Unduplicating '.$this->object.' ID from '.$undupID.' to '.$keepID;

		foreach ($field_list as $field) {
			$set = "$field = $keepID, sys_log = COALESCE(sys_log,'') || CURRENT_TIMESTAMP ||' - $message by staff ID: $UID\n'";
			$sql = "UPDATE $table SET $set WHERE $field = $undupID";
			$res = sql_query($sql);
			if (!$res) {
				array_push($this->errors,"Unable to update $field in $table:  $update_sql");
			}   
			$rows = sql_affected_rows($res); // doesn't return 0 on error
			if ($rows) {
				$ROWS += $rows;
			}

		}
		if ($ROWS > 0) {
			create_system_log($this->object.' unduplication', //event type 
						"$message --- changed $ROWS rows in table $table");
			array_push($this->mesg,indent()."--- changed $ROWS records in table ".bold($table));
		} else {
			array_push($this->mesg,indent().smaller('--- No rows were changed in table '.$table));
		}
	}

	function add_record_table() { //adds a record to the appropriate table

		global $UID;
		$record=array();
		switch($this->object) {
		case CG_MAIN_CLIENT_DB:
			$record['approved'] = 'false';
		case 'staff':
			$record['approved'] = 'true';
			$record['FIELD:approved_at'] = 'CURRENT_TIMESTAMP';
			$record['approved_by'] = $UID;
		}
		$record[$this->object.'_id']=$this->IDs['keep'];
		$record[$this->object.'_id_old']=$this->IDs['unduplicate'];
		$record['added_by']=$record['changed_by']=$UID;

		$table = $this->unduplication_table[$this->object];
		$res = sql_query(sql_insert($table,$record));
		if (!$res) {
			array_push($this->errors,'Failed to insert a record in the unduplication table ('.$table.').');
		} elseif ($this->object==CG_MAIN_OBJECT_DB) {
			array_push($this->mesg,CG_MAIN_OBJECT.' '.$this->IDs['unduplicate'].' flagged for unduplication');
		}
	}

	function update_record_table() { //for clients, flags the unduplication as approved

	}

	function staff_photo_transfer($newid,$oldid,$use_old_photo) {
		global $CG_STAFF_PHOTO_BY_FILE;
		$base = $CG_STAFF_PHOTO_BY_FILE.'/';

		$file = 'st_';
		$ext = '.jpg';

		$old_photo_file = $base.$file.$oldid.$ext;
		$cur_photo_file = $base.$file.$newid.$ext;
		if ($use_old_photo and is_readable($cur_photo_file)) {
			$res0 = rename($old_photo_file,$base.$file.$newid.'.old'.$ext);
			if (!$res0) {
				array_push($this->errors,'Failed to rename old photo.');
			}
			$res = rename($base.$file.$oldid.$ext,$base.$file.$newid.$ext);
		} elseif (is_readable($old_photo_file)) {
			$res = rename($old_photo_file,$base.$file.$newid.'.old'.$ext);
		}
		if (!$res) {
			array_push($this->errors,'Failed to transfer photos.');
		}

	}
}
?>
Return current item: CHASERS