diff --git a/Config/Schema/schema.php b/Config/Schema/schema.php index 47cf263..10c69bb 100644 --- a/Config/Schema/schema.php +++ b/Config/Schema/schema.php @@ -1,71 +1,70 @@ - array('type' => 'string', 'null' => false, 'default' => null, 'length' => 36, 'key' => 'primary', 'collate' => 'utf8_general_ci', 'comment' => '', 'charset' => 'utf8'), + 'audit_id' => array('type' => 'string', 'null' => false, 'default' => null, 'length' => 36, 'key' => 'index', 'collate' => 'utf8_general_ci', 'comment' => '', 'charset' => 'utf8'), + 'property_name' => array('type' => 'string', 'null' => false, 'default' => null, 'collate' => 'utf8_general_ci', 'comment' => '', 'charset' => 'utf8'), + 'old_value' => array('type' => 'text', 'null' => true, 'default' => null, 'collate' => 'utf8_general_ci', 'comment' => '', 'charset' => 'utf8'), + 'new_value' => array('type' => 'text', 'null' => true, 'default' => null, 'collate' => 'utf8_general_ci', 'comment' => '', 'charset' => 'utf8'), + 'indexes' => array('PRIMARY' => array('column' => 'id', 'unique' => 1), 'audit_id' => array('column' => 'audit_id', 'unique' => 0)), + 'tableParameters' => array('charset' => 'utf8', 'collate' => 'utf8_general_ci', 'engine' => 'InnoDB') + ); - /** - * After callback - * - * @param string Event - * @return boolean - * @access public - */ - public function after($event = array()) { - return true; - } - - /** - * Schema for audit_deltas table - * - * @var array - * @access public - */ - public $audit_deltas = array( - 'id' => array('type' => 'string', 'null' => false, 'default' => NULL, 'length' => 36, 'key' => 'primary', 'collate' => 'utf8_general_ci', 'comment' => '', 'charset' => 'utf8'), - 'audit_id' => array('type' => 'string', 'null' => false, 'default' => NULL, 'length' => 36, 'key' => 'index', 'collate' => 'utf8_general_ci', 'comment' => '', 'charset' => 'utf8'), - 'property_name' => array('type' => 'string', 'null' => false, 'default' => NULL, 'collate' => 'utf8_general_ci', 'comment' => '', 'charset' => 'utf8'), - 'old_value' => array('type' => 'text', 'null' => true, 'default' => NULL, 'collate' => 'utf8_general_ci', 'comment' => '', 'charset' => 'utf8'), - 'new_value' => array('type' => 'text', 'null' => true, 'default' => NULL, 'collate' => 'utf8_general_ci', 'comment' => '', 'charset' => 'utf8'), - 'indexes' => array('PRIMARY' => array('column' => 'id', 'unique' => 1), 'audit_id' => array('column' => 'audit_id', 'unique' => 0)), - 'tableParameters' => array('charset' => 'utf8', 'collate' => 'utf8_general_ci', 'engine' => 'InnoDB') - ); - - /** - * Schema for audits table - * - * @var array - * @access public - */ - public $audits = array( - 'id' => array('type' => 'string', 'null' => false, 'default' => NULL, 'length' => 36, 'key' => 'primary', 'collate' => 'utf8_general_ci', 'comment' => '', 'charset' => 'utf8'), - 'event' => array('type' => 'string', 'null' => false, 'default' => NULL, 'collate' => 'utf8_general_ci', 'comment' => '', 'charset' => 'utf8'), - 'model' => array('type' => 'string', 'null' => false, 'default' => NULL, 'collate' => 'utf8_general_ci', 'comment' => '', 'charset' => 'utf8'), - 'entity_id' => array('type' => 'string', 'null' => false, 'default' => NULL, 'length' => 36, 'collate' => 'utf8_general_ci', 'comment' => '', 'charset' => 'utf8'), - 'request_id' => array('type' => 'string', 'null' => false, 'default' => NULL, 'length' => 36, 'collate' => 'utf8_general_ci', 'comment' => '', 'charset' => 'utf8'), - 'json_object' => array('type' => 'text', 'null' => false, 'default' => NULL, 'collate' => 'utf8_general_ci', 'comment' => '', 'charset' => 'utf8'), - 'description' => array('type' => 'text', 'null' => true, 'default' => NULL, 'collate' => 'utf8_general_ci', 'comment' => '', 'charset' => 'utf8'), - 'source_id' => array('type' => 'string', 'null' => true, 'default' => NULL, 'collate' => 'utf8_general_ci', 'comment' => '', 'charset' => 'utf8'), - 'created' => array('type' => 'datetime', 'null' => false, 'default' => NULL, 'collate' => NULL, 'comment' => ''), - 'indexes' => array('PRIMARY' => array('column' => 'id', 'unique' => 1)), - 'tableParameters' => array('charset' => 'utf8', 'collate' => 'utf8_general_ci', 'engine' => 'InnoDB') - ); +/** + * Schema for audits table + * + * @var array + */ + public $audits = array( + 'id' => array('type' => 'string', 'null' => false, 'default' => null, 'length' => 36, 'key' => 'primary', 'collate' => 'utf8_general_ci', 'comment' => '', 'charset' => 'utf8'), + 'event' => array('type' => 'string', 'null' => false, 'default' => null, 'collate' => 'utf8_general_ci', 'comment' => '', 'charset' => 'utf8'), + 'model' => array('type' => 'string', 'null' => false, 'default' => null, 'collate' => 'utf8_general_ci', 'comment' => '', 'charset' => 'utf8'), + 'entity_id' => array('type' => 'string', 'null' => false, 'default' => null, 'length' => 36, 'collate' => 'utf8_general_ci', 'comment' => '', 'charset' => 'utf8'), + 'request_id' => array('type' => 'string', 'null' => false, 'default' => null, 'length' => 36, 'collate' => 'utf8_general_ci', 'comment' => '', 'charset' => 'utf8'), + 'json_object' => array('type' => 'text', 'null' => false, 'default' => null, 'collate' => 'utf8_general_ci', 'comment' => '', 'charset' => 'utf8'), + 'description' => array('type' => 'text', 'null' => true, 'default' => null, 'collate' => 'utf8_general_ci', 'comment' => '', 'charset' => 'utf8'), + 'source_id' => array('type' => 'string', 'null' => true, 'default' => null, 'collate' => 'utf8_general_ci', 'comment' => '', 'charset' => 'utf8'), + 'created' => array('type' => 'datetime', 'null' => false, 'default' => null, 'collate' => null, 'comment' => ''), + 'indexes' => array('PRIMARY' => array('column' => 'id', 'unique' => 1)), + 'tableParameters' => array('charset' => 'utf8', 'collate' => 'utf8_general_ci', 'engine' => 'InnoDB') + ); } diff --git a/Model/Behavior/AuditableBehavior.php b/Model/Behavior/AuditableBehavior.php index e5dcf15..4f5dd71 100644 --- a/Model/Behavior/AuditableBehavior.php +++ b/Model/Behavior/AuditableBehavior.php @@ -3,365 +3,350 @@ /** * Records changes made to an object during save operations. */ -class AuditableBehavior extends ModelBehavior -{ - /** - * A copy of the object as it existed prior to the save. We're going - * to store this off so we can calculate the deltas after save. - * - * @var Object - */ - private $_original = array(); - - /** - * The request_id, a unique ID generated once per request to allow multiple record changes to be grouped by request - */ - private static $_request_id = null; - - private function request_id() { - if (empty(self::$_request_id)) { - // Class 'String' was deprecated in CakePHP 2.7 and replaced by 'CakeText' (Issue #41) - $UuidClass = class_exists('CakeText') ? 'CakeText' : 'String'; - self::$_request_id = $UuidClass::uuid(); - } - - return self::$_request_id; - } - - /** - * Initiate behavior for the model using specified settings. - * - * Available settings: - * - ignore array, optional - * An array of property names to be ignored when records - * are created in the deltas table. - * - habtm array, optional - * An array of models that have a HABTM relationship with - * the acting model and whose changes should be monitored - * with the model. - * - * @param Model $Model Model using the behavior - * @param array $settings Settings overrides. - */ - public function setup( Model $Model, $settings = array() ) { - if( !isset( $this->settings[$Model->alias] ) ) { - $this->settings[$Model->alias] = array( - 'ignore' => array( 'created', 'updated', 'modified' ), - 'habtm' => count( $Model->hasAndBelongsToMany ) > 0 - ? array_keys( $Model->hasAndBelongsToMany ) - : array() - ); - } - if( !is_array( $settings ) ) { - $settings = array(); - } - $this->settings[$Model->alias] = array_merge_recursive( $this->settings[$Model->alias], $settings ); - - /* - * Ensure that no HABTM models which are already auditable - * snuck into the settings array. That would be bad. Same for - * any model which isn't a HABTM association. - */ - foreach( $this->settings[$Model->alias]['habtm'] as $index => $model_name ) { - /** - * Note the "===" in the condition. The type check is important, - * so don't change it just because it may look like a mistake. - */ - if( !array_key_exists( $model_name, $Model->hasAndBelongsToMany ) - || ( is_array($Model->$model_name->actsAs) - && array_search( 'Auditable', $Model->$model_name->actsAs ) === true ) ) { - unset( $this->settings[$Model->alias]['habtm'][$index] ); - } - } - } - - /** - * Executed before a save() operation. - * - * @param $Model - * @param array $options - * @return boolean - */ - public function beforeSave( Model $Model, $options = array() ) { - # If we're editing an existing object, save off a copy of - # the object as it exists before any changes. - if( !empty( $Model->id ) ) { - $this->_original[$Model->alias] = $this->_getModelData( $Model ); - } - - return true; - } - - /** - * Executed before a delete() operation. - * - * @param $Model - * @return boolean - */ - public function beforeDelete( Model $Model, $cascade = true ) { - $original = $Model->find( - 'first', - array( - 'contain' => false, - 'conditions' => array( $Model->alias . '.' . $Model->primaryKey => $Model->id ), - ) - ); - $this->_original[$Model->alias] = $original[$Model->alias]; - - return true; - } - - /** - * function afterSave - * Executed after a save operation completes. - * - * @param mixed $Model The Model that is used for the save operation - * @param boolean $created True if the save operation was an - * insertion. False otherwise. - * @return boolean - */ - public function afterSave( Model $Model, $created , $options = array() ) - { - $modelData = $this->_getModelData($Model); - if (!$modelData) { - $this->afterDelete($Model); - return true; - } - - $audit[$Model->alias] = $modelData; - $audit[$Model->alias][$Model->primaryKey] = $Model->id; - - /* - * Create a runtime association with the Audit model and bind the - * Audit model to its AuditDelta model. - */ - $Model->bindModel( - array( 'hasMany' => array( 'Audit' ) ) - ); - $Model->Audit->bindModel( - array( 'hasMany' => array( 'AuditDelta' ) ) - ); - - /* - * If a currentUser() method exists in the model class (or, of - * course, in a superclass) the call that method to pull all user - * data. Assume than an id field exists. - */ - $source = array(); - if ( $Model->hasMethod( 'currentUser' ) ) { - $source = $Model->currentUser(); - } else if ( $Model->hasMethod( 'current_user' ) ) { - $source = $Model->current_user(); - } - - $data = array( - 'Audit' => array( - 'event' => $created ? 'CREATE' : 'EDIT', - 'model' => $Model->alias, - 'entity_id' => $Model->id, - 'request_id' => self::request_id(), - 'json_object' => json_encode( $audit ), - 'source_id' => isset( $source['id'] ) ? $source['id'] : null, - 'description' => isset( $source['description'] ) ? $source['description'] : null, - ) - ); - - /* - * We have the audit_logs record, so let's collect the set of - * records that we'll insert into the audit_log_deltas table. - */ - $updates = array(); - foreach( $audit[$Model->alias] as $property => $value ) { - $delta = array(); - - /* - * Ignore virtual fields (Cake 1.3+) and specified properties - */ - if( ( $Model->hasMethod( 'isVirtualField' ) && $Model->isVirtualField( $property ) ) - || in_array( $property, $this->settings[$Model->alias]['ignore'] ) ) { - continue; - } - - if( $created ) { - if ( !empty( $value ) ) { - $delta = array( - 'AuditDelta' => array( - 'property_name' => $property, - 'old_value' => '', - 'new_value' => $value - ) - ); - } - } else { - if( array_key_exists( $property, $this->_original[$Model->alias] ) - && $this->_original[$Model->alias][$property] != $value ) { - /* - * If the property exists in the original _and_ the - * value is different, store it. - */ - $delta = array( - 'AuditDelta' => array( - 'property_name' => $property, - 'old_value' => $this->_original[$Model->alias][$property], - 'new_value' => $value - ) - ); - } - } - if ( !empty( $delta ) ) { - array_push( $updates, $delta ); - } - } - - # Insert an audit record if a new model record is being created - # or if something we care about actually changed. - if( $created || count( $updates ) ) { - $Model->Audit->create(); - $Model->Audit->save( $data ); - - if( $created ) { - if( $Model->hasMethod( 'afterAuditCreate' ) ) { - $Model->afterAuditCreate( $Model ); - } - } - else { - if( $Model->hasMethod( 'afterAuditUpdate' ) ) { - $Model->afterAuditUpdate( $Model, $this->_original, $updates, $Model->Audit->id ); - } - } - } - - # Insert a delta record if something changed. - if( count( $updates ) ) { - foreach( $updates as $delta ) { - $delta['AuditDelta']['audit_id'] = $Model->Audit->id; - - $Model->Audit->AuditDelta->create(); - $Model->Audit->AuditDelta->save( $delta ); - - if( !$created && $Model->hasMethod( 'afterAuditProperty' ) ) { - $Model->afterAuditProperty( - $Model, - $delta['AuditDelta']['property_name'], - $this->_original[$Model->alias][$delta['AuditDelta']['property_name']], - $delta['AuditDelta']['new_value'] - ); - } - } - } - - /* - * Destroy the runtime association with the Audit - */ - $Model->unbindModel( - array( 'hasMany' => array( 'Audit' ) ) - ); - - /* - * Unset the original object value so it's ready for the next - * call. - */ - if( isset( $this->_original ) ) { - unset( $this->_original[$Model->alias] ); - } - return true; - } - - /** - * Executed after a model is deleted. - * - * @param $Model - * @return void - */ - public function afterDelete( Model $Model ) { - /* - * If a currentUser() method exists in the model class (or, of - * course, in a superclass) the call that method to pull all user - * data. Assume than an id field exists. - */ - $source = array(); - if( $Model->hasMethod( 'currentUser' ) ) { - $source = $Model->currentUser(); - } else if ( $Model->hasMethod( 'current_user' ) ) { - $source = $Model->current_user(); - } - - $audit = array( $Model->alias => $this->_original[$Model->alias] ); - $data = array( - 'Audit' => array( - 'event' => 'DELETE', - 'model' => $Model->alias, - 'entity_id' => $Model->id, - 'request_id' => self::request_id(), - 'json_object' => json_encode( $audit ), - 'source_id' => isset( $source['id'] ) ? $source['id'] : null, - 'description' => isset( $source['description'] ) ? $source['description'] : null, - ) - ); - - $this->Audit = ClassRegistry::init( 'Audit' ); - $this->Audit->create(); - $this->Audit->save( $data ); - } - - /** - * function _getModelData - * Retrieves the entire set model data contained to the primary - * object and any/all HABTM associated data that has been configured - * with the behavior. - * - * Additionally, for the HABTM data, all we care about is the IDs, - * so the data will be reduced to an indexed array of those IDs. - * - * @param $Model - * @return array - */ - private function _getModelData( Model $Model ) { - /* - * turn cacheQueries off for model provided. - */ - $Model->cacheQueries = false; - - /* - * Retrieve the model data along with its appropriate HABTM - * model data. - */ - $data = $Model->find( - 'first', - array( - 'contain' => !empty( $this->settings[$Model->alias]['habtm'] ) - ? array_values( $this->settings[$Model->alias]['habtm'] ) - : array(), - 'conditions' => array( $Model->alias . '.' . $Model->primaryKey => $Model->id ) - ) - ); - - //If we are using a SoftDelete behavior, $data will return empty after a delete - if (empty($data)){ - return false; - } - - $audit_data = array( - $Model->alias => isset($data[$Model->alias]) ? $data[$Model->alias] : array() - ); - - foreach( $this->settings[$Model->alias]['habtm'] as $habtm_model ) { - if( array_key_exists( $habtm_model, $Model->hasAndBelongsToMany ) && isset( $data[$habtm_model] ) ) { - $habtm_ids = Hash::combine( - $data[$habtm_model], - '{n}.id', - '{n}.id' - ); - /* - * Grab just the id values and sort those - */ - $habtm_ids = array_values( $habtm_ids ); - sort( $habtm_ids ); - - $audit_data[$Model->alias][$habtm_model] = implode( ',', $habtm_ids ); - } - } - - return $audit_data[$Model->alias]; - } +class AuditableBehavior extends \ModelBehavior { + +/** + * A copy of the object as it existed prior to the save. We're going + * to store this off, so we can calculate the deltas after save. + * + * @var array + */ + protected $_original = array(); + +/** + * The requestId is a unique ID generated once per request to allow multiple record changes to be grouped by request + * + * @var string + */ + protected static $_requestId = null; + +/** + * Initiate behavior for the model using specified settings. + * + * Available settings: + * - ignore array, optional + * An array of property names to be ignored when records + * are created in the deltas table. + * - habtm array, optional + * An array of models that have a HABTM relationship with + * the acting model and whose changes should be monitored + * with the model. + * + * @param Model $Model The model using the behavior. + * @param array $settings The settings overrides. + * @return void + */ + public function setup(Model $Model, $settings = array()) { + if (!isset($this->settings[$Model->alias])) { + $this->settings[$Model->alias] = array( + 'ignore' => array('created', 'updated', 'modified'), + 'habtm' => count($Model->hasAndBelongsToMany) > 0 + ? array_keys($Model->hasAndBelongsToMany) + : array(), + ); + } + if (!is_array($settings)) { + $settings = array(); + } + $this->settings[$Model->alias] = array_merge_recursive($this->settings[$Model->alias], $settings); + + // Ensure that no HABTM models, which are already auditable, + // snuck into the settings array. That would be bad. Same for + // any model which isn't a HABTM association. + foreach ($this->settings[$Model->alias]['habtm'] as $index => $modelName) { + // Note the "===" in the condition. The type check is important, + // so don't change it just because it may look like a mistake. + if (!array_key_exists($modelName, $Model->hasAndBelongsToMany) + || (is_array($Model->$modelName->actsAs) + && array_search('Auditable', $Model->$modelName->actsAs) === true) + ) { + unset($this->settings[$Model->alias]['habtm'][$index]); + } + } + } + +/** + * Executed before a save operation. + * + * @param Model $Model The model using the behavior. + * @param array $options The options data (unused). + * @return true Always true. + */ + public function beforeSave(Model $Model, $options = array()) { + // If we're editing an existing object, save off a copy of + // the object as it exists before any changes. + if (!empty($Model->id)) { + $this->_original[$Model->alias] = $this->_getModelData($Model); + } + + return true; + } + +/** + * Executed before a delete operation. + * + * @param Model $Model The model using the behavior. + * @param bool $cascade Whether to cascade (unused). + * @return true Always true. + */ + public function beforeDelete(Model $Model, $cascade = true) { + $original = $Model->find( + 'first', + array( + 'contain' => false, + 'conditions' => array($Model->alias . '.' . $Model->primaryKey => $Model->id), + ) + ); + $this->_original[$Model->alias] = $original[$Model->alias]; + + return true; + } + +/** + * Executed after a save operation completes. + * + * @param Model $Model The model that is used for the save operation. + * @param bool $created True, if the save operation was an insertion, false otherwise. + * @param array $options The options data (unused). + * @return true Always true. + */ + public function afterSave(Model $Model, $created, $options = array()) { + $modelData = $this->_getModelData($Model); + if (!$modelData) { + $this->afterDelete($Model); + + return true; + } + + $audit[$Model->alias] = $modelData; + $audit[$Model->alias][$Model->primaryKey] = $Model->id; + + // Create a runtime association with the Audit model and bind the + // Audit model to its AuditDelta model. + $Model->bindModel( + array('hasMany' => array('Audit')) + ); + $Model->Audit->bindModel( + array('hasMany' => array('AuditDelta')) + ); + + // If a currentUser() method exists in the model class (or, of + // course, in a superclass) the call that method to pull all user + // data. Assume than an ID field exists. + $source = array(); + if ($Model->hasMethod('currentUser')) { + $source = $Model->currentUser(); + } elseif ($Model->hasMethod('current_user')) { + $source = $Model->current_user(); + } + + $data = array( + 'Audit' => array( + 'event' => $created ? 'CREATE' : 'EDIT', + 'model' => $Model->alias, + 'entity_id' => $Model->id, + 'request_id' => self::_requestId(), + 'json_object' => json_encode($audit), + 'source_id' => isset($source['id']) ? $source['id'] : null, + 'description' => isset($source['description']) ? $source['description'] : null, + ), + ); + + // We have the audit_logs record, so let's collect the set of + // records that we'll insert into the audit_log_deltas table. + $updates = array(); + foreach ($audit[$Model->alias] as $property => $value) { + $delta = array(); + + // Ignore virtual fields (Cake 1.3+) and specified properties. + if (($Model->hasMethod('isVirtualField') && $Model->isVirtualField($property)) + || in_array($property, $this->settings[$Model->alias]['ignore']) + ) { + continue; + } + + if ($created) { + if (!empty($value)) { + $delta = array( + 'AuditDelta' => array( + 'property_name' => $property, + 'old_value' => '', + 'new_value' => $value, + ), + ); + } + } else { + if (array_key_exists($property, $this->_original[$Model->alias]) + && $this->_original[$Model->alias][$property] != $value + ) { + // If the property exists in the original _and_ the + // value is different, store it. + $delta = array( + 'AuditDelta' => array( + 'property_name' => $property, + 'old_value' => $this->_original[$Model->alias][$property], + 'new_value' => $value, + ), + ); + } + } + if (!empty($delta)) { + array_push($updates, $delta); + } + } + + // Insert an audit record if a new model record is being created + // or if something we care about actually changed. + if ($created || count($updates)) { + $Model->Audit->create(); + $Model->Audit->save($data); + + if ($created) { + if ($Model->hasMethod('afterAuditCreate')) { + $Model->afterAuditCreate($Model); + } + } else { + if ($Model->hasMethod('afterAuditUpdate')) { + $Model->afterAuditUpdate($Model, $this->_original, $updates, $Model->Audit->id); + } + } + } + + // Insert a delta record if something changed. + if (count($updates)) { + foreach ($updates as $delta) { + $delta['AuditDelta']['audit_id'] = $Model->Audit->id; + + $Model->Audit->AuditDelta->create(); + $Model->Audit->AuditDelta->save($delta); + + if (!$created && $Model->hasMethod('afterAuditProperty')) { + $Model->afterAuditProperty( + $Model, + $delta['AuditDelta']['property_name'], + $this->_original[$Model->alias][$delta['AuditDelta']['property_name']], + $delta['AuditDelta']['new_value'] + ); + } + } + } + + // Destroy the runtime association with the Audit. + $Model->unbindModel( + array('hasMany' => array('Audit')) + ); + + /// Unset the original object value so it's ready for the next call. + if (isset($this->_original)) { + unset($this->_original[$Model->alias]); + } + + return true; + } + +/** + * Executed after a model is deleted. + * + * @param Model $Model The model that is used for the delete operation. + * @return void + */ + public function afterDelete(Model $Model) { + // If a currentUser() method exists in the model class (or, of + // course, in a superclass) the call that method to pull all user + // data. Assume than an ID field exists. + $source = array(); + if ($Model->hasMethod('currentUser')) { + $source = $Model->currentUser(); + } elseif ($Model->hasMethod('current_user')) { + $source = $Model->current_user(); + } + + $audit = array($Model->alias => $this->_original[$Model->alias]); + $data = array( + 'Audit' => array( + 'event' => 'DELETE', + 'model' => $Model->alias, + 'entity_id' => $Model->id, + 'request_id' => self::_requestId(), + 'json_object' => json_encode($audit), + 'source_id' => isset($source['id']) ? $source['id'] : null, + 'description' => isset($source['description']) ? $source['description'] : null, + ), + ); + + $this->Audit = ClassRegistry::init('Audit'); + $this->Audit->create(); + $this->Audit->save($data); + } + +/** + * Get model data + * + * Retrieves the entire set model data contained to the primary + * object and any/all HABTM associated data that has been configured + * with the behavior. + * + * Additionally, for the HABTM data, all we care about is the IDs, + * so the data will be reduced to an indexed array of those IDs. + * + * @param Model $Model The model that uses the behavior. + * @return array|false The model data or false. + */ + protected function _getModelData(Model $Model) { + // Turn cacheQueries off for model provided. + $Model->cacheQueries = false; + + // Retrieve the model data along with its appropriate HABTM model data. + $data = $Model->find( + 'first', + array( + 'contain' => !empty($this->settings[$Model->alias]['habtm']) + ? array_values($this->settings[$Model->alias]['habtm']) + : array(), + 'conditions' => array($Model->alias . '.' . $Model->primaryKey => $Model->id), + ) + ); + + // If we are using a SoftDelete behavior, $data will return empty after a delete. + if (empty($data)) { + return false; + } + + $auditData = array( + $Model->alias => isset($data[$Model->alias]) ? $data[$Model->alias] : array(), + ); + + foreach ($this->settings[$Model->alias]['habtm'] as $habtmModel) { + if (array_key_exists($habtmModel, $Model->hasAndBelongsToMany) && isset($data[$habtmModel])) { + $habtmIds = Hash::combine( + $data[$habtmModel], + '{n}.id', + '{n}.id' + ); + + // Grab just the ID values and sort those. + $habtmIds = array_values($habtmIds); + sort($habtmIds); + + $auditData[$Model->alias][$habtmModel] = implode(',', $habtmIds); + } + } + + return $auditData[$Model->alias]; + } + +/** + * Get request ID + * + * @return null|string The request ID. + */ + protected function _requestId() { + if (empty(self::$_requestId)) { + // Class 'String' was deprecated in CakePHP 2.7 and replaced by 'CakeText' (Issue #41) + $UuidClass = class_exists('CakeText') ? 'CakeText' : 'String'; + self::$_requestId = $UuidClass::uuid(); + } + + return self::$_requestId; + } } diff --git a/Test/Case/Behavior/AuditableTest.php b/Test/Case/Behavior/AuditableTest.php index 27db1c0..8ca8629 100644 --- a/Test/Case/Behavior/AuditableTest.php +++ b/Test/Case/Behavior/AuditableTest.php @@ -1,456 +1,515 @@ array( - 'ignore' => array( 'ignored_field' ), - ) - ); - public $belongsTo = array( 'Author' ); -} /** - * Author class + * The name of the model + * + * @var string + */ + public $name = 'Article'; + +/** + * The behaviors * - * @package cake - * @subpackage cake.tests.cases.libs.model + * @var array + */ + public $actsAs = array( + 'AuditLog.Auditable' => array( + 'ignore' => array('ignored_field'), + ), + ); + +/** + * Belongs to relationships + * + * @var array + */ + public $belongsTo = array('Author'); +} + +/** + * Author test model */ class Author extends CakeTestModel { - public $name = 'Author'; - public $actsAs = array( - 'AuditLog.Auditable' - ); - public $hasMany = array( 'Article' ); + +/** + * The name of the model + * + * @var string + */ + public $name = 'Author'; + +/** + * The behaviors + * + * @var array + */ + public $actsAs = array( + 'AuditLog.Auditable', + ); + +/** + * Has many relationships + * + * @var array + */ + public $hasMany = array('Article'); } +/** + * Audit test model + */ class Audit extends CakeTestModel { - public $hasMany = array( - 'AuditDelta' - ); + +/** + * Has many relationships + * + * @var array + */ + public $hasMany = array( + 'AuditDelta', + ); } +/** + * AuditDelta test model + */ class AuditDelta extends CakeTestModel { - public $belongsTo = array( - 'Audit' - ); + +/** + * Belongs to relationships + * + * @var array + */ + public $belongsTo = array( + 'Audit', + ); } /** - * AuditableBehavior test class. + * AuditableBehavior Tests */ class AuditableBehaviorTest extends CakeTestCase { - /** - * Fixtures associated with this test case - * - * @var array - * @access public - */ - public $fixtures = array( - 'plugin.audit_log.article', - 'plugin.audit_log.author', - 'plugin.audit_log.audit', - 'plugin.audit_log.audit_delta', - ); - - /** - * Method executed before each test - * - * @access public - */ - public function setUp() { - $this->Article = ClassRegistry::init( 'Article' ); - } - - /** - * Method executed after each test - * - * @access public - */ - public function tearDown() { - unset( $this->Article ); - - ClassRegistry::flush(); - } - - /** - * Test the action of creating a new record. - * - * @todo Test HABTM save - */ - public function testCreate() { - $new_article = array( - 'Article' => array( - 'user_id' => 1, - 'author_id' => 1, - 'title' => 'First Test Article', - 'body' => 'First Test Article Body', - 'published' => 'N', - ), - ); - - $this->Article->save( $new_article ); - $audit = ClassRegistry::init( 'Audit' )->find( - 'first', - array( - 'recursive' => -1, - 'conditions' => array( - 'Audit.event' => 'CREATE', - 'Audit.model' => 'Article', - 'Audit.entity_id' => $this->Article->getLastInsertId() - ) - ) - ); - $article = json_decode( $audit['Audit']['json_object'], true ); - - $deltas = ClassRegistry::init( 'AuditDelta' )->find( - 'all', - array( - 'recursive' => -1, - 'conditions' => array( 'AuditDelta.audit_id' => $audit['Audit']['id'] ), - ) - ); - - # Verify the audit record - $this->assertEqual( 1, $article['Article']['user_id'] ); - $this->assertEqual( 'First Test Article', $article['Article']['title'] ); - $this->assertEqual( 'N', $article['Article']['published'] ); - - #Verify that no delta record was created. - $this->assertTrue( empty( $deltas ) ); - } - - /** - * Test saving multiple records with Model::saveAll() - */ - public function testSaveAll() { - # TEST A MODEL AND A SINGLE ASSOCIATED MODEL - $data = array( - 'Article' => array( - 'user_id' => 1, - 'title' => 'Rob\'s Test Article', - 'body' => 'Rob\'s Test Article Body', - 'published' => 'Y', - ), - 'Author' => array( - 'first_name' => 'Rob', - 'last_name' => 'Wilkerson', - ), - ); - - $this->Article->saveAll( $data ); - $article_audit = ClassRegistry::init( 'Audit' )->find( - 'first', - array( - 'recursive' => -1, - 'conditions' => array( - 'Audit.event' => 'CREATE', - 'Audit.model' => 'Article', - 'Audit.entity_id' => $this->Article->getLastInsertId() - ) - ) - ); - $article = json_decode( $article_audit['Audit']['json_object'], true ); - - # Verify the audit record - $this->assertEqual( 1, $article['Article']['user_id'] ); - $this->assertEqual( 'Rob\'s Test Article', $article['Article']['title'] ); - $this->assertEqual( 'Y', $article['Article']['published'] ); - - # Verify that no delta record was created. - $this->assertTrue( !isset( $article_audit['AuditDelta'] ) ); - - $author_audit = ClassRegistry::init( 'Audit' )->find( - 'first', - array( - 'recursive' => -1, - 'conditions' => array( - 'Audit.event' => 'CREATE', - 'Audit.model' => 'Author', - 'Audit.entity_id' => $this->Article->Author->getLastInsertId() - ) - ) - ); - $author = json_decode( $author_audit['Audit']['json_object'], true ); - - # Verify the audit record - $this->assertEqual( $article['Article']['author_id'], $author['Author']['id'] ); - $this->assertEqual( 'Rob', $author['Author']['first_name'] ); - - # Verify that no delta record was created. - $this->assertTrue( !isset( $author_audit['AuditDelta'] ) ); - - # TEST MULTIPLE RECORDS OF ONE MODEL - - $data = array( - array( - 'Article' => array( - 'user_id' => 1, - 'author_id' => 1, - 'title' => 'Multiple Save 1 Title', - 'body' => 'Multiple Save 1 Body', - 'published' => 'Y', - ), - ), - array( - 'Article' => array( - 'user_id' => 2, - 'author_id' => 2, - 'title' => 'Multiple Save 2 Title', - 'body' => 'Multiple Save 2 Body', - 'published' => 'N', - 'ignored_field' => 1, - ) - ), - array( - 'Article' => array( - 'user_id' => 3, - 'author_id' => 3, - 'title' => 'Multiple Save 3 Title', - 'body' => 'Multiple Save 3 Body', - 'published' => 'Y', - ) - ), - ); - $this->Article->create(); - $this->Article->saveAll( $data ); - - # Retrieve the audits for the last 3 articles saved - $audits = ClassRegistry::init( 'Audit' )->find( - 'all', - array( - 'conditions' => array( - 'Audit.event' => 'CREATE', - 'Audit.model' => 'Article', - ), - 'order' => array( 'Audit.entity_id DESC' ), - 'limit' => 3 - ) - ); - - $article_1 = json_decode( $audits[2]['Audit']['json_object'], true ); - $article_2 = json_decode( $audits[1]['Audit']['json_object'], true ); - $article_3 = json_decode( $audits[0]['Audit']['json_object'], true ); - - # Verify the audit records - $this->assertEqual( 1, $article_1['Article']['user_id'] ); - $this->assertEqual( 'Multiple Save 1 Title', $article_1['Article']['title'] ); - $this->assertEqual( 'Y', $article_1['Article']['published'] ); - - $this->assertEqual( 2, $article_2['Article']['user_id'] ); - $this->assertEqual( 'Multiple Save 2 Title', $article_2['Article']['title'] ); - $this->assertEqual( 'N', $article_2['Article']['published'] ); - - $this->assertEqual( 3, $article_3['Article']['user_id'] ); - $this->assertEqual( 'Multiple Save 3 Title', $article_3['Article']['title'] ); - $this->assertEqual( 'Y', $article_3['Article']['published'] ); - - # Verify that no delta records were created. - $this->assertTrue( empty( $audits[0]['AuditDelta'] ) ); - $this->assertTrue( empty( $audits[1]['AuditDelta'] ) ); - $this->assertTrue( empty( $audits[2]['AuditDelta'] ) ); - } - - /** - * Test editing an existing record. - * - * @todo Test change to ignored field - * @todo Test HABTM save - */ - public function testEdit() { - $this->Audit = ClassRegistry::init( 'Audit' ); - $this->AuditDelta = ClassRegistry::init( 'AuditDelta' ); - - $new_article = array( - 'Article' => array( - 'user_id' => 1, - 'author_id' => 1, - 'title' => 'First Test Article', - 'body' => 'First Test Article Body', - 'ignored_field' => 1, - 'published' => 'N', - ), - ); - - # TEST SAVE WITH SINGLE PROPERTY UPDATE - - $this->Article->save( $new_article ); - $this->Article->saveField( 'title', 'First Test Article (Edited)' ); - - $audit_records = $this->Audit->find( - 'all', - array( - 'recursive' => 0, - 'conditions' => array( - 'Audit.model' => 'Article', - 'Audit.entity_id' => $this->Article->getLastInsertId() - ) - ) - ); - $delta_records = $this->AuditDelta->find( - 'all', - array( - 'recursive' => -1, - 'conditions' => array( 'AuditDelta.audit_id' => Hash::extract($audit_records, '{n}.Audit.id') ), - ) - ); - - $create_audit = Hash::extract($audit_records, '{n}.Audit[event=CREATE]'); - $update_audit = Hash::extract($audit_records, '{n}.Audit[event=EDIT]'); - - # There should be 1 CREATE and 1 EDIT record - $this->assertEqual( 2, count( $audit_records ) ); - - # There should be one audit record for each event. - $this->assertEqual( 1, count( $create_audit ) ); - $this->assertEqual( 1, count( $update_audit ) ); - - # Only one property was changed - $this->assertEqual( 1, count( $delta_records ) ); - - $delta = array_shift( $delta_records ); - $this->assertEqual( 'First Test Article', $delta['AuditDelta']['old_value'] ); - $this->assertEqual( 'First Test Article (Edited)', $delta['AuditDelta']['new_value'] ); - - # TEST UPDATE OF MULTIPLE PROPERTIES - # Pause to simulate a gap between edits - # This also allows us to retrieve the last edit for the next set - # of tests. - $this->Article->create(); # Clear the article id so we get a new record. - $new_article = array( - 'Article' => array( - 'user_id' => 1, - 'author_id' => 1, - 'title' => 'Second Test Article', - 'body' => 'Second Test Article Body', - 'ignored_field' => 1, - 'published' => 'N', - ), - ); - $this->Article->save( $new_article ); - - $updated_article = array( - 'Article' => array( - 'user_id' => 1, - 'author_id' => 1, - 'title' => 'Second Test Article (Newly Edited)', - 'body' => 'Second Test Article Body (Also Edited)', - 'ignored_field' => 0, - 'published' => 'Y', - ), - ); - $this->Article->save( $updated_article ); - - $last_audit = $this->Audit->find( - 'first', - array( - 'contain' => array( 'AuditDelta' ), - 'conditions' => array( - 'Audit.event' => 'EDIT', - 'Audit.model' => 'Article', - 'Audit.entity_id' => $this->Article->id - ), - 'order' => 'Audit.created DESC', - ) - ); - - # There are 4 changes, but one to an ignored field - $this->assertEqual( 3, count( $last_audit['AuditDelta'] ) ); - $result = Hash::extract($last_audit, '{n}.AuditDelta[property_name=title].old_value'); - $this->assertEqual( 'Second Test Article', array_shift( $result ) ); - - $result = Hash::extract($last_audit, '{n}.AuditDelta[property_name=title].new_value'); - $this->assertEqual( 'Second Test Article (Newly Edited)', array_shift( $result ) ); - - $result = Hash::extract($last_audit, '{n}.AuditDelta[property_name=body].old_value'); - $this->assertEqual( 'Second Test Article Body', array_shift( $result ) ); - - $result = Hash::extract($last_audit, '{n}.AuditDelta[property_name=body].new_value'); - $this->assertEqual( 'Second Test Article Body (Also Edited)', array_shift( $result ) ); - - $result = Hash::extract($last_audit, '{n}.AuditDelta[property_name=published].old_value'); - $this->assertEqual( 'N', array_shift( $result ) ); - - $result = Hash::extract($last_audit, '{n}.AuditDelta[property_name=published].new_value'); - $this->assertEqual( 'Y', array_shift( $result ) ); - - # No delta should be reported against the ignored field. - $this->assertIdentical( array(), Hash::extract($last_audit, '{n}.AuditDelta[property_name=ignored_field]') ); - } - - public function testIgnoredField() { - $this->Audit = ClassRegistry::init( 'Audit' ); - $this->AuditDelta = ClassRegistry::init( 'AuditDelta' ); - - $new_article = array( - 'Article' => array( - 'user_id' => 1, - 'author_id' => 1, - 'title' => 'First Test Article', - 'body' => 'First Test Article Body', - 'ignored_field' => 1, - 'published' => 'N', - ), - ); - - # TEST NO AUDIT RECORD IF ONLY CHANGE IS IGNORED FIELD - - $this->Article->save( $new_article ); - $this->Article->saveField( 'ignored_field', '5' ); - - $last_audit = $this->Audit->find( - 'count', - array( - 'contain' => array( 'AuditDelta' ), - 'conditions' => array( - 'Audit.event' => 'EDIT', - 'Audit.model' => 'Article', - 'Audit.entity_id' => $this->Article->id - ), - 'order' => 'Audit.created DESC', - ) - ); - - $this->assertEqual( 0, $last_audit ); - } - - public function testDelete() { - $this->Audit = ClassRegistry::init( 'Audit' ); - $this->AuditDelta = ClassRegistry::init( 'AuditDelta' ); - $article = $this->Article->find( - 'first', - array( - 'contain' => false, - 'order' => array( 'rand()' ), - ) - ); - - $id = $article['Article']['id']; - - $this->Article->delete( $id ); - - $last_audit = $this->Audit->find( - 'all', - array( - //'contain' => array( 'AuditDelta' ), <-- What does this solve? - 'conditions' => array( - 'Audit.event' => 'DELETE', - 'Audit.model' => 'Article', - 'Audit.entity_id' => $id, - ), - 'order' => 'Audit.created DESC', - ) - ); - - $this->assertEqual( 1, count( $last_audit ) ); - } + +/** + * Fixtures associated with this test case + * + * @var array + */ + public $fixtures = array( + 'plugin.audit_log.article', + 'plugin.audit_log.author', + 'plugin.audit_log.audit', + 'plugin.audit_log.audit_delta', + ); + +/** + * Method executed before each test + * + * @return void + */ + public function setUp() { + $this->Article = ClassRegistry::init('Article'); + } + +/** + * Method executed after each test + * + * @return void + */ + public function tearDown() { + unset($this->Article); + + ClassRegistry::flush(); + } + +/** + * Test the action of creating a new record. + * + * @return void + * @todo Test HABTM save. + */ + public function testCreate() { + $newArticle = array( + 'Article' => array( + 'user_id' => 1, + 'author_id' => 1, + 'title' => 'First Test Article', + 'body' => 'First Test Article Body', + 'published' => 'N', + ), + ); + + $this->Article->save($newArticle); + $audit = ClassRegistry::init('Audit')->find( + 'first', + array( + 'recursive' => -1, + 'conditions' => array( + 'Audit.event' => 'CREATE', + 'Audit.model' => 'Article', + 'Audit.entity_id' => $this->Article->getLastInsertId(), + ), + ) + ); + $article = json_decode($audit['Audit']['json_object'], true); + + $deltas = ClassRegistry::init('AuditDelta')->find( + 'all', + array( + 'recursive' => -1, + 'conditions' => array('AuditDelta.audit_id' => $audit['Audit']['id']), + ) + ); + + // Verify the audit record + $this->assertEqual(1, $article['Article']['user_id']); + $this->assertEqual('First Test Article', $article['Article']['title']); + $this->assertEqual('N', $article['Article']['published']); + + // Verify that no delta record was created. + $this->assertTrue(empty($deltas)); + } + +/** + * Test saving multiple records with Model::saveAll() + * + * @return void + */ + public function testSaveAll() { + // Test a model and a single associated model. + $data = array( + 'Article' => array( + 'user_id' => 1, + 'title' => 'Rob\'s Test Article', + 'body' => 'Rob\'s Test Article Body', + 'published' => 'Y', + ), + 'Author' => array( + 'first_name' => 'Rob', + 'last_name' => 'Wilkerson', + ), + ); + + $this->Article->saveAll($data); + $articleAudit = ClassRegistry::init('Audit')->find( + 'first', + array( + 'recursive' => -1, + 'conditions' => array( + 'Audit.event' => 'CREATE', + 'Audit.model' => 'Article', + 'Audit.entity_id' => $this->Article->getLastInsertId(), + ), + ) + ); + $article = json_decode($articleAudit['Audit']['json_object'], true); + + // Verify the audit record. + $this->assertEqual(1, $article['Article']['user_id']); + $this->assertEqual('Rob\'s Test Article', $article['Article']['title']); + $this->assertEqual('Y', $article['Article']['published']); + + // Verify that no delta record was created. + $this->assertTrue(!isset($articleAudit['AuditDelta'])); + + $authorAudit = ClassRegistry::init('Audit')->find( + 'first', + array( + 'recursive' => -1, + 'conditions' => array( + 'Audit.event' => 'CREATE', + 'Audit.model' => 'Author', + 'Audit.entity_id' => $this->Article->Author->getLastInsertId(), + ), + ) + ); + $author = json_decode($authorAudit['Audit']['json_object'], true); + + // Verify the audit record. + $this->assertEqual($article['Article']['author_id'], $author['Author']['id']); + $this->assertEqual('Rob', $author['Author']['first_name']); + + // Verify that no delta record was created. + $this->assertTrue(!isset($authorAudit['AuditDelta'])); + + // Test multiple records of one model. + $data = array( + array( + 'Article' => array( + 'user_id' => 1, + 'author_id' => 1, + 'title' => 'Multiple Save 1 Title', + 'body' => 'Multiple Save 1 Body', + 'published' => 'Y', + ), + ), + array( + 'Article' => array( + 'user_id' => 2, + 'author_id' => 2, + 'title' => 'Multiple Save 2 Title', + 'body' => 'Multiple Save 2 Body', + 'published' => 'N', + 'ignored_field' => 1, + ), + ), + array( + 'Article' => array( + 'user_id' => 3, + 'author_id' => 3, + 'title' => 'Multiple Save 3 Title', + 'body' => 'Multiple Save 3 Body', + 'published' => 'Y', + ), + ), + ); + $this->Article->create(); + $this->Article->saveAll($data); + + // Retrieve the audits for the last 3 articles saved. + $audits = ClassRegistry::init('Audit')->find( + 'all', + array( + 'conditions' => array( + 'Audit.event' => 'CREATE', + 'Audit.model' => 'Article', + ), + 'order' => array('Audit.entity_id DESC'), + 'limit' => 3, + ) + ); + + $article1 = json_decode($audits[2]['Audit']['json_object'], true); + $article2 = json_decode($audits[1]['Audit']['json_object'], true); + $article3 = json_decode($audits[0]['Audit']['json_object'], true); + + // Verify the audit records. + $this->assertEqual(1, $article1['Article']['user_id']); + $this->assertEqual('Multiple Save 1 Title', $article1['Article']['title']); + $this->assertEqual('Y', $article1['Article']['published']); + + $this->assertEqual(2, $article2['Article']['user_id']); + $this->assertEqual('Multiple Save 2 Title', $article2['Article']['title']); + $this->assertEqual('N', $article2['Article']['published']); + + $this->assertEqual(3, $article3['Article']['user_id']); + $this->assertEqual('Multiple Save 3 Title', $article3['Article']['title']); + $this->assertEqual('Y', $article3['Article']['published']); + + // Verify that no delta records were created. + $this->assertTrue(empty($audits[0]['AuditDelta'])); + $this->assertTrue(empty($audits[1]['AuditDelta'])); + $this->assertTrue(empty($audits[2]['AuditDelta'])); + } + +/** + * Test editing an existing record. + * + * @return void + * @todo Test change to ignored field. + * @todo Test HABTM save. + */ + public function testEdit() { + $this->Audit = ClassRegistry::init('Audit'); + $this->AuditDelta = ClassRegistry::init('AuditDelta'); + + $newArticle = array( + 'Article' => array( + 'user_id' => 1, + 'author_id' => 1, + 'title' => 'First Test Article', + 'body' => 'First Test Article Body', + 'ignored_field' => 1, + 'published' => 'N', + ), + ); + + // Test save with single property update. + $this->Article->save($newArticle); + $this->Article->saveField('title', 'First Test Article (Edited)'); + + $auditRecords = $this->Audit->find( + 'all', + array( + 'recursive' => 0, + 'conditions' => array( + 'Audit.model' => 'Article', + 'Audit.entity_id' => $this->Article->getLastInsertId(), + ), + ) + ); + $deltaRecords = $this->AuditDelta->find( + 'all', + array( + 'recursive' => -1, + 'conditions' => array('AuditDelta.audit_id' => Hash::extract($auditRecords, '{n}.Audit.id')), + ) + ); + + $createAudit = Hash::extract($auditRecords, '{n}.Audit[event=CREATE]'); + $updateAudit = Hash::extract($auditRecords, '{n}.Audit[event=EDIT]'); + + // There should be 1 CREATE and 1 EDIT record. + $this->assertEqual(2, count($auditRecords)); + + // There should be one audit record for each event. + $this->assertEqual(1, count($createAudit)); + $this->assertEqual(1, count($updateAudit)); + + // Only one property was changed. + $this->assertEqual(1, count($deltaRecords)); + + $delta = array_shift($deltaRecords); + $this->assertEqual('First Test Article', $delta['AuditDelta']['old_value']); + $this->assertEqual('First Test Article (Edited)', $delta['AuditDelta']['new_value']); + + // Test Update Of multiple properties. + // Pause to simulate a gap between edits. + // This also allows us to retrieve the last edit for the next set of tests. + $this->Article->create(); // Clear the article id so we get a new record. + $newArticle = array( + 'Article' => array( + 'user_id' => 1, + 'author_id' => 1, + 'title' => 'Second Test Article', + 'body' => 'Second Test Article Body', + 'ignored_field' => 1, + 'published' => 'N', + ), + ); + $this->Article->save($newArticle); + + $updatedArticle = array( + 'Article' => array( + 'user_id' => 1, + 'author_id' => 1, + 'title' => 'Second Test Article (Newly Edited)', + 'body' => 'Second Test Article Body (Also Edited)', + 'ignored_field' => 0, + 'published' => 'Y', + ), + ); + $this->Article->save($updatedArticle); + + $lastAudit = $this->Audit->find( + 'first', + array( + 'contain' => array('AuditDelta'), + 'conditions' => array( + 'Audit.event' => 'EDIT', + 'Audit.model' => 'Article', + 'Audit.entity_id' => $this->Article->id, + ), + 'order' => 'Audit.created DESC', + ) + ); + + // There are 4 changes, but one to an ignored field. + $this->assertEqual(3, count($lastAudit['AuditDelta'])); + $result = Hash::extract($lastAudit, '{n}.AuditDelta[property_name=title].old_value'); + $this->assertEqual('Second Test Article', array_shift($result)); + + $result = Hash::extract($lastAudit, '{n}.AuditDelta[property_name=title].new_value'); + $this->assertEqual('Second Test Article (Newly Edited)', array_shift($result)); + + $result = Hash::extract($lastAudit, '{n}.AuditDelta[property_name=body].old_value'); + $this->assertEqual('Second Test Article Body', array_shift($result)); + + $result = Hash::extract($lastAudit, '{n}.AuditDelta[property_name=body].new_value'); + $this->assertEqual('Second Test Article Body (Also Edited)', array_shift($result)); + + $result = Hash::extract($lastAudit, '{n}.AuditDelta[property_name=published].old_value'); + $this->assertEqual('N', array_shift($result)); + + $result = Hash::extract($lastAudit, '{n}.AuditDelta[property_name=published].new_value'); + $this->assertEqual('Y', array_shift($result)); + + // No delta should be reported against the ignored field. + $this->assertIdentical(array(), Hash::extract($lastAudit, '{n}.AuditDelta[property_name=ignored_field]')); + } + +/** + * Test ignoring fields + * + * @return void + */ + public function testIgnoredField() { + $this->Audit = ClassRegistry::init('Audit'); + $this->AuditDelta = ClassRegistry::init('AuditDelta'); + + $newArticle = array( + 'Article' => array( + 'user_id' => 1, + 'author_id' => 1, + 'title' => 'First Test Article', + 'body' => 'First Test Article Body', + 'ignored_field' => 1, + 'published' => 'N', + ), + ); + + // Test no audit record, if only change is on ignored field. + + $this->Article->save($newArticle); + $this->Article->saveField('ignored_field', '5'); + + $lastAudit = $this->Audit->find( + 'count', + array( + 'contain' => array('AuditDelta'), + 'conditions' => array( + 'Audit.event' => 'EDIT', + 'Audit.model' => 'Article', + 'Audit.entity_id' => $this->Article->id, + ), + 'order' => 'Audit.created DESC', + ) + ); + + $this->assertEqual(0, $lastAudit); + } + +/** + * Test delete action + * + * @return void + */ + public function testDelete() { + $this->Audit = ClassRegistry::init('Audit'); + $this->AuditDelta = ClassRegistry::init('AuditDelta'); + $article = $this->Article->find( + 'first', + array( + 'contain' => false, + 'order' => array('rand()'), + ) + ); + + $id = $article['Article']['id']; + + $this->Article->delete($id); + + $lastAudit = $this->Audit->find( + 'all', + array( + // 'contain' => array( 'AuditDelta' ), <-- What does this solve? + 'conditions' => array( + 'Audit.event' => 'DELETE', + 'Audit.model' => 'Article', + 'Audit.entity_id' => $id, + ), + 'order' => 'Audit.created DESC', + ) + ); + + $this->assertEqual(1, count($lastAudit)); + } } \ No newline at end of file diff --git a/Test/Fixture/ArticleFixture.php b/Test/Fixture/ArticleFixture.php index 4f7a9af..46e6133 100644 --- a/Test/Fixture/ArticleFixture.php +++ b/Test/Fixture/ArticleFixture.php @@ -1,29 +1,66 @@ array('type' => 'integer', 'key' => 'primary'), - 'user_id' => array('type' => 'integer', 'null' => false), - 'author_id' => array( 'type' => 'integer', 'null' => false ), - 'title' => array('type' => 'string', 'null' => false), - 'body' => 'text', - 'published' => array('type' => 'string', 'length' => 1, 'default' => 'N'), - 'ignored_field' => array( 'type' => 'integer', 'length' => 1, 'default' => 0 ), - 'created' => 'datetime', - 'updated' => 'datetime' - ); +/** + * Name of the fixture + * + * @var string + */ + public $name = 'Article'; - /** - * records property - * - * @var array - * @access public - */ - public $records = array( - array('user_id' => 1, 'author_id' => 1, 'title' => 'First Article', 'body' => 'First Article Body', 'published' => 'Y', 'created' => '2007-03-18 10:39:23', 'updated' => '2007-03-18 10:41:31'), - array('user_id' => 3, 'author_id' => 3, 'title' => 'Second Article', 'body' => 'Second Article Body', 'published' => 'Y', 'created' => '2007-03-18 10:41:23', 'updated' => '2007-03-18 10:43:31'), - array('user_id' => 1, 'author_id' => 1, 'title' => 'Third Article', 'body' => 'Third Article Body', 'published' => 'Y', 'created' => '2007-03-18 10:43:23', 'updated' => '2007-03-18 10:45:31') - ); +/** + * The fields + * + * @var array + */ + public $fields = array( + 'id' => array('type' => 'integer', 'key' => 'primary'), + 'user_id' => array('type' => 'integer', 'null' => false), + 'author_id' => array('type' => 'integer', 'null' => false), + 'title' => array('type' => 'string', 'null' => false), + 'body' => 'text', + 'published' => array('type' => 'string', 'length' => 1, 'default' => 'N'), + 'ignored_field' => array('type' => 'integer', 'length' => 1, 'default' => 0), + 'created' => 'datetime', + 'updated' => 'datetime', + ); + +/** + * The records + * + * @var array + */ + public $records = array( + array( + 'user_id' => 1, + 'author_id' => 1, + 'title' => 'First Article', + 'body' => 'First Article Body', + 'published' => 'Y', + 'created' => '2007-03-18 10:39:23', + 'updated' => '2007-03-18 10:41:31' + ), + array( + 'user_id' => 3, + 'author_id' => 3, + 'title' => 'Second Article', + 'body' => 'Second Article Body', + 'published' => 'Y', + 'created' => '2007-03-18 10:41:23', + 'updated' => '2007-03-18 10:43:31' + ), + array( + 'user_id' => 1, + 'author_id' => 1, + 'title' => 'Third Article', + 'body' => 'Third Article Body', + 'published' => 'Y', + 'created' => '2007-03-18 10:43:23', + 'updated' => '2007-03-18 10:45:31' + ), + ); } diff --git a/Test/Fixture/AuditDeltaFixture.php b/Test/Fixture/AuditDeltaFixture.php index 997b89b..3dc1b42 100644 --- a/Test/Fixture/AuditDeltaFixture.php +++ b/Test/Fixture/AuditDeltaFixture.php @@ -1,21 +1,34 @@ array( 'type' => 'string', 'length' => 36, 'null' => false ), - 'audit_id' => array( 'type' => 'string', 'length' => 36, 'null' => false ), - 'property_name' => array( 'type' => 'string', 'length' => 255, 'null' => false ), - 'old_value' => array( 'type' => 'string', 'length' => 255 ), - 'new_value' => array( 'type' => 'string', 'length' => 255 ), - ); +/** + * Name of the fixture + * + * @var string + */ + public $name = 'AuditDelta'; - /** - * records property - * - * @var array - * @access public - */ - public $records = array(); +/** + * The fields + * + * @var array + */ + public $fields = array( + 'id' => array('type' => 'string', 'length' => 36, 'null' => false), + 'audit_id' => array('type' => 'string', 'length' => 36, 'null' => false), + 'property_name' => array('type' => 'string', 'length' => 255, 'null' => false), + 'old_value' => array('type' => 'string', 'length' => 255), + 'new_value' => array('type' => 'string', 'length' => 255), + ); + +/** + * The records + * + * @var array + */ + public $records = array(); } diff --git a/Test/Fixture/AuditFixture.php b/Test/Fixture/AuditFixture.php index 8343e41..d4a2088 100644 --- a/Test/Fixture/AuditFixture.php +++ b/Test/Fixture/AuditFixture.php @@ -1,24 +1,37 @@ array( 'type' => 'string', 'length' => 36, 'null' => false ), - 'event' => array( 'type' => 'string', 'length' => 255, 'null' => false ), - 'model' => array( 'type' => 'string', 'length' => 255, 'null' => false ), - 'entity_id' => array( 'type' => 'string', 'length' => 36, 'null' => false ), - 'json_object' => array( 'type' => 'text', 'null' => false ), - 'description' => array( 'type' => 'text' ), - 'source_id' => array( 'type' => 'string', 'length' => 255 ), - 'created' => array( 'type' => 'datetime' ), - ); +/** + * Name of the fixture + * + * @var string + */ + public $name = 'Audit'; - /** - * records property - * - * @var array - * @access public - */ - public $records = array(); +/** + * The fields + * + * @var array + */ + public $fields = array( + 'id' => array('type' => 'string', 'length' => 36, 'null' => false), + 'event' => array('type' => 'string', 'length' => 255, 'null' => false), + 'model' => array('type' => 'string', 'length' => 255, 'null' => false), + 'entity_id' => array('type' => 'string', 'length' => 36, 'null' => false), + 'json_object' => array('type' => 'text', 'null' => false), + 'description' => array('type' => 'text'), + 'source_id' => array('type' => 'string', 'length' => 255), + 'created' => array('type' => 'datetime'), + ); + +/** + * The records + * + * @var array + */ + public $records = array(); } diff --git a/Test/Fixture/AuthorFixture.php b/Test/Fixture/AuthorFixture.php index b4d4712..be179fe 100644 --- a/Test/Fixture/AuthorFixture.php +++ b/Test/Fixture/AuthorFixture.php @@ -1,21 +1,34 @@ array('type' => 'integer', 'key' => 'primary'), - 'first_name' => array('type' => 'string', 'null' => false), - 'last_name' => array('type' => 'string', 'null' => false), - 'created' => 'datetime', - 'updated' => 'datetime' - ); +/** + * The name of the fixture + * + * @var string + */ + public $name = 'Author'; - /** - * records property - * - * @public array - * @access public - */ - public $records = array(); +/** + * The fields + * + * @var array + */ + public $fields = array( + 'id' => array('type' => 'integer', 'key' => 'primary'), + 'first_name' => array('type' => 'string', 'null' => false), + 'last_name' => array('type' => 'string', 'null' => false), + 'created' => 'datetime', + 'updated' => 'datetime', + ); + +/** + * The records + * + * @var array + */ + public $records = array(); }