From 46bbde18e796b598dd5d27519feba846e6d2878b Mon Sep 17 00:00:00 2001
From: Ingo Schommer
Date: Sun, 10 Aug 2008 23:03:35 +0000
Subject: [PATCH] (merged from branches/roa. use "svn log -c -g
" for detailed commit message)
git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@60261 467b73ca-7a2a-4603-9d3b-597d59a354a9
---
core/model/DataObject.php | 1 +
core/model/DataObjectLog.php | 18 +-
css/CodeViewer.css | 22 ++
dev/CodeViewer.php | 290 ++++++++++++++++++++++++++
dev/Debug.php | 58 +-----
dev/DebugView.php | 38 ++++
dev/DevelopmentAdmin.php | 12 +-
dev/TestRunner.php | 31 +--
forms/CheckboxSetField.php | 6 +-
forms/Form.php | 5 +-
forms/FormField.php | 2 +-
forms/OptionsetField.php | 4 +-
search/SearchContext.php | 49 ++++-
search/filters/CollectionFilter.php | 2 +-
search/filters/EndsWithFilter.php | 2 +-
search/filters/ExactMatchFilter.php | 2 +-
search/filters/FulltextFilter.php | 2 +-
search/filters/PartialMatchFilter.php | 2 +-
search/filters/SearchFilter.php | 28 ++-
search/filters/StartsWithFilter.php | 2 +-
search/filters/SubstringFilter.php | 2 +-
search/filters/WithinRangeFilter.php | 2 +-
templates/CodeViewer.ss | 9 +
templates/DefaultFieldHolder.ss | 4 +-
24 files changed, 480 insertions(+), 113 deletions(-)
create mode 100644 css/CodeViewer.css
create mode 100644 dev/CodeViewer.php
create mode 100644 templates/CodeViewer.ss
diff --git a/core/model/DataObject.php b/core/model/DataObject.php
index 472e0d291..a45707cfa 100644
--- a/core/model/DataObject.php
+++ b/core/model/DataObject.php
@@ -109,6 +109,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
$this->extension_instances = null;
$this->components = null;
$this->destroyed = true;
+ $this->flushCache();
}
/**
diff --git a/core/model/DataObjectLog.php b/core/model/DataObjectLog.php
index b5f2a2950..30b336304 100755
--- a/core/model/DataObjectLog.php
+++ b/core/model/DataObjectLog.php
@@ -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;
+ }
}
/**
diff --git a/css/CodeViewer.css b/css/CodeViewer.css
new file mode 100644
index 000000000..23223499b
--- /dev/null
+++ b/css/CodeViewer.css
@@ -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 {
+
+}
\ No newline at end of file
diff --git a/dev/CodeViewer.php b/dev/CodeViewer.php
new file mode 100644
index 000000000..6a3137fa5
--- /dev/null
+++ b/dev/CodeViewer.php
@@ -0,0 +1,290 @@
+ 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'] = "";
+ $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) . "";
+ } 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", "
", $comment);
+ return "
$comment
";
+ }
+
+ 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, '->assert') !== false) {
+ $this->currentMethod['content'] .= "" . $this->potentialMethodCall . " ";
+ } else {
+ $this->currentMethod['content'] .= $this->potentialMethodCall;
+ }
+ $this->potentialMethodCall = "";
+ }
+
+ /**
+ * Finish the block in method content.
+ * Will remove whitespace and blocks
+ */
+ function closeOffMethodContentPre() {
+ $this->currentMethod['content'] = trim($this->currentMethod['content']);
+ if(substr($this->currentMethod['content'],-5) == '') $this->currentMethod['content'] = substr($this->currentMethod['content'], 0,-5);
+ else $this->currentMethod['content'] .= ' ';
+ }
+
+ /**
+ * 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 "$tokenContent ";
+ }
+ }
+
+ protected $classes = array();
+ protected $currentMethod, $currentClass;
+
+ function Content() {
+ $className = $this->urlParams['ID'];
+ if($className && ClassInfo::exists($className)) {
+ return $this->testAnalysis(getClassFile($className));
+ } else {
+ $result = "View any of the following test classes ";
+ $classes = ClassInfo::subclassesFor('SapphireTest');
+ ksort($classes);
+ foreach($classes as $className) {
+ if($className == 'SapphireTest') continue;
+ $result .= " $className ";
+ }
+
+ $result .= "View any of the following other classes ";
+ 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 .= "$className ";
+ }
+ }
+ 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 "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 " 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 " ->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 "$classDef[name] ";
+ echo "$classDef[description]
";
+ 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 "$title ";
+ echo "$method[description]
";
+ echo $method['content'];
+ }
+ }
+ }
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/dev/Debug.php b/dev/Debug.php
index 2107846d0..4f5979208 100644
--- a/dev/Debug.php
+++ b/dev/Debug.php
@@ -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 '';
- echo "
" . strip_tags($errstr) . " ";
- echo "
{$_SERVER['REQUEST_METHOD']} {$_SERVER['REQUEST_URI']} ";
- echo "
Line $errline in $errfile
";
- echo '
';
+ $reporter->writeInfo(strip_tags($errstr), $_SERVER['REQUEST_METHOD'] . " " .$_SERVER['REQUEST_URI'],
+ "Line $errline in $errfile ");
echo 'Source ';
Debug::showLines($errfile, $errline);
echo '
Trace ';
@@ -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 '
'. $_SERVER['REQUEST_METHOD'] . ' ' .$_SERVER['REQUEST_URI'] .' ';
- echo '';
- echo '';
- echo '';
- }
-
- public function writeFooter() {
- echo "";
- }
-
-}
-
?>
\ No newline at end of file
diff --git a/dev/DebugView.php b/dev/DebugView.php
index 4595e09df..c17b3d7d0 100644
--- a/dev/DebugView.php
+++ b/dev/DebugView.php
@@ -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 .= "
$part → ";
+ }
+ }
+ return $path;
+ }
+
/**
* Render HTML header for development views
*/
@@ -36,6 +56,24 @@ class DebugView {
echo '';
}
+ /**
+ * Render the information header for the view
+ *
+ * @param string $title
+ * @param string $title
+ */
+ public function writeInfo($title, $subtitle, $description=false) {
+ echo '
';
+ echo "
$title ";
+ echo "
$subtitle ";
+ if ($description) {
+ echo "
$description
";
+ } else {
+ echo $this->Breadcrumbs();
+ }
+ echo '
';
+ }
+
/**
* Render HTML footer for development views
*/
diff --git a/dev/DevelopmentAdmin.php b/dev/DevelopmentAdmin.php
index 4a9c3c383..55996b94b 100644
--- a/dev/DevelopmentAdmin.php
+++ b/dev/DevelopmentAdmin.php
@@ -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 <<
Sapphire Development Tools
HTML;
@@ -42,6 +43,11 @@ HTML;
return $controller->handleRequest($request);
}
+ function viewcode($request) {
+ $controller = new CodeViewer();
+ return $controller->handleRequest($request);
+ }
+
}
?>
\ No newline at end of file
diff --git a/dev/TestRunner.php b/dev/TestRunner.php
index f28868830..43fb0fc65 100644
--- a/dev/TestRunner.php
+++ b/dev/TestRunner.php
@@ -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 '';
- if (count($classList) > 1) {
- echo "
Sapphire Tests ";
- echo "
Running test cases: " . implode(", ", $classList) . "
";
- } else {
- echo "
{$classList[0]} ";
- }
- echo "
";
- echo '';
- } 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 '
';
$reporter->writeResults();
if(!Director::is_cli()) echo '
';
diff --git a/forms/CheckboxSetField.php b/forms/CheckboxSetField.php
index 8385ba40f..a3be8fb83 100755
--- a/forms/CheckboxSetField.php
+++ b/forms/CheckboxSetField.php
@@ -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 .= "\n";
+ $options .= "\n";
}
diff --git a/forms/Form.php b/forms/Form.php
index 1d35eafe9..3f146ab50 100644
--- a/forms/Form.php
+++ b/forms/Form.php
@@ -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;
diff --git a/forms/FormField.php b/forms/FormField.php
index 9d550bf4a..8534c7038 100644
--- a/forms/FormField.php
+++ b/forms/FormField.php
@@ -312,7 +312,7 @@ class FormField extends RequestHandlingData {
$rightTitleBlock = (!empty($RightTitle)) ? "
id()}\">$RightTitle " : "";
return <<$titleBlock
$Field $rightTitleBlock$messageBlock
+
HTML;
}
diff --git a/forms/OptionsetField.php b/forms/OptionsetField.php
index c5bcfd3da..e8375cda3 100755
--- a/forms/OptionsetField.php
+++ b/forms/OptionsetField.php
@@ -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 .= " \n";
+ $options .= "\n";
}
$id = $this->id();
return "extraClass()}\">\n$options \n";
diff --git a/search/SearchContext.php b/search/SearchContext.php
index c752d78d2..b612734df 100644
--- a/search/SearchContext.php
+++ b/search/SearchContext.php
@@ -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);
+ }
+
}
?>
\ No newline at end of file
diff --git a/search/filters/CollectionFilter.php b/search/filters/CollectionFilter.php
index 5e08312b4..fd80c560d 100644
--- a/search/filters/CollectionFilter.php
+++ b/search/filters/CollectionFilter.php
@@ -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})");
}
}
diff --git a/search/filters/EndsWithFilter.php b/search/filters/EndsWithFilter.php
index 1ea57b84a..466a67982 100644
--- a/search/filters/EndsWithFilter.php
+++ b/search/filters/EndsWithFilter.php
@@ -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()}$");
}
}
diff --git a/search/filters/ExactMatchFilter.php b/search/filters/ExactMatchFilter.php
index 74ead0637..c4478fd30 100644
--- a/search/filters/ExactMatchFilter.php
+++ b/search/filters/ExactMatchFilter.php
@@ -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()}'");
}
}
diff --git a/search/filters/FulltextFilter.php b/search/filters/FulltextFilter.php
index 7b241d273..37b74dca1 100644
--- a/search/filters/FulltextFilter.php
+++ b/search/filters/FulltextFilter.php
@@ -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;
}
diff --git a/search/filters/PartialMatchFilter.php b/search/filters/PartialMatchFilter.php
index 32eef1aa0..5578a33c7 100644
--- a/search/filters/PartialMatchFilter.php
+++ b/search/filters/PartialMatchFilter.php
@@ -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()}%'");
}
}
diff --git a/search/filters/SearchFilter.php b/search/filters/SearchFilter.php
index f90c20c2e..b85f408a4 100644
--- a/search/filters/SearchFilter.php
+++ b/search/filters/SearchFilter.php
@@ -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;
}
}
}
diff --git a/search/filters/StartsWithFilter.php b/search/filters/StartsWithFilter.php
index 9f970b53f..8c3e7b30a 100644
--- a/search/filters/StartsWithFilter.php
+++ b/search/filters/StartsWithFilter.php
@@ -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");
}
}
diff --git a/search/filters/SubstringFilter.php b/search/filters/SubstringFilter.php
index 5859aeb62..bcae4862c 100644
--- a/search/filters/SubstringFilter.php
+++ b/search/filters/SubstringFilter.php
@@ -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");
}
}
diff --git a/search/filters/WithinRangeFilter.php b/search/filters/WithinRangeFilter.php
index 7a252694b..ac8efa3ec 100644
--- a/search/filters/WithinRangeFilter.php
+++ b/search/filters/WithinRangeFilter.php
@@ -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}");
}
}
diff --git a/templates/CodeViewer.ss b/templates/CodeViewer.ss
new file mode 100644
index 000000000..bb66f99b2
--- /dev/null
+++ b/templates/CodeViewer.ss
@@ -0,0 +1,9 @@
+
+
+<% base_tag %>
+
+
+
+ $Content
+
+
\ No newline at end of file
diff --git a/templates/DefaultFieldHolder.ss b/templates/DefaultFieldHolder.ss
index 2a1859e3f..3017510e3 100644
--- a/templates/DefaultFieldHolder.ss
+++ b/templates/DefaultFieldHolder.ss
@@ -1,8 +1,8 @@
\ No newline at end of file