(merged from branches/roa. use "svn log -c <changeset> -g <module-svn-path>" for detailed commit message)

git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@60228 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
Ingo Schommer 2008-08-09 06:18:32 +00:00
parent 8fd1a33d84
commit 0a8f2a67f6
14 changed files with 493 additions and 44 deletions

View File

@ -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());
}

View File

@ -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'] ) );

View File

@ -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.

View File

@ -135,12 +135,17 @@ class Debug {
if(!Director::isLive()) {
$caller = Debug::caller();
$file = basename($caller['file']);
echo "<p style=\"background-color: white; color: black; width: 95%; margin: 0.5em; padding: 0.3em; border: 1px #CCC solid\">\n";
if($showHeader) echo "<b>Debug (line $caller[line] of $file):</b>\n ";
echo Convert::raw2xml(trim($message)) . "</p>\n";
if(Director::is_cli()) {
if($showHeader) echo "Debug (line $caller[line] of $file):\n ";
echo trim($message) . "\n";
} else {
echo "<p style=\"background-color: white; color: black; width: 95%; margin: 0.5em; padding: 0.3em; border: 1px #CCC solid\">\n";
if($showHeader) echo "<b>Debug (line $caller[line] of $file):</b>\n ";
echo Convert::raw2xml(trim($message)) . "</p>\n";
}
}
}
/**
* Load an error handler
*
@ -208,20 +213,7 @@ class Debug {
echo "<p>Line <strong>$errline</strong> in <strong>$errfile</strong></p>";
echo '</div>';
echo '<div class="trace"><h3>Source</h3>';
$lines = file($errfile);
$offset = $errline-10;
$lines = array_slice($lines, $offset, 16);
echo '<pre>';
$offset++;
foreach($lines as $line) {
$line = htmlentities($line);
if ($offset == $errline) {
echo "<span>$offset</span> <span class=\"error\">$line</span>";
} else {
echo "<span>$offset</span> $line";
}
$offset++;
}
Debug::showLines($errfile, $errline);
echo '</pre><h3>Trace</h3>';
Debug::backtrace();
echo '</div>';
@ -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 '<pre>';
$offset++;
foreach($lines as $line) {
$line = htmlentities($line);
if ($offset == $errline) {
echo "<span>$offset</span> <span class=\"error\">$line</span>";
} else {
echo "<span>$offset</span> $line";
}
$offset++;
}
echo '</pre>';
}
static function emailError($emailAddress, $errno, $errstr, $errfile, $errline, $errcontext, $errorType = "Error") {
if(strtolower($errorType) == 'warning') {

View File

@ -0,0 +1,286 @@
<?php
/**
* Gathers details about PHPUnit2 test suites as they
* are been executed. This does not actually format any output
* but simply gathers extended information about the overall
* results of all suites & their tests for use elsewhere.
*
* Changelog:
* 0.6 First created [David Spurr]
* 0.7 Added fix to getTestException provided [Glen Ogilvie]
*
* @version 0.7 2006-03-12
* @author David Spurr
*/
require_once 'PHPUnit/Framework/TestResult.php';
require_once 'PHPUnit/Framework/TestListener.php';
/**#@+
* @var int
*/
/**
* Failure test status constant
*/
define('TEST_FAILURE', -1);
/**
* Error test status constant
*/
define('TEST_ERROR', 0);
/**
* Success test status constant
*/
define('TEST_SUCCESS', 1);
/**
* Incomplete test status constant
*/
define('TEST_INCOMPLETE', 2);
/**#@-*/
class SapphireTestReporter implements PHPUnit_Framework_TestListener {
/**
* Holds array of suites and total number of tests run
* @var array
*/
private $suiteResults;
/**
* Holds data of current suite that is been run
* @var array
*/
private $currentSuite;
/**
* Holds data of current test that is been run
* @var array
*/
private $currentTest;
/**
* Whether PEAR Benchmark_Timer is available for timing
* @var boolean
*/
private $hasTimer;
/**
* Holds the PEAR Benchmark_Timer object
* @var obj Benchmark_Timer
*/
private $timer;
/**
* Constructor, checks to see availability of PEAR Benchmark_Timer and
* sets up basic properties
*
* @access public
* @return void
*/
public function __construct() {
@include_once 'Benchmark/Timer.php';
if(class_exists('Benchmark_Timer')) {
$this->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 "<div class=\"failure\"><span>&otimes; ". $this->testNameToPhrase($test['name']) ."</span><br>";
echo "<pre>".htmlentities($test['message'])."</pre><br>";
echo "<code>In line {$test['exception']['line']} of {$test['exception']['file']}</code></div>";
}
}
}
$result = ($failCount > 0) ? 'fail' : 'pass';
echo "<div class=\"$result\">";
echo "<h2><span>$testCount</span> tests run: <span>$passCount</span> passes, <span>$failCount</span> fails, and <span>0</span> exceptions</h2>";
echo "</div>";
}
private function testNameToPhrase($name) {
return ucfirst(preg_replace("/([a-z])([A-Z])/", "$1 $2", $name));
}
}
?>

View File

@ -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 "<h3><a href=\"$test\">$test</a></h3>";
}
}
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 '<div class="info">';
echo "<h1>Sapphire PHPUnit Test Runner</h1>";
echo "<p>Using the following subclasses of SapphireTest for testing: " . implode(", ", $classList) . "</p>";
if (count($classList) > 1) {
echo "<h1>Sapphire Tests</h1>";
echo "<p>Running test cases: " . implode(", ", $classList) . "</p>";
} else {
echo "<h1>{$classList[0]}</h1>";
echo "<p>Running test case:</p>";
}
echo "</div>";
echo '<div class="trace">';
echo "<pre>";
} 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 "<p><a href=\"$coverageURL\">Coverage report available here</a></p>";
} else {
$suite->run($results);
//$testResult = PHPUnit_TextUI_TestRunner::run($suite);
}
$reporter->writeResults();
if(!Director::is_cli()) echo '</div>';
// Put the error handlers back

View File

@ -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()));
}
}
}

View File

@ -118,6 +118,7 @@ class Form extends RequestHandlingData {
static $url_handlers = array(
'field/$FieldName!' => 'handleField',
'$Action!' => 'handleAction',
'POST ' => 'httpSubmission',
'GET ' => 'httpSubmission',
);

View File

@ -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);
}
});
});
});
};

2
sake
View File

@ -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

View File

@ -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);

View File

@ -0,0 +1,29 @@
<?php
/**
* Checks if a value is in a given set.
* SQL syntax used: Column IN ('val1','val2')
*
* @todo Add negation (NOT IN)6
*
* @author Silverstripe Ltd., Ingo Schommer (<firstname>@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; $i<count($values); $i++) {
if(!is_numeric($values[$i])) {
// @todo Fix string replacement to only replace leading and tailing quotes
$values[$i] = str_replace("'", '', $values[$i]);
$values[$i] = Convert::raw2sql($values[$i]);
}
}
$SQL_valueStr = "'" . implode("','", $values) . "'";
return $query->where("{$this->getName()} IN ({$SQL_valueStr})");
}
}
?>

View File

@ -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",
);
}

View File

@ -61,4 +61,5 @@ SearchContextTest_AllFilterTypes:
ExactMatch: Match me exactly
PartialMatch: Match me partially
Negation: Shouldnt match me
HiddenValue: Filtered value
HiddenValue: Filtered value
CollectionMatch: ExistingCollectionValue