[EXTENSION] AuditTrail

Hey folks,

Just uploaded an extension to the extension section:

AuditTrail

I always wind up using roughly the same audit trail code, so I decided to modularize it and make it easy to use.

It includes:

  • a behavior similar to that in the cookbook,

  • a detailed install page,

  • an installer script (for mysql at least),

  • the AR model,

  • a widget you can include on your admin pages that shows recent changes to any object,

  • an admin section to review and filter all audit trail logs.

Please give it a try and give me any feedback I can use to make it better!

Link to extension: AuditTrail

Ha! That is what I get for uploading things at 1 in the morning. Thanks for pointing that out, post is edited :)

I just revamped the docs on the extension page. Sorry the documentation was not that good at first. I have addressed everything I have heard was missing from the docs. Please let me know if anything is unclear and I will update!

I just released a 1.1 version. xPortlet is no longer required, and zii CPortlet is used instead.

Hi,

Just tried this extension, and encountered a couple of issues with it :

  1. going to http://localhost/index.php?r=auditTrail simply gives me my default controller, default action page. Going to http://localhost/index.php/auditTrail gives me a 404.

  2. When including the code for the Widget, I get an Internal Server Error : Object configuration must be an array containing a "class" element.

Adding the behavior in my model works fine and the audit trail table is populated with all the changes, so the module works. Did I miss something during the configuration ?

Thanks.

Edit :

Here is a part of the stack trace, as it might be needed :




exception 'CException' with message 'Object configuration must be an array

containing a "class" element.' in

~/yii-1.1.5.r2654/framework/YiiBase.php:187

Stack trace:

#0 ~/yii-1.1.5.r2654/framework/base/CModule.php(371):

YiiBase::createComponent(Array)

#1 ~/framework/base/CModule.php(86):

CModule->getComponent('modules')

#2

~/yii-1.1.5.r2654/demos/blog/protected/modules/auditTrail/widgets/portlets/ShowAuditTrail.php(89):

CModule->__get('modules')

#3

~/yii-1.1.5.r2654/demos/blog/protected/modules/auditTrail/widgets/portlets/ShowAuditTrail.php(78):

ShowAuditTrail->getFromConfigOrObject('userClass')

#4

~/yii-1.1.5.r2654/demos/blog/protected/modules/auditTrail/widgets/portlets/ShowAuditTrail.php(39):

ShowAuditTrail->getEvalUserLabelCode()

#5

~/yii-1.1.5.r2654/framework/zii/widgets/CPortlet.php(95):

ShowAuditTrail->renderContent()



Can you either post or pm me your main.php configuration file ? I would like to set up a test that matches your environmet

Sure, here it is. Posted publicly in case this might help others having the same kind of behavior.




<?php


// uncomment the following to define a path alias

// Yii::setPathOfAlias('local','path/to/local-folder');


// This is the main Web application configuration. Any writable

// CWebApplication properties can be configured here.

return array(

	'basePath'=>dirname(__FILE__).DIRECTORY_SEPARATOR.'..',

	'name'=>'Yii Blog Demo',


	// preloading 'log' component

	'preload'=>array('log'),


	// autoloading model and component classes

	'import'=>array(

		'application.models.*',

		'application.components.*',

		'application.modules.auditTrail.models.AuditTrail',

	),


	'defaultController'=>'post',


	// application components

	'components'=>array(

		'user'=>array(

			// enable cookie-based authentication

			'allowAutoLogin'=>true,

		),

		/*'db'=>array(

			'connectionString' => 'sqlite:protected/data/blog.db',

			'tablePrefix' => 'tbl_',

		),

		// uncomment the following to use a MySQL database

		*/

		'db'=>array(

			'connectionString' => 'mysql:host=****;dbname=****',

			'emulatePrepare' => true,

			'username' => '****',

			'password' => '****',

			'charset' => 'utf8',

			'tablePrefix' => 'tbl_',

		),

	

		/*'errorHandler'=>array(

			// use 'site/error' action to display errors

			'errorAction'=>'site/error',

		),*/

        'urlManager'=>array(

        	'urlFormat'=>'path',

        	'rules'=>array(

        		'post/<id:\d+>/<title:.*?>'=>'post/view',

        		'posts/<tag:.*?>'=>'post/index',

        		'<controller:\w+>/<action:\w+>'=>'<controller>/<action>',

        	),

        ),

	'modules'=>array(

    	    'auditTrail'=>array(),

	    'gii'=>array(

               'class'=>'system.gii.GiiModule',

               'password'=>'gii321',

            ),

    ),

		'log'=>array(

			'class'=>'CLogRouter',

			'routes'=>array(

				array(

					'class'=>'CProfileLogRoute',

				),

			),

		),

	),


	// application-level parameters that can be accessed

	// using Yii::app()->params['paramName']

	'params'=>require(dirname(__FILE__).'/params.php'),

);




Interesting. I can duplicate this problem using your config file with the blog demo, but everything always works fine with a new web app generated from yiic. I am working on figuring out what is different between these two things and will post back what I find.

I figured out what your problem is. If you notice, you cannot get gii to work either with your configuration file. This is because you have all of your modules loaded under your components array. Your spacing makes it hard to figure this out (please always indent your code properly!), but you have modules inside of your components. Modules is its own thing, not a component. Ex:




<?php


// uncomment the following to define a path alias

// Yii::setPathOfAlias('local','path/to/local-folder');


// This is the main Web application configuration. Any writable

// CWebApplication properties can be configured here.

return array(

		'basePath'=>dirname(__FILE__).DIRECTORY_SEPARATOR.'..',

		'name'=>'Yii Blog Demo',


		// preloading 'log' component

		'preload'=>array('log'),


		// autoloading model and component classes

		'import'=>array(

				'application.models.*',

				'application.components.*',

				'application.modules.auditTrail.models.AuditTrail',

		),


		'defaultController'=>'post',


		'modules'=>array(

				 'auditTrail'=>array(),

				 'gii'=>array(

					'class'=>'system.gii.GiiModule',

					'password'=>'gii321',

				 ),

		 ),


		// application components

		'components'=>array(

				'user'=>array(

						// enable cookie-based authentication

						'allowAutoLogin'=>true,

				),

		........



My deepest apologies ! I wanted to try your extension quickly and felt experienced enough to overlook indentation. My bad, thanks alot !

Anyway, I still had to add ‘application.modules.auditTrail.AuditTrailModule’ in the import section of the config. Not doing it throws an error in the view I added the widget :




include(AuditTrailModule.php) [<a href='function.include'>function.include</a>]: failed to open stream: No such file or directory.



Was that import really needed ?

After the import, I still got another error :




Missing argument 1 for CModule::__construct(), called in ~/yii-1.1.5.r2654/demos/blog/protected/modules/auditTrail/widgets/portlets/ShowAuditTrail.php on line 94 and defined



Changing




$at = new AuditTrailModule(); 



to




$at = new AuditTrailModule('', null);



fixed it. Any thoughts ?

Thanks !

As additional apologies, here is the PostgreSQL code to generate the audit trail table, tested on the latest version (9.0.2). Hope it helps !





CREATE FUNCTION update_stamp() RETURNS trigger LANGUAGE plpgsql AS $$begin new.stamp := now(); return new; end;$$;


CREATE TABLE tbl_audit_trail ( 

    id serial NOT NULL CONSTRAINT tbl_audit_trail_pkey PRIMARY KEY,

    old_value text NOT NULL,

    new_value text NOT NULL,

    action character varying(20) NOT NULL,

    model character varying(255) NOT NULL,

    field character varying(64) NOT NULL,

    stamp timestamp without time zone DEFAULT ('now'::text)::timestamp without time zone NOT NULL,

    user_id integer NOT NULL,

    model_id character varying(65) NOT NULL

);


CREATE INDEX idx_action ON tbl_audit_trail USING btree (action);

CREATE INDEX idx_field ON tbl_audit_trail USING btree (field);

CREATE INDEX idx_model ON tbl_audit_trail USING btree (model);

CREATE INDEX idx_model_id ON tbl_audit_trail USING btree (model_id);

CREATE INDEX idx_new_value ON tbl_audit_trail USING btree (new_value);

CREATE INDEX idx_old_value ON tbl_audit_trail USING btree (old_value);

CREATE INDEX idx_user_id ON tbl_audit_trail USING btree (user_id);


CREATE TRIGGER stamp_update BEFORE UPDATE ON tbl_audit_trail FOR EACH ROW EXECUTE PROCEDURE update_stamp();




Hmm…

I am confused about where you explicitly calling the module.

In the import section, you should only have to inclue this line:

application.modules.auditTrail.models.AuditTrail




	...

	'import'=>array(

		'application.models.*',

		'application.components.*',

		'application.modules.auditTrail.models.AuditTrail',

	...



This is so the program knows where to find the AuditTrail model when the rest of the module needs it. If you are trying to use the widget, you would use it like this:




$this->widget(

	'application.modules.auditTrail.widgets.portlets.ShowAuditTrail',

	array(

		'model' => $model,

	)

);



I am not aware of an instance where you would ever need to explicitly instantiate the module or explicitly import the module. Can you tell me in where you use this code (which files)? Perhaps you could include a little bit of the code before and after these calls so I can see the context?

Sure, I modified the file _view.php file of a post (still using the blog demo) :




<?php


$this->widget(

    'application.modules.auditTrail.widgets.portlets.ShowAuditTrail',

    array(

        'model' => $data,

    )

);


?>

<div class="post">

	<div class="title">

		<?php echo CHtml::link(CHtml::encode($data->title), $data->url); ?>

	</div>

	<div class="author">

		posted by <?php echo $data->author->username . ' on ' . date('F j, Y',$data->create_time); ?>

	</div>

	<div class="content">

		<?php

			$this->beginWidget('CMarkdown', array('purifyOutput'=>true));

			echo $data->content;

			$this->endWidget();

		?>

	</div>

	<div class="nav">

		<b>Tags:</b>

		<?php echo implode(', ', $data->tagLinks); ?>

		<br/>

		<?php echo CHtml::link('Permalink', $data->url); ?> |

		<?php echo CHtml::link("Comments ({$data->commentCount})",$data->url.'#comments'); ?> |

		Last updated on <?php echo date('F j, Y',$data->update_time); ?>

	</div>

</div>




I was finally able to trigger the error you spoke of. I do not know why you get it in the blog demo but not in a basic web app generated by yiic. In any event, I modified the widget to fix your problem and be more resource efficient. Please try version 1.1.1 – it should be a drop in replacement.

i had problems with the widget… version 1.1.1 works like a charm :)

Awesome! Glad to hear!

Hi My Model Name is AuditTest:

I have problem with my widget

The Audit trail function does not work… i mean when ever i make any update there is not activity in the audit table

Here is my error


CException


Property "Audittest.id" is not defined.


C:\wamp\www\yii-install\framework\db\ar\CActiveRecord.php(110)


098      */

099     public function __get($name)

100     {

101         if(isset($this->_attributes[$name]))

102             return $this->_attributes[$name];

103         else if(isset($this->getMetaData()->columns[$name]))

104             return null;

105         else if(isset($this->_related[$name]))

106             return $this->_related[$name];

107         else if(isset($this->getMetaData()->relations[$name]))

108             return $this->getRelated($name);

109         else

110             return parent::__get($name);

111     }

112 

113     /**

114      * PHP setter magic method.

115      * This method is overridden so that AR attributes can be accessed like properties.

116      * @param string $name property name

117      * @param mixed $value property value

118      */

119     public function __set($name,$value)

120     {

121         if($this->setAttribute($name,$value)===false)

122         {

Stack Trace

#0	

+  C:\wamp\www\yii-install\framework\db\ar\CActiveRecord.php(110): CComponent->__get("id")

#1	

–  C:\wamp\www\audittest\protected\modules\auditTrail\widgets\portlets\ShowAuditTrail.php(30): CActiveRecord->__get("id")

25 

26     /**

27      * Sets the title of the portlet

28      */

29     public function init() {

30         $this->title = "Audit Trail For " . get_class($this->model) . " " . $this->model->id;

31         parent::init();

32     }

33 

34     /**

35      * generates content of widget the widget.

#2	

+  C:\wamp\www\yii-install\framework\web\CBaseController.php(140): ShowAuditTrail->init()

#3	

+  C:\wamp\www\yii-install\framework\web\CBaseController.php(165): CBaseController->createWidget("application.modules.auditTrail.widgets.portlets.ShowAuditTrail", array(Audittest))

#4	

–  C:\wamp\www\audittest\protected\views\audittest\admin.php(45): CBaseController->widget("application.modules.auditTrail.widgets.portlets.ShowAuditTrail", array(Audittest))

40 <?php $this->widget(

41     'application.modules.auditTrail.widgets.portlets.ShowAuditTrail',

42     array(

43         'model' => $model,

44     )

45 );?>

46 

47 <?php $this->widget('zii.widgets.grid.CGridView', array(

48     'id'=>'audittest-grid',

49     'dataProvider'=>$model->search(),

50     'filter'=>$model,

#5	

+  C:\wamp\www\yii-install\framework\web\CBaseController.php(119): require("C:\wamp\www\audittest\protected\views\audittest\admin.php")

#6	

+  C:\wamp\www\yii-install\framework\web\CBaseController.php(88): CBaseController->renderInternal("C:\wamp\www\audittest\protected\views/audittest\admin.php", array(Audittest), true)

#7	

+  C:\wamp\www\yii-install\framework\web\CController.php(833): CBaseController->renderFile("C:\wamp\www\audittest\protected\views/audittest\admin.php", array(Audittest), true)

#8	

+  C:\wamp\www\yii-install\framework\web\CController.php(746): CController->renderPartial("admin", array(Audittest), true)

#9	

–  C:\wamp\www\audittest\protected\controllers\AudittestController.php(148): CController->render("admin", array(Audittest))

143         if(isset($_GET['Audittest']))

144             $model->attributes=$_GET['Audittest'];

145 

146         $this->render('admin',array(

147             'model'=>$model,

148         ));

149     }

150 

151     /**

152      * Returns the data model based on the primary key given in the GET variable.

153      * If the data model is not found, an HTTP exception will be raised.

#10	

+  C:\wamp\www\yii-install\framework\web\actions\CInlineAction.php(57): AudittestController->actionAdmin()

#11	

+  C:\wamp\www\yii-install\framework\web\CController.php(300): CInlineAction->run()

#12	

+  C:\wamp\www\yii-install\framework\web\filters\CFilterChain.php(133): CController->runAction(CInlineAction)

#13	

+  C:\wamp\www\yii-install\framework\web\filters\CFilter.php(41): CFilterChain->run()

#14	

+  C:\wamp\www\yii-install\framework\web\CController.php(1088): CFilter->filter(CFilterChain)

#15	

+  C:\wamp\www\yii-install\framework\web\filters\CInlineFilter.php(59): CController->filterAccessControl(CFilterChain)

#16	

+  C:\wamp\www\yii-install\framework\web\filters\CFilterChain.php(130): CInlineFilter->filter(CFilterChain)

#17	

+  C:\wamp\www\yii-install\framework\web\CController.php(283): CFilterChain->run()

#18	

+  C:\wamp\www\yii-install\framework\web\CController.php(257): CController->runActionWithFilters(CInlineAction, array("accessControl"))

#19	

+  C:\wamp\www\yii-install\framework\web\CWebApplication.php(328): CController->run("admin")

#20	

+  C:\wamp\www\yii-install\framework\web\CWebApplication.php(121): CWebApplication->runController("audittest/admin")

#21	

+  C:\wamp\www\yii-install\framework\base\CApplication.php(155): CWebApplication->processRequest()

#22	

+  C:\wamp\www\audittest\index.php(13): CApplication->run()

2011-02-10 03:32:14 Apache/2.2.17 (Win32) PHP/5.3.5 Yii Framework/1.1.6

My Main.php


<?php


// uncomment the following to define a path alias

// Yii::setPathOfAlias('local','path/to/local-folder');


// This is the main Web application configuration. Any writable

// CWebApplication properties can be configured here.

return array(

	'basePath'=>dirname(__FILE__).DIRECTORY_SEPARATOR.'..',

	'name'=>'My Web Application',


	// preloading 'log' component

	'preload'=>array('log'),


	// autoloading model and component classes

	'import'=>array(

		'application.modules.auditTrail.models.AuditTrail',

		'application.modules.auditTrail.AuditTrailModule',

		'application.models.*',

		'application.components.*',

	),


	'modules'=>array(

	

		'auditTrail'=>array(),

		// uncomment the following to enable the Gii tool

		

		'gii'=>array(

			'class'=>'system.gii.GiiModule',

			'password'=>'roopesh',

		 	// If removed, Gii defaults to localhost only. Edit carefully to taste.

			'ipFilters'=>array('127.0.0.1','::1'),

		),

		

	),


	// application components

	'components'=>array(

		'user'=>array(

			// enable cookie-based authentication

			'allowAutoLogin'=>true,

		),

		// uncomment the following to enable URLs in path-format

		/*

		'urlManager'=>array(

			'urlFormat'=>'path',

			'rules'=>array(

				'<controller:\w+>/<id:\d+>'=>'<controller>/view',

				'<controller:\w+>/<action:\w+>/<id:\d+>'=>'<controller>/<action>',

				'<controller:\w+>/<action:\w+>'=>'<controller>/<action>',

			),

		),

		*/

		/*'db'=>array(

			'connectionString' => 'sqlite:'.dirname(__FILE__).'/../data/testdrive.db',

		),

		*/

		// uncomment the following to use a MySQL database

		

		'db'=>array(

			'connectionString' => 'mysql:host=localhost;dbname=edusoft',

			'emulatePrepare' => true,

			'username' => 'root',

			'password' => '',

			'charset' => 'utf8',

		),

		

		'errorHandler'=>array(

			// use 'site/error' action to display errors

            'errorAction'=>'site/error',

        ),

		'log'=>array(

			'class'=>'CLogRouter',

			'routes'=>array(

				array(

					'class'=>'CFileLogRoute',

					'levels'=>'error, warning',

				),

				// uncomment the following to show log messages on web pages

				/*

				array(

					'class'=>'CWebLogRoute',

				),

				*/

			),

		),

	),


	// application-level parameters that can be accessed

	// using Yii::app()->params['paramName']

	'params'=>array(

		// this is used in contact page

		'adminEmail'=>'webmaster@example.com',

	),

);

My Model file


<?php


/**

 * This is the model class for table "audittest".

 *

 * The followings are the available columns in table 'audittest':

 * @property string $First_Name

 * @property string $Last_Name

 * @property string $Gender

 */

class Audittest extends CActiveRecord

{


	public function behaviors()

	{

    return array(

        'LoggableBehavior'=>

            'application.modules.auditTrail.behaviors.LoggableBehavior',

		);

	}

	

	/**

	 * Returns the static model of the specified AR class.

	 * @return Audittest the static model class

	 */

	public static function model($className=__CLASS__)

	{

		return parent::model($className);

	}


	/**

	 * @return string the associated database table name

	 */

	public function tableName()

	{

		return 'audittest';

	}


	/**

	 * @return array validation rules for model attributes.

	 */

	public function rules()

	{

		// NOTE: you should only define rules for those attributes that

		// will receive user inputs.

		return array(

			array('First_Name, Last_Name, Gender', 'required'),

			array('First_Name, Last_Name', 'length', 'max'=>10),

			array('Gender', 'length', 'max'=>5),

			// The following rule is used by search().

			// Please remove those attributes that should not be searched.

			array('First_Name, Last_Name, Gender', 'safe', 'on'=>'search'),

		);

	}


	/**

	 * @return array relational rules.

	 */

	public function relations()

	{

		// NOTE: you may need to adjust the relation name and the related

		// class name for the relations automatically generated below.

		return array(

		);

	}


	/**

	 * @return array customized attribute labels (name=>label)

	 */

	public function attributeLabels()

	{

		return array(

			'First_Name' => 'First Name',

			'Last_Name' => 'Last Name',

			'Gender' => 'Gender',

		);

	}


	/**

	 * Retrieves a list of models based on the current search/filter conditions.

	 * @return CActiveDataProvider the data provider that can return the models based on the search/filter conditions.

	 */

	public function search()

	{

		// Warning: Please modify the following code to remove attributes that

		// should not be searched.


		$criteria=new CDbCriteria;


		$criteria->compare('First_Name',$this->First_Name,true);

		$criteria->compare('Last_Name',$this->Last_Name,true);

		$criteria->compare('Gender',$this->Gender,true);


		return new CActiveDataProvider(get_class($this), array(

			'criteria'=>$criteria,

		));

	}

}

please let me know if i am missing any thing

The auditTrail module assumes that you will have a field in each table called id that will be the primary key. I did not specify this in the documentation, and I apologize. I will update that now. As for your test, just add an auto increment field called id and then it will work. Please let me know if this fixes it!

I created the new ID in my table and one of the issue was solved:

No error

But the audit table is still not showing any new entry if i create or update any records

I am using admin admin as my login password

I have also but this code in the model file


public function behaviors()

{

    return array(

        'LoggableBehavior'=>

            'application.modules.auditTrail.behaviors.LoggableBehavior',

    );

}

Please let me know if i am missing any thing

thanks