mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
(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@60261 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
parent
7b26120582
commit
46bbde18e7
@ -109,6 +109,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
||||
$this->extension_instances = null;
|
||||
$this->components = null;
|
||||
$this->destroyed = true;
|
||||
$this->flushCache();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -7,6 +7,12 @@
|
||||
* @subpackage model
|
||||
*/
|
||||
class DataObjectLog extends Object {
|
||||
/**
|
||||
* This must be set to true for the DataObjectLog to work
|
||||
*/
|
||||
static $enabled = false;
|
||||
|
||||
|
||||
/**
|
||||
* The DataObjects that have been added to the database in this session.
|
||||
* @var array
|
||||
@ -29,7 +35,9 @@ class DataObjectLog extends Object {
|
||||
* @param DataObject $object
|
||||
*/
|
||||
static function addedObject($object) {
|
||||
self::$added[$object->class][] = $object;
|
||||
if(self::$enabled) {
|
||||
self::$added[$object->class][] = $object;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -37,7 +45,9 @@ class DataObjectLog extends Object {
|
||||
* @param DataObject $object
|
||||
*/
|
||||
static function deletedObject($object) {
|
||||
self::$deleted[$object->class][] = $object;
|
||||
if(self::$enabled) {
|
||||
self::$deleted[$object->class][] = $object;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -45,7 +55,9 @@ class DataObjectLog extends Object {
|
||||
* @param DataObject $object
|
||||
*/
|
||||
static function changedObject($object) {
|
||||
self::$changed[$object->class][] = $object;
|
||||
if(self::$enabled) {
|
||||
self::$changed[$object->class][] = $object;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
22
css/CodeViewer.css
Normal file
22
css/CodeViewer.css
Normal file
@ -0,0 +1,22 @@
|
||||
pre {
|
||||
border: 1px #777 solid;
|
||||
background-color: #CCC;
|
||||
color: #333;
|
||||
margin: 0.5em 2em;
|
||||
padding: 1em;
|
||||
line-height: 120%;
|
||||
}
|
||||
|
||||
pre strong {
|
||||
background-color: #FFC;
|
||||
color: #000;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.T_VARIABLE {
|
||||
|
||||
}
|
||||
|
||||
.T_NEW {
|
||||
|
||||
}
|
290
dev/CodeViewer.php
Normal file
290
dev/CodeViewer.php
Normal file
@ -0,0 +1,290 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Allows human reading of a test in a format suitable for agile documentation
|
||||
*/
|
||||
class CodeViewer extends Controller {
|
||||
/**
|
||||
* Define a simple finite state machine.
|
||||
* Top keys are the state names. 'start' is the first state, and 'die' is the error state.
|
||||
* Inner keys are token names/codes. The values are either a string, new state, or an array(new state, handler method).
|
||||
* The handler method will be passed the PHP token as an argument, and is expected to populate a property of the object.
|
||||
*/
|
||||
static $fsm = array(
|
||||
'start' => array(
|
||||
T_CLASS => array('className','createClass'),
|
||||
T_DOC_COMMENT => array('', 'saveClassComment'),
|
||||
),
|
||||
'className' => array(
|
||||
T_STRING => array('classSpec', 'setClassName'),
|
||||
),
|
||||
'classSpec' => array(
|
||||
'{' => 'classBody',
|
||||
),
|
||||
'classBody' => array(
|
||||
T_FUNCTION => array('methodName','createMethod'),
|
||||
'}' => array('start', 'completeClass'),
|
||||
T_DOC_COMMENT => array('', 'saveMethodComment'),
|
||||
),
|
||||
'methodName' => array(
|
||||
T_STRING => array('methodSpec', 'setMethodName'),
|
||||
),
|
||||
'methodSpec' => array(
|
||||
'{' => 'methodBody',
|
||||
),
|
||||
'methodBody' => array(
|
||||
'{' => array('!push','appendMethodContent'),
|
||||
'}' => array(
|
||||
'hasstack' => array('!pop', 'appendMethodContent'),
|
||||
'nostack' => array('classBody', 'completeMethod'),
|
||||
),
|
||||
T_VARIABLE => array('variable', 'potentialMethodCall'),
|
||||
T_COMMENT => array('', 'appendMethodComment'),
|
||||
T_DOC_COMMENT => array('', 'appendMethodComment'),
|
||||
'*' => array('', 'appendMethodContent'),
|
||||
),
|
||||
'variable' => array(
|
||||
T_OBJECT_OPERATOR => array('variableArrow', 'potentialMethodCall'),
|
||||
'*' => array('methodBody', 'appendMethodContent'),
|
||||
),
|
||||
'variableArrow' => array(
|
||||
T_STRING => array('methodOrProperty', 'potentialMethodCall'),
|
||||
T_WHITESPACE => array('', 'potentialMethodCall'),
|
||||
'*' => array('methodBody', 'appendMethodContent'),
|
||||
),
|
||||
'methodOrProperty' => array(
|
||||
'(' => array('methodCall', 'potentialMethodCall'),
|
||||
T_WHITESPACE => array('', 'potentialMethodCall'),
|
||||
'*' => array('methodBody', 'appendMethodContent'),
|
||||
),
|
||||
'methodCall' => array(
|
||||
'(' => array('!push/nestedInMethodCall', 'potentialMethodCall'),
|
||||
')' => array('methodBody', 'completeMethodCall'),
|
||||
'*' => array('', 'potentialMethodCall'),
|
||||
),
|
||||
'nestedInMethodCall' => array(
|
||||
'(' => array('!push', 'potentialMethodCall'),
|
||||
')' => array('!pop', 'potentialMethodCall'),
|
||||
'*' => array('', 'potentialMethodCall'),
|
||||
),
|
||||
);
|
||||
|
||||
function init() {
|
||||
if(!Permission::check('ADMIN')) Security::permissionFailure();
|
||||
parent::init();
|
||||
}
|
||||
|
||||
protected $classComment, $methodComment;
|
||||
|
||||
function saveClassComment($token) {
|
||||
$this->classComment = $this->prettyComment($token);
|
||||
}
|
||||
function saveMethodComment($token) {
|
||||
$this->methodComment = $this->prettyComment($token);
|
||||
}
|
||||
|
||||
function createClass($token) {
|
||||
$this->currentClass = array(
|
||||
"description" => $this->classComment
|
||||
);
|
||||
$ths->classComment = null;
|
||||
}
|
||||
function setClassName($token) {
|
||||
$this->currentClass['name'] = $token[1];
|
||||
}
|
||||
function completeClass($token) {
|
||||
$this->classes[] = $this->currentClass;
|
||||
}
|
||||
|
||||
function createMethod($token) {
|
||||
$this->currentMethod = array();
|
||||
$this->currentMethod['content'] = "<pre>";
|
||||
$this->currentMethod['description'] = $this->methodComment;
|
||||
$this->methodComment = null;
|
||||
|
||||
}
|
||||
function setMethodName($token) {
|
||||
$this->currentMethod['name'] = $token[1];
|
||||
}
|
||||
function appendMethodComment($token) {
|
||||
if(substr($token[1],0,2) == '/*') {
|
||||
$this->closeOffMethodContentPre();
|
||||
$this->currentMethod['content'] .= $this->prettyComment($token) . "<pre>";
|
||||
} else {
|
||||
$this->currentMethod['content'] .= $this->renderToken($token);
|
||||
}
|
||||
}
|
||||
|
||||
function prettyComment($token) {
|
||||
$comment = preg_replace('/^\/\*/','',$token[1]);
|
||||
$comment = preg_replace('/\*\/$/','',$comment);
|
||||
$comment = preg_replace('/(^|\n)[\t ]*\* */m',"\n",$comment);
|
||||
$comment = htmlentities($comment);
|
||||
$comment = str_replace("\n\n", "</p><p>", $comment);
|
||||
return "<p>$comment</p>";
|
||||
}
|
||||
|
||||
protected $isNewLine = true;
|
||||
|
||||
function appendMethodContent($token) {
|
||||
if($this->potentialMethodCall) {
|
||||
$this->currentMethod['content'] .= $this->potentialMethodCall;
|
||||
$this->potentialMethodCall = "";
|
||||
}
|
||||
//if($this->isNewLine && isset($token[2])) $this->currentMethod['content'] .= $token[2] . ": ";
|
||||
$this->isNewLine = false;
|
||||
$this->currentMethod['content'] .= $this->renderToken($token);
|
||||
}
|
||||
function completeMethod($token) {
|
||||
$this->closeOffMethodContentPre();
|
||||
$this->currentMethod['content'] = str_replace("\n\t\t","\n",$this->currentMethod['content']);
|
||||
$this->currentClass['methods'][] = $this->currentMethod;
|
||||
}
|
||||
|
||||
protected $potentialMethodCall = "";
|
||||
function potentialMethodCall($token) {
|
||||
$this->potentialMethodCall .= $this->renderToken($token);
|
||||
}
|
||||
function completeMethodCall($token) {
|
||||
$this->potentialMethodCall .= $this->renderToken($token);
|
||||
if(strpos($this->potentialMethodCall, '-></span><span class="T_STRING">assert') !== false) {
|
||||
$this->currentMethod['content'] .= "<strong>" . $this->potentialMethodCall . "</strong>";
|
||||
} else {
|
||||
$this->currentMethod['content'] .= $this->potentialMethodCall;
|
||||
}
|
||||
$this->potentialMethodCall = "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish the <pre> block in method content.
|
||||
* Will remove whitespace and <pre></pre> blocks
|
||||
*/
|
||||
function closeOffMethodContentPre() {
|
||||
$this->currentMethod['content'] = trim($this->currentMethod['content']);
|
||||
if(substr($this->currentMethod['content'],-5) == '<pre>') $this->currentMethod['content'] = substr($this->currentMethod['content'], 0,-5);
|
||||
else $this->currentMethod['content'] .= '</pre>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the given token as HTML
|
||||
*/
|
||||
function renderToken($token) {
|
||||
$tokenContent = htmlentities(is_array($token) ? $token[1] : $token);
|
||||
$tokenName = is_array($token) ? token_name($token[0]) : 'T_PUNCTUATION';
|
||||
|
||||
switch($tokenName) {
|
||||
case "T_WHITESPACE":
|
||||
if(strpos($tokenContent, "\n") !== false) $this->isNewLine = true;
|
||||
return $tokenContent;
|
||||
default:
|
||||
return "<span class=\"$tokenName\">$tokenContent</span>";
|
||||
}
|
||||
}
|
||||
|
||||
protected $classes = array();
|
||||
protected $currentMethod, $currentClass;
|
||||
|
||||
function Content() {
|
||||
$className = $this->urlParams['ID'];
|
||||
if($className && ClassInfo::exists($className)) {
|
||||
return $this->testAnalysis(getClassFile($className));
|
||||
} else {
|
||||
$result = "<h1>View any of the following test classes</h1>";
|
||||
$classes = ClassInfo::subclassesFor('SapphireTest');
|
||||
ksort($classes);
|
||||
foreach($classes as $className) {
|
||||
if($className == 'SapphireTest') continue;
|
||||
$result .= "<li><a href=\"dev/viewcode/show/$className\">$className</a></li>";
|
||||
}
|
||||
|
||||
$result .= "<h1>View any of the following other classes</h1>";
|
||||
global $_CLASS_MANIFEST;
|
||||
$classes = array_keys(ClassInfo::allClasses());
|
||||
sort($classes);
|
||||
foreach($classes as $className) {
|
||||
if(preg_match('/^[A-Za-z][A-Za-z0-9]*$/', $className) && isset($_CLASS_MANIFEST[$className])) {
|
||||
$result .= "<li><a href=\"dev/viewcode/show/$className\">$className</a></li>";
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
function testAnalysis($file) {
|
||||
$content = file_get_contents($file);
|
||||
$tokens = token_get_all($content);
|
||||
|
||||
// Execute a finite-state-machine with a built-in state stack
|
||||
// This FSM+stack gives us enough expressive power for simple PHP parsing
|
||||
$state = "start";
|
||||
$stateStack = array();
|
||||
|
||||
//echo "<li>state $state";
|
||||
foreach($tokens as $token) {
|
||||
// Get token name - some tokens are arrays, some arent'
|
||||
if(is_array($token)) $tokenName = $token[0]; else $tokenName = $token;
|
||||
//echo "<li>token '$tokenName'";
|
||||
|
||||
// Find the rule for that token in the current state
|
||||
if(isset(self::$fsm[$state][$tokenName])) $rule = self::$fsm[$state][$tokenName];
|
||||
else if(isset(self::$fsm[$state]['*'])) $rule = self::$fsm[$state]['*'];
|
||||
else $rule = null;
|
||||
|
||||
// Check to see if we have specified multiple rules depending on whether the stack is populated
|
||||
if(is_array($rule) && array_keys($rule) == array('hasstack', 'nostack')) {
|
||||
if($stateStack) $rule = $rule['hasstack'];
|
||||
else $rule = $rule = $rule['nostack'];
|
||||
}
|
||||
|
||||
if(is_array($rule)) {
|
||||
list($destState, $methodName) = $rule;
|
||||
$this->$methodName($token);
|
||||
} else if($rule) {
|
||||
$destState = $rule;
|
||||
} else {
|
||||
$destState = null;
|
||||
}
|
||||
//echo "<li>->state $destState";
|
||||
|
||||
if(preg_match('/!(push|pop)(\/[a-zA-Z0-9]+)?/', $destState, $parts)) {
|
||||
$action = $parts[1];
|
||||
$argument = isset($parts[2]) ? substr($parts[2],1) : null;
|
||||
$destState = null;
|
||||
|
||||
switch($action) {
|
||||
case "push":
|
||||
$stateStack[] = $state;
|
||||
if($argument) $destState = $argument;
|
||||
break;
|
||||
|
||||
case "pop":
|
||||
if($stateStack) $destState = array_pop($stateStack);
|
||||
else if($argument) $destState = $argument;
|
||||
else user_error("State transition '!pop' was attempted with an empty state-stack and no default option specified.", E_USER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
if($destState) $state = $destState;
|
||||
if(!isset(self::$fsm[$state])) user_error("Transition to unrecognised state '$state'", E_USER_ERROR);
|
||||
}
|
||||
|
||||
$subclasses = ClassInfo::subclassesFor('SapphireTest');
|
||||
foreach($this->classes as $classDef) {
|
||||
if(true ||in_array($classDef['name'], $subclasses)) {
|
||||
echo "<h1>$classDef[name]</h1>";
|
||||
echo "<div style=\"font-weight: bold\">$classDef[description]</div>";
|
||||
if(isset($classDef['methods'])) foreach($classDef['methods'] as $method) {
|
||||
if(true || substr($method['name'],0,4) == 'test') {
|
||||
//$title = ucfirst(strtolower(preg_replace('/([a-z])([A-Z])/', '$1 $2', substr($method['name'], 4))));
|
||||
$title = $method['name'];
|
||||
|
||||
echo "<h2>$title</h2>";
|
||||
echo "<div style=\"font-weight: bold\">$method[description]</div>";
|
||||
echo $method['content'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -284,13 +284,10 @@ class Debug {
|
||||
echo "ERROR:Error $errno: $errstr\n At l$errline in $errfile\n";
|
||||
Debug::backtrace();
|
||||
} else {
|
||||
$reporter = new SapphireDebugReporter();
|
||||
$reporter = new DebugView();
|
||||
$reporter->writeHeader();
|
||||
echo '<div class="info">';
|
||||
echo "<h1>" . strip_tags($errstr) . "</h1>";
|
||||
echo "<h3>{$_SERVER['REQUEST_METHOD']} {$_SERVER['REQUEST_URI']}</h3>";
|
||||
echo "<p>Line <strong>$errline</strong> in <strong>$errfile</strong></p>";
|
||||
echo '</div>';
|
||||
$reporter->writeInfo(strip_tags($errstr), $_SERVER['REQUEST_METHOD'] . " " .$_SERVER['REQUEST_URI'],
|
||||
"Line <strong>$errline</strong> in <strong>$errfile</strong>");
|
||||
echo '<div class="trace"><h3>Source</h3>';
|
||||
Debug::showLines($errfile, $errline);
|
||||
echo '<h3>Trace</h3>';
|
||||
@ -577,53 +574,4 @@ function errorHandler($errno, $errstr, $errfile, $errline, $errcontext) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for stylish rendering of a debug info report.
|
||||
*/
|
||||
interface DebugReporter {
|
||||
|
||||
/**
|
||||
* Render HTML markup for the header/top segment of debug report.
|
||||
*/
|
||||
function writeHeader();
|
||||
|
||||
/**
|
||||
* Render HTML markup for the footer and closing tags of debug report.
|
||||
*/
|
||||
function writeFooter();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Concrete class to render a Sapphire specific wrapper design
|
||||
* for developer errors, task runner, and test runner.
|
||||
*/
|
||||
class SapphireDebugReporter implements DebugReporter {
|
||||
|
||||
public function writeHeader() {
|
||||
echo '<!DOCTYPE html><html><head><title>'. $_SERVER['REQUEST_METHOD'] . ' ' .$_SERVER['REQUEST_URI'] .'</title>';
|
||||
echo '<style type="text/css">';
|
||||
echo 'body { background-color:#eee; margin:0; padding:0; font-family:Helvetica,Arial,sans-serif; }';
|
||||
echo '.info { border-bottom:1px dotted #333; background-color:#ccdef3; margin:0; padding:6px 12px; }';
|
||||
echo '.info h1 { margin:0; padding:0; color:#333; letter-spacing:-2px; }';
|
||||
echo '.header { margin:0; border-bottom:6px solid #ccdef3; height:23px; background-color:#666673; padding:4px 0 2px 6px; background-image:url('.Director::absoluteBaseURL().'cms/images/mainmenu/top-bg.gif); }';
|
||||
echo '.trace { padding:6px 12px; }';
|
||||
echo '.trace li { font-size:14px; margin:6px 0; }';
|
||||
echo 'pre { margin-left:18px; }';
|
||||
echo 'pre span { color:#999;}';
|
||||
echo 'pre .error { color:#f00; }';
|
||||
echo '.pass { margin-top:18px; padding:2px 20px 2px 40px; color:#006600; background:#E2F9E3 url('.Director::absoluteBaseURL() .'cms/images/alert-good.gif) no-repeat scroll 7px 50%; border:1px solid #8DD38D; }';
|
||||
echo '.fail { margin-top:18px; padding:2px 20px 2px 40px; color:#C80700; background:#FFE9E9 url('.Director::absoluteBaseURL() .'cms/images/alert-bad.gif) no-repeat scroll 7px 50%; border:1px solid #C80700; }';
|
||||
echo '.failure span { color:#C80700; font-weight:bold; }';
|
||||
echo '</style></head>';
|
||||
echo '<body>';
|
||||
echo '<div class="header"><img src="'. Director::absoluteBaseURL() .'cms/images/mainmenu/logo.gif" width="26" height="23"></div>';
|
||||
}
|
||||
|
||||
public function writeFooter() {
|
||||
echo "</body></html>";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
?>
|
@ -13,6 +13,26 @@
|
||||
*/
|
||||
class DebugView {
|
||||
|
||||
/**
|
||||
* Generate breadcrumb links to the URL path being displayed
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function Breadcrumbs() {
|
||||
$basePath = str_replace(Director::protocolAndHost(), '', Director::absoluteBaseURL());
|
||||
$parts = explode('/', str_replace($basePath, '', $_SERVER['REQUEST_URI']));
|
||||
$base = Director::absoluteBaseURL();
|
||||
$path = "";
|
||||
$pathPart = "";
|
||||
foreach($parts as $part) {
|
||||
if ($part != '') {
|
||||
$pathPart .= "$part/";
|
||||
$path .= "<a href=\"$base$pathPart\">$part</a>→ ";
|
||||
}
|
||||
}
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render HTML header for development views
|
||||
*/
|
||||
@ -36,6 +56,24 @@ class DebugView {
|
||||
echo '<div class="header"><img src="'. Director::absoluteBaseURL() .'cms/images/mainmenu/logo.gif" width="26" height="23"></div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the information header for the view
|
||||
*
|
||||
* @param string $title
|
||||
* @param string $title
|
||||
*/
|
||||
public function writeInfo($title, $subtitle, $description=false) {
|
||||
echo '<div class="info">';
|
||||
echo "<h1>$title</h1>";
|
||||
echo "<h3>$subtitle</h3>";
|
||||
if ($description) {
|
||||
echo "<p>$description</p>";
|
||||
} else {
|
||||
echo $this->Breadcrumbs();
|
||||
}
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render HTML footer for development views
|
||||
*/
|
||||
|
@ -17,15 +17,16 @@ class DevelopmentAdmin extends Controller {
|
||||
);
|
||||
|
||||
function index() {
|
||||
$renderer = new SapphireDebugReporter();
|
||||
$renderer = new DebugView();
|
||||
$renderer->writeHeader();
|
||||
$renderer->writeInfo("Sapphire Development Tools", Director::absoluteBaseURL());
|
||||
echo <<<HTML
|
||||
<div class="info"><h1>Sapphire Development Tools</h1></div>
|
||||
<div class="options">
|
||||
<ul>
|
||||
<li><a href="tests">/dev/tests: See a list of unit tests to run</a></li>
|
||||
<li><a href="tasks">/dev/tasks: See a list of build tasks to run</a></li>
|
||||
<li><a href="db/build?flush=1">/db/build?flush=1: Rebuild the database</a></li>
|
||||
<li><a href="viewcode">/dev/viewcode: Read source code in a literate programming style</a></li>
|
||||
<li><a href="../db/build?flush=1">/db/build?flush=1: Rebuild the database</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
HTML;
|
||||
@ -42,6 +43,11 @@ HTML;
|
||||
return $controller->handleRequest($request);
|
||||
}
|
||||
|
||||
function viewcode($request) {
|
||||
$controller = new CodeViewer();
|
||||
return $controller->handleRequest($request);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
?>
|
@ -47,7 +47,7 @@ class TestRunner extends Controller {
|
||||
|
||||
function init() {
|
||||
parent::init();
|
||||
if (!self::$default_reporter) self::set_reporter('SapphireDebugReporter');
|
||||
if (!self::$default_reporter) self::set_reporter('DebugView');
|
||||
}
|
||||
|
||||
public function Link() {
|
||||
@ -115,32 +115,14 @@ class TestRunner extends Controller {
|
||||
}
|
||||
|
||||
function runTests($classList, $coverage = false) {
|
||||
if(!Director::is_cli()) {
|
||||
self::$default_reporter->writeHeader();
|
||||
echo '<div class="info">';
|
||||
if (count($classList) > 1) {
|
||||
echo "<h1>Sapphire Tests</h1>";
|
||||
echo "<p>Running test cases: " . implode(", ", $classList) . "</p>";
|
||||
} else {
|
||||
echo "<h1>{$classList[0]}</h1>";
|
||||
}
|
||||
echo "</div>";
|
||||
echo '<div class="trace">';
|
||||
} else {
|
||||
echo "Sapphire PHPUnit Test Runner\n";
|
||||
echo "Using the following subclasses of SapphireTest for testing: " . implode(", ", $classList) . "\n\n";
|
||||
}
|
||||
|
||||
// Remove our error handler so that PHP can use its own
|
||||
//restore_error_handler();
|
||||
|
||||
// run tests before outputting anything to the client
|
||||
$suite = new PHPUnit_Framework_TestSuite();
|
||||
foreach($classList as $className) {
|
||||
// Ensure that the autoloader pulls in the test class, as PHPUnit won't know how to do this.
|
||||
class_exists($className);
|
||||
$suite->addTest(new PHPUnit_Framework_TestSuite($className));
|
||||
}
|
||||
|
||||
/*, array("reportDirectory" => "/Users/sminnee/phpunit-report")*/
|
||||
$reporter = new SapphireTestReporter();
|
||||
$results = new PHPUnit_Framework_TestResult();
|
||||
$results->addListener($reporter);
|
||||
@ -156,6 +138,13 @@ class TestRunner extends Controller {
|
||||
//$testResult = PHPUnit_TextUI_TestRunner::run($suite);
|
||||
}
|
||||
|
||||
self::$default_reporter->writeHeader();
|
||||
if (count($classList) > 1) {
|
||||
self::$default_reporter->writeInfo("All Tests", "Running test cases: " . implode(", ", $classList) .")");
|
||||
} else {
|
||||
self::$default_reporter->writeInfo($classList[0], "");
|
||||
}
|
||||
echo '<div class="trace">';
|
||||
$reporter->writeResults();
|
||||
|
||||
if(!Director::is_cli()) echo '</div>';
|
||||
|
@ -19,7 +19,9 @@ class CheckboxSetField extends OptionsetField {
|
||||
}
|
||||
|
||||
/**
|
||||
* Object handles arrays and dosets being passed by reference
|
||||
* Object handles arrays and dosets being passed by reference.
|
||||
*
|
||||
* @todo Should use CheckboxField FieldHolder rather than constructing own markup.
|
||||
*/
|
||||
function Field() {
|
||||
$values = $this->value;
|
||||
@ -94,7 +96,7 @@ class CheckboxSetField extends OptionsetField {
|
||||
|
||||
$this->disabled ? $disabled = " disabled=\"disabled\"" : $disabled = "";
|
||||
|
||||
$options .= "<li class=\"$extraClass\"><input id=\"$itemID\" name=\"$this->name[]\" type=\"checkbox\" value=\"$key\"$checked $disabled /> <label for=\"$itemID\">$value</label></li>\n";
|
||||
$options .= "<li class=\"$extraClass\"><input id=\"$itemID\" name=\"$this->name[]\" type=\"checkbox\" value=\"$key\"$checked $disabled class=\"checkbox\" /> <label for=\"$itemID\">$value</label></li>\n";
|
||||
}
|
||||
|
||||
|
||||
|
@ -293,7 +293,10 @@ class Form extends RequestHandlingData {
|
||||
Session::set('SecurityID', $securityID);
|
||||
}
|
||||
|
||||
$extraFields->push(new HiddenField('SecurityID', '', $securityID));
|
||||
$securityField = new HiddenField('SecurityID', '', $securityID);
|
||||
$securityField->setForm($this);
|
||||
$extraFields->push($securityField);
|
||||
$this->securityTokenAdded = true;
|
||||
}
|
||||
|
||||
return $extraFields;
|
||||
|
@ -312,7 +312,7 @@ class FormField extends RequestHandlingData {
|
||||
$rightTitleBlock = (!empty($RightTitle)) ? "<label class=\"right\" for=\"{$this->id()}\">$RightTitle</label>" : "";
|
||||
|
||||
return <<<HTML
|
||||
<div id="$Name" class="field $Type $extraClass">$titleBlock<span class="middleColumn">$Field</span>$rightTitleBlock$messageBlock</div>
|
||||
<div id="$Name" class="field $Type $extraClass">$titleBlock<div class="middleColumn">$Field</div>$rightTitleBlock$messageBlock</div>
|
||||
HTML;
|
||||
}
|
||||
|
||||
|
@ -22,6 +22,8 @@ class OptionsetField extends DropdownField {
|
||||
/**
|
||||
* Create a UL tag containing sets of radio buttons and labels. The IDs are set to
|
||||
* FieldID_ItemKey, where ItemKey is the key with all non-alphanumerics removed.
|
||||
*
|
||||
* @todo Should use CheckboxField FieldHolder rather than constructing own markup.
|
||||
*/
|
||||
function Field() {
|
||||
$options = '';
|
||||
@ -41,7 +43,7 @@ class OptionsetField extends DropdownField {
|
||||
$extraClass .= " val" . preg_replace('/[^a-zA-Z0-9\-\_]/','_', $key);
|
||||
$disabled = $this->disabled ? 'disabled="disabled"' : '';
|
||||
|
||||
$options .= "<li class=\"".$extraClass."\"><input id=\"$itemID\" name=\"$this->name\" type=\"radio\" value=\"$key\"$checked $disabled/> <label for=\"$itemID\">$value</label></li>\n";
|
||||
$options .= "<li class=\"".$extraClass."\"><input id=\"$itemID\" name=\"$this->name\" type=\"radio\" value=\"$key\"$checked $disabled class=\"radio\" /> <label for=\"$itemID\">$value</label></li>\n";
|
||||
}
|
||||
$id = $this->id();
|
||||
return "<ul id=\"$id\" class=\"optionset {$this->extraClass()}\">\n$options</ul>\n";
|
||||
|
@ -52,15 +52,12 @@ class SearchContext extends Object {
|
||||
* Usually these values come from a submitted searchform
|
||||
* in the form of a $_REQUEST object.
|
||||
* CAUTION: All values should be treated as insecure client input.
|
||||
*
|
||||
* @var array
|
||||
protected $params;
|
||||
*/
|
||||
|
||||
function __construct($modelClass, $fields = null, $filters = null) {
|
||||
$this->modelClass = $modelClass;
|
||||
$this->fields = $fields;
|
||||
$this->filters = $filters;
|
||||
$this->fields = ($fields) ? $fields : new FieldSet();
|
||||
$this->filters = ($filters) ? $filters : array();
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
@ -116,7 +113,6 @@ class SearchContext extends Object {
|
||||
|
||||
$SQL_sort = (!empty($sort)) ? Convert::raw2sql($sort) : singleton($this->modelClass)->stat('default_sort');
|
||||
$query->orderby($SQL_sort);
|
||||
|
||||
foreach($searchParams as $key => $value) {
|
||||
if ($value != '0') {
|
||||
$key = str_replace('__', '.', $key);
|
||||
@ -128,6 +124,7 @@ class SearchContext extends Object {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
@ -202,10 +199,28 @@ class SearchContext extends Object {
|
||||
$this->filters = $filters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a instance of {@link SearchFilter}.
|
||||
*
|
||||
* @param SearchFilter $filter
|
||||
*/
|
||||
public function addFilter($filter) {
|
||||
$this->filters[$filter->getName()] = $filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a filter by name.
|
||||
*
|
||||
* @param string $name
|
||||
*/
|
||||
public function removeFilterByName($name) {
|
||||
unset($this->filters[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of searchable fields in the current search context.
|
||||
*
|
||||
* @return array
|
||||
* @return FieldSet
|
||||
*/
|
||||
public function getFields() {
|
||||
return $this->fields;
|
||||
@ -214,11 +229,29 @@ class SearchContext extends Object {
|
||||
/**
|
||||
* Apply a list of searchable fields to the current search context.
|
||||
*
|
||||
* @param array $fields
|
||||
* @param FieldSet $fields
|
||||
*/
|
||||
public function setFields($fields) {
|
||||
$this->fields = $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new {@link FormField} instance.
|
||||
*
|
||||
* @param FormField $field
|
||||
*/
|
||||
public function addField($field) {
|
||||
$this->fields->push($field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an existing formfield instance by its name.
|
||||
*
|
||||
* @param string $fieldName
|
||||
*/
|
||||
public function removeFieldByName($fieldName) {
|
||||
$this->fields->removeByName($fieldName);
|
||||
}
|
||||
|
||||
}
|
||||
?>
|
@ -25,7 +25,7 @@ class CollectionFilter extends SearchFilter {
|
||||
}
|
||||
}
|
||||
$SQL_valueStr = "'" . implode("','", $values) . "'";
|
||||
return $query->where("{$this->getName()} IN ({$SQL_valueStr})");
|
||||
return $query->where("{$this->getDbName()} IN ({$SQL_valueStr})");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ class EndsWithFilter extends SearchFilter {
|
||||
*/
|
||||
public function apply(SQLQuery $query) {
|
||||
$query = $this->applyRelation($query);
|
||||
$query->where($this->getName(), "RLIKE", "{$this->getValue()}$");
|
||||
$query->where($this->getDbName(), "RLIKE", "{$this->getValue()}$");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ class ExactMatchFilter extends SearchFilter {
|
||||
*/
|
||||
public function apply(SQLQuery $query) {
|
||||
$query = $this->applyRelation($query);
|
||||
return $query->where("{$this->getName()} = '{$this->getValue()}'");
|
||||
return $query->where("{$this->getDbName()} = '{$this->getValue()}'");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -28,7 +28,7 @@
|
||||
class FulltextFilter extends SearchFilter {
|
||||
|
||||
public function apply(SQLQuery $query) {
|
||||
$query->where("MATCH ({$this->getName()} AGAINST ('{$this->getValue()}')");
|
||||
$query->where("MATCH ({$this->getDbName()} AGAINST ('{$this->getValue()}')");
|
||||
return $query;
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,7 @@ class PartialMatchFilter extends SearchFilter {
|
||||
|
||||
public function apply(SQLQuery $query) {
|
||||
$query = $this->applyRelation($query);
|
||||
return $query->where("{$this->getName()} LIKE '%{$this->getValue()}%'");
|
||||
return $query->where("{$this->getDbName()} LIKE '%{$this->getValue()}%'");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -67,10 +67,20 @@ abstract class SearchFilter extends Object {
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the field name to table mapping.
|
||||
* The original name of the field.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getName() {
|
||||
public function getName() {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the field name to table mapping.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getDbName() {
|
||||
// SRM: This code finds the table where the field named $this->name lives
|
||||
// Todo: move to somewhere more appropriate, such as DataMapper, the magical class-to-be?
|
||||
$candidateClass = $this->model;
|
||||
@ -97,20 +107,22 @@ abstract class SearchFilter extends Object {
|
||||
foreach($this->relation as $rel) {
|
||||
if ($component = $model->has_one($rel)) {
|
||||
$foreignKey = $model->getReverseAssociation($component);
|
||||
$query->leftJoin($component, "$component.ID = {$this->model}.{$foreignKey}ID");
|
||||
$query->leftJoin($component, "`$component`.`ID` = `{$this->model}`.`{$foreignKey}ID`");
|
||||
$this->model = $component;
|
||||
|
||||
} elseif ($component = $model->has_many($rel)) {
|
||||
$ancestry = $model->getClassAncestry();
|
||||
$model = singleton($component);
|
||||
$foreignKey = $model->getReverseAssociation($ancestry[0]);
|
||||
$foreignKey = ($foreignKey) ? $foreignKey : $ancestry[0];
|
||||
$query->leftJoin($component, "$component.{$foreignKey}ID = {$this->model}.ID");
|
||||
|
||||
$query->leftJoin($component, "`$component`.`{$foreignKey}ID` = `{$this->model}`.`ID`");
|
||||
$this->model = $component;
|
||||
|
||||
} elseif ($component = $model->many_many($rel)) {
|
||||
throw new Exception("Many-Many traversals not implemented");
|
||||
list($parentClass, $componentClass, $parentField, $componentField, $relationTable) = $component;
|
||||
$parentBaseClass = ClassInfo::baseDataClass($parentClass);
|
||||
$componentBaseClass = ClassInfo::baseDataClass($componentClass);
|
||||
$query->innerJoin($relationTable, "`$relationTable`.`$parentField` = `$parentBaseClass`.`ID`");
|
||||
$query->leftJoin($componentClass, "`$relationTable`.`$componentField` = `$componentClass`.`ID`");
|
||||
$this->model = $componentClass;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ class StartsWithFilter extends SearchFilter {
|
||||
*/
|
||||
public function apply(SQLQuery $query) {
|
||||
$query = $this->applyRelation($query);
|
||||
$query->where("LOCATE('{$this->getValue()}', {$this->getName()}) = 1");
|
||||
$query->where("LOCATE('{$this->getValue()}', {$this->getDbName()}) = 1");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -13,7 +13,7 @@
|
||||
class SubstringFilter extends SearchFilter {
|
||||
|
||||
public function apply(SQLQuery $query) {
|
||||
return $query->where("LOCATE('{$this->getValue()}', {$this->getName()}) != 0");
|
||||
return $query->where("LOCATE('{$this->getValue()}', {$this->getDbName()}) != 0");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ class WithinRangeFilter extends SearchFilter {
|
||||
}
|
||||
|
||||
function apply(SQLQuery $query) {
|
||||
$query->where("{$this->getName()} >= {$this->min} AND {$this->getName()} <= {$this->max}");
|
||||
$query->where("{$this->getDbName()} >= {$this->min} AND {$this->getDbName()} <= {$this->max}");
|
||||
}
|
||||
|
||||
}
|
||||
|
9
templates/CodeViewer.ss
Normal file
9
templates/CodeViewer.ss
Normal file
@ -0,0 +1,9 @@
|
||||
<html>
|
||||
<head>
|
||||
<% base_tag %>
|
||||
<link rel="stylesheet" href="sapphire/css/CodeViewer.css" />
|
||||
</head>
|
||||
<body>
|
||||
$Content
|
||||
</body>
|
||||
</html>
|
@ -1,8 +1,8 @@
|
||||
<div id="$Name" class="field $Type $extraClass">
|
||||
<% if Title %><label class="left" for="$id">$Title</label><% end_if %>
|
||||
<span class="middleColumn">
|
||||
<div class="middleColumn">
|
||||
$Field
|
||||
</span>
|
||||
</div>
|
||||
<% if RightTitle %><label class="right" for="$id">$RightTitle</label><% end_if %>
|
||||
<% if Message %><span class="message $MessageType">$Message</span><% end_if %>
|
||||
</div>
|
Loading…
Reference in New Issue
Block a user