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@60209 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
parent
a599df309c
commit
016cff2093
@ -32,10 +32,7 @@ Director::addRules(10, array(
|
|||||||
));
|
));
|
||||||
|
|
||||||
Director::addRules(1, array(
|
Director::addRules(1, array(
|
||||||
'$URLSegment/$Action/$ID/$OtherID' => array(
|
'$URLSegment//$Action/$ID/$OtherID' => 'ModelAsController',
|
||||||
'_PopTokeniser' => 1,
|
|
||||||
'Controller' => 'ModelAsController',
|
|
||||||
),
|
|
||||||
));
|
));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
42
api/DataFormatter.php
Normal file
42
api/DataFormatter.php
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A DataFormatter object handles transformation of data from Sapphire model objects to a particular output format, and vice versa.
|
||||||
|
* This is most commonly used in developing RESTful APIs.
|
||||||
|
*/
|
||||||
|
|
||||||
|
abstract class DataFormatter extends Object {
|
||||||
|
/**
|
||||||
|
* Get a DataFormatter object suitable for handling the given file extension
|
||||||
|
*/
|
||||||
|
static function for_extension($extension) {
|
||||||
|
$classes = ClassInfo::subclassesFor("DataFormatter");
|
||||||
|
array_shift($classes);
|
||||||
|
|
||||||
|
foreach($classes as $class) {
|
||||||
|
$formatter = singleton($class);
|
||||||
|
if(in_array($extension, $formatter->supportedExtensions())) {
|
||||||
|
return $formatter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an array of the extensions that this data formatter supports
|
||||||
|
*/
|
||||||
|
abstract function supportedExtensions();
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a single data object to this format. Return a string.
|
||||||
|
* @todo Add parameters for things like selecting output columns
|
||||||
|
*/
|
||||||
|
abstract function convertDataObject(DataObjectInterface $do);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a data object set to this format. Return a string.
|
||||||
|
* @todo Add parameters for things like selecting output columns
|
||||||
|
*/
|
||||||
|
abstract function convertDataObjectSet(DataObjectSet $set);
|
||||||
|
|
||||||
|
}
|
81
api/JSONDataFormatter.php
Normal file
81
api/JSONDataFormatter.php
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class JSONDataFormatter extends DataFormatter {
|
||||||
|
/**
|
||||||
|
* @todo pass this from the API to the data formatter somehow
|
||||||
|
*/
|
||||||
|
static $api_base = "api/v1/";
|
||||||
|
|
||||||
|
public function supportedExtensions() {
|
||||||
|
return array('json', 'js');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate an XML representation of the given {@link DataObject}.
|
||||||
|
*
|
||||||
|
* @param DataObject $obj
|
||||||
|
* @param $includeHeader Include <?xml ...?> header (Default: true)
|
||||||
|
* @return String XML
|
||||||
|
*/
|
||||||
|
public function convertDataObject(DataObjectInterface $obj) {
|
||||||
|
$className = $obj->class;
|
||||||
|
$id = $obj->ID;
|
||||||
|
|
||||||
|
$json = "{\n className : \"$className\",\n";
|
||||||
|
$dbFields = array_merge($obj->databaseFields(), array('ID'=>'Int'));
|
||||||
|
foreach($dbFields as $fieldName => $fieldType) {
|
||||||
|
if(is_object($obj->$fieldName)) {
|
||||||
|
$jsonParts[] = "$fieldName : " . $obj->$fieldName->toJSON();
|
||||||
|
} else {
|
||||||
|
$jsonParts[] = "$fieldName : \"" . Convert::raw2js($obj->$fieldName) . "\"";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($obj->has_one() as $relName => $relClass) {
|
||||||
|
$fieldName = $relName . 'ID';
|
||||||
|
if($obj->$fieldName) {
|
||||||
|
$href = Director::absoluteURL(self::$api_base . "$relClass/" . $obj->$fieldName);
|
||||||
|
} else {
|
||||||
|
$href = Director::absoluteURL(self::$api_base . "$className/$id/$relName");
|
||||||
|
}
|
||||||
|
$jsonParts[] = "$relName : { className : \"$relClass\", href : \"$href.json\", id : \"{$obj->$fieldName}\" }";
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($obj->has_many() as $relName => $relClass) {
|
||||||
|
$jsonInnerParts = array();
|
||||||
|
$items = $obj->$relName();
|
||||||
|
foreach($items as $item) {
|
||||||
|
//$href = Director::absoluteURL(self::$api_base . "$className/$id/$relName/$item->ID");
|
||||||
|
$href = Director::absoluteURL(self::$api_base . "$relClass/$item->ID");
|
||||||
|
$jsonInnerParts[] = "{ className : \"$relClass\", href : \"$href.json\", id : \"{$obj->$fieldName}\" }";
|
||||||
|
}
|
||||||
|
$jsonParts[] = "$relName : [\n " . implode(",\n ", $jsonInnerParts) . " \n ]";
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($obj->many_many() as $relName => $relClass) {
|
||||||
|
$jsonInnerParts = array();
|
||||||
|
$items = $obj->$relName();
|
||||||
|
foreach($items as $item) {
|
||||||
|
//$href = Director::absoluteURL(self::$api_base . "$className/$id/$relName/$item->ID");
|
||||||
|
$href = Director::absoluteURL(self::$api_base . "$relClass/$item->ID");
|
||||||
|
$jsonInnerParts[] = " { className : \"$relClass\", href : \"$href.json\", id : \"{$obj->$fieldName}\" }";
|
||||||
|
}
|
||||||
|
$jsonParts[] = "$relName : [\n " . implode(",\n ", $jsonInnerParts) . "\n ]";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "{\n " . implode(",\n ", $jsonParts) . "\n}"; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate an XML representation of the given {@link DataObjectSet}.
|
||||||
|
*
|
||||||
|
* @param DataObjectSet $set
|
||||||
|
* @return String XML
|
||||||
|
*/
|
||||||
|
public function convertDataObjectSet(DataObjectSet $set) {
|
||||||
|
$jsonParts = array();
|
||||||
|
foreach($set as $item) {
|
||||||
|
if($item->canView()) $jsonParts[] = $this->convertDataObject($item);
|
||||||
|
}
|
||||||
|
return "[\n" . implode(",\n", $jsonParts) . "\n]";
|
||||||
|
}
|
||||||
|
}
|
@ -72,15 +72,18 @@ class RestfulServer extends Controller {
|
|||||||
);
|
);
|
||||||
$contentType = isset($contentMap[$extension]) ? $contentMap[$extension] : 'text/xml';
|
$contentType = isset($contentMap[$extension]) ? $contentMap[$extension] : 'text/xml';
|
||||||
|
|
||||||
|
if(!$extension) $extension = "xml";
|
||||||
|
$formatter = DataFormatter::for_extension($extension); //$this->dataFormatterFromMime($contentType);
|
||||||
|
|
||||||
switch($requestMethod) {
|
switch($requestMethod) {
|
||||||
case 'GET':
|
case 'GET':
|
||||||
return $this->getHandler($className, $id, $relation, $contentType);
|
return $this->getHandler($className, $id, $relation, $formatter);
|
||||||
|
|
||||||
case 'PUT':
|
case 'PUT':
|
||||||
return $this->putHandler($className, $id, $relation, $contentType);
|
return $this->putHandler($className, $id, $relation, $formatter);
|
||||||
|
|
||||||
case 'DELETE':
|
case 'DELETE':
|
||||||
return $this->deleteHandler($className, $id, $relation, $contentType);
|
return $this->deleteHandler($className, $id, $relation, $formatter);
|
||||||
|
|
||||||
case 'POST':
|
case 'POST':
|
||||||
}
|
}
|
||||||
@ -118,7 +121,7 @@ class RestfulServer extends Controller {
|
|||||||
* @param String $contentType
|
* @param String $contentType
|
||||||
* @return String The serialized representation of the requested object(s) - usually XML or JSON.
|
* @return String The serialized representation of the requested object(s) - usually XML or JSON.
|
||||||
*/
|
*/
|
||||||
protected function getHandler($className, $id, $relation, $contentType) {
|
protected function getHandler($className, $id, $relation, $formatter) {
|
||||||
if($id) {
|
if($id) {
|
||||||
$obj = DataObject::get_by_id($className, $id);
|
$obj = DataObject::get_by_id($className, $id);
|
||||||
if(!$obj) {
|
if(!$obj) {
|
||||||
@ -143,175 +146,9 @@ class RestfulServer extends Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TO DO - inspect that Accept header as well. $_GET['accept'] can still be checked, as it's handy for debugging
|
if($obj instanceof DataObjectSet) return $formatter->convertDataObjectSet($obj);
|
||||||
switch($contentType) {
|
else return $formatter->convertDataObject($obj);
|
||||||
case "text/xml":
|
|
||||||
$this->getResponse()->addHeader("Content-type", "text/xml");
|
|
||||||
if($obj instanceof DataObjectSet) return $this->dataObjectSetAsXML($obj);
|
|
||||||
else return $this->dataObjectAsXML($obj);
|
|
||||||
|
|
||||||
case "text/json":
|
|
||||||
//$this->getResponse()->addHeader("Content-type", "text/json");
|
|
||||||
if($obj instanceof DataObjectSet) return $this->dataObjectSetAsJSON($obj);
|
|
||||||
else return $this->dataObjectAsJSON($obj);
|
|
||||||
|
|
||||||
case "text/html":
|
|
||||||
case "application/xhtml+xml":
|
|
||||||
if($obj instanceof DataObjectSet) return $this->dataObjectSetAsXHTML($obj);
|
|
||||||
else return $this->dataObjectAsXHTML($obj);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate an XML representation of the given {@link DataObject}.
|
|
||||||
*
|
|
||||||
* @param DataObject $obj
|
|
||||||
* @param $includeHeader Include <?xml ...?> header (Default: true)
|
|
||||||
* @return String XML
|
|
||||||
*/
|
|
||||||
public function dataObjectAsXML(DataObject $obj, $includeHeader = true) {
|
|
||||||
$className = $obj->class;
|
|
||||||
$id = $obj->ID;
|
|
||||||
$objHref = Director::absoluteURL(self::$api_base . "$obj->class/$obj->ID");
|
|
||||||
|
|
||||||
$json = "";
|
|
||||||
if($includeHeader) $json .= "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
|
|
||||||
$json .= "<$className href=\"$objHref.xml\">\n";
|
|
||||||
$dbFields = array_merge($obj->databaseFields(), array('ID'=>'Int'));
|
|
||||||
foreach($dbFields as $fieldName => $fieldType) {
|
|
||||||
if(is_object($obj->$fieldName)) {
|
|
||||||
$json .= $obj->$fieldName->toXML();
|
|
||||||
} else {
|
|
||||||
$json .= "<$fieldName>" . Convert::raw2xml($obj->$fieldName) . "</$fieldName>\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
foreach($obj->has_one() as $relName => $relClass) {
|
|
||||||
$fieldName = $relName . 'ID';
|
|
||||||
if($obj->$fieldName) {
|
|
||||||
$href = Director::absoluteURL(self::$api_base . "$relClass/" . $obj->$fieldName);
|
|
||||||
} else {
|
|
||||||
$href = Director::absoluteURL(self::$api_base . "$className/$id/$relName");
|
|
||||||
}
|
|
||||||
$json .= "<$relName linktype=\"has_one\" href=\"$href.xml\" id=\"{$obj->$fieldName}\" />\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach($obj->has_many() as $relName => $relClass) {
|
|
||||||
$json .= "<$relName linktype=\"has_many\" href=\"$objHref/$relName.xml\">\n";
|
|
||||||
$items = $obj->$relName();
|
|
||||||
foreach($items as $item) {
|
|
||||||
//$href = Director::absoluteURL(self::$api_base . "$className/$id/$relName/$item->ID");
|
|
||||||
$href = Director::absoluteURL(self::$api_base . "$relClass/$item->ID");
|
|
||||||
$json .= "<$relClass href=\"$href.xml\" id=\"{$item->ID}\" />\n";
|
|
||||||
}
|
|
||||||
$json .= "</$relName>\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach($obj->many_many() as $relName => $relClass) {
|
|
||||||
$json .= "<$relName linktype=\"many_many\" href=\"$objHref/$relName.xml\">\n";
|
|
||||||
$items = $obj->$relName();
|
|
||||||
foreach($items as $item) {
|
|
||||||
$href = Director::absoluteURL(self::$api_base . "$relClass/$item->ID");
|
|
||||||
$json .= "<$relClass href=\"$href.xml\" id=\"{$item->ID}\" />\n";
|
|
||||||
}
|
|
||||||
$json .= "</$relName>\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
$json .= "</$className>";
|
|
||||||
|
|
||||||
return $json;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate an XML representation of the given {@link DataObjectSet}.
|
|
||||||
*
|
|
||||||
* @param DataObjectSet $set
|
|
||||||
* @return String XML
|
|
||||||
*/
|
|
||||||
public function dataObjectSetAsXML(DataObjectSet $set) {
|
|
||||||
$className = $set->class;
|
|
||||||
|
|
||||||
$xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<$className>\n";
|
|
||||||
foreach($set as $item) {
|
|
||||||
if($item->canView()) $xml .= $this->dataObjectAsXML($item, false);
|
|
||||||
}
|
|
||||||
$xml .= "</$className>";
|
|
||||||
|
|
||||||
return $xml;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate an JSON representation of the given {@link DataObject}.
|
|
||||||
*
|
|
||||||
* @see http://json.org
|
|
||||||
*
|
|
||||||
* @param DataObject $obj
|
|
||||||
* @return String JSON
|
|
||||||
*/
|
|
||||||
public function dataObjectAsJSON(DataObject $obj) {
|
|
||||||
$className = $obj->class;
|
|
||||||
$id = $obj->ID;
|
|
||||||
|
|
||||||
$json = "{\n className : \"$className\",\n";
|
|
||||||
$dbFields = array_merge($obj->databaseFields(), array('ID'=>'Int'));
|
|
||||||
foreach($dbFields as $fieldName => $fieldType) {
|
|
||||||
if(is_object($obj->$fieldName)) {
|
|
||||||
$jsonParts[] = "$fieldName : " . $obj->$fieldName->toJSON();
|
|
||||||
} else {
|
|
||||||
$jsonParts[] = "$fieldName : \"" . Convert::raw2js($obj->$fieldName) . "\"";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach($obj->has_one() as $relName => $relClass) {
|
|
||||||
$fieldName = $relName . 'ID';
|
|
||||||
if($obj->$fieldName) {
|
|
||||||
$href = Director::absoluteURL(self::$api_base . "$relClass/" . $obj->$fieldName);
|
|
||||||
} else {
|
|
||||||
$href = Director::absoluteURL(self::$api_base . "$className/$id/$relName");
|
|
||||||
}
|
|
||||||
$jsonParts[] = "$relName : { className : \"$relClass\", href : \"$href.json\", id : \"{$obj->$fieldName}\" }";
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach($obj->has_many() as $relName => $relClass) {
|
|
||||||
$jsonInnerParts = array();
|
|
||||||
$items = $obj->$relName();
|
|
||||||
foreach($items as $item) {
|
|
||||||
//$href = Director::absoluteURL(self::$api_base . "$className/$id/$relName/$item->ID");
|
|
||||||
$href = Director::absoluteURL(self::$api_base . "$relClass/$item->ID");
|
|
||||||
$jsonInnerParts[] = "{ className : \"$relClass\", href : \"$href.json\", id : \"{$obj->$fieldName}\" }";
|
|
||||||
}
|
|
||||||
$jsonParts[] = "$relName : [\n " . implode(",\n ", $jsonInnerParts) . " \n ]";
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach($obj->many_many() as $relName => $relClass) {
|
|
||||||
$jsonInnerParts = array();
|
|
||||||
$items = $obj->$relName();
|
|
||||||
foreach($items as $item) {
|
|
||||||
//$href = Director::absoluteURL(self::$api_base . "$className/$id/$relName/$item->ID");
|
|
||||||
$href = Director::absoluteURL(self::$api_base . "$relClass/$item->ID");
|
|
||||||
$jsonInnerParts[] = " { className : \"$relClass\", href : \"$href.json\", id : \"{$obj->$fieldName}\" }";
|
|
||||||
}
|
|
||||||
$jsonParts[] = "$relName : [\n " . implode(",\n ", $jsonInnerParts) . "\n ]";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "{\n " . implode(",\n ", $jsonParts) . "\n}";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate an JSON representation of the given {@link DataObjectSet}.
|
|
||||||
*
|
|
||||||
* @param DataObjectSet $set
|
|
||||||
* @return String JSON
|
|
||||||
*/
|
|
||||||
public function dataObjectSetAsJSON(DataObjectSet $set) {
|
|
||||||
$jsonParts = array();
|
|
||||||
foreach($set as $item) {
|
|
||||||
if($item->canView()) $jsonParts[] = $this->dataObjectAsJSON($item);
|
|
||||||
}
|
|
||||||
return "[\n" . implode(",\n", $jsonParts) . "\n]";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler for object delete
|
* Handler for object delete
|
||||||
@ -324,7 +161,6 @@ class RestfulServer extends Controller {
|
|||||||
} else {
|
} else {
|
||||||
return $this->permissionFailure();
|
return $this->permissionFailure();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
96
api/XMLDataFormatter.php
Normal file
96
api/XMLDataFormatter.php
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class XMLDataFormatter extends DataFormatter {
|
||||||
|
/**
|
||||||
|
* @todo pass this from the API to the data formatter somehow
|
||||||
|
*/
|
||||||
|
static $api_base = "api/v1/";
|
||||||
|
|
||||||
|
public function supportedExtensions() {
|
||||||
|
return array('xml');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate an XML representation of the given {@link DataObject}.
|
||||||
|
*
|
||||||
|
* @param DataObject $obj
|
||||||
|
* @param $includeHeader Include <?xml ...?> header (Default: true)
|
||||||
|
* @return String XML
|
||||||
|
*/
|
||||||
|
public function convertDataObject(DataObjectInterface $obj) {
|
||||||
|
Controller::curr()->getResponse()->addHeader("Content-type", "text/xml");
|
||||||
|
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" . $this->convertDataObjectWithoutHeader($obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function convertDataObjectWithoutHeader(DataObject $obj) {
|
||||||
|
$className = $obj->class;
|
||||||
|
$id = $obj->ID;
|
||||||
|
$objHref = Director::absoluteURL(self::$api_base . "$obj->class/$obj->ID");
|
||||||
|
|
||||||
|
$json = "<$className href=\"$objHref.xml\">\n";
|
||||||
|
$dbFields = array_merge($obj->databaseFields(), array('ID'=>'Int'));
|
||||||
|
foreach($dbFields as $fieldName => $fieldType) {
|
||||||
|
if(is_object($obj->$fieldName)) {
|
||||||
|
$json .= $obj->$fieldName->toXML();
|
||||||
|
} else {
|
||||||
|
$json .= "<$fieldName>" . Convert::raw2xml($obj->$fieldName) . "</$fieldName>\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
foreach($obj->has_one() as $relName => $relClass) {
|
||||||
|
$fieldName = $relName . 'ID';
|
||||||
|
if($obj->$fieldName) {
|
||||||
|
$href = Director::absoluteURL(self::$api_base . "$relClass/" . $obj->$fieldName);
|
||||||
|
} else {
|
||||||
|
$href = Director::absoluteURL(self::$api_base . "$className/$id/$relName");
|
||||||
|
}
|
||||||
|
$json .= "<$relName linktype=\"has_one\" href=\"$href.xml\" id=\"{$obj->$fieldName}\" />\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($obj->has_many() as $relName => $relClass) {
|
||||||
|
$json .= "<$relName linktype=\"has_many\" href=\"$objHref/$relName.xml\">\n";
|
||||||
|
$items = $obj->$relName();
|
||||||
|
foreach($items as $item) {
|
||||||
|
//$href = Director::absoluteURL(self::$api_base . "$className/$id/$relName/$item->ID");
|
||||||
|
$href = Director::absoluteURL(self::$api_base . "$relClass/$item->ID");
|
||||||
|
$json .= "<$relClass href=\"$href.xml\" id=\"{$item->ID}\" />\n";
|
||||||
|
}
|
||||||
|
$json .= "</$relName>\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($obj->many_many() as $relName => $relClass) {
|
||||||
|
$json .= "<$relName linktype=\"many_many\" href=\"$objHref/$relName.xml\">\n";
|
||||||
|
$items = $obj->$relName();
|
||||||
|
foreach($items as $item) {
|
||||||
|
$href = Director::absoluteURL(self::$api_base . "$relClass/$item->ID");
|
||||||
|
$json .= "<$relClass href=\"$href.xml\" id=\"{$item->ID}\" />\n";
|
||||||
|
}
|
||||||
|
$json .= "</$relName>\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$json .= "</$className>";
|
||||||
|
|
||||||
|
return $json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate an XML representation of the given {@link DataObjectSet}.
|
||||||
|
*
|
||||||
|
* @param DataObjectSet $set
|
||||||
|
* @return String XML
|
||||||
|
*/
|
||||||
|
public function convertDataObjectSet(DataObjectSet $set) {
|
||||||
|
Controller::curr()->getResponse()->addHeader("Content-type", "text/xml");
|
||||||
|
$className = $set->class;
|
||||||
|
|
||||||
|
$xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<$className>\n";
|
||||||
|
foreach($set as $item) {
|
||||||
|
if($item->canView()) $xml .= $this->convertDataObjectWithoutHeader($item);
|
||||||
|
}
|
||||||
|
$xml .= "</$className>";
|
||||||
|
|
||||||
|
return $xml;
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,10 @@
|
|||||||
#!/usr/bin/php5
|
#!/usr/bin/php5
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
if(isset($_SERVER['HTTP_HOST'])) {
|
||||||
|
echo "cli-script.php can't be run from a web request, you have to run it on the command-line.";
|
||||||
|
die();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* File similar to main.php designed for command-line scripts
|
* File similar to main.php designed for command-line scripts
|
||||||
@ -98,7 +102,7 @@ require_once(MANIFEST_FILE);
|
|||||||
|
|
||||||
if(isset($_GET['debugmanifest'])) Debug::show(file_get_contents(MANIFEST_FILE));
|
if(isset($_GET['debugmanifest'])) Debug::show(file_get_contents(MANIFEST_FILE));
|
||||||
|
|
||||||
if(!isset(Director::$environment_type)) Director::set_environment_type($envType);
|
if(!isset(Director::$environment_type) && $envType) Director::set_environment_type($envType);
|
||||||
|
|
||||||
// Load error handlers
|
// Load error handlers
|
||||||
Debug::loadErrorHandlers();
|
Debug::loadErrorHandlers();
|
||||||
|
@ -62,6 +62,13 @@ class ArrayData extends ViewableData {
|
|||||||
return $arr;
|
return $arr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is pretty crude, but it helps diagnose error situations
|
||||||
|
*/
|
||||||
|
function forTemplate() {
|
||||||
|
return var_export($this->array, true);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
?>
|
?>
|
@ -109,6 +109,7 @@ class SSViewer extends Object {
|
|||||||
*/
|
*/
|
||||||
public function dontRewriteHashlinks() {
|
public function dontRewriteHashlinks() {
|
||||||
$this->rewriteHashlinks = false;
|
$this->rewriteHashlinks = false;
|
||||||
|
self::$options['rewriteHashlinks'] = false;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -577,7 +577,8 @@ class Director {
|
|||||||
*/
|
*/
|
||||||
static function set_environment_type($et) {
|
static function set_environment_type($et) {
|
||||||
if($et != 'dev' && $et != 'test' && $et != 'live') {
|
if($et != 'dev' && $et != 'test' && $et != 'live') {
|
||||||
user_error("Director::set_environment_type passed '$et'. It should be passed dev, test, or live");
|
Debug::backtrace();
|
||||||
|
user_error("Director::set_environment_type passed '$et'. It should be passed dev, test, or live", E_USER_WARNING);
|
||||||
} else {
|
} else {
|
||||||
self::$environment_type = $et;
|
self::$environment_type = $et;
|
||||||
}
|
}
|
||||||
|
@ -1206,7 +1206,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
|||||||
$model = singleton($component);
|
$model = singleton($component);
|
||||||
$records = DataObject::get($component);
|
$records = DataObject::get($component);
|
||||||
$collect = ($model->hasMethod('customSelectOption')) ? 'customSelectOption' : current($model->summary_fields());
|
$collect = ($model->hasMethod('customSelectOption')) ? 'customSelectOption' : current($model->summary_fields());
|
||||||
$options = $records->filter_map('ID', $collect);
|
$options = $records ? $records->filter_map('ID', $collect) : array();
|
||||||
$fields->push(new DropdownField($relationship.'ID', $relationship, $options));
|
$fields->push(new DropdownField($relationship.'ID', $relationship, $options));
|
||||||
}
|
}
|
||||||
return $fields;
|
return $fields;
|
||||||
@ -1216,9 +1216,21 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
|||||||
* Add the scaffold-generated relation fields to the given field set
|
* Add the scaffold-generated relation fields to the given field set
|
||||||
*/
|
*/
|
||||||
protected function addScaffoldRelationFields($fieldSet) {
|
protected function addScaffoldRelationFields($fieldSet) {
|
||||||
|
|
||||||
|
if($this->has_many()) {
|
||||||
|
// Refactor the fields that we have been given into a tab, "Main", in a tabset
|
||||||
|
$oldFields = $fieldSet;
|
||||||
|
$fieldSet = new FieldSet(
|
||||||
|
new TabSet("Root", new Tab("Main"))
|
||||||
|
);
|
||||||
|
foreach($oldFields as $field) $fieldSet->addFieldToTab("Root.Main", $field);
|
||||||
|
|
||||||
|
// Add each relation as a separate tab
|
||||||
foreach($this->has_many() as $relationship => $component) {
|
foreach($this->has_many() as $relationship => $component) {
|
||||||
$relationshipFields = array_keys($this->searchable_fields());
|
$relationshipFields = singleton($component)->summary_fields();
|
||||||
$fieldSet->push(new ComplexTableField($this, $relationship, $component, $relationshipFields));
|
$foreignKey = $this->getComponentJoinField($relationship);
|
||||||
|
$fieldSet->addFieldToTab("Root.$relationship", new ComplexTableField($this, $relationship, $component, $relationshipFields, "getCMSFields", "$foreignKey = $this->ID"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return $fieldSet;
|
return $fieldSet;
|
||||||
}
|
}
|
||||||
@ -1249,7 +1261,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
|||||||
$fields = $this->scaffoldFormFields();
|
$fields = $this->scaffoldFormFields();
|
||||||
// If we don't have an ID, then relation fields don't work
|
// If we don't have an ID, then relation fields don't work
|
||||||
if($this->ID) {
|
if($this->ID) {
|
||||||
$this->addScaffoldRelationFields($fields);
|
$fields = $this->addScaffoldRelationFields($fields);
|
||||||
}
|
}
|
||||||
return $fields;
|
return $fields;
|
||||||
}
|
}
|
||||||
@ -2085,7 +2097,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
|||||||
public function searchable_fields() {
|
public function searchable_fields() {
|
||||||
$fields = $this->stat('searchable_fields');
|
$fields = $this->stat('searchable_fields');
|
||||||
if (!$fields) {
|
if (!$fields) {
|
||||||
$fields = array_fill_keys($this->summary_fields(), 'TextField');
|
$fields = array_fill_keys(array_keys($this->summary_fields()), 'TextField');
|
||||||
}
|
}
|
||||||
return $fields;
|
return $fields;
|
||||||
}
|
}
|
||||||
@ -2101,11 +2113,19 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
|||||||
$fields = $this->stat('summary_fields');
|
$fields = $this->stat('summary_fields');
|
||||||
if (!$fields) {
|
if (!$fields) {
|
||||||
$fields = array();
|
$fields = array();
|
||||||
if ($this->hasField('Name')) $fields[] = 'Name';
|
if ($this->hasField('Name')) $fields['Name'] = 'Name';
|
||||||
if ($this->hasField('Title')) $fields[] = 'Title';
|
if ($this->hasField('Title')) $fields['Title'] = 'Title';
|
||||||
if ($this->hasField('Description')) $fields[] = 'Description';
|
if ($this->hasField('Description')) $fields['Description'] = 'Description';
|
||||||
if ($this->hasField('Firstname')) $fields[] = 'Firstname';
|
if ($this->hasField('Firstname')) $fields['Firstname'] = 'Firstname';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Final fail-over, just list all the fields :-S
|
||||||
|
if(!$fields) {
|
||||||
|
foreach(array_keys($this->db()) as $field) {
|
||||||
|
$fields[$field] = $field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $fields;
|
return $fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2128,7 +2148,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
|||||||
} else {
|
} else {
|
||||||
if (is_array($type)) {
|
if (is_array($type)) {
|
||||||
$filter = current($type);
|
$filter = current($type);
|
||||||
$filters[$name] = new $filter();
|
$filters[$name] = new $filter($name);
|
||||||
} else {
|
} else {
|
||||||
if (is_subclass_of($type, 'SearchFilter')) {
|
if (is_subclass_of($type, 'SearchFilter')) {
|
||||||
$filters[$name] = new $type($name);
|
$filters[$name] = new $type($name);
|
||||||
|
@ -75,7 +75,7 @@ class DatabaseAdmin extends Controller {
|
|||||||
* Updates the database schema, creating tables & fields as necessary.
|
* Updates the database schema, creating tables & fields as necessary.
|
||||||
*/
|
*/
|
||||||
function build() {
|
function build() {
|
||||||
if(Director::isLive() && Security::database_is_ready() && (!Member::currentUser() || !Member::currentUser()->isAdmin())) {
|
if(Director::isLive() && Security::database_is_ready() && !Director::is_cli() && (!Member::currentUser() || !Member::currentUser()->isAdmin())) {
|
||||||
Security::permissionFailure($this,
|
Security::permissionFailure($this,
|
||||||
"This page is secured and you need administrator rights to access it. " .
|
"This page is secured and you need administrator rights to access it. " .
|
||||||
"Enter your credentials below and we will send you right along.");
|
"Enter your credentials below and we will send you right along.");
|
||||||
|
@ -68,6 +68,7 @@ class SQLQuery extends Object {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a new SQLQuery.
|
* Construct a new SQLQuery.
|
||||||
|
*
|
||||||
* @param array $select An array of fields to select.
|
* @param array $select An array of fields to select.
|
||||||
* @param array $from An array of join clauses. The first one should be just the table name.
|
* @param array $from An array of join clauses. The first one should be just the table name.
|
||||||
* @param array $where An array of filters, to be inserted into the WHERE clause.
|
* @param array $where An array of filters, to be inserted into the WHERE clause.
|
||||||
@ -76,7 +77,7 @@ class SQLQuery extends Object {
|
|||||||
* @param array $having An array of having clauses.
|
* @param array $having An array of having clauses.
|
||||||
* @param string $limit A LIMIT clause.
|
* @param string $limit A LIMIT clause.
|
||||||
*/
|
*/
|
||||||
function __construct($select = array(), $from = array(), $where = "", $orderby = "", $groupby = "", $having = "", $limit = "") {
|
function __construct($select = "*", $from = array(), $where = "", $orderby = "", $groupby = "", $having = "", $limit = "") {
|
||||||
if($select) $this->select = is_array($select) ? $select : array($select);
|
if($select) $this->select = is_array($select) ? $select : array($select);
|
||||||
if($from) $this->from = is_array($from) ? $from : array(str_replace('`','',$from) => $from);
|
if($from) $this->from = is_array($from) ? $from : array(str_replace('`','',$from) => $from);
|
||||||
if($where) $this->where = is_array($where) ? $where : array($where);
|
if($where) $this->where = is_array($where) ? $where : array($where);
|
||||||
@ -88,6 +89,75 @@ class SQLQuery extends Object {
|
|||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify the list of columns to be selected by the query.
|
||||||
|
*
|
||||||
|
* <code>
|
||||||
|
* // pass fields to select as single parameter array
|
||||||
|
* $query->select(array("Col1","Col2"))->from("MyTable");
|
||||||
|
*
|
||||||
|
* // pass fields to select as multiple parameters
|
||||||
|
* $query->select("Col1", "Col2")->from("MyTable");
|
||||||
|
* </code>
|
||||||
|
*
|
||||||
|
* @param mixed $fields
|
||||||
|
* @return SQLQuery
|
||||||
|
*/
|
||||||
|
public function select($fields) {
|
||||||
|
if (func_num_args() > 1) {
|
||||||
|
$this->select = func_get_args();
|
||||||
|
} else {
|
||||||
|
$this->select = is_array($fields) ? $fields : array($fields);
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify the target table to select from.
|
||||||
|
*
|
||||||
|
* <code>
|
||||||
|
* $query->from("MyTable"); // SELECT * FROM MyTable
|
||||||
|
* </code>
|
||||||
|
*
|
||||||
|
* @param string $table
|
||||||
|
* @return SQLQuery
|
||||||
|
*/
|
||||||
|
public function from($table) {
|
||||||
|
$this->from[] = $table;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a predicate filter to the where clause.
|
||||||
|
*
|
||||||
|
* Accepts a variable length of arguments, which represent
|
||||||
|
* different ways of formatting a predicate in a where clause:
|
||||||
|
*
|
||||||
|
* <code>
|
||||||
|
* // the entire predicate as a single string
|
||||||
|
* $query->where("Column = 'Value'");
|
||||||
|
*
|
||||||
|
* // an exact match predicate with a key value pair
|
||||||
|
* $query->where("Column", "Value");
|
||||||
|
*
|
||||||
|
* // a predicate with user defined operator
|
||||||
|
* $query->where("Column", "!=", "Value");
|
||||||
|
* </code>
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function where() {
|
||||||
|
$args = func_get_args();
|
||||||
|
if (func_num_args() == 3) {
|
||||||
|
$filter = "{$args[0]} {$args[1]} '{$args[2]}'";
|
||||||
|
} elseif (func_num_args() == 2) {
|
||||||
|
$filter = "{$args[0]} = '{$args[1]}'";
|
||||||
|
} else {
|
||||||
|
$filter = $args[0];
|
||||||
|
}
|
||||||
|
$this->where[] = $filter;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use the disjunctive operator 'OR' to join filter expressions in the WHERE clause.
|
* Use the disjunctive operator 'OR' to join filter expressions in the WHERE clause.
|
||||||
*/
|
*/
|
||||||
@ -147,19 +217,21 @@ class SQLQuery extends Object {
|
|||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
function getFilter() {
|
function getFilter() {
|
||||||
return implode(") {$this->connective} (" , $this->where);
|
return ($this->where) ? implode(") {$this->connective} (" , $this->where) : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate the SQL statement for this query.
|
* Generate the SQL statement for this query.
|
||||||
|
*
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
function sql() {
|
function sql() {
|
||||||
|
if (!$this->from) return '';
|
||||||
$distinct = $this->distinct ? "DISTINCT " : "";
|
$distinct = $this->distinct ? "DISTINCT " : "";
|
||||||
if($this->select) {
|
if($this->delete) {
|
||||||
|
$text = "DELETE ";
|
||||||
|
} else if($this->select) {
|
||||||
$text = "SELECT $distinct" . implode(", ", $this->select);
|
$text = "SELECT $distinct" . implode(", ", $this->select);
|
||||||
} else {
|
|
||||||
if($this->delete) $text = "DELETE ";
|
|
||||||
}
|
}
|
||||||
$text .= " FROM " . implode(" ", $this->from);
|
$text .= " FROM " . implode(" ", $this->from);
|
||||||
|
|
||||||
@ -172,6 +244,15 @@ class SQLQuery extends Object {
|
|||||||
return $text;
|
return $text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the generated SQL string for this query
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function __toString() {
|
||||||
|
return $this->sql();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute this query.
|
* Execute this query.
|
||||||
* @return Query
|
* @return Query
|
||||||
|
@ -53,6 +53,9 @@ class ComplexTableField extends TableListField {
|
|||||||
*/
|
*/
|
||||||
protected $permissions = array(
|
protected $permissions = array(
|
||||||
"add",
|
"add",
|
||||||
|
"edit",
|
||||||
|
"show",
|
||||||
|
"delete",
|
||||||
//"export",
|
//"export",
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -213,7 +216,6 @@ JS;
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->sourceItems = DataObject::get($this->sourceClass, $this->sourceFilter, $sort, $this->sourceJoin, $limitClause);
|
$this->sourceItems = DataObject::get($this->sourceClass, $this->sourceFilter, $sort, $this->sourceJoin, $limitClause);
|
||||||
|
|
||||||
$this->unpagedSourceItems = DataObject::get($this->sourceClass, $this->sourceFilter, $sort, $this->sourceJoin);
|
$this->unpagedSourceItems = DataObject::get($this->sourceClass, $this->sourceFilter, $sort, $this->sourceJoin);
|
||||||
|
|
||||||
$this->totalCount = ($this->unpagedSourceItems) ? $this->unpagedSourceItems->TotalItems() : null;
|
$this->totalCount = ($this->unpagedSourceItems) ? $this->unpagedSourceItems->TotalItems() : null;
|
||||||
@ -436,8 +438,11 @@ JS;
|
|||||||
// add relational fields
|
// add relational fields
|
||||||
$detailFields->push(new HiddenField("ctf[parentClass]"," ",$this->getParentClass()));
|
$detailFields->push(new HiddenField("ctf[parentClass]"," ",$this->getParentClass()));
|
||||||
|
|
||||||
if( $this->relationAutoSetting )
|
if( $this->relationAutoSetting ) {
|
||||||
$detailFields->push(new HiddenField("$parentIdName"," ",$childData->ID));
|
// Hack for model admin: model admin will have included a dropdown for the relation itself
|
||||||
|
$detailFields->removeByName($parentIdName);
|
||||||
|
$detailFields->push(new HiddenField("$parentIdName"," ",$this->sourceID()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -485,18 +490,17 @@ JS;
|
|||||||
*
|
*
|
||||||
* @see {Form::ReferencedField}).
|
* @see {Form::ReferencedField}).
|
||||||
*/
|
*/
|
||||||
function saveComplexTableField($params) {
|
function saveComplexTableField($data, $form, $params) {
|
||||||
$className = $this->sourceClass();
|
$className = $this->sourceClass();
|
||||||
$childData = new $className();
|
$childData = new $className();
|
||||||
|
$form->saveInto($childData);
|
||||||
$this->saveInto($childData);
|
|
||||||
$childData->write();
|
$childData->write();
|
||||||
|
|
||||||
// if ajax-call in an iframe, update window
|
// if ajax-call in an iframe, update window
|
||||||
if(Director::is_ajax()) {
|
if(Director::is_ajax()) {
|
||||||
// Newly saved objects need their ID reflected in the reloaded form to avoid double saving
|
// Newly saved objects need their ID reflected in the reloaded form to avoid double saving
|
||||||
$form = $this->controller->DetailForm();
|
$childRequestHandler = new ComplexTableField_ItemRequest($this, $childData->ID);
|
||||||
//$form->loadDataFrom($this->dataObject);
|
$form = $childRequestHandler->DetailForm();
|
||||||
FormResponse::update_dom_id($form->FormName(), $form->formHtmlContent(), true, 'update');
|
FormResponse::update_dom_id($form->FormName(), $form->formHtmlContent(), true, 'update');
|
||||||
return FormResponse::respond();
|
return FormResponse::respond();
|
||||||
} else {
|
} else {
|
||||||
@ -542,19 +546,21 @@ class ComplexTableField_ItemRequest extends RequestHandlingData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->methodName = "show";
|
$this->methodName = "show";
|
||||||
/*
|
|
||||||
$this->sourceItems = $this->ctg->sourceItems();
|
|
||||||
|
|
||||||
$this->pageSize = 1;
|
|
||||||
|
|
||||||
if(isset($_REQUEST['ctf'][$this->Name()]['start']) && is_numeric($_REQUEST['ctf'][$this->Name()]['start'])) {
|
|
||||||
$this->unpagedSourceItems->setPageLimits($_REQUEST['ctf'][$this->Name()]['start'], $this->pageSize, $this->totalCount);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
echo $this->renderWith($this->ctf->templatePopup);
|
echo $this->renderWith($this->ctf->templatePopup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a 1-element data object set that can be used for pagination.
|
||||||
|
*/
|
||||||
|
/* this doesn't actually work :-(
|
||||||
|
function Paginator() {
|
||||||
|
$paginatingSet = new DataObjectSet(array($this->dataObj()));
|
||||||
|
$start = isset($_REQUEST['ctf']['start']) ? $_REQUEST['ctf']['start'] : 0;
|
||||||
|
$paginatingSet->setPageLimits($start, 1, $this->ctf->TotalCount());
|
||||||
|
return $paginatingSet;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Just a hook, processed in {DetailForm()}
|
* Just a hook, processed in {DetailForm()}
|
||||||
*
|
*
|
||||||
@ -566,25 +572,23 @@ class ComplexTableField_ItemRequest extends RequestHandlingData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->methodName = "edit";
|
$this->methodName = "edit";
|
||||||
/*
|
|
||||||
$this->sourceItems = $this->sourceItems();
|
|
||||||
|
|
||||||
$this->pageSize = 1;
|
|
||||||
|
|
||||||
if(is_numeric($_REQUEST['ctf']['start'])) {
|
|
||||||
$this->unpagedSourceItems->setPageLimits($_REQUEST['ctf']['start'], $this->pageSize, $this->totalCount);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
echo $this->renderWith($this->ctf->templatePopup);
|
echo $this->renderWith($this->ctf->templatePopup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function delete() {
|
||||||
|
if($this->ctf->Can('delete') !== true) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->dataObj()->delete();
|
||||||
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the data object being manipulated
|
* Return the data object being manipulated
|
||||||
*/
|
*/
|
||||||
function obj() {
|
function dataObj() {
|
||||||
// used to discover fields if requested and for population of field
|
// used to discover fields if requested and for population of field
|
||||||
if(is_numeric($this->itemID)) {
|
if(is_numeric($this->itemID)) {
|
||||||
// we have to use the basedataclass, otherwise we might exclude other subclasses
|
// we have to use the basedataclass, otherwise we might exclude other subclasses
|
||||||
@ -605,7 +609,7 @@ class ComplexTableField_ItemRequest extends RequestHandlingData {
|
|||||||
* @param int $childID
|
* @param int $childID
|
||||||
*/
|
*/
|
||||||
function DetailForm($childID = null) {
|
function DetailForm($childID = null) {
|
||||||
$childData = $this->obj();
|
$childData = $this->dataObj();
|
||||||
|
|
||||||
$fields = $this->ctf->getFieldsFor($childData);
|
$fields = $this->ctf->getFieldsFor($childData);
|
||||||
$validator = $this->ctf->getValidatorFor($childData);
|
$validator = $this->ctf->getValidatorFor($childData);
|
||||||
@ -631,13 +635,13 @@ class ComplexTableField_ItemRequest extends RequestHandlingData {
|
|||||||
* @see {Form::ReferencedField}).
|
* @see {Form::ReferencedField}).
|
||||||
*/
|
*/
|
||||||
function saveComplexTableField($data, $form, $request) {
|
function saveComplexTableField($data, $form, $request) {
|
||||||
$form->saveInto($this->obj());
|
$form->saveInto($this->dataObj());
|
||||||
$this->obj()->write();
|
$this->dataObj()->write();
|
||||||
|
|
||||||
// if ajax-call in an iframe, update window
|
// if ajax-call in an iframe, update window
|
||||||
if(Director::is_ajax()) {
|
if(Director::is_ajax()) {
|
||||||
// Newly saved objects need their ID reflected in the reloaded form to avoid double saving
|
// Newly saved objects need their ID reflected in the reloaded form to avoid double saving
|
||||||
$form = $this->controller->DetailForm();
|
$form = $this->DetailForm();
|
||||||
//$form->loadDataFrom($this->dataObject);
|
//$form->loadDataFrom($this->dataObject);
|
||||||
FormResponse::update_dom_id($form->FormName(), $form->formHtmlContent(), true, 'update');
|
FormResponse::update_dom_id($form->FormName(), $form->formHtmlContent(), true, 'update');
|
||||||
return FormResponse::respond();
|
return FormResponse::respond();
|
||||||
@ -647,58 +651,52 @@ class ComplexTableField_ItemRequest extends RequestHandlingData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function PopupBaseLink() {
|
|
||||||
$link = $this->FormAction() . "&action_callfieldmethod&fieldName={$this->Name()}";
|
|
||||||
if(!strpos($link,'ctf[ID]')) {
|
|
||||||
$link = str_replace('&','&',HTTP::setGetVar('ctf[ID]',$this->sourceID(),$link));
|
|
||||||
}
|
|
||||||
return $link;
|
|
||||||
}
|
|
||||||
|
|
||||||
function PopupCurrentItem() {
|
function PopupCurrentItem() {
|
||||||
return $_REQUEST['ctf']['start']+1;
|
return $_REQUEST['ctf']['start']+1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PopupFirstLink() {
|
function PopupFirstLink() {
|
||||||
if(!is_numeric($_REQUEST['ctf']['start']) || $_REQUEST['ctf']['start'] == 0) {
|
$this->ctf->LinkToItem();
|
||||||
|
|
||||||
|
if(!isset($_REQUEST['ctf']['start']) || !is_numeric($_REQUEST['ctf']['start']) || $_REQUEST['ctf']['start'] == 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$item = $this->unpagedSourceItems->First();
|
$item = $this->unpagedSourceItems->First();
|
||||||
$start = 0;
|
$start = 0;
|
||||||
return Convert::raw2att($this->PopupBaseLink() . "&methodName={$_REQUEST['methodName']}&ctf[childID]={$item->ID}&ctf[start]={$start}");
|
return Controller::join_links($this->Link(), "$this->methodName?ctf[start]={$start}");
|
||||||
}
|
}
|
||||||
|
|
||||||
function PopupLastLink() {
|
function PopupLastLink() {
|
||||||
if(!is_numeric($_REQUEST['ctf']['start']) || $_REQUEST['ctf']['start'] == $this->totalCount-1) {
|
if(!isset($_REQUEST['ctf']['start']) || !is_numeric($_REQUEST['ctf']['start']) || $_REQUEST['ctf']['start'] == $this->totalCount-1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$item = $this->unpagedSourceItems->Last();
|
$item = $this->unpagedSourceItems->Last();
|
||||||
$start = $this->totalCount - 1;
|
$start = $this->totalCount - 1;
|
||||||
return Convert::raw2att($this->PopupBaseLink() . "&methodName={$_REQUEST['methodName']}&ctf[childID]={$item->ID}&ctf[start]={$start}");
|
return Controller::join_links($this->Link(), "$this->methodName?ctf[start]={$start}");
|
||||||
}
|
}
|
||||||
|
|
||||||
function PopupNextLink() {
|
function PopupNextLink() {
|
||||||
if(!is_numeric($_REQUEST['ctf']['start']) || $_REQUEST['ctf']['start'] == $this->totalCount-1) {
|
if(!isset($_REQUEST['ctf']['start']) || !is_numeric($_REQUEST['ctf']['start']) || $_REQUEST['ctf']['start'] == $this->totalCount-1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$item = $this->unpagedSourceItems->getIterator()->getOffset($_REQUEST['ctf']['start'] + 1);
|
$item = $this->unpagedSourceItems->getIterator()->getOffset($_REQUEST['ctf']['start'] + 1);
|
||||||
|
|
||||||
$start = $_REQUEST['ctf']['start'] + 1;
|
$start = $_REQUEST['ctf']['start'] + 1;
|
||||||
return Convert::raw2att($this->PopupBaseLink() . "&methodName={$_REQUEST['methodName']}&ctf[childID]={$item->ID}&ctf[start]={$start}");
|
return Controller::join_links($this->Link(), "$this->methodName?ctf[start]={$start}");
|
||||||
}
|
}
|
||||||
|
|
||||||
function PopupPrevLink() {
|
function PopupPrevLink() {
|
||||||
if(!is_numeric($_REQUEST['ctf']['start']) || $_REQUEST['ctf']['start'] == 0) {
|
if(!isset($_REQUEST['ctf']['start']) || !is_numeric($_REQUEST['ctf']['start']) || $_REQUEST['ctf']['start'] == 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$item = $this->unpagedSourceItems->getIterator()->getOffset($_REQUEST['ctf']['start'] - 1);
|
$item = $this->unpagedSourceItems->getIterator()->getOffset($_REQUEST['ctf']['start'] - 1);
|
||||||
|
|
||||||
$start = $_REQUEST['ctf']['start'] - 1;
|
$start = $_REQUEST['ctf']['start'] - 1;
|
||||||
return Convert::raw2att($this->PopupBaseLink() . "&methodName={$_REQUEST['methodName']}&ctf[childID]={$item->ID}&ctf[start]={$start}");
|
return Controller::join_links($this->Link(), "$this->methodName?ctf[start]={$start}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -722,7 +720,7 @@ class ComplexTableField_ItemRequest extends RequestHandlingData {
|
|||||||
for($i = $offset;$i <= $offset + $this->pageSize && $i <= $this->totalCount;$i++) {
|
for($i = $offset;$i <= $offset + $this->pageSize && $i <= $this->totalCount;$i++) {
|
||||||
$start = $i - 1;
|
$start = $i - 1;
|
||||||
$item = $this->unpagedSourceItems->getIterator()->getOffset($i-1);
|
$item = $this->unpagedSourceItems->getIterator()->getOffset($i-1);
|
||||||
$links['link'] = Convert::raw2att($this->PopupBaseLink() . "&methodName={$_REQUEST['methodName']}&ctf[childID]={$item->ID}&ctf[start]={$start}");
|
$links['link'] = Controller::join_links($this->Link() . "$this->methodName?ctf[start]={$start}");
|
||||||
$links['number'] = $i;
|
$links['number'] = $i;
|
||||||
$links['active'] = $i == $currentItem ? false : true;
|
$links['active'] = $i == $currentItem ? false : true;
|
||||||
$result->push(new ArrayData($links));
|
$result->push(new ArrayData($links));
|
||||||
@ -730,6 +728,9 @@ class ComplexTableField_ItemRequest extends RequestHandlingData {
|
|||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ShowPagination() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -886,10 +887,6 @@ class ComplexTableField_Popup extends Form {
|
|||||||
function FieldHolder() {
|
function FieldHolder() {
|
||||||
return $this->renderWith('ComplexTableField_Form');
|
return $this->renderWith('ComplexTableField_Form');
|
||||||
}
|
}
|
||||||
|
|
||||||
function ShowPagination() {
|
|
||||||
return $this->controller->ShowPagination();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
@ -32,6 +32,7 @@ class FieldSet extends DataObjectSet {
|
|||||||
if($field->hasData()) {
|
if($field->hasData()) {
|
||||||
$name = $field->Name();
|
$name = $field->Name();
|
||||||
if(isset($list[$name])) {
|
if(isset($list[$name])) {
|
||||||
|
$errSuffix = "";
|
||||||
if($this->form) $errSuffix = " in your '{$this->form->class}' form called '" . $this->form->Name() . "'";
|
if($this->form) $errSuffix = " in your '{$this->form->class}' form called '" . $this->form->Name() . "'";
|
||||||
else $errSuffix = '';
|
else $errSuffix = '';
|
||||||
user_error("collateDataFields() I noticed that a field called '$name' appears twice$errSuffix.", E_USER_ERROR);
|
user_error("collateDataFields() I noticed that a field called '$name' appears twice$errSuffix.", E_USER_ERROR);
|
||||||
|
@ -813,6 +813,15 @@ class Form extends RequestHandlingData {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a rendered version of this form, suitable for ajax post-back.
|
||||||
|
* It triggers slightly different behaviour, such as disabling the rewriting of # links
|
||||||
|
*/
|
||||||
|
function forAjaxTemplate() {
|
||||||
|
$view = new SSViewer("Form");
|
||||||
|
return $view->dontRewriteHashlinks()->process($this);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an HTML rendition of this form, without the <form> tag itself.
|
* Returns an HTML rendition of this form, without the <form> tag itself.
|
||||||
* Attaches 3 extra hidden files, _form_action, _form_name, _form_method, and _form_enctype. These are
|
* Attaches 3 extra hidden files, _form_action, _form_name, _form_method, and _form_enctype. These are
|
||||||
|
@ -222,7 +222,7 @@ JS
|
|||||||
|
|
||||||
// sorting links (only if we have a form to refresh with)
|
// sorting links (only if we have a form to refresh with)
|
||||||
if($this->form) {
|
if($this->form) {
|
||||||
$sortLink = $this->BaseLink();
|
$sortLink = $this->Link();
|
||||||
$sortLink = HTTP::setGetVar("ctf[{$this->Name()}][sort]", $fieldName, $sortLink);
|
$sortLink = HTTP::setGetVar("ctf[{$this->Name()}][sort]", $fieldName, $sortLink);
|
||||||
if(isset($_REQUEST['ctf'][$this->Name()]['dir'])) {
|
if(isset($_REQUEST['ctf'][$this->Name()]['dir'])) {
|
||||||
$XML_sort = (isset($_REQUEST['ctf'][$this->Name()]['dir'])) ? Convert::raw2xml($_REQUEST['ctf'][$this->Name()]['dir']) : null;
|
$XML_sort = (isset($_REQUEST['ctf'][$this->Name()]['dir'])) ? Convert::raw2xml($_REQUEST['ctf'][$this->Name()]['dir']) : null;
|
||||||
@ -644,7 +644,7 @@ JS
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->BaseLink() . "&ctf[{$this->Name()}][start]={$start}{$this->filterString()}";
|
return $this->Link() . "/ajax_refresh?ctf[{$this->Name()}][start]={$start}{$this->filterString()}";
|
||||||
}
|
}
|
||||||
|
|
||||||
function PrevLink() {
|
function PrevLink() {
|
||||||
@ -656,7 +656,7 @@ JS
|
|||||||
|
|
||||||
$start = ($_REQUEST['ctf'][$this->Name()]['start'] - $this->pageSize < 0) ? 0 : $_REQUEST['ctf'][$this->Name()]['start'] - $this->pageSize;
|
$start = ($_REQUEST['ctf'][$this->Name()]['start'] - $this->pageSize < 0) ? 0 : $_REQUEST['ctf'][$this->Name()]['start'] - $this->pageSize;
|
||||||
|
|
||||||
return $this->BaseLink() . "&ctf[{$this->Name()}][start]=$start{$this->filterString()}";
|
return $this->Link() . "/ajax_refresh?ctf[{$this->Name()}][start]=$start{$this->filterString()}";
|
||||||
}
|
}
|
||||||
|
|
||||||
function NextLink() {
|
function NextLink() {
|
||||||
@ -665,7 +665,7 @@ JS
|
|||||||
if($currentStart >= $start-1) {
|
if($currentStart >= $start-1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return $this->BaseLink() . "&ctf[{$this->Name()}][start]={$start}{$this->filterString()}";
|
return $this->Link() . "/ajax_refresh?ctf[{$this->Name()}][start]={$start}{$this->filterString()}";
|
||||||
}
|
}
|
||||||
|
|
||||||
function LastLink() {
|
function LastLink() {
|
||||||
@ -678,7 +678,7 @@ JS
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->BaseLink() . "&ctf[{$this->Name()}][start]=$start{$this->filterString()}";
|
return $this->Link() . "/ajax_refresh?ctf[{$this->Name()}][start]=$start{$this->filterString()}";
|
||||||
}
|
}
|
||||||
|
|
||||||
function FirstItem() {
|
function FirstItem() {
|
||||||
@ -815,7 +815,7 @@ JS
|
|||||||
* We need to instanciate this button manually as a normal button has no means of adding inline onclick-behaviour.
|
* We need to instanciate this button manually as a normal button has no means of adding inline onclick-behaviour.
|
||||||
*/
|
*/
|
||||||
function ExportLink() {
|
function ExportLink() {
|
||||||
return Director::absoluteURL($this->FormAction()) . "&action_callfieldmethod&fieldName={$this->Name()}&methodName=export";
|
return Controller::join_links($this->Link(), 'export');
|
||||||
}
|
}
|
||||||
|
|
||||||
function printall() {
|
function printall() {
|
||||||
@ -832,7 +832,7 @@ JS
|
|||||||
}
|
}
|
||||||
|
|
||||||
function PrintLink() {
|
function PrintLink() {
|
||||||
$link = Director::absoluteURL($this->FormAction()) . "&action_callfieldmethod&fieldName={$this->Name()}&methodName=printall";
|
$link = Controller::join_links($this->Link(), 'printall');
|
||||||
if(isset($_REQUEST['ctf'][$this->Name()]['sort'])) {
|
if(isset($_REQUEST['ctf'][$this->Name()]['sort'])) {
|
||||||
$link = HTTP::setGetVar("ctf[{$this->Name()}][sort]",Convert::raw2xml($_REQUEST['ctf'][$this->Name()]['sort']), $link);
|
$link = HTTP::setGetVar("ctf[{$this->Name()}][sort]",Convert::raw2xml($_REQUEST['ctf'][$this->Name()]['sort']), $link);
|
||||||
}
|
}
|
||||||
@ -915,46 +915,8 @@ JS
|
|||||||
}
|
}
|
||||||
|
|
||||||
function BaseLink() {
|
function BaseLink() {
|
||||||
return $this->FormAction() . "&action_callfieldmethod&fieldName={$this->Name()}&ctf[ID]={$this->sourceID()}&methodName=ajax_refresh&SecurityID=" . Session::get('SecurityID');
|
user_error("TableListField::BaseLink() deprecated, use Link() instead", E_USER_NOTICE);
|
||||||
}
|
return $this->Link();
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the action of the surrounding form - needed to maintain context on subsequent calls.
|
|
||||||
* It is only needed to embed this field into a form if you want to use more than "display-functionality".
|
|
||||||
* We try to mirror the existing GET-properties to achieve the same application-state.
|
|
||||||
*
|
|
||||||
* @return String
|
|
||||||
*/
|
|
||||||
function FormAction() {
|
|
||||||
$params = $_GET;
|
|
||||||
|
|
||||||
// we don't want this to be overriding our new actions
|
|
||||||
unset($params['executeForm']);
|
|
||||||
unset($params['fieldName']);
|
|
||||||
unset($params['url']);
|
|
||||||
unset($params['methodName']);
|
|
||||||
unset($params['forcehtml']);
|
|
||||||
// TODO Refactor
|
|
||||||
unset($params['ctf']);
|
|
||||||
$params['ctf'][$this->Name()]['search'] = (isset($_REQUEST['ctf'][$this->Name()]['search'])) ? $_REQUEST['ctf'][$this->Name()]['search'] : null;
|
|
||||||
$params['SecurityID'] = Session::get('SecurityID');
|
|
||||||
|
|
||||||
// unset all actions (otherwise they override action_callfieldmethod)
|
|
||||||
foreach($params as $paramKey => $paramVal) {
|
|
||||||
if(strpos($paramKey, 'action_') === 0) {
|
|
||||||
unset($params[$paramKey]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
|
||||||
$link = $this->form->FormAction();
|
|
||||||
$link .= (!strpos($link,'?')) ? '?' : '&';
|
|
||||||
$link .= urldecode (http_build_query($params));
|
|
||||||
} catch(Exception $e) {
|
|
||||||
user_error('Please embed this field into a form if you want to use actions such as "add", "edit" or "delete"', E_USER_ERROR);
|
|
||||||
}
|
|
||||||
return $link;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1069,11 +1031,15 @@ class TableListField_Item extends ViewableData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function BaseLink() {
|
function BaseLink() {
|
||||||
return $this->parent->FormAction() . "&action_callfieldmethod&fieldName={$this->parent->Name()}&ctf[childID]={$this->item->ID}";
|
user_error("TableListField_Item::BaseLink() deprecated, use Link() instead", E_USER_NOTICE);
|
||||||
|
return $this->Link() . '/ajax_refresh';
|
||||||
|
}
|
||||||
|
function Link() {
|
||||||
|
return Controller::join_links($this->parent->Link() . "item/" . $this->item->ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DeleteLink() {
|
function DeleteLink() {
|
||||||
return $this->BaseLink() . "&methodName=delete";
|
return Controller::join_links($this->Link(), "delete");
|
||||||
}
|
}
|
||||||
|
|
||||||
function MarkingCheckbox() {
|
function MarkingCheckbox() {
|
||||||
|
@ -92,7 +92,7 @@ ComplexTableField.prototype = {
|
|||||||
var table = Event.findElement(e,"table");
|
var table = Event.findElement(e,"table");
|
||||||
if(Event.element(e).nodeName == "IMG") {
|
if(Event.element(e).nodeName == "IMG") {
|
||||||
link = Event.findElement(e,"a");
|
link = Event.findElement(e,"a");
|
||||||
popupLink = link.href+"&ajax=1";
|
popupLink = link.href+"?ajax=1";
|
||||||
} else {
|
} else {
|
||||||
el = Event.findElement(e,"tr");
|
el = Event.findElement(e,"tr");
|
||||||
var link = $$("a",el)[0];
|
var link = $$("a",el)[0];
|
||||||
@ -112,16 +112,13 @@ ComplexTableField.prototype = {
|
|||||||
|
|
||||||
GB_OpenerObj = this;
|
GB_OpenerObj = this;
|
||||||
// use same url to refresh the table after saving the popup, but use a generic rendering method
|
// use same url to refresh the table after saving the popup, but use a generic rendering method
|
||||||
GB_RefreshLink = popupLink;
|
GB_RefreshLink = this.getAttribute('href');
|
||||||
GB_RefreshLink = GB_RefreshLink.replace(/(methodName=)[^&]*/,"$1ajax_refresh");
|
|
||||||
// dont include pagination index
|
|
||||||
GB_RefreshLink = GB_RefreshLink.replace(/ctf\[start\][^&]*/,"");
|
|
||||||
GB_RefreshLink += '&forcehtml=1';
|
|
||||||
|
|
||||||
if(this.GB_Caption) {
|
if(this.GB_Caption) {
|
||||||
var title = this.GB_Caption;
|
var title = this.GB_Caption;
|
||||||
} else {
|
} else {
|
||||||
type = popupLink.match(/methodName=([^&]*)/);
|
// Getting the title from the URL is pretty ugly, but it works for now
|
||||||
|
type = popupLink.match(/[0-9]+\/([^\/?&]*)([?&]|$)/);
|
||||||
var title = (type && type[1]) ? type[1].ucfirst() : "";
|
var title = (type && type[1]) ? type[1].ucfirst() : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,11 +4,12 @@ ComplexTableFieldPopupForm.prototype = {
|
|||||||
errorMessage: "Error talking to server",
|
errorMessage: "Error talking to server",
|
||||||
|
|
||||||
initialize: function() {
|
initialize: function() {
|
||||||
Behaviour.register({
|
var rules = {};
|
||||||
"form#ComplexTableField_Popup_DetailForm .Actions input.action": {
|
rules["#" + this.id + " .Actions input.action"] = {
|
||||||
onclick: this.submitForm.bind(this)
|
'onclick' : this.submitForm.bind(this)
|
||||||
}
|
};
|
||||||
});
|
|
||||||
|
Behaviour.register(rules);
|
||||||
},
|
},
|
||||||
|
|
||||||
loadNewPage : function(content) {
|
loadNewPage : function(content) {
|
||||||
@ -33,7 +34,6 @@ ComplexTableFieldPopupForm.prototype = {
|
|||||||
submitButton.disabled = true;
|
submitButton.disabled = true;
|
||||||
Element.addClassName(submitButton,'loading');
|
Element.addClassName(submitButton,'loading');
|
||||||
}
|
}
|
||||||
|
|
||||||
new parent.parent.Ajax.Request(
|
new parent.parent.Ajax.Request(
|
||||||
theForm.getAttribute("action"),
|
theForm.getAttribute("action"),
|
||||||
{
|
{
|
||||||
@ -58,7 +58,6 @@ ComplexTableFieldPopupForm.prototype = {
|
|||||||
|
|
||||||
// don't update when validation is present and failed
|
// don't update when validation is present and failed
|
||||||
if(!this.validate || (this.validate && !hasHadFormError())) {
|
if(!this.validate || (this.validate && !hasHadFormError())) {
|
||||||
alert("GB:" + parent.parent.GB_RefreshLink);
|
|
||||||
new parent.parent.Ajax.Request(
|
new parent.parent.Ajax.Request(
|
||||||
parent.parent.GB_RefreshLink,
|
parent.parent.GB_RefreshLink,
|
||||||
{
|
{
|
||||||
@ -113,8 +112,9 @@ ComplexTableFieldPopupForm.prototype = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// causes IE6 to go nuts
|
// causes IE6 to go nuts
|
||||||
//this.GB_hide();
|
this.GB_hide();
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ComplexTableFieldPopupForm.applyTo('form#ComplexTableField_Popup_DetailForm');
|
ComplexTableFieldPopupForm.applyTo('#ComplexTableField_Popup_DetailForm');
|
||||||
|
ComplexTableFieldPopupForm.applyTo('#ComplexTableField_Popup_AddForm');
|
40
sake
Executable file
40
sake
Executable file
@ -0,0 +1,40 @@
|
|||||||
|
# Check for an argument
|
||||||
|
if [ $1 = "" ]; then
|
||||||
|
echo "Sapphire Sake
|
||||||
|
|
||||||
|
Usage: $0 (command-url) (params)
|
||||||
|
Executes a Sapphire command"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Special case for "sake installsake"
|
||||||
|
if [ "$1" = "installsake" ]; then
|
||||||
|
echo "Installing sake to /usr/bin..."
|
||||||
|
cp $0 /usr/bin
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find the PHP binary
|
||||||
|
for candidatephp in php5 php; do
|
||||||
|
if [ -f `which $candidatephp` ]; then
|
||||||
|
php=`which $candidatephp`
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$php" = "" ]; then
|
||||||
|
echo "Can't find any php binary"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -d ./sapphire ]; then
|
||||||
|
$php ./sapphire/cli-script.php $1 $2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f ./cli-script.php ]; then
|
||||||
|
$php ./cli-script.php $1 $2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Can't find ./sapphire/cli-script.php or ./cli-script.php"
|
@ -91,7 +91,8 @@ class SearchContext extends Object {
|
|||||||
foreach($searchParams as $key => $value) {
|
foreach($searchParams as $key => $value) {
|
||||||
$filter = $this->getFilter($key);
|
$filter = $this->getFilter($key);
|
||||||
if ($filter) {
|
if ($filter) {
|
||||||
$query->where[] = $filter->apply($value);
|
$filter->setValue($value);
|
||||||
|
$filter->apply($query);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $query;
|
return $query;
|
||||||
@ -108,6 +109,7 @@ class SearchContext extends Object {
|
|||||||
* @return DataObjectSet
|
* @return DataObjectSet
|
||||||
*/
|
*/
|
||||||
public function getResults($searchParams, $start = false, $limit = false) {
|
public function getResults($searchParams, $start = false, $limit = false) {
|
||||||
|
$searchParams = array_filter($searchParams, array($this,'clearEmptySearchFields'));
|
||||||
$query = $this->getQuery($searchParams, $start, $limit);
|
$query = $this->getQuery($searchParams, $start, $limit);
|
||||||
//
|
//
|
||||||
// use if a raw SQL query is needed
|
// use if a raw SQL query is needed
|
||||||
@ -121,6 +123,17 @@ class SearchContext extends Object {
|
|||||||
return DataObject::get($this->modelClass, $query->getFilter(), "", "", $limit);
|
return DataObject::get($this->modelClass, $query->getFilter(), "", "", $limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback map function to filter fields with empty values from
|
||||||
|
* being included in the search expression.
|
||||||
|
*
|
||||||
|
* @param unknown_type $value
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
function clearEmptySearchFields($value) {
|
||||||
|
return ($value != '');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @todo documentation
|
* @todo documentation
|
||||||
* @todo implementation
|
* @todo implementation
|
||||||
@ -138,8 +151,15 @@ class SearchContext extends Object {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
$query->where = $conditions;
|
$query->where = $conditions;
|
||||||
|
return $query;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accessor for the filter attached to a named field.
|
||||||
|
*
|
||||||
|
* @param string $name
|
||||||
|
* @return SearchFilter
|
||||||
|
*/
|
||||||
public function getFilter($name) {
|
public function getFilter($name) {
|
||||||
if (isset($this->filters[$name])) {
|
if (isset($this->filters[$name])) {
|
||||||
return $this->filters[$name];
|
return $this->filters[$name];
|
||||||
@ -148,14 +168,11 @@ class SearchContext extends Object {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getFields() {
|
/**
|
||||||
return $this->fields;
|
* Get the map of filters in the current search context.
|
||||||
}
|
*
|
||||||
|
* @return array
|
||||||
public function setFields($fields) {
|
*/
|
||||||
$this->fields = $fields;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getFilters() {
|
public function getFilters() {
|
||||||
return $this->filters;
|
return $this->filters;
|
||||||
}
|
}
|
||||||
@ -164,13 +181,29 @@ class SearchContext extends Object {
|
|||||||
$this->filters = $filters;
|
$this->filters = $filters;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearEmptySearchFields($value) {
|
/**
|
||||||
return ($value != '');
|
* Get the list of searchable fields in the current search context.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getFields() {
|
||||||
|
return $this->fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a list of searchable fields to the current search context.
|
||||||
|
*
|
||||||
|
* @param array $fields
|
||||||
|
*/
|
||||||
|
public function setFields($fields) {
|
||||||
|
$this->fields = $fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Placeholder, until I figure out the rest of the SQLQuery stuff
|
* Placeholder, until I figure out the rest of the SQLQuery stuff
|
||||||
* and link the $searchable_fields array to the SearchContext
|
* and link the $searchable_fields array to the SearchContext
|
||||||
|
*
|
||||||
|
* @deprecated in favor of getResults
|
||||||
*/
|
*/
|
||||||
public function getResultSet($fields) {
|
public function getResultSet($fields) {
|
||||||
$filter = "";
|
$filter = "";
|
||||||
|
@ -15,8 +15,8 @@ class ExactMatchFilter extends SearchFilter {
|
|||||||
*
|
*
|
||||||
* @return unknown
|
* @return unknown
|
||||||
*/
|
*/
|
||||||
public function apply($value) {
|
public function apply(SQLQuery $query) {
|
||||||
return "{$this->name}='$value'";
|
return $query->where("{$this->name} = '{$this->value}'");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,28 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Matches FULLTEXT indices.
|
* Filters by full-text matching on the given field.
|
||||||
|
*
|
||||||
|
* Full-text indexes are only available with MyISAM tables. The following column types are
|
||||||
|
* supported:
|
||||||
|
* - Char
|
||||||
|
* - Varchar
|
||||||
|
* - Text
|
||||||
|
*
|
||||||
|
* To enable full-text matching on fields, you also need to add an index to the
|
||||||
|
* database table, using the {$indexes} hash in your DataObject subclass:
|
||||||
|
*
|
||||||
|
* <code>
|
||||||
|
* static $indexes = array(
|
||||||
|
* 'SearchFields' => 'fulltext(Name, Title, Description)'
|
||||||
|
* );
|
||||||
|
* </code>
|
||||||
*
|
*
|
||||||
* @package sapphire
|
* @package sapphire
|
||||||
* @subpackage search
|
* @subpackage search
|
||||||
*/
|
*/
|
||||||
class FulltextFilter extends SearchFilter {
|
class FulltextFilter extends SearchFilter {
|
||||||
|
|
||||||
public function apply($value) {
|
public function apply(SQLQuery $query) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,8 +12,8 @@
|
|||||||
*/
|
*/
|
||||||
class NegationFilter extends SearchFilter {
|
class NegationFilter extends SearchFilter {
|
||||||
|
|
||||||
public function apply($value) {
|
public function apply(SQLQuery $query) {
|
||||||
return "{$this->name} != '$value'";
|
return $query->where("{$this->name} != '{$this->value}'");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -7,8 +7,8 @@
|
|||||||
*/
|
*/
|
||||||
class PartialMatchFilter extends SearchFilter {
|
class PartialMatchFilter extends SearchFilter {
|
||||||
|
|
||||||
public function apply($value) {
|
public function apply(SQLQuery $query) {
|
||||||
return "{$this->name} LIKE '%$value%'";
|
return $query->where("{$this->name} LIKE '%{$this->value}%'");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -8,12 +8,23 @@
|
|||||||
abstract class SearchFilter extends Object {
|
abstract class SearchFilter extends Object {
|
||||||
|
|
||||||
protected $name;
|
protected $name;
|
||||||
|
protected $value;
|
||||||
|
|
||||||
function __construct($name) {
|
function __construct($name, $value = false) {
|
||||||
$this->name = $name;
|
$this->name = $name;
|
||||||
|
$this->value = $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract public function apply($value);
|
public function setValue($value) {
|
||||||
|
$this->value = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply filter criteria to a SQL query.
|
||||||
|
*
|
||||||
|
* @param SQLQuery $query
|
||||||
|
*/
|
||||||
|
abstract public function apply(SQLQuery $query);
|
||||||
|
|
||||||
}
|
}
|
||||||
?>
|
?>
|
16
search/filters/SubstringFilter.php
Normal file
16
search/filters/SubstringFilter.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Uses a substring match against content in column rows.
|
||||||
|
*
|
||||||
|
* @package sapphire
|
||||||
|
* @subpackage search
|
||||||
|
*/
|
||||||
|
class SubstringFilter extends SearchFilter {
|
||||||
|
|
||||||
|
public function apply(SQLQuery $query) {
|
||||||
|
return $query->where("LOCATE({$this->name}, $value)");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
@ -1,16 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Uses a substring match against content in column rows.
|
|
||||||
*
|
|
||||||
* @package sapphire
|
|
||||||
* @subpackage search
|
|
||||||
*/
|
|
||||||
class SubstringMatchFilter extends SearchFilter {
|
|
||||||
|
|
||||||
public function apply($value) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
?>
|
|
@ -24,7 +24,7 @@ class BasicAuth extends Object {
|
|||||||
*/
|
*/
|
||||||
static function requireLogin($realm, $permissionCode) {
|
static function requireLogin($realm, $permissionCode) {
|
||||||
if(self::$disabled) return true;
|
if(self::$disabled) return true;
|
||||||
if(!Security::database_is_ready()) return true;
|
if(!Security::database_is_ready() || Director::is_cli()) return true;
|
||||||
|
|
||||||
|
|
||||||
if(isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) {
|
if(isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div id="$id" class="$CSSClasses field">
|
<div id="$id" class="$CSSClasses field" href="$Link">
|
||||||
<div class="middleColumn">
|
<div class="middleColumn">
|
||||||
<% include TableListField_PageControls %>
|
<% include TableListField_PageControls %>
|
||||||
<table class="data">
|
<table class="data">
|
||||||
|
@ -7,20 +7,19 @@
|
|||||||
<div class="right $PopupClasses">
|
<div class="right $PopupClasses">
|
||||||
$DetailForm
|
$DetailForm
|
||||||
</div>
|
</div>
|
||||||
<% if IsAddMode %>
|
|
||||||
<% else %>
|
|
||||||
<% if ShowPagination %>
|
<% if ShowPagination %>
|
||||||
<table id="ComplexTableField_Pagination">
|
<table id="ComplexTableField_Pagination">
|
||||||
<tr>
|
<tr>
|
||||||
<% if PopupPrevLink %>
|
<% if Paginator.PrevLink %>
|
||||||
<td id="ComplexTableField_Pagination_Previous">
|
<td id="ComplexTableField_Pagination_Previous">
|
||||||
<a href="$PopupPrevLink"><img src="cms/images/pagination/record-prev.png" /><% _t('PREVIOUS', 'Previous') %></a>
|
<a href="$Paginator.PrevLink"><img src="cms/images/pagination/record-prev.png" /><% _t('PREVIOUS', 'Previous') %></a>
|
||||||
</td>
|
</td>
|
||||||
<% end_if %>
|
<% end_if %>
|
||||||
<% if TotalCount == 1 %>
|
<% if xdsfdsf %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<td>
|
<td>
|
||||||
<% control Pagination %>
|
<% control Paginator.Pages %>
|
||||||
<% if active %>
|
<% if active %>
|
||||||
<a href="$link">$number</a>
|
<a href="$link">$number</a>
|
||||||
<% else %>
|
<% else %>
|
||||||
@ -29,14 +28,13 @@
|
|||||||
<% end_control %>
|
<% end_control %>
|
||||||
</td>
|
</td>
|
||||||
<% end_if %>
|
<% end_if %>
|
||||||
<% if PopupNextLink %>
|
<% if Paginator.NextLink %>
|
||||||
<td id="ComplexTableField_Pagination_Next">
|
<td id="ComplexTableField_Pagination_Next">
|
||||||
<a href="$PopupNextLink"><% _t('NEXT', 'Next') %><img src="cms/images/pagination/record-next.png" /></a>
|
<a href="$Paginator.NextLink"><% _t('NEXT', 'Next') %><img src="cms/images/pagination/record-next.png" /></a>
|
||||||
</td>
|
</td>
|
||||||
<% end_if %>
|
<% end_if %>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<% end_if %>
|
<% end_if %>
|
||||||
<% end_if %>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
72
tests/SQLQueryTest.php
Normal file
72
tests/SQLQueryTest.php
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class SQLQueryTest extends SapphireTest {
|
||||||
|
|
||||||
|
static $fixture_file = null;
|
||||||
|
|
||||||
|
function testEmptyQueryReturnsNothing() {
|
||||||
|
$query = new SQLQuery();
|
||||||
|
$this->assertEquals('', $query->sql());
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSelectFromBasicTable() {
|
||||||
|
$query = new SQLQuery();
|
||||||
|
$query->from[] = "MyTable";
|
||||||
|
$this->assertEquals("SELECT * FROM MyTable", $query->sql());
|
||||||
|
$query->from[] = "MyJoin";
|
||||||
|
$this->assertEquals("SELECT * FROM MyTable, MyJoin", $query->sql());
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSelectFromUserSpecifiedFields() {
|
||||||
|
$query = new SQLQuery();
|
||||||
|
$query->select = array("Name", "Title", "Description");
|
||||||
|
$query->from[] = "MyTable";
|
||||||
|
$this->assertEquals("SELECT Name, Title, Description FROM MyTable", $query->sql());
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSelectWithWhereClauseFilter() {
|
||||||
|
$query = new SQLQuery();
|
||||||
|
$query->select = array("Name","Meta");
|
||||||
|
$query->from[] = "MyTable";
|
||||||
|
$query->where[] = "Name = 'Name'";
|
||||||
|
$query->where[] = "Meta = 'Test'";
|
||||||
|
$this->assertEquals("SELECT Name, Meta FROM MyTable WHERE (Name = 'Name') AND (Meta = 'Test')", $query->sql());
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSelectWithConstructorParameters() {
|
||||||
|
$query = new SQLQuery(array("Foo", "Bar"), "FooBarTable");
|
||||||
|
$this->assertEquals("SELECT Foo, Bar FROM FooBarTable", $query->sql());
|
||||||
|
$query = new SQLQuery(array("Foo", "Bar"), "FooBarTable", array("Foo = 'Boo'"));
|
||||||
|
$this->assertEquals("SELECT Foo, Bar FROM FooBarTable WHERE (Foo = 'Boo')", $query->sql());
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSelectWithChainedMethods() {
|
||||||
|
$query = new SQLQuery();
|
||||||
|
$query->select("Name","Meta")->from("MyTable")->where("Name", "Name")->where("Meta", "Test");
|
||||||
|
$this->assertEquals("SELECT Name, Meta FROM MyTable WHERE (Name = 'Name') AND (Meta = 'Test')", $query->sql());
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSelectWithChainedFilterParameters() {
|
||||||
|
$query = new SQLQuery();
|
||||||
|
$query->select(array("Name","Meta"))->from("MyTable");
|
||||||
|
$query->where("Name = 'Name'")->where("Meta","Test")->where("Beta", "!=", "Gamma");
|
||||||
|
$this->assertEquals("SELECT Name, Meta FROM MyTable WHERE (Name = 'Name') AND (Meta = 'Test') AND (Beta != 'Gamma')", $query->sql());
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSelectWithPredicateFilters() {
|
||||||
|
$query = new SQLQuery();
|
||||||
|
$query->select(array("Name"))->from("MyTable");
|
||||||
|
$match = new ExactMatchFilter("Name", "Value");
|
||||||
|
$match->apply($query);
|
||||||
|
$match = new PartialMatchFilter("Meta", "Value");
|
||||||
|
$match->apply($query);
|
||||||
|
$this->assertEquals("SELECT Name FROM MyTable WHERE (Name = 'Value') AND (Meta LIKE '%Value%')", $query->sql());
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSelectWithLimitClause() {
|
||||||
|
// not implemented
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
@ -7,15 +7,14 @@ class SearchContextTest extends SapphireTest {
|
|||||||
$person = singleton('SearchContextTest_Person');
|
$person = singleton('SearchContextTest_Person');
|
||||||
$context = $person->getDefaultSearchContext();
|
$context = $person->getDefaultSearchContext();
|
||||||
|
|
||||||
$results = $context->getResultSet(array('Name'=>''));
|
$results = $context->getResults(array('Name'=>''));
|
||||||
$this->assertEquals(5, $results->Count());
|
$this->assertEquals(5, $results->Count());
|
||||||
|
|
||||||
$results = $context->getResultSet(array('EyeColor'=>'green'));
|
$results = $context->getResults(array('EyeColor'=>'green'));
|
||||||
$this->assertEquals(2, $results->Count());
|
$this->assertEquals(2, $results->Count());
|
||||||
|
|
||||||
$results = $context->getResultSet(array('EyeColor'=>'green', 'HairColor'=>'black'));
|
$results = $context->getResults(array('EyeColor'=>'green', 'HairColor'=>'black'));
|
||||||
$this->assertEquals(1, $results->Count());
|
$this->assertEquals(1, $results->Count());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function testSummaryIncludesDefaultFieldsIfNotDefined() {
|
function testSummaryIncludesDefaultFieldsIfNotDefined() {
|
||||||
@ -58,26 +57,26 @@ class SearchContextTest extends SapphireTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function testUserDefinedFiltersAppearInSearchContext() {
|
function testUserDefinedFiltersAppearInSearchContext() {
|
||||||
//$company = singleton('SearchContextTest_Company');
|
$company = singleton('SearchContextTest_Company');
|
||||||
//$context = $company->getDefaultSearchContext();
|
$context = $company->getDefaultSearchContext();
|
||||||
|
|
||||||
/*$this->assertEquals(
|
$this->assertEquals(
|
||||||
array(
|
array(
|
||||||
"Name" => new PartialMatchFilter("Name"),
|
"Name" => new PartialMatchFilter("Name"),
|
||||||
"Industry" => new ExactMatchFilter("Industry"),
|
"Industry" => new ExactMatchFilter("Industry"),
|
||||||
"AnnualProfit" => new PartialMatchFilter("AnnualProfit")
|
"AnnualProfit" => new PartialMatchFilter("AnnualProfit")
|
||||||
),
|
),
|
||||||
$context->getFilters()
|
$context->getFilters()
|
||||||
);*/
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testRelationshipObjectsLinkedInSearch() {
|
function testRelationshipObjectsLinkedInSearch() {
|
||||||
//$project = singleton('SearchContextTest_Project');
|
$project = singleton('SearchContextTest_Project');
|
||||||
//$context = $project->getDefaultSearchContext();
|
$context = $project->getDefaultSearchContext();
|
||||||
|
|
||||||
//$query = array("Name"=>"Blog Website");
|
$query = array("Name"=>"Blog Website");
|
||||||
|
|
||||||
//$results = $context->getQuery($query);
|
$results = $context->getQuery($query);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testCanGenerateQueryUsingAllFilterTypes() {
|
function testCanGenerateQueryUsingAllFilterTypes() {
|
||||||
@ -194,13 +193,15 @@ class SearchContextTest_AllFilterTypes extends DataObject implements TestOnly {
|
|||||||
"ExactMatch" => "Text",
|
"ExactMatch" => "Text",
|
||||||
"PartialMatch" => "Text",
|
"PartialMatch" => "Text",
|
||||||
"Negation" => "Text",
|
"Negation" => "Text",
|
||||||
"HiddenValue" => "Text"
|
"SubstringMatch" => "Text",
|
||||||
|
"HiddenValue" => "Text",
|
||||||
);
|
);
|
||||||
|
|
||||||
static $searchable_fields = array(
|
static $searchable_fields = array(
|
||||||
"ExactMatch" => "ExactMatchFilter",
|
"ExactMatch" => "ExactMatchFilter",
|
||||||
"PartialMatch" => "PartialMatchFilter",
|
"PartialMatch" => "PartialMatchFilter",
|
||||||
"Negation" => "NegationFilter"
|
"Negation" => "NegationFilter",
|
||||||
|
"SubstringMatch" => "SubstringFilter"
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user