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 .= "
  • name[]\" type=\"checkbox\" value=\"$key\"$checked $disabled />
  • \n"; + $options .= "
  • name[]\" type=\"checkbox\" value=\"$key\"$checked $disabled class=\"checkbox\" />
  • \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)) ? "" : ""; return <<$titleBlock$Field$rightTitleBlock$messageBlock +
    $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 .= "
  • name\" type=\"radio\" value=\"$key\"$checked $disabled/>
  • \n"; + $options .= "
  • name\" type=\"radio\" value=\"$key\"$checked $disabled class=\"radio\" />
  • \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 @@
    <% if Title %><% end_if %> - +
    $Field - +
    <% if RightTitle %><% end_if %> <% if Message %>$Message<% end_if %>
    \ No newline at end of file