diff --git a/CHANGELOG b/CHANGELOG index e3d074252..59ffdd79e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -13,19 +13,25 @@ Version 1.1a to be released Version 1.0.7 to be released ---------------------------- +- Bug #367: CUploadedFile may fail if a form contains multiple file uploads in different array dimensions (Qiang) +- Bug #368: CUploadedFile::getInstance() should return null if no file is uploaded (Qiang) - Bug #372: CCacheHttpSession should initialize cache first before using it (Qiang) - Bug #388: 'params' options passed to linkButton are not cleared after submit (Qiang) - Bug #393: Greek language code should be 'el' insead of 'gr' (Qiang) +- Bug #402: CNumberFormatter does not format decimals and percentages correctly (Qiang) - Bug #404: AR would fail when CDbLogRoute uses the same DB connection (Qiang) - Bug: CMemCache has a typo when using memcached (Qiang) - Bug: COciCommandBuilder is referencing undefined variable (Qiang) - Bug: yiic webapp may generate incorrect path to yii.php (Qiang) - Bug: SQL with OFFSET generated by command builder for Oracle is incorrect (Qiang) - Bug: yiic shell model command may fail when a foreign key is named as ID (Qiang) +- Bug: yiic shell controller command does not generate correct controller class file when the controller is under a sub-folder (Qiang) - Chg #391: defaultScope should not be applied to UPDATE (Qiang) - New #360: Added anchor parameter to CController::redirect (Qiang) +- New #375: Added support to allow logout a user without cleaning up non-auth session data (Qiang) - New #378: Added support to allow dynamically turning off and on log routes (Qiang) - New #396: Improved error display when running yiic commands (Qiang) +- New #406: Added support to allow stopping saving and deletion by an ActiveRecord behavior (Qiang) - New: Rolled back the change about treating tinyint(1) in MySQL as boolean (Qiang) - New: Added support for displaying call stack information in trace messages (Qiang) - New: Added 'index' option to AR relations so that related objects can be indexed by specific column value (Qiang) diff --git a/UPGRADE b/UPGRADE index a1ccaffad..0ccfcef00 100644 --- a/UPGRADE +++ b/UPGRADE @@ -21,6 +21,9 @@ Upgrading from v1.0.6 DELETE queries. It is only applied to SELECT queries. You should be aware of this change if you override CActiveRecord::defaultScope() in your code. +- The signature of CWebUser::logout() is changed. If you override this method, +you will need to modify your method declaration accordingly. + Upgrading from v1.0.5 --------------------- diff --git a/framework/base/CModelBehavior.php b/framework/base/CModelBehavior.php index a5bf94677..303b9f111 100644 --- a/framework/base/CModelBehavior.php +++ b/framework/base/CModelBehavior.php @@ -36,7 +36,7 @@ class CModelBehavior extends CBehavior /** * Responds to {@link CModel::onBeforeValidate} event. * Overrides this method if you want to handle the corresponding event of the {@link owner}. - * You may set {@link CModelEvent::isValid} to be false if you want to stop the current validation process. + * You may set {@link CModelEvent::isValid} to be false to quit the validation process. * @param CModelEvent event parameter */ public function beforeValidate($event) diff --git a/framework/base/CModelEvent.php b/framework/base/CModelEvent.php index 05def6f46..0782ca27e 100644 --- a/framework/base/CModelEvent.php +++ b/framework/base/CModelEvent.php @@ -22,8 +22,11 @@ class CModelEvent extends CEvent { /** - * @var boolean whether the model is valid. Defaults to true. - * If this is set false, {@link CModel::validate()} will return false and quit the current validation process. + * @var boolean whether the model is in valid status and should continue its normal method execution cycles. Defaults to true. + * For example, when this event is raised in a {@link CFormModel} object when executing {@link CModel::beforeValidate}, + * if this property is false by the event handler, the {@link CModel::validate} method will quit after handling this event. + * If true, the normal execution cycles will continue, including performing the real validations and calling + * {@link CModel::afterValidate}. */ public $isValid=true; } diff --git a/framework/cli/commands/shell/ModelCommand.php b/framework/cli/commands/shell/ModelCommand.php index 9a8e8813c..a73e3e27c 100644 --- a/framework/cli/commands/shell/ModelCommand.php +++ b/framework/cli/commands/shell/ModelCommand.php @@ -378,6 +378,7 @@ EOD; foreach($table->columns as $column) { $label=ucwords(trim(strtolower(str_replace(array('-','_'),' ',preg_replace('/(?name))))); + $label=preg_replace('/\s+/',' ',$label); if(strcasecmp(substr($label,-3),' id')===0) $label=substr($label,0,-3); $labels[]="'{$column->name}'=>'$label'"; diff --git a/framework/cli/views/shell/model/model.php b/framework/cli/views/shell/model/model.php index 5111c03c9..3a10a533e 100644 --- a/framework/cli/views/shell/model/model.php +++ b/framework/cli/views/shell/model/model.php @@ -15,7 +15,7 @@ class extends CActiveRecord { /** - * The followings are the available columns in table '': + * The followings are the available columns in table '': * @var type.' $'.$column->name."\n"; ?> diff --git a/framework/db/ar/CActiveRecord.php b/framework/db/ar/CActiveRecord.php index 4274607d8..55a02130f 100644 --- a/framework/db/ar/CActiveRecord.php +++ b/framework/db/ar/CActiveRecord.php @@ -750,8 +750,9 @@ abstract class CActiveRecord extends CModel */ protected function beforeSave() { - $this->onBeforeSave(new CEvent($this)); - return true; + $event=new CModelEvent($this); + $this->onBeforeSave($event); + return $event->isValid; } /** @@ -774,8 +775,9 @@ abstract class CActiveRecord extends CModel */ protected function beforeDelete() { - $this->onBeforeDelete(new CEvent($this)); - return true; + $event=new CModelEvent($this); + $this->onBeforeDelete($event); + return $event->isValid; } /** diff --git a/framework/db/ar/CActiveRecordBehavior.php b/framework/db/ar/CActiveRecordBehavior.php index 4ccbc21f8..3df3a799b 100644 --- a/framework/db/ar/CActiveRecordBehavior.php +++ b/framework/db/ar/CActiveRecordBehavior.php @@ -41,7 +41,8 @@ class CActiveRecordBehavior extends CModelBehavior /** * Responds to {@link CActiveRecord::onBeforeSave} event. * Overrides this method if you want to handle the corresponding event of the {@link CBehavior::owner owner}. - * @param CEvent event parameter + * You may set {@link CModelEvent::isValid} to be false to quit the saving process. + * @param CModelEvent event parameter */ public function beforeSave($event) { @@ -50,7 +51,7 @@ class CActiveRecordBehavior extends CModelBehavior /** * Responds to {@link CActiveRecord::onAfterSave} event. * Overrides this method if you want to handle the corresponding event of the {@link CBehavior::owner owner}. - * @param CEvent event parameter + * @param CModelEvent event parameter */ public function afterSave($event) { @@ -59,6 +60,7 @@ class CActiveRecordBehavior extends CModelBehavior /** * Responds to {@link CActiveRecord::onBeforeDelete} event. * Overrides this method if you want to handle the corresponding event of the {@link CBehavior::owner owner}. + * You may set {@link CModelEvent::isValid} to be false to quit the deletion process. * @param CEvent event parameter */ public function beforeDelete($event) diff --git a/framework/i18n/CNumberFormatter.php b/framework/i18n/CNumberFormatter.php index fab4c40f2..716bd566f 100644 --- a/framework/i18n/CNumberFormatter.php +++ b/framework/i18n/CNumberFormatter.php @@ -138,8 +138,9 @@ class CNumberFormatter extends CComponent * @param array format with the following structure: *
 	 * array(
-	 * 	'decimalDigits'=>2,     // number of required digits after decimal point; if -1, it means we should drop decimal point
-	 * 	'integerDigits'=>1,     // number of required digits before decimal point
+	 * 	'decimalDigits'=>2,     // number of required digits after decimal point; 0s will be padded if not enough digits; if -1, it means we should drop decimal point
+	 *  'maxDecimalDigits'=>3,  // maximum number of digits after decimal point. Additional digits will be truncated.
+	 * 	'integerDigits'=>1,     // number of required digits before decimal point; 0s will be padded if not enough digits
 	 * 	'groupSize1'=>3,        // the primary grouping size; if 0, it means no grouping
 	 * 	'groupSize2'=>0,        // the secondary grouping size; if 0, it means no secondary grouping
 	 * 	'positivePrefix'=>'+',  // prefix to positive number
@@ -156,17 +157,24 @@ class CNumberFormatter extends CComponent
 	{
 		$negative=$value<0;
 		$value=abs($value*$format['multiplier']);
-		if($format['decimalDigits']>=0)
-			$value=round($value,$format['decimalDigits']);
-		list($integer,$decimal)=explode('.',sprintf('%F',$value));
-
-		if($format['decimalDigits']>=0)
+		if($format['maxDecimalDigits']>=0)
+			$value=round($value,$format['maxDecimalDigits']);
+		$value="$value";
+		if(($pos=strpos($value,'.'))!==false)
 		{
-			$decimal=rtrim(substr($decimal,0,$format['decimalDigits']),'0');
-			$decimal=$this->_locale->getNumberSymbol('decimal').str_pad($decimal,$format['decimalDigits'],'0');
+			$integer=substr($value,0,$pos);
+			$decimal=substr($value,$pos+1);
 		}
 		else
+		{
+			$integer=$value;
 			$decimal='';
+		}
+
+		if($format['decimalDigits']>strlen($decimal))
+			$decimal=str_pad($decimal,$format['decimalDigits'],'0');
+		if(strlen($decimal)>0)
+			$decimal=$this->_locale->getNumberSymbol('decimal').$decimal;
 
 		$integer=str_pad($integer,$format['integerDigits'],'0',STR_PAD_LEFT);
 		if($format['groupSize1']>0 && strlen($integer)>$format['groupSize1'])
@@ -202,13 +210,22 @@ class CNumberFormatter extends CComponent
 
 		// find out prefix and suffix for positive and negative patterns
 		$patterns=explode(';',$pattern);
-		list($format['positivePrefix'],$format['positiveSuffix'])=preg_split('/[#,\.0]+/',$patterns[0]);
-		if(isset($patterns[1]))  // with a negative pattern
-			list($format['negativePrefix'],$format['negativeSuffix'])=preg_split('/[#,\.0]+/',$patterns[1]);
+		$format['positivePrefix']=$format['positiveSuffix']=$format['negativePrefix']=$format['negativeSuffix']='';
+		if(preg_match('/^(.*?)[#,\.0]+(.*?)$/',$patterns[0],$matches))
+		{
+			$format['positivePrefix']=$matches[1];
+			$format['positiveSuffix']=$matches[2];
+		}
+
+		if(isset($patterns[1]) && preg_match('/^(.*?)[#,\.0]+(.*?)$/',$patterns[1],$matches))  // with a negative pattern
+		{
+			$format['negativePrefix']=$matches[1];
+			$format['negativeSuffix']=$matches[2];
+		}
 		else
 		{
-			$format['negativePrefix']=$this->_locale->getNumberSymbol('minusSign');
-			$format['negativeSuffix']='';
+			$format['negativePrefix']=$this->_locale->getNumberSymbol('minusSign').$format['positivePrefix'];
+			$format['negativeSuffix']=$format['positiveSuffix'];
 		}
 		$pattern=$patterns[0];
 
@@ -227,21 +244,30 @@ class CNumberFormatter extends CComponent
 				$format['decimalDigits']=$pos2-$pos;
 			else
 				$format['decimalDigits']=0;
+			if(($pos3=strrpos($pattern,'#'))>=$pos2)
+				$format['maxDecimalDigits']=$pos3-$pos;
+			else
+				$format['maxDecimalDigits']=$format['decimalDigits'];
 			$pattern=substr($pattern,0,$pos);
 		}
 		else   // no decimal part
-			$format['decimalDigits']=-1; // do not display decimal point
+		{
+			$format['decimalDigits']=0;
+			$format['maxDecimalDigits']=0;
+		}
 
 		// find out things about integer part
-		if(($pos=strpos($pattern,'0'))!==false)
-			$format['integerDigits']=strlen(str_replace(',','',substr($pattern,$pos)));
+		$p=str_replace(',','',$pattern);
+		if(($pos=strpos($p,'0'))!==false)
+			$format['integerDigits']=strrpos($p,'0')-$pos+1;
 		else
 			$format['integerDigits']=0;
 		// find out group sizes. some patterns may have two different group sizes
+		$p=str_replace('#','0',$pattern);
 		if(($pos=strrpos($pattern,','))!==false)
 		{
-			$format['groupSize1']=strlen($pattern)-$pos-1;
-			if(($pos2=strrpos(substr($pattern,0,$pos),','))!==false)
+			$format['groupSize1']=strrpos($p,'0')-$pos;
+			if(($pos2=strrpos(substr($p,0,$pos),','))!==false)
 				$format['groupSize2']=$pos-$pos2-1;
 			else
 				$format['groupSize2']=0;
diff --git a/framework/logging/CLogRouter.php b/framework/logging/CLogRouter.php
index faf11b2cd..767721372 100644
--- a/framework/logging/CLogRouter.php
+++ b/framework/logging/CLogRouter.php
@@ -73,7 +73,7 @@ class CLogRouter extends CApplicationComponent
 	 */
 	public function getRoutes()
 	{
-		return $this->_routes;
+		return new CMap($this->_routes);
 	}
 
 	/**
@@ -98,7 +98,7 @@ class CLogRouter extends CApplicationComponent
 	public function collectLogs($param)
 	{
 		$logger=Yii::getLogger();
-		foreach($this->getRoutes() as $route)
+		foreach($this->_routes as $route)
 		{
 			if($route->enabled)
 				$route->collectLogs($logger);
diff --git a/framework/web/CUploadedFile.php b/framework/web/CUploadedFile.php
index 9fb2a9d89..7eb0145aa 100644
--- a/framework/web/CUploadedFile.php
+++ b/framework/web/CUploadedFile.php
@@ -66,20 +66,16 @@ class CUploadedFile extends CComponent
 				{
 					if(is_array($info['name']))
 					{
-						$first=reset($info['name']);
 						$keys=array_keys($info['name']);
-						if(is_array($first))
+						foreach($keys as $key)
 						{
-							foreach($keys as $key)
+							if(is_array($info['name'][$key]))
 							{
 								$subKeys=array_keys($info['name'][$key]);
 								foreach($subKeys as $subKey)
 									$files["{$class}[{$key}][{$subKey}]"]=new CUploadedFile($info['name'][$key][$subKey],$info['tmp_name'][$key][$subKey],$info['type'][$key][$subKey],$info['size'][$key][$subKey],$info['error'][$key][$subKey]);
 							}
-						}
-						else
-						{
-							foreach($keys as $key)
+							else
 								$files["{$class}[{$key}]"]=new CUploadedFile($info['name'][$key],$info['tmp_name'][$key],$info['type'][$key],$info['size'][$key],$info['error'][$key]);
 						}
 					}
@@ -89,7 +85,7 @@ class CUploadedFile extends CComponent
 			}
 		}
 
-		return isset($files[$name]) ? $files[$name] : null;
+		return isset($files[$name]) && $files[$name]->getError()!=UPLOAD_ERR_NO_FILE ? $files[$name] : null;
 	}
 
 	/**
diff --git a/framework/web/auth/CWebUser.php b/framework/web/auth/CWebUser.php
index 9dc075fbb..3f3141f59 100644
--- a/framework/web/auth/CWebUser.php
+++ b/framework/web/auth/CWebUser.php
@@ -52,6 +52,7 @@ class CWebUser extends CApplicationComponent implements IWebUser
 {
 	const FLASH_KEY_PREFIX='Yii.CWebUser.flash.';
 	const FLASH_COUNTERS='Yii.CWebUser.flash.counters';
+	const STATES_VAR='__states';
 
 	/**
 	 * @var boolean whether to enable cookie-based login. Defaults to false.
@@ -186,13 +187,21 @@ class CWebUser extends CApplicationComponent implements IWebUser
 
 	/**
 	 * Logs out the current user.
-	 * The session will be destroyed.
+	 * This will remove authentication-related session data.
+	 * If the parameter is true, the whole session will be destroyed as well.
+	 * @param boolean whether to destroy the whole session. Defaults to true. If false,
+	 * then {@link clearStates} will be called, which removes only the data stored via {@link setState}.
+	 * This parameter has been available since version 1.0.7. Before 1.0.7, the behavior
+	 * is to destroy the whole session.
 	 */
-	public function logout()
+	public function logout($destroySession=true)
 	{
 		if($this->allowAutoLogin)
 			Yii::app()->getRequest()->getCookies()->remove($this->getStateKeyPrefix());
-		$this->clearStates();
+		if($destroySession)
+			Yii::app()->getSession()->destroy();
+		else
+			$this->clearStates();
 	}
 
 	/**
@@ -226,7 +235,7 @@ class CWebUser extends CApplicationComponent implements IWebUser
 	 */
 	public function getName()
 	{
-		if(($name=$this->getState('_name'))!==null)
+		if(($name=$this->getState('__name'))!==null)
 			return $name;
 		else
 			return $this->guestName;
@@ -239,7 +248,7 @@ class CWebUser extends CApplicationComponent implements IWebUser
 	 */
 	public function setName($value)
 	{
-		$this->setState('_name',$value);
+		$this->setState('__name',$value);
 	}
 
 	/**
@@ -251,7 +260,7 @@ class CWebUser extends CApplicationComponent implements IWebUser
 	 */
 	public function getReturnUrl()
 	{
-		return $this->getState('_returnUrl',Yii::app()->getRequest()->getScriptUrl());
+		return $this->getState('__returnUrl',Yii::app()->getRequest()->getScriptUrl());
 	}
 
 	/**
@@ -259,7 +268,7 @@ class CWebUser extends CApplicationComponent implements IWebUser
 	 */
 	public function setReturnUrl($value)
 	{
-		$this->setState('_returnUrl',$value);
+		$this->setState('__returnUrl',$value);
 	}
 
 	/**
@@ -418,11 +427,18 @@ class CWebUser extends CApplicationComponent implements IWebUser
 
 	/**
 	 * Clears all user identity information from persistent storage.
-	 * The default implementation simply destroys the session.
+	 * This will remove the data stored via {@link setState}.
 	 */
 	public function clearStates()
 	{
-		Yii::app()->getSession()->destroy();
+		$keys=array_keys($_SESSION);
+		$prefix=$this->getStateKeyPrefix();
+		$n=strlen($prefix);
+		foreach($keys as $key)
+		{
+			if(!strncmp($key,$prefix,$n))
+				unset($_SESSION[$key]);
+		}
 	}
 
 	/**
@@ -495,7 +511,7 @@ class CWebUser extends CApplicationComponent implements IWebUser
 	protected function saveIdentityStates()
 	{
 		$states=array();
-		foreach($this->getState('__states',array()) as $name=>$dummy)
+		foreach($this->getState(self::STATES_VAR,array()) as $name=>$dummy)
 			$states[$name]=$this->getState($name);
 		return $states;
 	}
@@ -506,18 +522,16 @@ class CWebUser extends CApplicationComponent implements IWebUser
 	 */
 	protected function loadIdentityStates($states)
 	{
+		$names=array();
 		if(is_array($states))
 		{
-			$names=array();
 			foreach($states as $name=>$value)
 			{
 				$this->setState($name,$value);
 				$names[$name]=true;
 			}
-			$this->setState('__states',$names);
 		}
-		else
-			$this->setState('__states',array());
+		$this->setState(self::STATES_VAR,$names);
 	}
 
 	/**
diff --git a/tests/unit/framework/i18n/CNumberFormatterTest.php b/tests/unit/framework/i18n/CNumberFormatterTest.php
new file mode 100644
index 000000000..e1fefd825
--- /dev/null
+++ b/tests/unit/framework/i18n/CNumberFormatterTest.php
@@ -0,0 +1,70 @@
+usFormatter=new CNumberFormatter('en_us');
+		$this->deFormatter=new CNumberFormatter('de');
+	}
+
+	public function testFormatCurrency()
+	{
+		$numbers=array(
+			array(0, '$0.00', '0,00 $'),
+			array(100, '$100.00', '100,00 $'),
+			array(-100, '($100.00)', '−100,00 $'),
+			array(100.123, '$100.12', '100,12 $'),
+			array(100.1, '$100.10', '100,10 $'),
+			array(100.126, '$100.13', '100,13 $'),
+			array(1000.126, '$1,000.13', '1.000,13 $'),
+			array(1000000.123, '$1,000,000.12', '1.000.000,12 $'),
+		);
+
+		foreach($numbers as $number)
+		{
+			$this->assertEquals($number[1],$this->usFormatter->formatCurrency($number[0],'USD'));
+			$this->assertEquals($number[2],$this->deFormatter->formatCurrency($number[0],'USD'));
+		}
+	}
+
+	public function testFormatDecimal()
+	{
+		$numbers=array(
+			array(0, '0', '0'),
+			array(100, '100', '100'),
+			array(-100, '-100', '−100'),
+			array(100.123, '100.123', '100,123'),
+			array(100.1, '100.1', '100,1'),
+			array(100.1206, '100.121', '100,121'),
+			array(1000.1206, '1,000.121', '1.000,121'),
+			array(1000000.123, '1,000,000.123', '1.000.000,123'),
+		);
+
+		foreach($numbers as $number)
+		{
+			$this->assertEquals($number[1],$this->usFormatter->formatDecimal($number[0]));
+			$this->assertEquals($number[2],$this->deFormatter->formatDecimal($number[0]));
+		}
+	}
+
+	public function testFormatPercentage()
+	{
+		$numbers=array(
+			array(0, '0%', '0 %'),
+			array(0.123, '12%', '12 %'),
+			array(-0.123, '-12%', '−12 %'),
+			array(10.12, '1,012%', '1.012 %'),
+			array(10000.1, '1,000,010%', '1.000.010 %'),
+		);
+
+		foreach($numbers as $number)
+		{
+			$this->assertEquals($number[1],$this->usFormatter->formatPercentage($number[0]));
+			$this->assertEquals($number[2],$this->deFormatter->formatPercentage($number[0]));
+		}
+	}
+}