diff --git a/api/RestfulServer.php b/api/RestfulServer.php index 27e555c15..7714b8de5 100644 --- a/api/RestfulServer.php +++ b/api/RestfulServer.php @@ -162,12 +162,12 @@ class RestfulServer extends Controller { } } else { - $obj = $this->search($className, $this->request->getVars(), $sort, $limit); - // show empty serialized result when no records are present - if(!$obj) $obj = new DataObjectSet(); if(!singleton($className)->stat('api_access')) { return $this->permissionFailure(); } + $obj = $this->search($className, $this->request->getVars(), $sort, $limit); + // show empty serialized result when no records are present + if(!$obj) $obj = new DataObjectSet(); } if($obj instanceof DataObjectSet) return $formatter->convertDataObjectSet($obj); @@ -187,9 +187,13 @@ class RestfulServer extends Controller { * @return DataObjectSet */ protected function search($className, $params = null, $sort = null, $limit = null, $existingQuery = null) { - $searchContext = singleton($className)->getDefaultSearchContext(); + if(singleton($className)->hasMethod('getRestfulSearchContext')) { + $searchContext = singleton($className)->{'getRestfulSearchContext'}(); + } else { + $searchContext = singleton($className)->getDefaultSearchContext(); + } $query = $searchContext->getQuery($params, $sort, $limit, $existingQuery); - + return singleton($className)->buildDataObjectSet($query->execute()); } diff --git a/cli-script.php b/cli-script.php index 0e4dffa1a..920fda1b6 100755 --- a/cli-script.php +++ b/cli-script.php @@ -49,7 +49,10 @@ if( preg_match( '/(test\.totallydigital\.co\.nz|dev\.totallydigital\.co\.nz\/tes } else { echo "Error: could not determine server configuration {$_SERVER['SCRIPT_FILENAME']}\n"; exit(); -} +} + +// set request method (doesn't allow POST through cli) +$_SERVER['REQUEST_METHOD'] = "GET"; $baseURL = dirname( dirname( $_SERVER['SCRIPT_NAME'] ) ); diff --git a/core/model/DataObject.php b/core/model/DataObject.php index c6a58e887..a76075ee2 100644 --- a/core/model/DataObject.php +++ b/core/model/DataObject.php @@ -1081,8 +1081,9 @@ class DataObject extends ViewableData implements DataObjectInterface { if ($this->many_many()) { foreach($this->many_many() as $relationship => $component) { $relationshipFields = singleton($component)->summary_fields(); + $filterWhere = $this->getManyManyFilter($relationship, $component); $filterJoin = $this->getManyManyJoin($relationship, $component); - $tableField = new ComplexTableField($this, $relationship, $component, $relationshipFields, "getCMSFields", '', '', $filterJoin); + $tableField = new ComplexTableField($this, $relationship, $component, $relationshipFields, "getCMSFields", $filterWhere, '', $filterJoin); $tableField->popupClass = "ScaffoldingComplexTableField_Popup"; $fieldSet->addFieldToTab("Root.$relationship", $tableField); } @@ -1111,6 +1112,12 @@ class DataObject extends ViewableData implements DataObjectInterface { return "LEFT JOIN `$table` ON (`$componentField` = `$componentClass`.`ID` AND `$parentField` = '{$this->ID}')"; } } + + function getManyManyFilter($componentName, $baseTable) { + list($parentClass, $componentClass, $parentField, $componentField, $table) = $this->many_many($componentName); + + return "`$table`.`$parentField` = '{$this->ID}'"; + } /** * Return the class of a one-to-one component. If $component is null, return all of the one-to-one components and their classes. diff --git a/dev/Debug.php b/dev/Debug.php index 75f16f6ff..b7fde16aa 100644 --- a/dev/Debug.php +++ b/dev/Debug.php @@ -135,12 +135,17 @@ class Debug { if(!Director::isLive()) { $caller = Debug::caller(); $file = basename($caller['file']); - echo "

\n"; - if($showHeader) echo "Debug (line $caller[line] of $file):\n "; - echo Convert::raw2xml(trim($message)) . "

\n"; + if(Director::is_cli()) { + if($showHeader) echo "Debug (line $caller[line] of $file):\n "; + echo trim($message) . "\n"; + } else { + echo "

\n"; + if($showHeader) echo "Debug (line $caller[line] of $file):\n "; + echo Convert::raw2xml(trim($message)) . "

\n"; + } } } - + /** * Load an error handler * @@ -208,20 +213,7 @@ class Debug { echo "

Line $errline in $errfile

"; echo ''; echo '

Source

'; - $lines = file($errfile); - $offset = $errline-10; - $lines = array_slice($lines, $offset, 16); - echo '
';
-			$offset++;
-			foreach($lines as $line) {
-				$line = htmlentities($line);
-				if ($offset == $errline) {
-					echo "$offset $line";
-				} else {
-					echo "$offset $line";
-				}
-				$offset++;
-			}
+			Debug::showLines($errfile, $errline);
 			echo '

Trace

'; Debug::backtrace(); echo '
'; @@ -229,6 +221,24 @@ class Debug { die(); } } + + static function showLines($errfile, $errline) { + $lines = file($errfile); + $offset = $errline-10; + $lines = array_slice($lines, $offset, 16); + echo '
';
+		$offset++;
+		foreach($lines as $line) {
+			$line = htmlentities($line);
+			if ($offset == $errline) {
+				echo "$offset $line";
+			} else {
+				echo "$offset $line";
+			}
+			$offset++;
+		}
+		echo '
'; + } static function emailError($emailAddress, $errno, $errstr, $errfile, $errline, $errcontext, $errorType = "Error") { if(strtolower($errorType) == 'warning') { diff --git a/dev/SapphireTestReporter.php b/dev/SapphireTestReporter.php new file mode 100644 index 000000000..4aeee0067 --- /dev/null +++ b/dev/SapphireTestReporter.php @@ -0,0 +1,286 @@ +timer = new Benchmark_Timer(); + $this->hasTimer = true; + } else { + $this->hasTimer = false; + } + + $this->suiteResults = array( + 'suites' => array(), // array of suites run + 'hasTimer' => $this->hasTimer, // availability of PEAR Benchmark_Timer + 'totalTests' => 0 // total number of tests run + ); + } + + /** + * Returns the suite results + * + * @access public + * @return array Suite results + */ + public function getSuiteResults() { + return $this->suiteResults; + } + + /** + * Sets up the container for result details of the current test suite when + * each suite is first run + * + * @access public + * @param obj PHPUnit2_Framework_TestSuite, the suite that is been run + * @return void + */ + public function startTestSuite( PHPUnit_Framework_TestSuite $suite) { + if(strlen($suite->getName())) { + $this->currentSuite = array( + 'suite' => $suite, // the test suite + 'tests' => array(), // the tests in the suite + 'errors' => 0, // number of tests with errors + 'failures' => 0, // number of tests which failed + 'incomplete' => 0); // number of tests that were not completed correctly + } + } + + /** + * Sets up the container for result details of the current test when each + * test is first run + * + * @access public + * @param obj PHPUnit_Framework_Test, the test that is being run + * @return void + */ + public function startTest(PHPUnit_Framework_Test $test) { + if($test instanceof PHPUnit_Framework_TestCase) { + $this->currentTest = array( + 'name' => preg_replace('(\(.*\))', '', $test->toString()), // the name of the test (without the suite name) + 'timeElapsed' => 0, // execution time of the test + 'status' => TEST_SUCCESS, // status of the test execution + 'message' => '', // user message of test result + 'exception' => NULL, // original caught exception thrown by test upon failure/error + 'uid' => md5(microtime()) // a unique ID for this test (used for identification purposes in results) + ); + if($this->hasTimer) $this->timer->start(); + } + } + + /** + * Adds the failure detail to the current test and increases the failure + * count for the current suite + * + * @access public + * @param obj PHPUnit_Framework_Test, current test that is being run + * @param obj PHPUnit_Framework_AssertationFailedError, PHPUnit error + * @return void + */ + public function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time) { + $this->currentSuite['failures']++; + $this->currentTest['status'] = TEST_FAILURE; + $this->currentTest['message'] = $e->toString(); + $this->currentTest['exception'] = $this->getTestException($test, $e); + } + + /** + * Adds the error detail to the current test and increases the error + * count for the current suite + * + * @access public + * @param obj PHPUnit_Framework_Test, current test that is being run + * @param obj PHPUnit_Framework_AssertationFailedError, PHPUnit error + * @return void + */ + public function addError(PHPUnit_Framework_Test $test, Exception $e, $time) { + $this->currentSuite['errors']++; + $this->currentTest['status'] = TEST_ERROR; + $this->currentTest['message'] = $e->getMessage(); + $this->currentTest['exception'] = $this->getTestException($test, $e); + } + + /** + * Adds the test incomplete detail to the current test and increases the incomplete + * count for the current suite + * + * @access public + * @param obj PHPUnit_Framework_Test, current test that is being run + * @param obj PHPUnit_Framework_AssertationFailedError, PHPUnit error + * @return void + */ + public function addIncompleteTest(PHPUnit_Framework_Test $test, Exception $e, $time) { + $this->currentSuite['incomplete']++; + $this->currentTest['status'] = TEST_INCOMPLETE; + $this->currentTest['message'] = $e->toString(); + $this->currentTest['exception'] = $this->getTestException($test, $e); + } + + /** + * Not used + * + * @param PHPUnit_Framework_Test $test + * @param unknown_type $time + */ + public function addSkippedTest(PHPUnit_Framework_Test $test, Exception $e, $time) { + // not implemented + } + + /** + * Upon completion of a test, records the execution time (if available) and adds the test to + * the tests performed in the current suite. + * + * @access public + * @param obj PHPUnit_Framework_Test, current test that is being run + * @return void + */ + public function endTest( PHPUnit_Framework_Test $test, $time) { + if($this->hasTimer) { + $this->timer->stop(); + $this->currentTest['timeElapsed'] = $this->timer->timeElapsed(); + } + array_push($this->currentSuite['tests'], $this->currentTest); + } + + /** + * Upon completion of a test suite adds the suite to the suties performed + * + * @acces public + * @param obj PHPUnit_Framework_TestSuite, current suite that is being run + * @return void + */ + public function endTestSuite( PHPUnit_Framework_TestSuite $suite) { + if(strlen($suite->getName())) { + array_push($this->suiteResults['suites'], $this->currentSuite); + } + } + + /** + * Trys to get the original exception thrown by the test on failure/error + * to enable us to give a bit more detail about the failure/error + * + * @access private + * @param obj PHPUnit_Framework_Test, current test that is being run + * @param obj PHPUnit_Framework_AssertationFailedError, PHPUnit error + * @return array + */ + private function getTestException(PHPUnit_Framework_Test $test, Exception $e) { + // get the name of the testFile from the test + $testName = ereg_replace('(.*)\((.*[^)])\)', '\\2', $test->toString()); + $trace = $e->getTrace(); + // loop through the exception trace to find the original exception + for($i = 0; $i < count($trace); $i++) { + + if(array_key_exists('file', $trace[$i])) { + if(stristr($trace[$i]['file'], $testName.'.php') != false) return $trace[$i]; + } + if(array_key_exists('file:protected', $trace[$i])) { + if(stristr($trace[$i]['file:protected'], $testName.'.php') != false) return $trace[$i]; + } + } + } + + /** + * Display error bar if it exists + */ + public function writeResults() { + $passCount = 0; + $failCount = 0; + $testCount = 0; + $errorCount = 0; + + foreach($this->suiteResults['suites'] as $suite) { + foreach($suite['tests'] as $test) { + $testCount++; + ($test['status'] == 1) ? $passCount++ : $failCount++; + if ($test['status'] == -1) { + echo "
⊗ ". $this->testNameToPhrase($test['name']) ."
"; + echo "
".htmlentities($test['message'])."

"; + echo "In line {$test['exception']['line']} of {$test['exception']['file']}
"; + } + } + } + $result = ($failCount > 0) ? 'fail' : 'pass'; + echo "
"; + echo "

$testCount tests run: $passCount passes, $failCount fails, and 0 exceptions

"; + echo "
"; + + } + + private function testNameToPhrase($name) { + return ucfirst(preg_replace("/([a-z])([A-Z])/", "$1 $2", $name)); + } + +} + +?> \ No newline at end of file diff --git a/dev/TestRunner.php b/dev/TestRunner.php index ff1361126..fe58eef7c 100644 --- a/dev/TestRunner.php +++ b/dev/TestRunner.php @@ -31,8 +31,8 @@ class TestRunner extends Controller { private static $default_reporter; static $url_handlers = array( - '' => 'index', - '$TestCase' => 'only' + '' => 'browse', + '$TestCase' => 'only', ); /** @@ -53,7 +53,7 @@ class TestRunner extends Controller { /** * Run all test classes */ - function index() { + function all() { if(hasPhpUnit()) { $tests = ClassInfo::subclassesFor('SapphireTest'); array_shift($tests); @@ -65,6 +65,16 @@ class TestRunner extends Controller { } } + /** + * Browse all enabled test cases in the environment + */ + function browse() { + $tests = ClassInfo::subclassesFor('SapphireTest'); + foreach ($tests as $test) { + echo "

$test

"; + } + } + function coverage() { if(hasPhpUnit()) { ManifestBuilder::includeEverything(); @@ -86,7 +96,7 @@ class TestRunner extends Controller { if(class_exists($className)) { $this->runTests(array($className)); } else { - echo "Class '$className' not found"; + if ($className == 'all') $this->all(); } } @@ -94,11 +104,15 @@ class TestRunner extends Controller { if(!Director::is_cli()) { self::$default_reporter->writeHeader(); echo '
'; - echo "

Sapphire PHPUnit Test Runner

"; - echo "

Using the following subclasses of SapphireTest for testing: " . implode(", ", $classList) . "

"; + if (count($classList) > 1) { + echo "

Sapphire Tests

"; + echo "

Running test cases: " . implode(", ", $classList) . "

"; + } else { + echo "

{$classList[0]}

"; + echo "

Running test case:

"; + } echo "
"; echo '
'; - echo "
";
 		} else {
 			echo "Sapphire PHPUnit Test Runner\n";
 			echo "Using the following subclasses of SapphireTest for testing: " . implode(", ", $classList) . "\n\n";
@@ -114,18 +128,23 @@ class TestRunner extends Controller {
 			$suite->addTest(new PHPUnit_Framework_TestSuite($className));
 		}
 
+		$reporter = new SapphireTestReporter();
+		$results = new PHPUnit_Framework_TestResult();		
+		$results->addListener($reporter);
+
 		/*, array("reportDirectory" => "/Users/sminnee/phpunit-report")*/
 		if($coverage) {
-			$testResult = PHPUnit_TextUI_TestRunner::run($suite, array("reportDirectory" => "../assets/coverage-report"));
-		} else {
-			$testResult = PHPUnit_TextUI_TestRunner::run($suite);
-		}
-		
-		if($coverage) {
+			$suite->run($results);
+			//$testResult = PHPUnit_TextUI_TestRunner::run($suite, array("reportDirectory" => "../assets/coverage-report"));
 			$coverageURL = Director::absoluteURL('assets/coverage-report');
 			echo "

Coverage report available here

"; + } else { + $suite->run($results); + //$testResult = PHPUnit_TextUI_TestRunner::run($suite); } + $reporter->writeResults(); + if(!Director::is_cli()) echo '
'; // Put the error handlers back diff --git a/forms/ComplexTableField.php b/forms/ComplexTableField.php index 310fb6610..20df3eff0 100755 --- a/forms/ComplexTableField.php +++ b/forms/ComplexTableField.php @@ -894,6 +894,10 @@ class ComplexTableField_Popup extends Form { class ScaffoldingComplexTableField_Popup extends Form { protected $sourceClass; protected $dataObject; + + public static $allowed_actions = array( + 'filter', 'record', 'httpSubmission', 'handleAction' + ); function __construct($controller, $name, $fields, $validator, $readonly, $dataObject) { $this->dataObject = $dataObject; @@ -945,12 +949,76 @@ class ScaffoldingComplexTableField_Popup extends Form { $saveAction->addExtraClass('save'); } + $fields->push(new HiddenField("ComplexTableField_Path", Director::absoluteBaseURL())); + parent::__construct($controller, $name, $fields, $actions, $validator); } function FieldHolder() { return $this->renderWith('ComplexTableField_Form'); } + + + /** + * Handle a generic action passed in by the URL mapping. + * + * @param HTTPRequest $request + */ + public function handleAction($request) { + $action = str_replace("-","_",$request->param('Action')); + if(!$this->action) $this->action = 'index'; + + if($this->checkAccessAction($action)) { + if($this->hasMethod($action)) { + $result = $this->$action($request); + + // Method returns an array, that is used to customise the object before rendering with a template + if(is_array($result)) { + return $this->getViewer($action)->process($this->customise($result)); + + // Method returns a string / object, in which case we just return that + } else { + return $result; + } + + // There is no method, in which case we just render this object using a (possibly alternate) template + } else { + return $this->getViewer($action)->process($this); + } + } else { + return $this->httpError(403, "Action '$action' isn't allowed on class $this->class"); + } + } + + /** + * Action to render results for an autocomplete filter. + * + * @param HTTPRequest $request + * @return void + */ + function filter($request) { + //$model = singleton($this->modelClass); + $context = $this->dataObject->getDefaultSearchContext(); + $value = $request->getVar('q'); + $results = $context->getResults(array("Name"=>$value)); + header("Content-Type: text/plain"); + foreach($results as $result) { + echo $result->Name . "\n"; + } + } + + /** + * Action to populate edit box with a single data object via Ajax query + */ + function record($request) { + $type = $request->getVar('type'); + $value = $request->getVar('value'); + if ($type && $value) { + $record = DataObject::get_one($this->dataObject->class, "$type = '$value'"); + header("Content-Type: text/plain"); + echo json_encode(array("record"=>$record->toMap())); + } + } } diff --git a/forms/Form.php b/forms/Form.php index 60b9c44ce..1d35eafe9 100644 --- a/forms/Form.php +++ b/forms/Form.php @@ -118,6 +118,7 @@ class Form extends RequestHandlingData { static $url_handlers = array( 'field/$FieldName!' => 'handleField', + '$Action!' => 'handleAction', 'POST ' => 'httpSubmission', 'GET ' => 'httpSubmission', ); diff --git a/javascript/ScaffoldComplexTableField.js b/javascript/ScaffoldComplexTableField.js index c4b3594a1..81ba45bf7 100644 --- a/javascript/ScaffoldComplexTableField.js +++ b/javascript/ScaffoldComplexTableField.js @@ -1,5 +1,24 @@ window.onload = function() { + + resourcePath = jQuery('form').attr('action'); jQuery("fieldset input:first").attr('autocomplete', 'off').autocomplete({list: ["mark rickerby", "maxwell sparks"]}); + jQuery("fieldset input:first").bind('activate.autocomplete', function(e){ + + type = jQuery("fieldset input:first").attr('name'); + value = jQuery("fieldset input:first").val(); + + jQuery.getJSON(resourcePath + '/record', {'type':type, 'value':value}, function(data) { + jQuery('form input').each(function(i, elm){ + if(elm.name in data.record) { + val = data.record[elm.name]; + if (val != null) elm.setAttribute('value', val); + } + }); + }); + + }); + + }; \ No newline at end of file diff --git a/sake b/sake index 5196b10ef..11d40b8f5 100755 --- a/sake +++ b/sake @@ -15,7 +15,7 @@ if [ "$1" = "installsake" ]; then fi # Find the PHP binary -for candidatephp in php5 php; do +for candidatephp in "php5 php"; do if [ -f `which $candidatephp` ]; then php=`which $candidatephp` break diff --git a/search/SearchContext.php b/search/SearchContext.php index 8e3e22b9c..addfb472c 100644 --- a/search/SearchContext.php +++ b/search/SearchContext.php @@ -125,11 +125,10 @@ class SearchContext extends Object { if($existingQuery) { $query = $existingQuery; - $query->select = array_merge($query->select,$fields); } else { $query = $model->buildSQL(); - $query->select($fields); } + $query->select = array_merge($query->select,$fields); $SQL_limit = Convert::raw2sql($limit); $query->limit($SQL_limit); diff --git a/search/filters/CollectionFilter.php b/search/filters/CollectionFilter.php new file mode 100644 index 000000000..ddc307326 --- /dev/null +++ b/search/filters/CollectionFilter.php @@ -0,0 +1,29 @@ +@silverstripe.com) + */ +class CollectionFilter extends SearchFilter { + + public function apply(SQLQuery $query) { + $query = $this->applyRelation($query); + $values = explode(',',$this->value); + if(!$values) return false; + + for($i=0; $iwhere("{$this->getName()} IN ({$SQL_valueStr})"); + } + +} +?> \ No newline at end of file diff --git a/tests/SearchContextTest.php b/tests/SearchContextTest.php index 4eb47e479..3fbbe05f9 100644 --- a/tests/SearchContextTest.php +++ b/tests/SearchContextTest.php @@ -99,7 +99,8 @@ class SearchContextTest extends SapphireTest { $params = array( "ExactMatch" => "Match Me Exactly", "PartialMatch" => "partially", - "Negation" => "undisclosed" + "Negation" => "undisclosed", + "CollectionMatch" => "ExistingCollectionValue,NonExistingCollectionValue,4,Inline'Quotes'", ); $results = $context->getResults($params); @@ -209,13 +210,15 @@ class SearchContextTest_AllFilterTypes extends DataObject implements TestOnly { "Negation" => "Text", "SubstringMatch" => "Text", "HiddenValue" => "Text", + "CollectionMatch" => "Text", ); static $searchable_fields = array( "ExactMatch" => "ExactMatchFilter", "PartialMatch" => "PartialMatchFilter", "Negation" => "NegationFilter", - "SubstringMatch" => "SubstringFilter" + "SubstringMatch" => "SubstringFilter", + "CollectionMatch" => "CollectionFilter", ); } diff --git a/tests/SearchContextTest.yml b/tests/SearchContextTest.yml index e889a9783..d8296d6f4 100644 --- a/tests/SearchContextTest.yml +++ b/tests/SearchContextTest.yml @@ -61,4 +61,5 @@ SearchContextTest_AllFilterTypes: ExactMatch: Match me exactly PartialMatch: Match me partially Negation: Shouldnt match me - HiddenValue: Filtered value \ No newline at end of file + HiddenValue: Filtered value + CollectionMatch: ExistingCollectionValue \ No newline at end of file