diff --git a/CHANGELOG b/CHANGELOG index 27639668f..b9318eadb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -8,19 +8,23 @@ Version 1.1a to be released Version 1.0.1 to be released ---------------------------- -- Fixed issue #38: CHtml links and buttons don't work when they are updated via ajax (Qiang) -- Fixed issue #41: missing function CHttpRequest:: getCsrfTokenFromCookie() (Qiang) -- Fixed issue #42: Wrong links in crud generated admin view (Qiang) -- Fixed issue #45: Many-to-many relation does not work when both foreign tables are the same (Qiang) -- Fixed issue #47: Wrong url parsing when CUrlManager is set in path format (Qiang) -- Fixed issue #48: Typo in CActiveRecord::setAttribute (Qiang) -- Fixed issue #49: Invalid markup generated by yiic tool (Qiang) -- Fixed issue #53: tabular form input causes AR to fail (Qiang) -- Fixed issue #54: typoe in CHtml::listOptions (Qiang) -- Fixed issue #56: Allow specifying customized 'on' when a validator can be applied (Qiang) -- Fixed inaccurate error message when adding an item as a child of itself in CAuthManager (Qiang) -- Added CHtml::activeId and CHtml::activeName (Qiang) -- Added German and Spanish core message translations (mikl, sebas) +- Bug #38: CHtml links and buttons don't work when they are updated via ajax (Qiang) +- Bug #41: missing function CHttpRequest:: getCsrfTokenFromCookie() (Qiang) +- Bug #42: Wrong links in crud generated admin view (Qiang) +- Bug #45: Many-to-many relation does not work when both foreign tables are the same (Qiang) +- Bug #47: Wrong url parsing when CUrlManager is set in path format (Qiang) +- Bug #48: Typo in CActiveRecord::setAttribute (Qiang) +- Bug #49: Invalid markup generated by yiic tool (Qiang) +- Bug #53: tabular form input causes AR to fail (Qiang) +- Bug #54: typo in CHtml::listOptions (Qiang) +- Bug #57: Explicit column aliasing is not working in relational AR when the column appears in ORDER BY (Qiang) +- Enhancement #44: Make "yiic shell" command to support controllers organized in subdirectories (Qiang) +- Enhancement #56: Allow specifying customized 'on' when a validator can be applied (Qiang) +- Enhancement #58: Allow specifying HAVING clause in DB criteria (Qiang) +- Enhancement: Fixed inaccurate error message when adding an item as a child of itself in CAuthManager (Qiang) +- Enhancement: CHtml::activeId and CHtml::activeName (Qiang) +- Enhancement: Added German and Spanish core message translations (mikl, sebas) +- Enhancement: Added CController::init() (Qiang) Version 1.0 December 3, 2008 ---------------------------- diff --git a/docs/guide/database.arr.txt b/docs/guide/database.arr.txt index 994aa6f2a..a1339511a 100644 --- a/docs/guide/database.arr.txt +++ b/docs/guide/database.arr.txt @@ -252,6 +252,11 @@ during lazy loading: references need to be disambiguated using `aliasToken` (e.g. `??.age`). This option only applies to `HAS_MANY` and `MANY_MANY` relationships. + - `having`: the `HAVING` clause. It defaults to empty. Note, column +references need to be disambiguated using `aliasToken` (e.g. `??.age`). +This option only applies to `HAS_MANY` and `MANY_MANY` relationships. Note: +option has been available since version 1.0.1. + - `limit`: limit of the rows to be selected. This option does NOT apply to `BELONGS_TO` relation. diff --git a/docs/guide/database.dao.txt b/docs/guide/database.dao.txt index b0abeafda..26079343c 100644 --- a/docs/guide/database.dao.txt +++ b/docs/guide/database.dao.txt @@ -128,7 +128,7 @@ construct to retrieve row by row. ~~~ [php] -$dataReader=$command->query($sql); +$dataReader=$command->query(); // calling read() repeatedly until it returns false while(($row=$dataReader->read())!==false) { ... } // using foreach to traverse through every row of data diff --git a/docs/guide/form.model.txt b/docs/guide/form.model.txt index ec719cc11..8f5c6b778 100644 --- a/docs/guide/form.model.txt +++ b/docs/guide/form.model.txt @@ -101,7 +101,7 @@ array('AttributeList', 'Validator', 'on'=>'ScenarioList', ...additional options) where `AttributeList` is a string of comma-separated attribute names which need to be validated according to the rule; `Validator` specifies what kind of validation should be performed; the `on` parameter is optional which specifies -a list of scenarios that the rule should be applied; and additional options +a list of scenarios where the rule should be applied; and additional options are name-value pairs which are used to initialize the corresponding validator's property values. @@ -197,20 +197,43 @@ to save the model to database, validation will be automatically performed. By default, when we call [validate()|CModel::validate], all attributes specified in [rules()|CModel::rules] will be validated. Sometimes we may -only want to validate part of those attributes, while sometimes we want to -apply different validation rules in different scenarios. We can achieve -these goals by passing two parameters to [validate()|CModel::validate]. -For example, if we only want to validate three attributes under -the `register` scenario, we can use the following code: +only want to validate part of those attributes. To do so, we can pass +the names of the attributes to be validated as the first parameter of the +[validate()|CModel::validate] call, like the following: + +~~~ +[php] +$model->validate(array('username','password')); +~~~ + + +Scenario-based Validation +------------------------- + +> Note: scenario-based validation has been available since version 1.0.1. + +It is often needed that a model be used in different scenarios. For example, +a user model may be used in `login`, 'register`, `insert` or `update` scenarios. +Different scenario may require different validation rules. We call this +scenario-based validation. + +Scenario-based validation can be achieved by using the `on` parameter mentioned +in the [Declaring Validation Rules](#declaring-validation-rules) section. +In particular, if a validation rule should only be applied in certain scenarios, +we would assign to its `on` parameter with the list of the scenario names. +And then, when we call [validate()|CModel::validate] to perform the validation, +we will specify the second parameter as the needed scenario name. + +For example, we execute the following statement to perform the validation +when registering a user: ~~~ [php] $model->validate(array('username','password','password_repeat'), 'register'); ~~~ -In order to allow scenario-based validation as described above, we should -specify the `on` option in the relevant validation rules. For the above -example, we would need the following rules: +Because the validation is done in the `register` scenario, we would need +the following rules: ~~~ [php] diff --git a/framework/base/CModel.php b/framework/base/CModel.php index e233b20c4..cde1a56a5 100644 --- a/framework/base/CModel.php +++ b/framework/base/CModel.php @@ -29,7 +29,7 @@ abstract class CModel extends CComponent * Errors found during the validation can be retrieved via {@link getErrors}. * @param array the list of attributes to be validated. Defaults to null, * meaning every attribute as listed in {@link rules} will be validated. - * @param string the set of the validation rules that should be applied. + * @param string the scenario that the validation rules should be applied. * This is used to match the {@link CValidator::on on} property set in * the validation rules. Defaults to null, meaning all validation rules * should be applied. If this parameter is a non-empty string (e.g. 'register'), diff --git a/framework/cli/commands/shell/ControllerCommand.php b/framework/cli/commands/shell/ControllerCommand.php index ee6e10750..a7fe9091d 100644 --- a/framework/cli/commands/shell/ControllerCommand.php +++ b/framework/cli/commands/shell/ControllerCommand.php @@ -29,17 +29,16 @@ class ControllerCommand extends CConsoleCommand { return << [action-name] ... + controller [action-ID] ... DESCRIPTION This command generates a controller and views associated with the specified actions. PARAMETERS - * controller-name: required, the controller name. - * action-name: optional, action name. You may supply one or - or several action names. A default 'index' action will always - be generated. + * controller-ID: required, controller ID (e.g. 'post', 'admin.user') + * action-ID: optional, action ID. You may supply one or several + action IDs. A default 'index' action will always be generated. EOD; } @@ -56,24 +55,36 @@ EOD; echo $this->getHelp(); return; } - $controllerName=$args[0]; - $controllerName[0]=strtolower($controllerName); + + $controllerID=$args[0]; + if(($pos=strrpos($controllerID,'.'))===false) + { + $controllerClass=ucfirst($controllerID).'Controller'; + $controllerFile=Yii::app()->controllerPath.DIRECTORY_SEPARATOR.$controllerClass.'.php'; + $controllerID[0]=strtolower($controllerID[0]); + } + else + { + $controllerClass=ucfirst(substr($controllerID,$pos+1)).'Controller'; + $controllerFile=Yii::app()->controllerPath.DIRECTORY_SEPARATOR.str_replace('.',DIRECTORY_SEPARATOR,substr($controllerID,0,$pos)).DIRECTORY_SEPARATOR.$controllerClass.'.php'; + $controllerID[$pos+1]=strtolower($controllerID[$pos+1]); + } + $args[]='index'; $actions=array_unique(array_splice($args,1)); - $controllerFile=ucfirst($controllerName).'Controller.php'; $templateFile=$this->templateFile===null?YII_PATH.'/cli/views/shell/controller/controller.php':$this->templateFile; $list=array( - $controllerFile=>array( + basename($controllerFile)=>array( 'source'=>$templateFile, - 'target'=>Yii::app()->controllerPath.DIRECTORY_SEPARATOR.$controllerFile, + 'target'=>$controllerFile, 'callback'=>array($this,'generateController'), - 'params'=>array(ucfirst($controllerName).'Controller', $actions), + 'params'=>array($controllerClass, $actions), ), ); - $viewPath=Yii::app()->viewPath.DIRECTORY_SEPARATOR.$controllerName; + $viewPath=Yii::app()->viewPath.DIRECTORY_SEPARATOR.str_replace('.',DIRECTORY_SEPARATOR,$controllerID); foreach($actions as $name) { $list[$name.'.php']=array( @@ -84,14 +95,13 @@ EOD; $this->copyFiles($list); - $path=Yii::app()->controllerPath.DIRECTORY_SEPARATOR.$controllerFile; echo << [controller-name] ... + crud [controller-ID] ... DESCRIPTION This command generates a controller and views that accomplish @@ -40,8 +40,8 @@ DESCRIPTION PARAMETERS * model-class: required, the name of the data model class. This can also be specified using dot syntax (e.g. application.models.Post) - * controller-name: optional, the name of the controller. If not given, - it will use the model class as the controller name. + * controller-ID: optional, the controller ID (e.g. 'post', 'admin.user'). + If absent, the model class name will be used as the ID. EOD; } @@ -62,24 +62,42 @@ EOD; if(strpos($modelClass,'.')===false) $modelClass='application.models.'.$modelClass; $modelClass=Yii::import($modelClass); + if(isset($args[1])) - $controllerName=$args[1]; + { + $controllerID=$args[1]; + if(($pos=strrpos($controllerID,'.'))===false) + { + $controllerClass=ucfirst($controllerID).'Controller'; + $controllerFile=Yii::app()->controllerPath.DIRECTORY_SEPARATOR.$controllerClass.'.php'; + $controllerID[0]=strtolower($controllerID[0]); + } + else + { + $controllerClass=ucfirst(substr($controllerID,$pos+1)).'Controller'; + $controllerFile=Yii::app()->controllerPath.DIRECTORY_SEPARATOR.str_replace('.',DIRECTORY_SEPARATOR,substr($controllerID,0,$pos)).DIRECTORY_SEPARATOR.$controllerClass.'.php'; + $controllerID[$pos+1]=strtolower($controllerID[$pos+1]); + } + } else { - $controllerName=$modelClass; - $controllerName[0]=strtolower($controllerName[0]); + $controllerID=$modelClass; + $controllerClass=ucfirst($controllerID).'Controller'; + $controllerFile=Yii::app()->controllerPath.DIRECTORY_SEPARATOR.$controllerClass.'.php'; + $controllerID[0]=strtolower($controllerID[0]); } - $controllerClass=ucfirst($controllerName).'Controller'; + $templatePath=$this->templatePath===null?YII_PATH.'/cli/views/shell/crud':$this->templatePath; - $viewPath=Yii::app()->viewPath.DIRECTORY_SEPARATOR.$controllerName; + $viewPath=Yii::app()->viewPath.DIRECTORY_SEPARATOR.str_replace('.',DIRECTORY_SEPARATOR,$controllerID); $list=array( - $controllerClass.'.php'=>array( + basename($controllerFile)=>array( 'source'=>$templatePath.'/controller.php', - 'target'=>Yii::app()->controllerPath.'/'.$controllerClass.'.php', + 'target'=>$controllerFile, 'callback'=>array($this,'generateController'), 'params'=>array($controllerClass,$modelClass), ), ); + foreach(array('create','update','list','show','admin') as $action) { $list[$action.'.php']=array( @@ -92,8 +110,8 @@ EOD; $this->copyFiles($list); - echo "\nCrud '{$controllerName}' has been successfully created. You may access it via:\n"; - echo "http://hostname/path/to/index.php?r={$controllerName}\n"; + echo "\nCrud '{$controllerID}' has been successfully created. You may access it via:\n"; + echo "http://hostname/path/to/index.php?r={$controllerID}\n"; } public function generateController($source,$params) diff --git a/framework/console/CConsoleCommand.php b/framework/console/CConsoleCommand.php index 3eb747562..e429b7f28 100644 --- a/framework/console/CConsoleCommand.php +++ b/framework/console/CConsoleCommand.php @@ -148,8 +148,8 @@ abstract class CConsoleCommand extends CComponent } else { - echo " generate $name\n"; $this->ensureDirectory(dirname($target)); + echo " generate $name\n"; } file_put_contents($target,$content); } diff --git a/framework/db/ar/CActiveFinder.php b/framework/db/ar/CActiveFinder.php index cb238d4dc..af0489780 100644 --- a/framework/db/ar/CActiveFinder.php +++ b/framework/db/ar/CActiveFinder.php @@ -299,6 +299,7 @@ class CJoinElement $query->limit=$child->relation->limit; $query->offset=$child->relation->offset; $query->groups[]=str_replace($child->relation->aliasToken.'.',$child->_tableAlias.'.',$child->relation->group); + $query->havings[]=str_replace($child->relation->aliasToken.'.',$child->_tableAlias.'.',$child->relation->having); } $child->buildQuery($query); $this->runQuery($query); @@ -462,7 +463,7 @@ class CJoinElement $name=trim($name); $matches=array(); if(($pos=strrpos($name,'.'))!==false) - $key=substr($name,0,$pos); + $key=substr($name,$pos+1); else $key=$name; if(isset($this->_columnAliases[$key])) // simple column names @@ -474,9 +475,11 @@ class CJoinElement { $alias=$matches[2]; if(!isset($this->_columnAliases[$alias])) - $this->_columnAliases[$alias]='t'.$this->id.'_c'.count($this->_columnAliases); - $columns[]=$matches[1].' AS '.$this->_columnAliases[$alias]; - $selected[$matches[1]]=1; + { + $this->_columnAliases[$alias]=$alias; + $columns[]=$name; + $selected[$alias]=1; + } } else throw new CDbException(Yii::t('yii','Active record "{class}" is trying to select an invalid column "{column}". Note, the column must exist in the table or be an expression with alias.', @@ -692,6 +695,10 @@ class CJoinQuery * @var array list of GROUP BY clauses */ public $groups=array(); + /** + * @var array list of HAVING clauses + */ + public $havings=array(); /** * @var integer row limit */ @@ -724,6 +731,7 @@ class CJoinQuery $this->conditions[]=$criteria->condition; $this->orders[]=$criteria->order; $this->groups[]=$criteria->group; + $this->havings[]=$criteria->having; $this->limit=$criteria->limit; $this->offset=$criteria->offset; $this->params=$criteria->params; @@ -774,6 +782,13 @@ class CJoinQuery if($groups!==array()) $sql.=' GROUP BY ' . implode(', ',$groups); + $havings=array(); + foreach($this->havings as $having) + if($having!=='') + $havings[]=$having; + if($havings!==array()) + $sql.=' HAVING ' . implode(' AND ',$havings); + $orders=array(); foreach($this->orders as $order) if($order!=='') diff --git a/framework/db/ar/CActiveRecord.php b/framework/db/ar/CActiveRecord.php index 1653239b5..173273fee 100644 --- a/framework/db/ar/CActiveRecord.php +++ b/framework/db/ar/CActiveRecord.php @@ -542,6 +542,8 @@ abstract class CActiveRecord extends CModel *
    *
  • 'group': string, the GROUP BY clause. Defaults to empty. Note, column references need to * be disambiguated with prefix '??.' (e.g. ??.age). This option only applies to HAS_MANY and MANY_MANY relations.
  • + *
  • 'having': string, the HAVING clause. Defaults to empty. Note, column references need to + * be disambiguated with prefix '??.' (e.g. ??.age). This option only applies to HAS_MANY and MANY_MANY relations.
  • *
  • 'limit': limit of the rows to be selected. This option does not apply to BELONGS_TO relation.
  • *
  • 'offset': offset of the rows to be selected. This option does not apply to BELONGS_TO relation.
  • *
@@ -1469,6 +1471,10 @@ class CHasManyRelation extends CActiveRelation * @var string GROUP BY clause. Column names referenced here should be prefixed with '??.'. */ public $group=''; + /** + * @var string HAVING clause. Column names referenced here should be prefixed with '??.'. + */ + public $having=''; /** * @var integer limit of the rows to be selected. It is effective only for lazy loading this related object. Defaults to -1, meaning no limit. */ diff --git a/framework/db/schema/CDbCommandBuilder.php b/framework/db/schema/CDbCommandBuilder.php index 028fa21ca..7ca1d497d 100644 --- a/framework/db/schema/CDbCommandBuilder.php +++ b/framework/db/schema/CDbCommandBuilder.php @@ -72,6 +72,7 @@ class CDbCommandBuilder extends CComponent $sql=$this->applyJoin($sql,$criteria->join); $sql=$this->applyCondition($sql,$criteria->condition); $sql=$this->applyGroup($sql,$criteria->group); + $sql=$this->applyHaving($sql,$criteria->having); $sql=$this->applyOrder($sql,$criteria->order); $sql=$this->applyLimit($sql,$criteria->limit,$criteria->offset); $command=$this->_connection->createCommand($sql); @@ -103,6 +104,7 @@ class CDbCommandBuilder extends CComponent $sql=$this->applyJoin($sql,$criteria->join); $sql=$this->applyCondition($sql,$criteria->condition); $sql=$this->applyGroup($sql,$criteria->group); + $sql=$this->applyHaving($sql,$criteria->having); $sql=$this->applyOrder($sql,$criteria->order); $sql=$this->applyLimit($sql,$criteria->limit,$criteria->offset); $command=$this->_connection->createCommand($sql); @@ -306,6 +308,21 @@ class CDbCommandBuilder extends CComponent return $sql; } + /** + * Alters the SQL to apply HAVING. + * @param string SQL query string without HAVING + * @param string HAVING + * @return string SQL with HAVING + * @since 1.0.1 + */ + public function applyHaving($sql,$having) + { + if($having!=='') + return $sql.' HAVING '.$having; + else + return $sql; + } + /** * Binds parameter values for an SQL command. * @param CDbCommand database command diff --git a/framework/db/schema/CDbCriteria.php b/framework/db/schema/CDbCriteria.php index 29a46701d..9b40b8dee 100644 --- a/framework/db/schema/CDbCriteria.php +++ b/framework/db/schema/CDbCriteria.php @@ -56,6 +56,12 @@ class CDbCriteria * For example, 'LEFT JOIN users ON users.id=authorID'. */ public $join=''; + /** + * @var string the condition to be applied with GROUP-BY clause. + * For example, 'SUM(revenue)<50000'. + * @since 1.0.1 + */ + public $having=''; /** * Constructor. diff --git a/framework/web/CController.php b/framework/web/CController.php index e5aaba97a..bb732b673 100644 --- a/framework/web/CController.php +++ b/framework/web/CController.php @@ -97,6 +97,16 @@ class CController extends CBaseController $this->_id=$id; } + /** + * Initializes the controller. + * This method is called by the application before the controller starts to execute. + * You may override this method to perform the needed initialization for the controller. + * @since 1.0.1 + */ + public function init() + { + } + /** * Returns the filter configurations. * diff --git a/framework/web/CWebApplication.php b/framework/web/CWebApplication.php index 0a64a5865..dea125e4f 100644 --- a/framework/web/CWebApplication.php +++ b/framework/web/CWebApplication.php @@ -146,6 +146,7 @@ class CWebApplication extends CApplication { $oldController=$this->_controller; $this->_controller=$controller; + $controller->init(); $controller->run($actionID); $this->_controller=$oldController; } diff --git a/tests/ut/framework/db/ar/CActiveRecordTest.php b/tests/ut/framework/db/ar/CActiveRecordTest.php index 18f3fa336..80066826a 100644 --- a/tests/ut/framework/db/ar/CActiveRecordTest.php +++ b/tests/ut/framework/db/ar/CActiveRecordTest.php @@ -537,4 +537,16 @@ class CActiveRecordTest extends CTestCase 'params'=>array(':id'=>2))); $this->assertTrue($posts[0] instanceof Post); } + + public function testRelationWithColumnAlias() + { + $users=User::model()->with('posts')->findAll(array( + 'select'=>'id, username AS username2', + 'order'=>'username2', + )); + + $this->assertEquals(3,count($users)); + $this->assertEquals($users[1]->username,null); + $this->assertEquals($users[1]->username2,'user2'); + } } diff --git a/tests/ut/framework/db/data/models.php b/tests/ut/framework/db/data/models.php index 7bb520a57..f50d7815e 100644 --- a/tests/ut/framework/db/data/models.php +++ b/tests/ut/framework/db/data/models.php @@ -2,6 +2,8 @@ class User extends CActiveRecord { + public $username2; + public static function model($class=__CLASS__) { return parent::model($class);