Browse Source

Merge branch '2.0-extract-model-validation' into 2.0

Jose Lorenzo Rodriguez 14 years ago
parent
commit
765164f33b

+ 117 - 30
lib/Cake/Console/Command/Task/ExtractTask.php

@@ -98,6 +98,20 @@ class ExtractTask extends Shell {
 	protected $_exclude = array();
 
 /**
+ * Holds whether this call should extract model validation messages
+ *
+ * @var boolean
+ */
+	protected $_extractValidation = true;
+
+/**
+ * Holds the validation string domain to use for validation messages when extracting
+ *
+ * @var boolean
+ */
+	protected $_validationDomain = 'default';
+
+/**
  * Execution method always used for tasks
  *
  * @return void
@@ -113,7 +127,7 @@ class ExtractTask extends Shell {
 		if (isset($this->params['paths'])) {
 			$this->_paths = explode(',', $this->params['paths']);
 		} else {
-			$defaultPath = APP_PATH;
+			$defaultPath = APP;
 			$message = __d('cake_console', "What is the path you would like to extract?\n[Q]uit [D]one");
 			while (true) {
 				$response = $this->in($message, null, $defaultPath);
@@ -133,6 +147,17 @@ class ExtractTask extends Shell {
 			}
 		}
 
+		if (!empty($this->params['exclude-plugins']) && $this->_isExtractingApp()) {
+			$this->_exclude = array_merge($this->_exclude, App::path('plugins'));
+		}
+
+		if (!empty($this->params['ignore-model-validation']) || !$this->_isExtractingApp()) {
+			$this->_extractValidation = false;
+		}
+		if (!empty($this->params['validation-domain'])) {
+			$this->_validationDomain = $this->params['validation-domain'];
+		}
+
 		if (isset($this->params['output'])) {
 			$this->_output = $this->params['output'];
 		} else {
@@ -184,10 +209,12 @@ class ExtractTask extends Shell {
 		$this->out(__d('cake_console', 'Output Directory: ') . $this->_output);
 		$this->hr();
 		$this->_extractTokens();
+		$this->_extractValidationMessages();
 		$this->_buildFiles();
 		$this->_writeFiles();
 		$this->_paths = $this->_files = $this->_storage = array();
 		$this->_strings = $this->_tokens = array();
+		$this->_extractValidation = true;
 		$this->out();
 		$this->out(__d('cake_console', 'Done.'));
 	}
@@ -208,40 +235,25 @@ class ExtractTask extends Shell {
 			))
 			->addOption('output', array('help' => __d('cake_console', 'Full path to output directory.')))
 			->addOption('files', array('help' => __d('cake_console', 'Comma separated list of files.')))
+			->addOption('exclude-plugins', array(
+				'boolean' => true,
+				'default' => true,
+				'help' => __d('cake_console', 'Ignores all files in plugins if this command is run inside from the same app directory')
+			))
+			->addOption('ignore-model-validation', array(
+				'boolean' => true,
+				'default' => false,
+				'help' => __d('cake_console', 'Ignores validation messages in the $validate property. If this flag is not set and the command is run from the same app directory, all messages in model validation rules will be extracted as tokens')
+			))
+			->addOption('validation-domain', array(
+				'help' => __d('cake_console', 'If set to a value, the localization domain to be used for model validation messages')
+			))
 			->addOption('exclude', array(
 				'help' => __d('cake_console', 'Comma separated list of directories to exclude. Any path containing a path segment with the provided values will be skipped. E.g. test,vendors')
 			));
 	}
 
 /**
- * Show help options
- *
- * @return void
- */
-	public function help() {
-		$this->out(__d('cake_console', 'CakePHP Language String Extraction:'));
-		$this->hr();
-		$this->out(__d('cake_console', 'The Extract script generates .pot file(s) with translations'));
-		$this->out(__d('cake_console', 'By default the .pot file(s) will be place in the locale directory of -app'));
-		$this->out(__d('cake_console', 'By default -app is ROOT/app'));
-		$this->hr();
-		$this->out(__d('cake_console', 'Usage: cake i18n extract <command> <param1> <param2>...'));
-		$this->out();
-		$this->out(__d('cake_console', 'Params:'));
-		$this->out(__d('cake_console', '   -app [path...]: directory where your application is located'));
-		$this->out(__d('cake_console', '   -root [path...]: path to install'));
-		$this->out(__d('cake_console', '   -core [path...]: path to cake directory'));
-		$this->out(__d('cake_console', '   -paths [comma separated list of paths]'));
-		$this->out(__d('cake_console', '   -merge [yes|no]: Merge all domains strings into the default.pot file'));
-		$this->out(__d('cake_console', '   -output [path...]: Full path to output directory'));
-		$this->out(__d('cake_console', '   -files: [comma separated list of files]'));
-		$this->out();
-		$this->out(__d('cake_console', 'Commands:'));
-		$this->out(__d('cake_console', '   cake i18n extract help: Shows this help message.'));
-		$this->out();
-	}
-
-/**
  * Extract tokens out of all files to be processed
  *
  * @return void
@@ -322,6 +334,64 @@ class ExtractTask extends Shell {
 	}
 
 /**
+ * Looks for models in the application and extracts the validation messages
+ * to be added to the translation map
+ *
+ * @return void
+ */
+	protected function _extractValidationMessages() {
+		if (!$this->_extractValidation) {
+			return;
+		}
+		$models = App::objects('Model', null, false);
+		App::uses('AppModel', 'Model');
+		foreach ($models as $model) {
+			App::uses($model, 'Model');
+			$reflection = new ReflectionClass($model);
+			$properties = $reflection->getDefaultProperties();
+			$validate = $properties['validate'];
+			if (empty($validate)) {
+				continue;
+			}
+
+			$file = $reflection->getFileName();
+			$domain = $this->_validationDomain;
+			if (!empty($properties['validationDomain'])) {
+				$domain = $properties['validationDomain'];
+			}
+			foreach ($validate as $field => $rules) {
+				$this->_processValidationRules($field, $rules, $file, $domain);
+			}
+		}
+	}
+
+/**
+ * Process a validation rule for a field and looks for a message to be added
+ * to the translation map
+ *
+ * @param string $field the name of the field that is being processed
+ * @param array $rules the set of validation rules for the field
+ * @param string $file the file name where this validation rule was found
+ * @param string domain default domain to bind the validations to
+ * @return void
+ */
+	protected function _processValidationRules($field, $rules, $file, $domain) {
+		if (is_array($rules)) {
+
+			$dims = Set::countDim($rules);
+			if ($dims == 1 || ($dims == 2 && isset($rules['message']))) {
+				$rules = array($rules);
+			}
+
+			foreach ($rules as $rule => $validateProp) {
+				if (isset($validateProp['message'])) {
+					$this->_strings[$domain][$validateProp['message']][$file][] = 'validation for field ' . $field;
+				}
+			}
+		}
+	}
+
+/**
  * Build the translate template file contents out of obtained strings
  *
  * @return void
@@ -524,7 +594,14 @@ class ExtractTask extends Shell {
 	protected function _searchFiles() {
 		$pattern = false;
 		if (!empty($this->_exclude)) {
-			$pattern = '/[\/\\\\]' . implode('|', $this->_exclude) . '[\/\\\\]/';
+			$exclude = array();
+			foreach ($this->_exclude as $e) {
+				if ($e[0] !== DS) {
+					$e = DS . $e;
+				}
+				$exclude[] = preg_quote($e, '/');
+			}
+			$pattern =  '/' . implode('|', $exclude) . '/';
 		}
 		foreach ($this->_paths as $path) {
 			$Folder = new Folder($path);
@@ -540,4 +617,14 @@ class ExtractTask extends Shell {
 			$this->_files = array_merge($this->_files, $files);
 		}
 	}
+
+/**
+ * Returns whether this execution is meant to extract string only from directories in folder represented by the
+ * APP constant, i.e. this task is extracting strings from same application.
+ *
+ * @return boolean
+ */
+	protected function _isExtractingApp() {
+		return $this->_paths === array(APP);
+	}
 }

+ 15 - 26
lib/Cake/Model/Model.php

@@ -134,6 +134,15 @@ class Model extends Object {
  */
 	public $validationErrors = array();
 
+
+/**
+ * Nane of the validation string domain to use when translating validation errors.
+ *
+ * @var string
+ * @access public
+ */
+	public $validationDomain = null;
+
 /**
  * Database table prefix for tables in model.
  *
@@ -169,14 +178,6 @@ class Model extends Object {
 	public $tableToModel = array();
 
 /**
- * Whether or not to log transactions for this model.
- *
- * @var boolean
- * @access public
- */
-	public $logTransactions = false;
-
-/**
  * Whether or not to cache queries for this model.  This enables in-memory
  * caching only, the results are not stored beyond the current request.
  *
@@ -346,22 +347,6 @@ class Model extends Object {
 	private $__insertID = null;
 
 /**
- * The number of records returned by the last query.
- *
- * @var integer
- * @access private
- */
-	private $__numRows = null;
-
-/**
- * The number of records affected by the last query.
- *
- * @var integer
- * @access private
- */
-	private $__affectedRows = null;
-
-/**
  * Has the datasource been configured.
  *
  * @var boolean
@@ -2599,6 +2584,10 @@ class Model extends Object {
 				}
 				$validator = array_merge($default, $validator);
 
+				$validationDomain = $this->validationDomain;
+				if (empty($validationDomain)) {
+					$validationDomain = 'default';
+				}
 				if (isset($validator['message'])) {
 					$message = $validator['message'];
 				} else {
@@ -2618,7 +2607,7 @@ class Model extends Object {
 					);
 
 					if ($required) {
-						$this->invalidate($fieldName, $message);
+						$this->invalidate($fieldName, __d($validationDomain, $message));
 						if ($validator['last']) {
 							break;
 						}
@@ -2662,7 +2651,7 @@ class Model extends Object {
 								} elseif (is_numeric($index) && count($ruleSet) > 1) {
 									$validator['message'] = $index + 1;
 								} else {
-									$validator['message'] = $message;
+									$validator['message'] = __d($validationDomain, $message);
 								}
 							}
 							$this->invalidate($fieldName, $validator['message']);

+ 112 - 0
lib/Cake/Test/Case/Console/Command/Task/ExtractTaskTest.php

@@ -60,6 +60,7 @@ class ExtractTaskTest extends CakeTestCase {
 
 		$Folder = new Folder($this->path);
 		$Folder->delete();
+		App::build();
 	}
 
 /**
@@ -202,4 +203,115 @@ class ExtractTaskTest extends CakeTestCase {
 		$pattern = '/msgid "Add User"/';
 		$this->assertPattern($pattern, $result);
 	}
+
+/**
+ * Tests that it is possible to exclude plugin paths by enabling the param option for the ExtractTask
+ *
+ * @return void
+ */
+	public function testExtractExcludePlugins() {
+		App::build(array(
+			'plugins' => array(CAKE . 'Test' . DS . 'test_app' . DS . 'Plugin' . DS)
+		));
+		$this->out = $this->getMock('ConsoleOutput', array(), array(), '', false);
+		$this->in = $this->getMock('ConsoleInput', array(), array(), '', false);
+		$this->Task = $this->getMock('ExtractTask',
+			array('_isExtractingApp', '_extractValidationMessages', 'in', 'out', 'err', 'clear', '_stop'),
+			array($this->out, $this->out, $this->in)
+		);
+		$this->Task->expects($this->exactly(2))->method('_isExtractingApp')->will($this->returnValue(true));
+
+		$this->Task->params['paths'] = CAKE . 'Test' . DS . 'test_app' . DS;
+		$this->Task->params['output'] = $this->path . DS;
+		$this->Task->params['exclude-plugins'] = true;
+
+		$this->Task->execute();
+		$result = file_get_contents($this->path . DS . 'default.pot');
+		$this->assertNoPattern('#TesPlugin#', $result);
+	}
+
+/**
+ * Tests that the task will inspect application models and extract the validation messages from them
+ *
+ * @return void
+ */
+	public function testExtractModelValidation() {
+		App::build(array(
+			'Model' => array(CAKE . 'Test' . DS . 'test_app' . DS . 'Model' . DS),
+			'plugins' => array(CAKE . 'Test' . DS . 'test_app' . DS . 'Plugin' . DS)
+		), App::RESET);
+		$this->out = $this->getMock('ConsoleOutput', array(), array(), '', false);
+		$this->in = $this->getMock('ConsoleInput', array(), array(), '', false);
+		$this->Task = $this->getMock('ExtractTask',
+			array('_isExtractingApp', 'in', 'out', 'err', 'clear', '_stop'),
+			array($this->out, $this->out, $this->in)
+		);
+		$this->Task->expects($this->exactly(2))->method('_isExtractingApp')->will($this->returnValue(true));
+
+		$this->Task->params['paths'] = CAKE . 'Test' . DS . 'test_app' . DS;
+		$this->Task->params['output'] = $this->path . DS;
+		$this->Task->params['exclude-plugins'] = true;
+		$this->Task->params['ignore-model-validation'] = false;
+
+		$this->Task->execute();
+		$result = file_get_contents($this->path . DS . 'default.pot');
+
+		$pattern = '#Model/PersisterOne.php:validation for field title#';
+		$this->assertPattern($pattern, $result);
+
+		$pattern = '#Model/PersisterOne.php:validation for field body#';
+		$this->assertPattern($pattern, $result);
+
+		$pattern = '#msgid "Post title is required"#';
+		$this->assertPattern($pattern, $result);
+
+		$pattern = '#msgid "Post body is required"#';
+		$this->assertPattern($pattern, $result);
+
+		$pattern = '#msgid "Post body is super required"#';
+		$this->assertPattern($pattern, $result);
+	}
+
+/**
+ *  Tests that the task will inspect application models and extract the validation messages from them
+ *	while using a custom validation domain for the messages set on the model itself
+ *
+ * @return void
+ */
+	public function testExtractModelValidationWithDomainInModel() {
+		App::build(array(
+			'Model' => array(CAKE . 'Test' . DS . 'test_app' . DS . 'Plugin' . DS . 'TestPlugin' . DS . 'Model' . DS)
+		));
+		$this->out = $this->getMock('ConsoleOutput', array(), array(), '', false);
+		$this->in = $this->getMock('ConsoleInput', array(), array(), '', false);
+		$this->Task = $this->getMock('ExtractTask',
+			array('_isExtractingApp', 'in', 'out', 'err', 'clear', '_stop'),
+			array($this->out, $this->out, $this->in)
+		);
+		$this->Task->expects($this->exactly(2))->method('_isExtractingApp')->will($this->returnValue(true));
+
+		$this->Task->params['paths'] = CAKE . 'Test' . DS . 'test_app' . DS;
+		$this->Task->params['output'] = $this->path . DS;
+		$this->Task->params['exclude-plugins'] = true;
+		$this->Task->params['ignore-model-validation'] = false;
+
+		$this->Task->execute();
+		$result = file_get_contents($this->path . DS . 'test_plugin.pot');
+
+		$pattern = '#Plugin/TestPlugin/Model/TestPluginPost.php:validation for field title#';
+		$this->assertPattern($pattern, $result);
+
+		$pattern = '#Plugin/TestPlugin/Model/TestPluginPost.php:validation for field body#';
+		$this->assertPattern($pattern, $result);
+
+		$pattern = '#msgid "Post title is required"#';
+		$this->assertPattern($pattern, $result);
+
+		$pattern = '#msgid "Post body is required"#';
+		$this->assertPattern($pattern, $result);
+
+		$pattern = '#msgid "Post body is super required"#';
+		$this->assertPattern($pattern, $result);
+	}
+
 }

+ 22 - 0
lib/Cake/Test/test_app/Model/PersisterOne.php

@@ -25,4 +25,26 @@ class PersisterOne extends AppModel {
 	public $actsAs = array('PersisterOneBehavior', 'TestPlugin.TestPluginPersisterOne');
 
 	public $hasMany = array('Comment', 'TestPlugin.TestPluginComment');
+	public $validate = array(
+		'title' => array(
+			'rule' => array('custom', '.*'),
+			'allowEmpty' => true,
+			'required' => false,
+			'message' => 'Post title is required'
+		),
+		'body' => array(
+			'first_rule' => array(
+				'rule' => array('custom', '.*'),
+				'allowEmpty' => true,
+				'required' => false,
+				'message' => 'Post body is required'
+			),
+			'second_rule' => array(
+				'rule' => array('custom', '.*'),
+				'allowEmpty' => true,
+				'required' => false,
+				'message' => 'Post body is super required'
+			)
+		),
+	);
 }

+ 36 - 0
lib/Cake/Test/test_app/Plugin/TestPlugin/Model/TestPluginPost.php

@@ -33,4 +33,40 @@ class TestPluginPost extends TestPluginAppModel {
  * @var string
  */
 	public $useTable = 'posts';
+
+/**
+ * Validation rules
+ *
+ * @var array
+ */
+	public $validate = array(
+		'title' => array(
+			'rule' => array('custom', '.*'),
+			'allowEmpty' => true,
+			'required' => false,
+			'message' => 'Post title is required'
+		),
+		'body' => array(
+			'first_rule' => array(
+				'rule' => array('custom', '.*'),
+				'allowEmpty' => true,
+				'required' => false,
+				'message' => 'Post body is required'
+			),
+			'second_rule' => array(
+				'rule' => array('custom', '.*'),
+				'allowEmpty' => true,
+				'required' => false,
+				'message' => 'Post body is super required'
+			)
+		),
+	);
+
+/**
+ * Translation domain to use for validation messages
+ *
+ * @var string
+ */
+	public $validationDomain = 'test_plugin';
+
 }

+ 1 - 0
lib/Cake/Test/test_app/Plugin/TestPlugin/View/Elements/translate.ctp

@@ -0,0 +1 @@
+<?php __('This is a translatable string'); ?>