diff --git a/app/config/sql/i18n.sql b/app/config/sql/i18n.sql index 17a3412b1..26ef91d4f 100644 --- a/app/config/sql/i18n.sql +++ b/app/config/sql/i18n.sql @@ -1,18 +1,30 @@ +# $Id$ +# +# Copyright 2005-2007, Cake Software Foundation, Inc. +# 1785 E. Sahara Avenue, Suite 490-204 +# Las Vegas, Nevada 89104 +# +# Licensed under The MIT License +# Redistributions of files must retain the above copyright notice. +# http://www.opensource.org/licenses/mit-license.php The MIT License + CREATE TABLE i18n ( - id int(10) NOT NULL auto_increment, - locale varchar(6) NOT NULL, - i18n_content_id int(10) NOT NULL, - model varchar(255) NOT NULL, - row_id int(10) NOT NULL, - field varchar(255) NOT NULL, - PRIMARY KEY (id), - KEY row_id (row_id), - KEY model (model), - KEY field (field) + id int(10) NOT NULL auto_increment, + locale varchar(6) NOT NULL, + i18n_content_id int(10) NOT NULL, + model varchar(255) NOT NULL, + row_id int(10) NOT NULL, + field varchar(255) NOT NULL, + PRIMARY KEY (id), + KEY locale (locale), + KEY i18n_content_id (i18n_content_id), + KEY row_id (row_id), + KEY model (model), + KEY field (field) ); CREATE TABLE i18n_content ( - id int(10) NOT NULL auto_increment, - content text, - PRIMARY KEY (id) -); + id int(10) NOT NULL auto_increment, + content text, + PRIMARY KEY (id) +); \ No newline at end of file diff --git a/cake/libs/model/behaviors/translate.php b/cake/libs/model/behaviors/translate.php index d9af9574e..e3fd2feeb 100644 --- a/cake/libs/model/behaviors/translate.php +++ b/cake/libs/model/behaviors/translate.php @@ -26,10 +26,6 @@ * @lastmodified $Date$ * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ -/** - * Included libraries. - */ -uses('l10n'); /** * Short description for file. * @@ -37,37 +33,431 @@ uses('l10n'); * * @package cake * @subpackage cake.cake.libs.model.behaviors - * */ class TranslateBehavior extends ModelBehavior { - var $locale = null; +/** + * Used for runtime configuration of model + */ + var $runtime = array(); +/** + * Instance of I18nModel class, used internally + */ + var $_model = null; +/** + * Constructor + */ + function __construct() { + parent::__construct(); + $this->_model =& new I18nModel(); + ClassRegistry::addObject('I18nModel', $this->_model); + } +/** + * Callback + * + * $config for TranslateBehavior should be + * array( 'fields' => array('field_one', + * 'field_two' => 'FieldAssoc', 'field_three')) + * + * With above example only one permanent hasMany will be joined (for field_two + * as FieldAssoc) + * + * $config could be empty - and translations configured dynamically by + * bindTranslation() method + */ function setup(&$model, $config = array()) { + $this->settings[$model->name] = array(); + $this->runtime[$model->name] = array('fields' => array()); + $db =& ConnectionManager::getDataSource($model->useDbConfig); + if(!$db->connected) { + trigger_error('Datasource '.$model->useDbConfig.' for I18nBehavior of model '.$model->name.' is not connected', E_USER_ERROR); + return false; + } + $this->runtime[$model->name]['tablePrefix'] = $db->config['prefix']; + return $this->bindTranslation($model, $config, false); } - +/** + * Callback + */ function beforeFind(&$model, $query) { + if(is_string($query['fields']) && 'COUNT(*) AS count' == $query['fields']) { + $this->runtime[$model->name]['count'] = true; + return $query; + } + $locale = $this->_getLocale($model); + if(empty($locale) || is_array($locale)) { + return $query; + } + $autoFields = false; + + if(empty($query['fields'])) { + $query['fields'] = array($model->name.'.*'); + + foreach(array('hasOne', 'belongsTo') as $type) { + foreach($model->{$type} as $key => $value) { + + if(empty($value['fields'])) { + $query['fields'][] = $key.'.*'; + } else { + foreach($value['fields'] as $field) { + $query['fields'][] = $key.'.'.$field; + } + } + } + } + $autoFields = true; + } + + $fields = am($this->settings[$model->name], $this->runtime[$model->name]['fields']); + $tablePrefix = $this->runtime[$model->name]['tablePrefix']; + $addFields = array(); + + if(is_array($query['fields'])) { + if(in_array($model->name.'.*', $query['fields'])) { + foreach($fields as $key => $value) { + $addFields[] = ife(is_numeric($key), $value, $key); + } + } else { + foreach($fields as $key => $value) { + $field = ife(is_numeric($key), $value, $key); + if($autoFields || in_array($model->name.'.'.$field, $query['fields'])) { + $addFields[] = $field; + } + } + } + } + + if($addFields) { + $db =& ConnectionManager::getDataSource($model->useDbConfig); + + foreach($addFields as $field) { + $key = array_search($model->name.'.'.$field, $query['fields']); + if(false !== $key) { + unset($query['fields'][$key]); + } + + $query['fields'][] = 'I18n__'.$field.'.content'; + + $query['joins'][] = 'LEFT JOIN '.$db->name($tablePrefix.'i18n').' AS '.$db->name('I18n__'.$field.'Model').' ON '.$db->name($model->name.'.id').' = '.$db->name('I18n__'.$field.'Model.row_id'); + $query['joins'][] = 'LEFT JOIN '.$db->name($tablePrefix.'i18n_content').' AS '.$db->name('I18n__'.$field).' ON '.$db->name('I18n__'.$field.'Model.i18n_content_id').' = '.$db->name('I18n__'.$field.'.id'); + $query['conditions'][$db->name('I18n__'.$field.'Model.model')] = $model->name; + $query['conditions'][$db->name('I18n__'.$field.'Model.field')] = $field; + $query['conditions'][$db->name('I18n__'.$field.'Model`.`locale')] = $locale; + } + } + $query['fields'] = am($query['fields']); + $this->runtime[$model->name]['beforeFind'] = $addFields; + return $query; } - +/** + * Callback + */ function afterFind(&$model, $results, $primary) { + if(!empty($this->runtime[$model->name]['count'])) { + unset($this->runtime[$model->name]['count']); + return $results; + } + $this->runtime[$model->name]['fields'] = array(); + $locale = $this->_getLocale($model); + if(empty($locale) || empty($results)) { + return $results; + } + + if(is_array($locale)) { + $fields = am($this->settings[$model->name], $this->runtime[$model->name]['fields']); + $emptyFields = array('locale' => ''); + + foreach($fields as $key => $value) { + $field = ife(is_numeric($key), $value, $key); + $emptyFields[$field] = ''; + } + unset($fields); + + foreach($results as $key => $row) { + $results[$key][$model->name] = am($results[$key][$model->name], $emptyFields); + } + unset($emptyFields); + } elseif(!empty($this->runtime[$model->name]['beforeFind'])) { + $beforeFind = $this->runtime[$model->name]['beforeFind']; + + foreach($results as $key => $row) { + $results[$key][$model->name]['locale'] = $locale; + + foreach($beforeFind as $field) { + $value = ife(empty($results[$key]['I18n__'.$field]['content']), '', $results[$key]['I18n__'.$field]['content']); + $results[$key][$model->name][$field] = $value; + unset($results[$key]['I18n__'.$field]); + } + } + } + return $results; } - +/** + * Callback + */ function beforeSave(&$model) { + $locale = $this->_getLocale($model); + if(empty($locale) || is_array($locale)) { + return true; + } + $fields = am($this->settings[$model->name], $this->runtime[$model->name]['fields']); + $tempData = array(); + + foreach($fields as $key => $value) { + $field = ife(is_numeric($key), $value, $key); + + if(isset($model->data[$model->name][$field])) { + $tempData[$field] = $model->data[$model->name][$field]; + unset($model->data[$model->name][$field]); + } else { + $tempData[$field] = ''; + } + } + $this->runtime[$model->name]['beforeSave'] = $tempData; + $this->runtime[$model->name]['ignoreUserAbort'] = ignore_user_abort(); + @ignore_user_abort(true); + return true; } - +/** + * Callback + */ function afterSave(&$model, $created) { + $locale = $this->_getLocale($model); + if(empty($locale) || is_array($locale) || empty($this->runtime[$model->name]['beforeSave'])) { + return true; + } + $tempData = $this->runtime[$model->name]['beforeSave']; + unset($this->runtime[$model->name]['beforeSave']); + + $conditions = array('locale' => $locale, + 'model' => $model->name, + 'row_id' => $model->id); + + if($created) { + foreach($tempData as $field => $value) { + $this->_model->Content->create(); + $this->_model->Content->save(array('I18nContent' => array('content' => $value))); + + $this->_model->create(); + $this->_model->save(array('I18nModel' => am($conditions, array( + 'i18n_content_id' => $this->_model->Content->getInsertID(), + 'field' => $field)))); + } + } else { + $this->_model->recursive = -1; + $translations = $this->_model->findAll($conditions, array('field', 'i18n_content_id')); + $fields = Set::extract($translations, '{n}.I18nModel.field'); + $ids = Set::extract($translations, '{n}.I18nModel.i18n_content_id'); + + foreach($fields as $key => $field) { + if(array_key_exists($field, $tempData)) { + $this->_model->Content->create(); + $this->_model->Content->save(array('I18nContent' => array( + 'id' => $ids[$key], + 'content' => $tempData[$field]))); + } + } + } + @ignore_user_abort((bool) $this->runtime[$model->name]['ignoreUserAbort']); + unset($this->runtime[$model->name]['ignoreUserAbort']); } - +/** + * Callback + */ function beforeDelete(&$model) { + $this->runtime[$model->name]['ignoreUserAbort'] = ignore_user_abort(); + @ignore_user_abort(true); + return true; + } +/** + * Callback + */ + function afterDelete(&$model) { + $this->_model->recursive = -1; + $conditions = array('model' => $model->name, 'row_id' => $model->id); + $translations = $this->_model->findAll($conditions, array('i18n_content_id')); + $ids = Set::extract($translations, '{n}.I18nModel.i18n_content_id'); + $db =& ConnectionManager::getDataSource($model->useDbConfig); + $db->delete($this->_model->Content, array('id' => $ids)); + $db->delete($this->_model, $conditions); + + @ignore_user_abort((bool) $this->runtime[$model->name]['ignoreUserAbort']); + unset($this->runtime[$model->name]['ignoreUserAbort']); + } +/** + * Autodetects locale for application + * + * @todo + * @return string + */ + function _autoDetectLocale() { + // just fast hack to obtain selected locale + __d('core', 'Notice', true); + $I18n =& I18n::getInstance(); + return $I18n->locale; + } +/** + * Get selected locale for model + * + * @return mixed string or false + */ + function _getLocale(&$model) { + if(!isset($model->locale) || is_null($model->locale)) { + $model->locale = $this->_autoDetectLocale(); + } + return $model->locale; } - function afterDelete(&$model) { +/** + * Bind translation for fields, optionally with hasMany association for + * fake field + * + * @param object instance of model + * @param mixed string with field or array(field1, field2=>AssocName, field3) + * @param boolead $reset + * @return boolean + */ + function bindTranslation(&$model, $fields, $reset = true) { + if(empty($fields)) { + return true; + } + if(is_string($fields)) { + $fields = array($fields); + } + $settings =& $this->settings[$model->name]; + $runtime =& $this->runtime[$model->name]['fields']; + $associations = array(); + + $default = array('className' => 'I18nModel', 'foreignKey' => 'row_id'); + + foreach($fields as $key => $value) { + + if(is_numeric($key)) { + $field = $value; + $association = null; + } else { + $field = $key; + $association = $value; + } + + if(in_array($field, $settings)) { + $this->settings[$model->name] = array_diff_assoc($settings, array($field)); + } elseif(array_key_exists($field, $settings)) { + unset($settings[$field]); + } + + if(in_array($field, $runtime)) { + $this->runtime[$model->name]['fields'] = array_diff_assoc($runtime, array($field)); + } elseif(array_key_exists($field, $runtime)) { + unset($runtime[$field]); + } + + if(is_null($association)) { + if($reset) { + $runtime[] = $field; + } else { + $settings[] = $field; + } + } else { + if($reset) { + $runtime[$field] = $association; + } else { + $settings[$field] = $association; + } + + foreach(array('hasOne', 'hasMany', 'belongsTo', 'hasAndBelongsToMany') as $type) { + if(isset($model->{$type}[$association]) || isset($model->__backAssociation[$type][$association])) { + trigger_error('Association '.$association.' is already binded to model '.$model->name, E_USER_ERROR); + return false; + } + } + $associations[$association] = am($default, array('conditions' => array( + 'model' => $model->name, + 'field' => $field))); + } + } + + if(!empty($associations)) { + $model->bindModel(array('hasMany' => $associations), $reset); + } + return true; + } +/** + * Unbind translation for fields, optionally unbinds hasMany association for + * fake field + * + * @param object instance of model + * @param mixed string with field or array(field1, field2=>AssocName, field3) + * @return boolean + */ + function unbindTranslation(&$model, $fields) { + if(empty($fields)) { + return true; + } + + if(is_string($fields)) { + $fields = array($fields); + } + $settings =& $this->settings[$model->name]; + $runtime =& $this->runtime[$model->name]['fields']; + + $default = array('className' => 'I18nModel', 'foreignKey' => 'row_id'); + $associations = array(); + + foreach($fields as $key => $value) { + if(is_numeric($key)) { + $field = $value; + $association = null; + } else { + $field = $key; + $association = $value; + } + + if(in_array($field, $settings)) { + $this->settings[$model->name] = array_diff_assoc($settings, array($field)); + } elseif (array_key_exists($field, $settings)) { + unset($settings[$field]); + } + + if(in_array($field, $runtime)) { + $this->runtime[$model->name]['fields'] = array_diff_assoc($runtime, array($field)); + } elseif(array_key_exists($field, $runtime)) { + unset($runtime[$field]); + } + + if(!is_null($association) && (isset($model->hasMany[$association]) || isset($model->__backAssociation['hasMany'][$association]))) { + $associations[] = $association; + } + } + + if(!empty($associations)) { + $model->unbindModel(array('hasMany' => $associations), false); + } + return true; } } +/** + * @package cake + * @subpackage cake.cake.libs.model.behaviors + */ +class I18nContent extends AppModel { + var $name = 'I18nContent'; + var $useTable = 'i18n_content'; +} +/** + * @package cake + * @subpackage cake.cake.libs.model.behaviors + */ +class I18nModel extends AppModel { + var $name = 'I18nModel'; + var $useTable = 'i18n'; + var $belongsTo = array('Content' => array('className' => 'I18nContent', 'foreignKey' => 'i18n_content_id')); +} ?> \ No newline at end of file diff --git a/cake/libs/model/datasources/dbo_source.php b/cake/libs/model/datasources/dbo_source.php index 34f46c313..f0181b422 100644 --- a/cake/libs/model/datasources/dbo_source.php +++ b/cake/libs/model/datasources/dbo_source.php @@ -685,7 +685,9 @@ class DboSource extends DataSource { if (isset($model->{$className}) && is_object($model->{$className})) { $data = $model->{$className}->afterFind(array(array($key => $results[$i][$key])), false); } - $results[$i][$key] = $data[0][$key]; + if (isset($data[0][$key])) { + $results[$i][$key] = $data[0][$key]; + } } } } diff --git a/cake/tests/cases/libs/view/helpers/form.test.php b/cake/tests/cases/libs/view/helpers/form.test.php index b920fac38..ea5687ee8 100644 --- a/cake/tests/cases/libs/view/helpers/form.test.php +++ b/cake/tests/cases/libs/view/helpers/form.test.php @@ -66,7 +66,7 @@ )); } } - + /** * Short description for class. * @@ -77,12 +77,12 @@ var $useTable = false; var $primaryKey = 'id'; var $name = 'UserForm'; - + var $hasMany = array('OpenidUrl' => array( 'className' => 'OpenidUrl', 'foreignKey' => 'user_form_id' )); - + function loadInfo() { return new Set(array( array('name' => 'id', 'type' => 'integer', 'null' => '', 'default' => '', 'length' => '8'), @@ -92,7 +92,7 @@ )); } } - + /** * Short description for class. * @@ -103,12 +103,12 @@ var $useTable = false; var $primaryKey = 'id'; var $name = 'OpenidUrl'; - + var $belongsTo = array('UserForm' => array( 'className' => 'UserForm', 'foreignKey' => 'user_form_id' )); - + function loadInfo() { return new Set(array( array('name' => 'id', 'type' => 'integer', 'null' => '', 'default' => '', 'length' => '8'), @@ -116,7 +116,7 @@ array('name' => 'url', 'type' => 'string', 'null' => '', 'default' => '', 'length' => '255'), )); } - + function beforeValidate() { $this->invalidate('openid_not_registered'); return true; @@ -138,11 +138,11 @@ class FormHelperTest extends UnitTestCase { ClassRegistry::addObject('view', $view); ClassRegistry::addObject('Contact', new Contact()); } - + function testFormValidationAssociated() { $this->UserForm =& new UserForm(); $this->UserForm->OpenidUrl =& new OpenidUrl(); - + $data = array( 'UserForm' => array( 'name' => 'user' @@ -151,27 +151,27 @@ class FormHelperTest extends UnitTestCase { 'url' => 'http://www.cricava.com' ) ); - + $result = $this->UserForm->OpenidUrl->create($data); $this->assertTrue($result); - + $result = $this->UserForm->OpenidUrl->validates(); $this->assertFalse($result); - + $result = $this->Form->create('UserForm', array('type' => 'post', 'action' => 'login')); $this->assertPattern('/^]*>$/', $result); - + $expected = array( 'OpenidUrl' => array( 'openid_not_registered' => 1 ) ); - + $this->assertEqual($this->Form->validationErrors, $expected); - + $result = $this->Form->error('OpenidUrl.openid_not_registered', 'Error, not registered', array('wrap' => false)); $this->assertEqual($result, 'Error, not registered'); - + unset($this->UserForm->OpenidUrl); unset($this->UserForm); } @@ -187,7 +187,7 @@ class FormHelperTest extends UnitTestCase { $result = $this->Form->input('test', array('options' => array('First', 'Second'), 'empty' => true)); $this->assertPattern('/]+value="Test Submit"[^<>]+\/><\/div>$/', $result); - + $result = $this->Form->submit('Test Submit', array('class' => 'save', 'div' => false)); $this->assertPattern('/^]+value="Test Submit"[^<>]+\/>$/', $result); $this->assertPattern('/^<[^<>]+class="save"[^<>]+\/>$/', $result);