(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@60276 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
Ingo Schommer 2008-08-11 00:03:57 +00:00
parent 124a6e3934
commit bf9f349210
30 changed files with 298 additions and 118 deletions

View File

@ -62,6 +62,16 @@ abstract class DataFormatter extends Object {
*/
protected $outputContentType = null;
/**
* Used to set totalSize properties on the output
* of {@link convertDataObjectSet()}, shows the
* total number of records without the "limit" and "offset"
* GET parameters. Useful to implement pagination.
*
* @var int
*/
protected $totalSize;
/**
* Get a DataFormatter object suitable for handling the given file extension.
*
@ -182,6 +192,20 @@ abstract class DataFormatter extends Object {
return $this->outputContentType;
}
/**
* @param int $size
*/
public function setTotalSize($size) {
$this->totalSize = (int)$size;
}
/**
* @return int
*/
public function getTotalSize() {
return $this->totalSize;
}
/**
* Returns all fields on the object which should be shown
* in the output. Can be customised through {@link self::setCustomFields()}.

View File

@ -90,7 +90,14 @@ class JSONDataFormatter extends DataFormatter {
foreach($set as $item) {
if($item->canView()) $jsonParts[] = $this->convertDataObject($item);
}
return "[\n" . implode(",\n", $jsonParts) . "\n]";
$json = "{\n";
$json .= 'totalSize: ';
$json .= (is_numeric($this->totalSize)) ? $this->totalSize : 'null';
$json .= ",\n";
$json .= "items: [\n" . implode(",\n", $jsonParts) . "\n]\n";
$json .= "}\n";
return $json;
}
public function convertStringToArray($strData) {

View File

@ -140,7 +140,7 @@ class RestfulServer extends Controller {
* @param String $relation
* @return String The serialized representation of the requested object(s) - usually XML or JSON.
*/
protected function getHandler($className, $id, $relation) {
protected function getHandler($className, $id, $relationName) {
$sort = array(
'sort' => $this->request->getVar('sort'),
'dir' => $this->request->getVar('dir')
@ -150,44 +150,45 @@ class RestfulServer extends Controller {
'limit' => $this->request->getVar('limit')
);
$params = $this->request->getVars();
$responseFormatter = $this->getResponseDataFormatter();
if(!$responseFormatter) return $this->unsupportedMediaType();
// $obj can be either a DataObject or a DataObjectSet,
// depending on the request
if($id) {
$obj = DataObject::get_by_id($className, $id);
// Format: /api/v1/<MyClass>/<ID>
$query = $this->getObjectQuery($className, $id, $params);
$obj = singleton($className)->buildDataObjectSet($query->execute());
if(!$obj) return $this->notFound();
$obj = $obj->First();
if(!$obj->canView()) return $this->permissionFailure();
if($relation) {
if($relationClass = $obj->many_many($relation)) {
$query = $obj->getManyManyComponentsQuery($relation);
} elseif($relationClass = $obj->has_many($relation)) {
$query = $obj->getComponentsQuery($relation);
} elseif($relationClass = $obj->has_one($relation)) {
$query = null;
} elseif($obj->hasMethod("{$relation}Query")) {
// @todo HACK Switch to ComponentSet->getQuery() once we implement it (and lazy loading)
$query = $obj->{"{$relation}Query"}(null, $sort, null, $limit);
$relationClass = $obj->{"{$relation}Class"}();
} else {
return $this->notFound();
}
// get all results
$obj = $this->search($relationClass, $this->request->getVars(), $sort, $limit, $query);
if(!$obj) $obj = new DataObjectSet();
// Format: /api/v1/<MyClass>/<ID>/<Relation>
if($relationName) {
$query = $this->getObjectRelationQuery($obj, $params, $sort, $limit, $relationName);
if($query === false) return $this->notFound();
$obj = singleton($className)->buildDataObjectSet($query->execute());
}
} else {
$obj = $this->search($className, $this->request->getVars(), $sort, $limit);
// Format: /api/v1/<MyClass>
$query = $this->getObjectsQuery($className, $params, $sort, $limit);
$obj = singleton($className)->buildDataObjectSet($query->execute());
// show empty serialized result when no records are present
if(!$obj) $obj = new DataObjectSet();
}
$this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType());
if($obj instanceof DataObjectSet) return $responseFormatter->convertDataObjectSet($obj);
else return $responseFormatter->convertDataObject($obj);
if($obj instanceof DataObjectSet) {
$responseFormatter->setTotalSize($query->unlimitedRowCount());
return $responseFormatter->convertDataObjectSet($obj);
} else {
return $responseFormatter->convertDataObject($obj);
}
}
/**
@ -202,7 +203,7 @@ class RestfulServer extends Controller {
* @param array $params
* @return DataObjectSet
*/
protected function search($className, $params = null, $sort = null, $limit = null, $existingQuery = null) {
protected function getSearchQuery($className, $params = null, $sort = null, $limit = null, $existingQuery = null) {
if(singleton($className)->hasMethod('getRestfulSearchContext')) {
$searchContext = singleton($className)->{'getRestfulSearchContext'}();
} else {
@ -210,7 +211,7 @@ class RestfulServer extends Controller {
}
$query = $searchContext->getQuery($params, $sort, $limit, $existingQuery);
return singleton($className)->buildDataObjectSet($query->execute());
return $query;
}
/**
@ -355,6 +356,61 @@ class RestfulServer extends Controller {
return $obj;
}
/**
* Gets a single DataObject by ID,
* through a request like /api/v1/<MyClass>/<MyID>
*
* @param string $className
* @param int $id
* @param array $params
* @return SQLQuery
*/
protected function getObjectQuery($className, $id, $params) {
$baseClass = ClassInfo::baseDataClass($className);
return singleton($className)->buildSQL(
"`$baseClass`.ID = {$id}"
);
}
/**
* @param DataObject $obj
* @param array $params
* @param int|array $sort
* @param int|array $limit
* @return SQLQuery
*/
protected function getObjectsQuery($className, $params, $sort, $limit) {
return $this->getSearchQuery($className, $params, $sort, $limit);
}
/**
* @param DataObject $obj
* @param array $params
* @param int|array $sort
* @param int|array $limit
* @param string $relationName
* @return SQLQuery|boolean
*/
protected function getObjectRelationQuery($obj, $params, $sort, $limit, $relationName) {
if($relationClass = $obj->many_many($relationName)) {
$query = $obj->getManyManyComponentsQuery($relationName);
} elseif($relationClass = $obj->has_many($relationName)) {
$query = $obj->getComponentsQuery($relationName);
} elseif($relationClass = $obj->has_one($relationName)) {
$query = null;
} elseif($obj->hasMethod("{$relation}Query")) {
// @todo HACK Switch to ComponentSet->getQuery() once we implement it (and lazy loading)
$query = $obj->{"{$relation}Query"}(null, $sort, null, $limit);
$relationClass = $obj->{"{$relation}Class"}();
} else {
return false;
}
// get all results
return $this->getSearchQuery($relationClass, $params, $sort, $limit);
}
protected function permissionFailure() {
// return a 401
$this->getResponse()->setStatusCode(403);

View File

@ -96,7 +96,8 @@ class XMLDataFormatter extends DataFormatter {
Controller::curr()->getResponse()->addHeader("Content-type", "text/xml");
$className = $set->class;
$xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<$className>\n";
$xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
$xml .= (is_numeric($this->totalSize)) ? "<$className totalSize=\"{$this->totalSize}\">\n" : "<$className>\n";
foreach($set as $item) {
if($item->canView()) $xml .= $this->convertDataObjectWithoutHeader($item);
}

View File

@ -105,7 +105,7 @@ require_once(MANIFEST_FILE);
if(isset($_GET['debugmanifest'])) Debug::show(file_get_contents(MANIFEST_FILE));
if(!isset(Director::$environment_type) && $envType) Director::set_environment_type($envType);
//if(!isset(Director::$environment_type) && $envType) Director::set_environment_type($envType);
// Load error handlers
Debug::loadErrorHandlers();

View File

@ -11,7 +11,12 @@
* This is loaded into the TEMP_FOLDER define on start up
*/
function getTempFolder() {
$cachefolder = "silverstripe-cache" . str_replace(array("/",":", "\\"),"-", substr($_SERVER['SCRIPT_FILENAME'], 0, strlen($_SERVER['SCRIPT_FILENAME']) - strlen('/sapphire/main.php')));
if(preg_match('/^(.*)\/sapphire\/[^\/]+$/', $_SERVER['SCRIPT_FILENAME'], $matches)) {
$cachefolder = "silverstripe-cache" . str_replace(array(' ',"/",":", "\\"),"-", $matches[1]);
} else {
$cachefolder = "silverstripe-cache";
}
$ssTmp = dirname(dirname($_SERVER['SCRIPT_FILENAME'])) . "/silverstripe-cache";
if(@file_exists($ssTmp)) {
return $ssTmp;

View File

@ -2,7 +2,7 @@
/**
* Define a constant for the name of the manifest file
*/
define("MANIFEST_FILE", TEMP_FOLDER . "/manifest" . str_replace(array(' ','\\','/',':'),"_", $_SERVER['SCRIPT_FILENAME']));
define("MANIFEST_FILE", TEMP_FOLDER . "/manifest-" . str_replace('.php','',basename($_SERVER['SCRIPT_FILENAME'])));
/**
* The ManifestBuilder class generates the manifest file and keeps it fresh.

View File

@ -80,6 +80,13 @@ class HTTPResponse extends Object {
return $this->statusCode;
}
/**
* Returns true if this HTTP response is in error
*/
function isError() {
return $this->statusCode && ($this->statusCode < 200 || $this->statusCode > 399);
}
function setBody($body) {
$this->body = $body;
}

View File

@ -75,7 +75,7 @@ class RequestHandlingData extends ViewableData {
foreach($this->stat('url_handlers') as $rule => $action) {
if(isset($_REQUEST['debug_request'])) Debug::message("Testing '$rule' with '" . $request->remaining() . "' on $this->class");
if($params = $request->match($rule, true)) {
if(isset($_REQUEST['debug_request'])) Debug::message("Rule '$rule' matched on $this->class");
if(isset($_REQUEST['debug_request'])) Debug::message("Rule '$rule' matched to action '$action' on $this->class");
// Actions can reference URL parameters, eg, '$Action/$ID/$OtherID' => '$Action',
if($action[0] == '$') $action = $params[substr($action,1)];
@ -86,6 +86,11 @@ class RequestHandlingData extends ViewableData {
return $this->httpError(403, "Action '$action' isn't allowed on class $this->class");
}
if($result instanceof HTTPResponse && $result->isError()) {
if(isset($_REQUEST['debug_request'])) Debug::message("Rule resulted in HTTP error; breaking");
return $result;
}
// If we return a RequestHandlingData, call handleRequest() on that, even if there is no more URL to parse.
// It might have its own handler. However, we only do this if we haven't just parsed an empty rule ourselves,
// to prevent infinite loops

View File

@ -2630,7 +2630,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
* User defined permissions for search result table by ModelAdmin to act on.
* Such as print search
*/
public static $results_permissions = null;
public static $results_permissions = array();
}

View File

@ -24,8 +24,7 @@ class PrimaryKey extends Int {
public function scaffoldFormField($title = null) {
$objs = DataObject::get($this->object->class);
$first = $objs->First();
$titleField = isset($first->Title) ? "Title" : "Name";
$titleField = (singleton($this->object->class)->hasField('Title')) ? "Title" : "Name";
$map = ($objs) ? $objs->toDropdownMap("ID", $titleField) : false;

View File

@ -23,6 +23,10 @@ table.CMSList td {
border-style:none;
}
.TableListField table.data td.nolabel{
display: none;
}
table.TableField th,
table.TableListField th,
.TableListField table.data th,
@ -114,7 +118,7 @@ table.CMSList tbody td.checkbox {
table.TableField tbody tr.over td,
.TableListField table.data tbody tr.over td,
table.CMSList tbody td.over td{
background-color: #FFC;
background-color: #FF6600;
}
table.TableField tbody tr.current td,

View File

@ -12,12 +12,18 @@ class TaskRunner extends Controller {
function index() {
$tasks = ClassInfo::subclassesFor('BuildTask');
if(Director::is_cli()) {
echo "Tasks available:\n\n";
foreach($tasks as $task) echo " * $task: sake dev/tasks/$task\n";
} else {
echo "<h1>Tasks available</h1>\n";
echo "<ul>";
foreach($tasks as $task) {
echo "<li><a href=\"$task\">$task</a></li>";
echo "<li><a href=\"$task\">$task</a></li>\n";
}
echo "</ul>";
}
}
function runTask($request) {
$TaskName = $request->param('TaskName');
@ -29,6 +35,8 @@ class TaskRunner extends Controller {
if (!$task->isDisabled()) $task->run($request);
} else {
echo "Build task '$TaskName' not found.";
if(class_exists($TaskName)) echo " It isn't a subclass of BuildTask.";
echo "\n";
}
}

View File

@ -96,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 class=\"checkbox\" /> <label for=\"$itemID\">$value</label></li>\n";
$options .= "<li class=\"$extraClass\"><input id=\"$itemID\" name=\"$this->name[$key]\" type=\"checkbox\" value=\"$key\"$checked $disabled class=\"checkbox\" /> <label for=\"$itemID\">$value</label></li>\n";
}

View File

@ -185,44 +185,6 @@ JS;
return $this->renderWith($this->template);
}
/**
* Returns non-paginated items.
* Please use Items() for pagination.
* This function is called whenever a complete result-set is needed,
* so even if a single record is displayed in a popup, we need the results
* to make pagination work.
*
* @todo Merge with more efficient querying of TableListField
*/
function sourceItems() {
if($this->sourceItems) {
return $this->sourceItems;
}
$limitClause = "";
if($this->pageSize) {
$limitClause = "{$this->pageSize}";
} else {
$limitClause = "0";
}
if(isset($_REQUEST['ctf'][$this->Name()]['start']) && is_numeric($_REQUEST['ctf'][$this->Name()]['start'])) {
$SQL_start = intval($_REQUEST['ctf'][$this->Name()]['start']);
$limitClause .= " OFFSET {$SQL_start}";
}
$sort = $this->sourceSort;
if(isset($_REQUEST['ctf'][$this->Name()]['sort'])) {
$sort = Convert::raw2sql($_REQUEST['ctf'][$this->Name()]['sort']);
}
$this->sourceItems = DataObject::get($this->sourceClass, $this->sourceFilter, $sort, $this->sourceJoin, $limitClause);
$this->unpagedSourceItems = DataObject::get($this->sourceClass, $this->sourceFilter, $sort, $this->sourceJoin);
$this->totalCount = ($this->unpagedSourceItems) ? $this->unpagedSourceItems->TotalItems() : null;
return $this->sourceItems;
}
function sourceClass() {
return $this->sourceClass;
}

View File

@ -1045,6 +1045,8 @@ class TableListField_Item extends ViewableData {
function Fields() {
$list = $this->parent->FieldList();
foreach($list as $fieldName => $fieldTitle) {
$value = "";
// This supports simple FieldName syntax
if(strpos($fieldName,'.') === false) {
$value = ($this->item->val($fieldName)) ? $this->item->val($fieldName) : $this->item->$fieldName;

View File

@ -272,5 +272,5 @@ TableListRecord.prototype = {
}
}
//TableListRecord.applyTo('div.TableListField tr');
TableListRecord.applyTo('div.TableListField tr');
TableListField.applyTo('div.TableListField');

View File

@ -114,18 +114,14 @@ 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) {
/*We add $value!='' here to not include a filter like this: $fieldname like '%%', which abviously filter out some
records with the $fieldname set to null. and this is not the search intention.
*/
if ($value != '0'&&$value!='') {
$key = str_replace('__', '.', $key);
$filter = $this->getFilter($key);
if ($filter) {
if($filter = $this->getFilter($key)) {
$filter->setModel($this->modelClass);
$filter->setValue($value);
if(! $filter->isEmpty()) {
$filter->apply($query);
}
//}
}
}
return $query;

View File

@ -28,5 +28,8 @@ class EndsWithFilter extends SearchFilter {
$query->where($this->getDbName(), "RLIKE", "{$this->getValue()}$");
}
public function isEmpty() {
return $this->getValue() == null || $this->getValue() == '';
}
}
?>

View File

@ -21,11 +21,12 @@ class ExactMatchFilter extends SearchFilter {
* @return unknown
*/
public function apply(SQLQuery $query) {
if($this->getValue()) {
$query = $this->applyRelation($query);
return $query->where("{$this->getDbName()} = '{$this->getValue()}'");
}
}
public function isEmpty() {
return $this->getValue() == null || $this->getValue() == '';
}
}
?>

View File

@ -13,11 +13,9 @@
class ExactMatchMultiFilter extends SearchFilter {
public function apply(SQLQuery $query) {
if($this->getValue()) {
$query = $this->applyRelation($query);
$values = explode(',',$this->getValue());
if(! $values) return false;
for($i = 0; $i < count($values); $i++) {
if(! is_numeric($values[$i])) {
// @todo Fix string replacement to only replace leading and tailing quotes
@ -26,9 +24,12 @@ class ExactMatchMultiFilter extends SearchFilter {
}
}
$SQL_valueStr = "'" . implode("','", $values) . "'";
return $query->where("{$this->getDbName()} IN ({$SQL_valueStr})");
}
}
public function isEmpty() {
return $this->getValue() == null || $this->getValue() == '';
}
}
?>

View File

@ -32,5 +32,8 @@ class FulltextFilter extends SearchFilter {
return $query;
}
public function isEmpty() {
return $this->getValue() == null || $this->getValue() == '';
}
}
?>

View File

@ -0,0 +1,24 @@
<?php
/**
* Selects numerical/date content greater than the input
*
* @todo documentation
*
* @package sapphire
* @subpackage search
*/
class GreaterThanFilter extends SearchFilter {
/**
* @return $query
*/
public function apply(SQLQuery $query) {
$query = $this->applyRelation($query);
return $query->where("{$this->getDbName()} > '{$this->getValue()}'");
}
public function isEmpty() {
return $this->getValue() == null || $this->getValue() == '';
}
}
?>

View File

@ -0,0 +1,24 @@
<?php
/**
* Selects numerical/date content smaller than the input
*
* @todo documentation
*
* @package sapphire
* @subpackage search
*/
class LessThanFilter extends SearchFilter {
/**
* @return $query
*/
public function apply(SQLQuery $query) {
$query = $this->applyRelation($query);
return $query->where("{$this->getDbName()} < '{$this->getValue()}'");
}
public function isEmpty() {
return $this->getValue() == null || $this->getValue() == '';
}
}
?>

View File

@ -17,5 +17,8 @@ class PartialMatchFilter extends SearchFilter {
return $query->where("{$this->getDbName()} LIKE '%{$this->getValue()}%'");
}
public function isEmpty() {
return $this->getValue() == null || $this->getValue() == '';
}
}
?>

View File

@ -139,5 +139,19 @@ abstract class SearchFilter extends Object {
*/
abstract public function apply(SQLQuery $query);
/**
* Determines if a field has a value,
* and that the filter should be applied.
* Relies on the field being populated with
* {@link setValue()}
*
* @usedby SearchContext
*
* @return boolean
*/
public function isEmpty() {
return false;
}
}
?>

View File

@ -0,0 +1,23 @@
<?php
/**
* Selects numerical/date content smaller than the input
*
* @todo documentation
*
* @package sapphire
* @subpackage search
*/
class SmallerThanFilter extends SearchFilter {
/**
* @return $query
*/
public function apply(SQLQuery $query) {
if($this->getValue()) {
$query = $this->applyRelation($query);
return $query->where("{$this->getDbName()} < '{$this->getValue()}'");
}
}
}
?>

View File

@ -28,5 +28,8 @@ class StartsWithFilter extends SearchFilter {
$query->where("LOCATE('{$this->getValue()}', {$this->getDbName()}) = 1");
}
public function isEmpty() {
return $this->getValue() == null || $this->getValue() == '';
}
}
?>

View File

@ -13,7 +13,6 @@
class StartsWithMultiFilter extends SearchFilter {
public function apply(SQLQuery $query) {
if($this->getValue()) {
$query = $this->applyRelation($query);
$values = explode(',', $this->getValue());
@ -21,9 +20,12 @@ class StartsWithMultiFilter extends SearchFilter {
$SQL_value = Convert::raw2sql(str_replace("'", '', $value));
$matches[] = "{$this->getDbName()} LIKE '$SQL_value%'";
}
return $query->where(implode(" OR ", $matches));
}
}
public function isEmpty() {
return $this->getValue() == null || $this->getValue() == '';
}
}
?>

View File

@ -16,6 +16,9 @@ class SubstringFilter extends SearchFilter {
return $query->where("LOCATE('{$this->getValue()}', {$this->getDbName()}) != 0");
}
public function isEmpty() {
return $this->getValue() == null || $this->getValue() == '';
}
}
?>