From ccabb6d5f72fe9e5a9d8e8fd07e6af94899f5403 Mon Sep 17 00:00:00 2001 From: Stefan Becker Date: Thu, 10 Sep 2009 08:20:27 +0000 Subject: [PATCH] added History Log for timesheet --- timesheet/inc/class.timesheet_bo.inc.php | 218 ++++++++++++++++++ .../inc/class.timesheet_tracking.inc.php | 129 +++++++++++ timesheet/inc/class.timesheet_ui.inc.php | 27 ++- timesheet/setup/etemplates.inc.php | 2 +- timesheet/templates/default/edit.xet | 18 +- 5 files changed, 385 insertions(+), 9 deletions(-) create mode 100644 timesheet/inc/class.timesheet_tracking.inc.php diff --git a/timesheet/inc/class.timesheet_bo.inc.php b/timesheet/inc/class.timesheet_bo.inc.php index 9698dfd429..c0bdf51a01 100644 --- a/timesheet/inc/class.timesheet_bo.inc.php +++ b/timesheet/inc/class.timesheet_bo.inc.php @@ -41,6 +41,12 @@ class timesheet_bo extends so_sql_cf * * @var array */ + var $user; + /** + * current user + * + * @var int + */ var $timestamps = array( 'ts_start','ts_modified' ); @@ -124,6 +130,62 @@ class timesheet_bo extends so_sql_cf /** * Name of the timesheet table storing custom fields */ + var $historylog; + /** + * Instance of the timesheet_tracking object + * + * @var timesheet_tracking + */ + var $field2label = array( + 'ts_project' => 'Project', + 'ts_title' => 'Title', + 'cat_id' => 'Category', + 'ts_description' => 'Description', + 'ts_start' => 'Start', + 'ts_duration' => 'Duration', + 'ts_quantity' => 'Quantity', + 'ts_unitprice' => 'Unitprice', + 'ts_owner' => 'Owner', + 'ts_modifier' => 'Modifier', + 'ts_status' => 'Status', + 'pm_id' => 'Projectid', + // pseudo fields used in edit + //'link_to' => 'Attachments & Links', + 'customfields' => 'Custom fields', + ); + /** + * Translates field / acl-names to labels + * + * @var array + */ + var $field2history = array( + 'ts_project' => 'Pro', + 'ts_title' => 'Tit', + 'cat_id' => 'Cat', + 'ts_description' => 'Des', + 'ts_start' => 'Sta', + 'ts_duration' => 'Dur', + 'ts_quantity' => 'Qua', + 'ts_unitprice' => 'Uni', + 'ts_owner' => 'Own', + 'ts_modifier' => 'Mid', + 'ts_status' => 'Sta', + 'pm_id' => 'Pri', + // pseudo fields used in edit + //'link_to' => 'Att', + 'customfields' => '#c', + ); + /** + * Translate field-name to 2-char history status + * + * @var array + */ + var $tracking; + /** + * Names of all config vars + * + * @var array + */ const EXTRA_TABLE = 'egw_timesheet_extra'; function __construct() @@ -458,12 +520,54 @@ class timesheet_bo extends so_sql_cf { $this->data['ts_modifier'] = $GLOBALS['egw_info']['user']['account_id']; $this->data['ts_modified'] = $this->now; + $this->user = $this->data['ts_modifier']; } + + // check if we have a real modification + // read the old record + $new =& $this->data; + unset($this->data); + $this->read($new['ts_id']); + $old =& $this->data; + $this->data =& $new; + $changed[] = array(); + if (isset($old)) foreach($old as $name => $value) + { + if (isset($new[$name]) && $new[$name] != $value) $changed[] = $name; + } + if (!$changed) + { + return false; + } + + if (!is_object($this->tracking)) + { + $this->tracking = new timesheet_tracking($this); + + $this->tracking->html_content_allow = true; + } + if ($this->customfields) + { + $data_custom = $old_custom = array(); + foreach($this->customfields as $name => $custom) + { + if (isset($this->data['#'.$name]) && (string)$this->data['#'.$name]!=='') $data_custom[] = $custom['label'].': '.$this->data['#'.$name]; + if (isset($old['#'.$name]) && (string)$old['#'.$name]!=='') $old_custom[] = $custom['label'].': '.$old['#'.$name]; + } + $this->data['customfields'] = implode("\n",$data_custom); + $old['customfields'] = implode("\n",$old_custom); + } + if (!$this->tracking->track($this->data,$old,$this->user)) + { + return implode(', ',$this->tracking->errors); + } if (!($err = parent::save())) { // notify the link-class about the update, as other apps may be subscribt to it egw_link::notify_update(TIMESHEET_APP,$this->data['ts_id'],$this->data); } + + return $err; } @@ -714,4 +818,118 @@ class timesheet_bo extends so_sql_cf } return array(); } + /** + * Tracks the changes in one entry $data, by comparing it with the last version in $old + * + * @param array $data current entry + * @param array $old=null old/last state of the entry or null for a new entry + * @param int $user=null user who made the changes, default to current user + * @param boolean $deleted=null can be set to true to let the tracking know the item got deleted or undelted + * @param array $changed_fields=null changed fields from ealier call to $this->changed_fields($data,$old), to not compute it again + * @return int|boolean false on error, integer number of changes logged or true for new entries ($old == null) + */ + public function track(array $data,array $old=null,$user=null,$deleted=null,array $changed_fields=null) + { + $this->user = !is_null($user) ? $user : $GLOBALS['egw_info']['user']['account_id']; + + $changes = true; + if ($old && $this->field2history) + { + $changes = $this->save_history($data,$old,$deleted,$changed_fields); + } + + return $changes; + } + + /** + * Save changes to the history log + * + * @internal use only track($data,$old) + * @param array $data current entry + * @param array $old=null old/last state of the entry or null for a new entry + * @param boolean $deleted=null can be set to true to let the tracking know the item got deleted or undelted + * @param array $changed_fields=null changed fields from ealier call to $this->changed_fields($data,$old), to not compute it again + * @return int number of log-entries made + */ + protected function save_history(array $data,array $old=null,$deleted=null,array $changed_fields=null) + { + if (is_null($changed_fields)) + { + $changed_fields = self::changed_fields($data,$old); + } + if (!$changed_fields) return 0; + + if (!is_object($this->historylog) || $this->historylog->user != $this->user) + { + $this->historylog = new historylog($this->app,$this->user); + } + foreach($changed_fields as $name) + { + $status = $this->field2history[$name]; + if (is_array($status)) // 1:N relation --> remove common rows + { + self::compact_1_N_relation($data[$name],$status); + self::compact_1_N_relation($old[$name],$status); + $added = array_diff($data[$name],$old[$name]); + $removed = array_diff($old[$name],$data[$name]); + $n = max(array(count($added),count($removed))); + for($i = 0; $i < $n; ++$i) + { + $this->historylog->add($name,$data[$this->id_field],$added[$i],$removed[$i]); + } + } + else + { + $this->historylog->add($status,$data[$this->id_field], + is_array($data[$name]) ? implode(',',$data[$name]) : $data[$name], + is_array($old[$name]) ? implode(',',$old[$name]) : $old[$name]); + } + } + + return count($changed_fields); + } + +/** + * Compute changes between new and old data + * + * Can be used to check if saving the data is really necessary or user just pressed save + * + * @param array $data + * @param array $old=null + * @return array of keys with different values in $data and $old + */ + public function changed_fields(array $data,array $old=null) + { + if (is_null($old)) return array_keys($data); + + $changed_fields = array(); + foreach($this->field2history as $name => $status) + { + if (is_array($status)) // 1:N relation + { + self::compact_1_N_relation($data[$name],$status); + self::compact_1_N_relation($old[$name],$status); + } + if ($old[$name] != $data[$name] && !(!$old[$name] && !$data[$name])) + { + // normalize arrays, we do NOT care for the order of multiselections + if (is_array($data[$name]) || is_array($old[$name])) + { + if (!is_array($data[$name])) $data[$name] = explode(',',$data[$name]); + if (!is_array($old[$name])) $old[$name] = explode(',',$old[$name]); + if (count($data[$name]) == count($old[$name])) + { + sort($data[$name]); + sort($old[$name]); + if ($data[$name] == $old[$name]) continue; + } + } + $changed_fields[] = $name; + } + } + return $changed_fields; + } + + + } \ No newline at end of file diff --git a/timesheet/inc/class.timesheet_tracking.inc.php b/timesheet/inc/class.timesheet_tracking.inc.php new file mode 100644 index 0000000000..f76a1aaca3 --- /dev/null +++ b/timesheet/inc/class.timesheet_tracking.inc.php @@ -0,0 +1,129 @@ + + * @package tracker + * @copyright (c) 2006-8 by Ralf Becker + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @version $Id: class.timesheet_tracking.inc.php 26515 2009-03-24 11:50:16Z leithoff $ + */ + +/** + * Timesheet - tracking object for the tracker + */ +class timesheet_tracking extends timesheet_bo +{ + /** + * Application we are tracking (required!) + * + * @var string + */ + var $app = 'timesheet'; + /** + * Name of the id-field, used as id in the history log (required!) + * + * @var string + */ + var $id_field = 'ts_id'; + /** + * Name of the field with the creator id, if the creator of an entry should be notified + * + * @var string + */ + var $creator_field = 'ts_owner'; + /** + * Name of the field with the id(s) of assinged users, if they should be notified + * + * @var string + */ + var $assigned_field = 'ts_assigned'; + /** + * Translate field-name to 2-char history status + * + * @var array + */ + var $field2history = array(); + /** + * Should the user (passed to the track method or current user if not passed) be used as sender or get_config('sender') + * + * @var boolean + */ + var $prefer_user_as_sender = false; + /** + * Instance of the timesheet_bo class calling us + * + * @access private + * @var timesheet_bo + */ + var $timesheet; + + /** + * Constructor + * + * @param timesheet_bo $botimesheet + * @return timesheet_tracking + */ + function __construct(&$botimesheet) + { + parent::__construct(); // calling the constructor of the extended class + + $this->timesheet =& $botimesheet; + $this->field2history =& $botimesheet->field2history; + } + + /** + * Get a notification-config value + * + * @param string $what + * - 'copy' array of email addresses notifications should be copied too, can depend on $data + * - 'lang' string lang code for copy mail + * - 'sender' string send email address + * @param array $data current entry + * @param array $old=null old/last state of the entry or null for a new entry + * @return mixed + */ + function get_config($name,$data,$old=null) + { + $timesheet = $data['ts_id']; + + //$config = $this->timesheet->notification[$timesheet][$name] ? $this->timesheet->notification[$timesheet][$name] : $this->$timesheet->notification[0][$name]; + //no nitify configert (ToDo) + return $config; + } + + /** + * Get the subject for a given entry, reimplementation for get_subject in bo_tracking + * + * Default implementation uses the link-title + * + * @param array $data + * @param array $old + * @return string + */ + function get_subject($data,$old) + { + return '#'.$data['ts_id'].' - '.$data['ts_title']; + } + + /** + * Get the modified / new message (1. line of mail body) for a given entry, can be reimplemented + * + * @param array $data + * @param array $old + * @return string + */ + function get_message($data,$old) + { + if (!$data['ts_modified'] || !$old) + { + return lang('New timesheet submitted by %1 at %2', + $GLOBALS['egw']->common->grab_owner_name($data['ts_creator']), + $this->datetime($data['ts_created']-$this->tracker->tz_offset_s)); + } + return lang('Timesheet modified by %1 at %2', + $data['ts_modifier'] ? $GLOBALS['egw']->common->grab_owner_name($data['ts_modifier']) : lang('Timesheet'), + $this->datetime($data['ts_modified']-$this->timesheet->tz_offset_s)); + } +} diff --git a/timesheet/inc/class.timesheet_ui.inc.php b/timesheet/inc/class.timesheet_ui.inc.php index c56364181f..2329c951ad 100644 --- a/timesheet/inc/class.timesheet_ui.inc.php +++ b/timesheet/inc/class.timesheet_ui.inc.php @@ -58,7 +58,7 @@ class timesheet_ui extends timesheet_bo function edit($content = null,$view = false) { - $tabs = 'general|notes|links|customfields'; + $tabs = 'general|notes|links|customfields|history'; $etpl = new etemplate('timesheet.edit'); if (!is_array($content)) { @@ -305,6 +305,21 @@ class timesheet_ui extends timesheet_bo { $preserv['ts_project'] = $preserv['ts_project_blur']; } + $content['history'] = array( + 'id' => $this->data['ts_id'], + 'app' => 'timesheet', + 'status-widgets' => array( + 'Sta' => $this->status_labels, + 'Mod' => 'select-account', + 'Dur' => 'date-time', + 'Cat' => 'select-cat', + ), + ); + foreach($this->field2history as $field => $status) + { + $sel_options['status'][$status] = $this->field2label[$field]; + } + // the actual title-blur is either the preserved title blur (if we are called from infolog entry), // or the preserved project-blur comming from the current selected project $content['ts_title_blur'] = $preserv['ts_title_blur'] ? $preserv['ts_title_blur'] : $preserv['ts_project_blur']; @@ -315,6 +330,7 @@ class timesheet_ui extends timesheet_bo 'button[save_new]' => $view, 'button[apply]' => $view, ); + if ($view) { foreach(array_merge(array_keys($this->data),array('pm_id','pl_id','link_to')) as $key) @@ -328,6 +344,8 @@ class timesheet_ui extends timesheet_bo { $readonlys['ts_owner'] = true; } + $sel_options['ts_owner'] = $edit_grants; + $sel_options['ts_status'] = $this->status_labels; $GLOBALS['egw_info']['flags']['app_header'] = lang('timesheet').' - '. ($view ? lang('View') : ($this->data['ts_id'] ? lang('Edit') : lang('Add'))); @@ -343,11 +361,9 @@ class timesheet_ui extends timesheet_bo $content['ts_viewtype'] = $readonlys[$tabs]['notes'] = true; } if (!$this->customfields) $readonlys[$tabs]['customfields'] = true; // suppress tab if there are not customfields + if (!$this->data['ts_id']) $readonlys[$tabs]['history'] = true; //suppress history for the first loading without ID - return $etpl->exec(TIMESHEET_APP.'.timesheet_ui.edit',$content,array( - 'ts_owner' => $edit_grants, - 'ts_status' => $this->status_labels, - ),$readonlys,$preserv,2); + return $etpl->exec(TIMESHEET_APP.'.timesheet_ui.edit',$content,$sel_options,$readonlys,$preserv,2); } /** @@ -736,7 +752,6 @@ class timesheet_ui extends timesheet_bo 'ts_status' => $this->status_labels+array(lang('No status')), ); $content['nm']['no_status'] = !$sel_options['ts_status']; - $status =array(); $sel_options['action'] ['delete']= lang('Delete Timesheet'); foreach ($this->status_labels as $status_id => $label_status) diff --git a/timesheet/setup/etemplates.inc.php b/timesheet/setup/etemplates.inc.php index ca4e23c0c3..69f7c72f31 100644 --- a/timesheet/setup/etemplates.inc.php +++ b/timesheet/setup/etemplates.inc.php @@ -2,7 +2,7 @@ /** * eGroupWare - eTemplates for Application timesheet * http://www.egroupware.org - * generated by soetemplate::dump4setup() 2009-09-08 11:20 + * generated by soetemplate::dump4setup() 2009-09-10 10:18 * * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package timesheet diff --git a/timesheet/templates/default/edit.xet b/timesheet/templates/default/edit.xet index 5cd523af69..12b13a7004 100644 --- a/timesheet/templates/default/edit.xet +++ b/timesheet/templates/default/edit.xet @@ -101,7 +101,19 @@ -