Merged from branches/nzct-trunk. Use 'svn log -c <changeset> -g' for full commit message. Merge includes stability fixes and minor refactor of TableListField and ComplexTableField.

git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@63806 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
Hayden Smith 2008-10-08 02:00:12 +00:00
parent f2abae719e
commit 634ed7b70c
28 changed files with 416 additions and 131 deletions

View File

@ -48,12 +48,6 @@ Object::useCustomClass('Datetime','SSDatetime',true);
$path = Director::baseFolder().'/sapphire/parsers/'; $path = Director::baseFolder().'/sapphire/parsers/';
set_include_path(get_include_path() . PATH_SEPARATOR . $path); set_include_path(get_include_path() . PATH_SEPARATOR . $path);
/**
* Register the {@link OpenIDAuthenticator OpenID authenticator}
*/
Authenticator::register_authenticator('MemberAuthenticator');
Authenticator::set_default_authenticator('MemberAuthenticator');
/** /**
* Define a default language different than english * Define a default language different than english
*/ */

View File

@ -12,7 +12,8 @@ abstract class CliController extends Controller {
function index() { function index() {
// Always re-compile the manifest (?flush=1) // Always re-compile the manifest (?flush=1)
ManifestBuilder::compileManifest(); ManifestBuilder::update_db_tables(DB::getConn()->tableList(), $_ALL_CLASSES);
ManifestBuilder::write_manifest();
foreach( ClassInfo::subclassesFor( $this->class ) as $subclass ) { foreach( ClassInfo::subclassesFor( $this->class ) as $subclass ) {
echo $subclass; echo $subclass;

View File

@ -23,7 +23,7 @@ class ArrayData extends ViewableData {
public function __construct($array) { public function __construct($array) {
if(is_object($array)) { if(is_object($array)) {
$this->array = self::object_to_array($array); $this->array = self::object_to_array($array);
} elseif(is_array($array) && ArrayLib::is_associative($array)) { } elseif(is_array($array) && (ArrayLib::is_associative($array) || count($array) === 0)) {
$this->array = $array; $this->array = $array;
} else { } else {
$this->array = $array; $this->array = $array;

View File

@ -44,8 +44,7 @@ class Convert extends Object {
} else { } else {
$val = str_replace(array('&','"',"'",'<','>'),array('&amp;','&quot;','&#39;','&lt;','&gt;'),$val); $val = str_replace(array('&','"',"'",'<','>'),array('&amp;','&quot;','&#39;','&lt;','&gt;'),$val);
$val = preg_replace('^[a-zA-Z0-9\-_]','_', $val); $val = preg_replace('/[^a-zA-Z0-9\-_]*/','', $val);
$val = preg_replace('^[0-9]*','', $val); //
return $val; return $val;
} }
} }

View File

@ -608,4 +608,4 @@ class ManifestBuilder {
} }
?> ?>

View File

@ -1705,10 +1705,9 @@ class DataObject extends ViewableData implements DataObjectInterface {
} }
if ($fields) foreach($fields as $name => $level) { if ($fields) foreach($fields as $name => $level) {
if(!isset($this->original[$name])) continue;
$changedFields[$name] = array( $changedFields[$name] = array(
'before' => $this->original[$name], 'before' => array_key_exists($name, $this->original) ? $this->original[$name] : null,
'after' => $this->record[$name], 'after' => array_key_exists($name, $this->record) ? $this->record[$name] : null,
'level' => $level 'level' => $level
); );
} }
@ -1733,18 +1732,15 @@ class DataObject extends ViewableData implements DataObjectInterface {
} else { } else {
$defaults = $this->stat('defaults'); $defaults = $this->stat('defaults');
// if a field is not existing or has strictly changed // if a field is not existing or has strictly changed
if(!isset($this->record[$fieldName]) || $this->record[$fieldName] !== $val) { if(!array_key_exists($fieldName, $this->record) || $this->record[$fieldName] !== $val) {
// TODO Add check for php-level defaults which are not set in the db // TODO Add check for php-level defaults which are not set in the db
// TODO Add check for hidden input-fields (readonly) which are not set in the db // TODO Add check for hidden input-fields (readonly) which are not set in the db
if( // At the very least, the type has changed
// Main non type-based check $this->changed[$fieldName] = 1;
(isset($this->record[$fieldName]) && $this->record[$fieldName] != $val)
) { if(!array_key_exists($fieldName, $this->record) || $this->record[$fieldName] != $val) {
// Non-strict check fails, so value really changed, e.g. "abc" != "cde" // Value has changed as well, not just the type
$this->changed[$fieldName] = 2; $this->changed[$fieldName] = 2;
} else {
// Record change-level 1 if only the type changed, e.g. 0 !== NULL
$this->changed[$fieldName] = 1;
} }
// value is always saved back when strict check succeeds // value is always saved back when strict check succeeds

View File

@ -424,27 +424,31 @@ class SiteTree extends DataObject {
/** /**
* Return a breadcrumb trail to this page. * Return a breadcrumb trail to this page. Excludes "hidden" pages
* (with ShowInMenus=0).
* *
* @param int $maxDepth The maximum depth to traverse. * @param int $maxDepth The maximum depth to traverse.
* @param boolean $unlinked Do not make page names links * @param boolean $unlinked Do not make page names links
* @param string $stopAtPageType ClassName of a page to stop the upwards traversal. * @param string $stopAtPageType ClassName of a page to stop the upwards traversal.
* @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
* @return string The breadcrumb trail. * @return string The breadcrumb trail.
*/ */
public function Breadcrumbs($maxDepth = 20, $unlinked = false, public function Breadcrumbs($maxDepth = 20, $unlinked = false, $stopAtPageType = false, $showHidden = false) {
$stopAtPageType = false) {
$page = $this; $page = $this;
$parts = array(); $parts = array();
$i = 0; $i = 0;
while(($page && (sizeof($parts) < $maxDepth)) || while(
($stopAtPageType && $page->ClassName != $stopAtPageType)) { $page
if($page->ShowInMenus || ($page->ID == $this->ID)) { && (!$maxDepth || sizeof($parts) < $maxDepth)
if($page->URLSegment == 'home') { && ($stopAtPageType && $page->ClassName != $stopAtPageType)
$hasHome = true; ) {
if($showHidden || $page->ShowInMenus || ($page->ID == $this->ID)) {
if($page->URLSegment == 'home') $hasHome = true;
if(($page->ID == $this->ID) || $unlinked) {
$parts[] = Convert::raw2xml($page->Title);
} else {
$parts[] = ("<a href=\"" . $page->Link() . "\">" . Convert::raw2xml($page->Title) . "</a>");
} }
$parts[] = (($page->ID == $this->ID) || $unlinked)
? Convert::raw2xml($page->Title)
: ("<a href=\"" . $page->Link() . "\">" . Convert::raw2xml($page->Title) . "</a>");
} }
$page = $page->Parent; $page = $page->Parent;
} }

View File

@ -23,6 +23,10 @@ class Boolean extends DBField {
function Nice() { function Nice() {
return ($this->value) ? "yes" : "no"; return ($this->value) ? "yes" : "no";
} }
function NiceAsBoolean() {
return ($this->value) ? "true" : "false";
}
/** /**
* Saves this field to the given data object. * Saves this field to the given data object.

View File

@ -24,27 +24,6 @@
.ComplexTableField { .ComplexTableField {
margin-bottom: 10px; margin-bottom: 10px;
} }
.ComplexTableField .PageControls {
margin: 5px 0;
text-align:center;
display:block;
margin-bottom: 5px;
background:#ebeadb;
border: 1px #cbc7b7 solid;
}
.ComplexTableField .PageControls * {
display:inline;
vertical-align: middle;
font-weight: bold;
}
.ComplexTableField .PageControls .Last{
float:right; display:block;
width:40px; text-align:right;
}
.ComplexTableField .PageControls .First{
float:left; display:block;
width:40px; text-align:left;
}
.ComplexTableField tbody td { .ComplexTableField tbody td {
cursor: pointer; cursor: pointer;

View File

@ -171,6 +171,33 @@ form .TableField .message {
width: auto; width: auto;
} }
.TableListField .PageControls {
margin: 5px 0;
text-align:center;
display:block;
margin-bottom: 5px;
background:#ebeadb;
border: 1px #cbc7b7 solid;
position: relative;
}
.TableListField .PageControls * {
display:inline;
vertical-align: middle;
font-weight: bold;
}
.TableListField .PageControls .Last{
display: block;
width: 40px;
text-align: right;
position: absolute;
right: 0px;
top: 0px;
}
.TableListField .PageControls .First{
float:left; display:block;
width:40px; text-align:left;
}
#Pagination { #Pagination {
margin-top: 10px; margin-top: 10px;

View File

@ -93,7 +93,12 @@ class Email extends ViewableData {
} }
public function attachFile($filename, $attachedFilename = null, $mimetype = null) { public function attachFile($filename, $attachedFilename = null, $mimetype = null) {
$this->attachFileFromString(file_get_contents(Director::getAbsFile($filename)), $attachedFilename, $mimetype); $absoluteFileName = Director::getAbsFile($filename);
if(file_exists($absoluteFileName)) {
$this->attachFileFromString(file_get_contents($absoluteFileName), $attachedFilename, $mimetype);
} else {
user_error("Could not attach '$absoluteFileName' to email. File does not exist.", E_USER_NOTICE);
}
} }
public function setFormat($format) { public function setFormat($format) {

View File

@ -40,7 +40,7 @@ class BankAccountField extends FormField {
$field = new FieldGroup($this->name); $field = new FieldGroup($this->name);
$field->setID("{$this->name}_Holder"); $field->setID("{$this->name}_Holder");
$valueArr = $this->valueArray; $valueArr = $this->valueArr;
$valueArr = self::convert_format_nz($valueArr); $valueArr = self::convert_format_nz($valueArr);

View File

@ -29,7 +29,7 @@ class CompositeField extends FormField {
protected $columnCount = null; protected $columnCount = null;
public function __construct($children = null) { public function __construct($children = null) {
if(is_a($children, 'FieldSet')) { if($children instanceof FieldSet) {
$this->children = $children; $this->children = $children;
} elseif(is_array($children)) { } elseif(is_array($children)) {
$this->children = new FieldSet($children); $this->children = new FieldSet($children);
@ -239,7 +239,7 @@ class CompositeField extends FormField {
$valid = true; $valid = true;
foreach($this->children as $idx => $child){ foreach($this->children as $idx => $child){
$valid = ($child->validate($validator) && $valid); $valid = ($child && $child->validate($validator) && $valid);
} }
return $valid; return $valid;

View File

@ -38,9 +38,8 @@ class DateField extends TextField {
return $field; return $field;
} }
function jsValidation($formID = null) function jsValidation() {
{ $formID = $this->form->FormName();
if(!$formID)$formID = $this->form->FormName();
$error = _t('DateField.VALIDATIONJS', 'Please enter a valid date format (DD/MM/YYYY).'); $error = _t('DateField.VALIDATIONJS', 'Please enter a valid date format (DD/MM/YYYY).');
$jsFunc =<<<JS $jsFunc =<<<JS
Behaviour.register({ Behaviour.register({
@ -77,7 +76,7 @@ JS;
{ {
$validator->validationError( $validator->validationError(
$this->name, $this->name,
_t('DateField.VALIDDATEFORMAT', "Please enter a valid date format (DD/MM/YYYY)."), _t('DateField.VALIDDATEFORMAT', "Please enter a valid date format (DD/MM/YYYY)."),
"validation", "validation",
false false
); );
@ -130,5 +129,9 @@ class DateField_Disabled extends DateField {
function php() { function php() {
return true; return true;
} }
function validate($validator) {
return true;
}
} }
?> ?>

View File

@ -370,7 +370,7 @@ class TableField extends TableListField {
if($dataObjects) { if($dataObjects) {
foreach ($dataObjects as $objectid => $fieldValues) { foreach ($dataObjects as $objectid => $fieldValues) {
// we have to "sort" new data first, and process it in a seperate saveData-call (see setValue()) // we have to "sort" new data first, and process it in a seperate saveData-call (see setValue())
if($objectid === "new") continue; if($objectid === "new") continue;
// extra data was creating fields, but // extra data was creating fields, but
if($this->extraData) { if($this->extraData) {
@ -381,7 +381,7 @@ class TableField extends TableListField {
$obj = new $this->sourceClass(); $obj = new $this->sourceClass();
if($ExistingValues) { if($ExistingValues) {
$obj->ID = $objectid; $obj = DataObject::get_by_id($this->sourceClass, $objectid);
} }
// Legacy: Use the filter as a predefined relationship-ID // Legacy: Use the filter as a predefined relationship-ID
@ -553,7 +553,10 @@ JS;
if($data['methodName'] != 'delete'){ if($data['methodName'] != 'delete'){
$fields = $this->FieldSet(); $fields = $this->FieldSet();
$fields = new FieldSet($fields); $fields = new FieldSet($fields);
foreach($fields as $field){
$valid = $field->validate($this) && $valid;
}
return $valid;
}else{ }else{
return $valid; return $valid;
} }

View File

@ -211,6 +211,8 @@ class TableListField extends FormField {
*/ */
public $fieldFormatting = array(); public $fieldFormatting = array();
public $csvFieldFormatting = array();
/** /**
* @var string * @var string
*/ */
@ -227,6 +229,8 @@ class TableListField extends FormField {
*/ */
protected $extraLinkParams; protected $extraLinkParams;
protected $__cachedQuery;
function __construct($name, $sourceClass, $fieldList = null, $sourceFilter = null, function __construct($name, $sourceClass, $fieldList = null, $sourceFilter = null,
$sourceSort = null, $sourceJoin = null) { $sourceSort = null, $sourceJoin = null) {
@ -259,6 +263,19 @@ class TableListField extends FormField {
return $this->FieldHolder(); return $this->FieldHolder();
} }
static $url_handlers = array(
'item/$ID' => 'handleItem',
'$Action!' => '$Action',
);
function sourceClass() {
return $this->sourceClass;
}
function handleItem($request) {
return new TableListField_ItemRequest($this, $request->param('ID'));
}
function FieldHolder() { function FieldHolder() {
if($this->clickAction) { if($this->clickAction) {
@ -321,7 +338,7 @@ JS
return false; return false;
} }
if($this->__cachedSQL) { if($this->__cachedQuery) {
$query = $this->__cachedQuery; $query = $this->__cachedQuery;
} else { } else {
$query = $this->__cachedQuery = $this->getQuery(); $query = $this->__cachedQuery = $this->getQuery();
@ -423,7 +440,7 @@ JS
*/ */
function getQuery() { function getQuery() {
if($this->customQuery) { if($this->customQuery) {
$query = $this->customQuery; $query = clone $this->customQuery;
$baseClass = ClassInfo::baseDataClass($this->sourceClass); $baseClass = ClassInfo::baseDataClass($this->sourceClass);
$query->select[] = "{$baseClass}.ID AS ID"; $query->select[] = "{$baseClass}.ID AS ID";
$query->select[] = "{$baseClass}.ClassName AS ClassName"; $query->select[] = "{$baseClass}.ClassName AS ClassName";
@ -448,7 +465,7 @@ JS
$query->orderby = $SQL_sort; $query->orderby = $SQL_sort;
} }
} }
return clone $query; return $query;
} }
function getCsvQuery() { function getCsvQuery() {
@ -605,6 +622,8 @@ JS
$summaryFields[] = new ArrayData(array( $summaryFields[] = new ArrayData(array(
'Function' => $function, 'Function' => $function,
'SummaryValue' => $summaryValue, 'SummaryValue' => $summaryValue,
'Name' => DBField::create('Varchar', $fieldName),
'Title' => DBField::create('Varchar', $fieldTitle),
)); ));
} }
return new DataObjectSet($summaryFields); return new DataObjectSet($summaryFields);
@ -879,6 +898,7 @@ JS
function export() { function export() {
$now = Date("d-m-Y-H-i"); $now = Date("d-m-Y-H-i");
$fileName = "export-$now.csv"; $fileName = "export-$now.csv";
$separator = $this->csvSeparator; $separator = $this->csvSeparator;
$csvColumns = ($this->fieldListCsv) ? $this->fieldListCsv : $this->fieldList; $csvColumns = ($this->fieldListCsv) ? $this->fieldListCsv : $this->fieldList;
$fileData = ""; $fileData = "";
@ -889,14 +909,15 @@ JS
} }
// get data // get data
$dataQuery = $this->getCsvQuery(); if(isset($this->customSourceItems)){
$records = $dataQuery->execute(); $items = $this->customSourceItems;
}else{
$sourceClass = $this->sourceClass; $dataQuery = $this->getCsvQuery();
$dataobject = new $sourceClass(); $records = $dataQuery->execute();
$sourceClass = $this->sourceClass;
// @todo Will create a large unpaginated dataobjectset based on how many records are in table (performance issue) $dataobject = new $sourceClass();
$items = $dataobject->buildDataObjectSet($records, 'DataObjectSet'); $items = $dataobject->buildDataObjectSet($records, 'DataObjectSet');
}
$fieldItems = new DataObjectSet(); $fieldItems = new DataObjectSet();
if($items && $items->count()) foreach($items as $item) { if($items && $items->count()) foreach($items as $item) {
@ -911,26 +932,31 @@ JS
if($fieldItems) { if($fieldItems) {
foreach($fieldItems as $fieldItem) { foreach($fieldItems as $fieldItem) {
$columnData = array();
$fields = $fieldItem->Fields(); $fields = $fieldItem->Fields();
foreach($fields as $field) { foreach($fields as $field) {
// replace <br/ >s with newlines for csv
$field->Value = str_replace('<br />', "\n", $field->Value); $value = $field->Value;
// remove double quotes
$field->Value = str_replace('"', "", $field->Value); // TODO This should be replaced with casting
$fileData .= "\"" . $field->Value . "\""; if(array_key_exists($field->Name, $this->csvFieldFormatting)) {
if($field->Last()) { $format = str_replace('$value', "__VAL__", $this->csvFieldFormatting[$columnName]);
$fileData .= "\n"; $format = preg_replace('/\$([A-Za-z0-9-_]+)/','$item->$1', $format);
} else { $format = str_replace('__VAL__', '$value', $format);
$fileData .= $this->csvSeparator; eval('$value = "' . $format . '";');
} }
$value = str_replace(array("\r", "\n"), "\n", $value);
$tmpColumnData = "\"" . str_replace("\"", "\"\"", $value) . "\"";
$columnData[] = $tmpColumnData;
} }
$fileData .= implode($separator, $columnData);
$fileData .= "\n";
} }
return HTTPRequest::send_file($fileData, $fileName); return HTTPRequest::send_file($fileData, $fileName);
} else { } else {
user_error("No records found", E_USER_ERROR); user_error("No records found", E_USER_ERROR);
} }
} }
/** /**
@ -948,12 +974,20 @@ JS
Requirements::css(CMS_DIR . '/css/typography.css'); Requirements::css(CMS_DIR . '/css/typography.css');
Requirements::css(CMS_DIR . '/css/cms_right.css'); Requirements::css(CMS_DIR . '/css/cms_right.css');
Requirements::css(SAPPHIRE_DIR . '/css/TableListField_print.css'); Requirements::css(SAPPHIRE_DIR . '/css/TableListField_print.css');
$vd = new ViewableData();
return $vd->customise(array( unset($this->cachedSourceItems);
'Content' => $this->customise(array( $oldShowPagination = $this->showPagination;
'Print' => true $this->showPagination = false;
))->renderWith($this->template) $oldLimit = ini_get('max_execution_time');
))->renderWith('TableListField_printable'); set_time_limit(0);
$result = $this->renderWith(array($this->template . '_printable', 'TableListField_printable'));
$this->showPagination = $oldShowPagination;
set_time_limit($oldLimit);
return $result;
} }
function PrintLink() { function PrintLink() {
@ -1010,6 +1044,10 @@ JS
$this->fieldFormatting = $formatting; $this->fieldFormatting = $formatting;
} }
function setCSVFieldFormatting($formatting) {
$this->csvFieldFormatting = $formatting;
}
/** /**
* @return String * @return String
*/ */
@ -1024,19 +1062,19 @@ JS
// adding this to TODO probably add a method to the classes // adding this to TODO probably add a method to the classes
// to return they're translated string // to return they're translated string
// added by ruibarreiros @ 27/11/2007 // added by ruibarreiros @ 27/11/2007
return singleton($this->sourceClass)->singular_name(); return $this->sourceClass ? singleton($this->sourceClass)->singular_name() : $this->Name();
} }
function NameSingular() { function NameSingular() {
// same as Title() // same as Title()
// added by ruibarreiros @ 27/11/2007 // added by ruibarreiros @ 27/11/2007
return singleton($this->sourceClass)->singular_name(); return $this->sourceClass ? singleton($this->sourceClass)->singular_name() : $this->Name();
} }
function NamePlural() { function NamePlural() {
// same as Title() // same as Title()
// added by ruibarreiros @ 27/11/2007 // added by ruibarreiros @ 27/11/2007
return singleton($this->sourceClass)->plural_name(); return $this->sourceClass ? singleton($this->sourceClass)->plural_name() : $this->Name();
} }
function setTemplate($template) { function setTemplate($template) {
@ -1216,6 +1254,16 @@ class TableListField_Item extends ViewableData {
return $this->parent->Can($mode); return $this->parent->Can($mode);
} }
function Link() {
if($this->parent->getForm()) {
return Controller::join_links($this->parent->Link() . "item/" . $this->item->ID);
} else {
// allow for instanciation of this FormField outside of a controller/form
// context (e.g. for unit tests)
return false;
}
}
/** /**
* Returns all row-based actions not disallowed through permissions. * Returns all row-based actions not disallowed through permissions.
* See TableListField->Action for a similiar dummy-function to work * See TableListField->Action for a similiar dummy-function to work
@ -1240,17 +1288,7 @@ class TableListField_Item extends ViewableData {
return $allowedActions; return $allowedActions;
} }
function Link() {
if($this->parent->getForm()) {
return Controller::join_links($this->parent->Link() . "item/" . $this->item->ID);
} else {
// allow for instanciation of this FormField outside of a controller/form
// context (e.g. for unit tests)
return false;
}
}
function BaseLink() { function BaseLink() {
user_error("TableListField_Item::BaseLink() deprecated, use Link() instead", E_USER_NOTICE); user_error("TableListField_Item::BaseLink() deprecated, use Link() instead", E_USER_NOTICE);
return $this->Link(); return $this->Link();
@ -1293,7 +1331,78 @@ class TableListField_Item extends ViewableData {
function isReadonly() { function isReadonly() {
return $this->parent->Can('delete'); return $this->parent->Can('delete');
} }
} }
?> class TableListField_ItemRequest extends RequestHandlingData {
protected $ctf;
protected $itemID;
protected $methodName;
static $url_handlers = array(
'$Action!' => '$Action',
'' => 'index',
);
function Link() {
return $this->ctf->Link() . '/item/' . $this->itemID;
}
function __construct($ctf, $itemID) {
$this->ctf = $ctf;
$this->itemID = $itemID;
}
function delete() {
if($this->ctf->Can('delete') !== true) {
return false;
}
$this->dataObj()->delete();
}
///////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Return the data object being manipulated
*/
function dataObj() {
// used to discover fields if requested and for population of field
if(is_numeric($this->itemID)) {
// we have to use the basedataclass, otherwise we might exclude other subclasses
return DataObject::get_by_id(ClassInfo::baseDataClass(Object::getCustomClass($this->ctf->sourceClass())), $this->itemID);
}
}
/**
* Returns the db-fieldname of the currently used has_one-relationship.
*/
function getParentIdName( $parentClass, $childClass ) {
return $this->getParentIdNameRelation( $childClass, $parentClass, 'has_one' );
}
/**
* Manually overwrites the parent-ID relations.
* @see setParentClass()
*
* @param String $str Example: FamilyID (when one Individual has_one Family)
*/
function setParentIdName($str) {
$this->parentIdName = $str;
}
/**
* Returns the db-fieldname of the currently used relationship.
*/
function getParentIdNameRelation($parentClass, $childClass, $relation) {
if($this->parentIdName) return $this->parentIdName;
$relations = singleton($parentClass)->$relation();
$classes = ClassInfo::ancestry($childClass);
foreach($relations as $k => $v) {
if(array_key_exists($v, $classes)) return $k . 'ID';
}
return false;
}
}
?>

View File

@ -4,13 +4,16 @@ GB_RefreshLink = "";
ComplexTableField = Class.create(); ComplexTableField = Class.create();
ComplexTableField.prototype = { ComplexTableField.prototype = {
// TODO adjust dynamically // These are defaults used if setPopupSize encounters errors
popupWidth: 560, defaultPopupWidth: 560,
popupHeight: 390, defaultPopupHeight: 390,
initialize: function() { initialize: function() {
var rules = {}; var rules = {};
rules['#'+this.id+' table.data a.popuplink'] = {onclick: this.openPopup.bind(this)}; rules['#'+this.id+' table.data a.popuplink'] = {onclick: this.openPopup.bind(this)};
// Assume that the delete link uses the deleteRecord method
rules['#'+this.id+' table.data a.deletelink'] = {onclick: this.deleteRecord.bind(this)};
rules['#'+this.id+' table.data tbody td'] = {onclick: this.openPopup.bind(this)}; rules['#'+this.id+' table.data tbody td'] = {onclick: this.openPopup.bind(this)};
// invoke row action-link based on default-action set in classname // invoke row action-link based on default-action set in classname
@ -25,10 +28,22 @@ ComplexTableField.prototype = {
} }
Behaviour.register('ComplexTableField_'+this.id,rules); Behaviour.register('ComplexTableField_'+this.id,rules);
this.setPopupSize();
// HACK If already in a popup, we can't allow add (doesn't save existing relation correctly) // HACK If already in a popup, we can't allow add (doesn't save existing relation correctly)
if(window != top) $$('#'+this.id+' table.data a.addlink').each(function(el) {Element.hide(el);}); if(window != top) $$('#'+this.id+' table.data a.addlink').each(function(el) {Element.hide(el);});
}, },
setPopupSize: function() {
try {
this.popupHeight = parseInt($(this.id + '_PopupHeight').value);
this.popupWidth = parseInt($(this.id + '_PopupWidth').value);
} catch (ex) {
this.popupHeight = this.defaultPopupHeight;
this.popupWidth = this.defaultPopupWidth;
}
},
getDefaultAction: function() { getDefaultAction: function() {
// try to get link class from <td class="action default"><a href="... // try to get link class from <td class="action default"><a href="...
var links = $$('#'+this.id+' table.data tbody .default a'); var links = $$('#'+this.id+' table.data tbody .default a');
@ -45,6 +60,8 @@ ComplexTableField.prototype = {
// of opening a nested lightwindow // of opening a nested lightwindow
if(window != top) return true; if(window != top) return true;
this.setPopupSize();
var el,type; var el,type;
var popupLink = ""; var popupLink = "";
if(_popupLink) { if(_popupLink) {

View File

@ -102,6 +102,7 @@ function require(fieldName,cachedError) {
} }
var baseEl; var baseEl;
var fieldHolder = el;
// Sometimes require events are triggered of // Sometimes require events are triggered of
// associative elements like labels ;-p // associative elements like labels ;-p
@ -155,7 +156,8 @@ function require(fieldName,cachedError) {
} else { } else {
if(!hasHadFormError()) { if(!hasHadFormError()) {
clearErrorMessage(baseEl.parentNode); if(baseEl) fieldHolder = baseEl.parentNode;
clearErrorMessage(fieldHolder);
} }
return true; return true;
} }
@ -195,6 +197,12 @@ function findParentLabel(el) {
return findParentLabel(el.parentNode); return findParentLabel(el.parentNode);
} }
} else { } else {
// Try to find a label with a for value of this field.
if(el.id) {
var labels = $$('label[for=' + el.id + ']');
if(labels && labels.length > 0) return labels[0].innerHTML;
}
return findParentLabel(el.parentNode); return findParentLabel(el.parentNode);
} }
} }

View File

@ -16,7 +16,7 @@ abstract class Authenticator extends Object {
* *
* @var array * @var array
*/ */
private static $authenticators = array(); private static $authenticators = array('MemberAuthenticator');
/** /**
* Used to influence the order of authenticators on the login-screen * Used to influence the order of authenticators on the login-screen
@ -24,7 +24,7 @@ abstract class Authenticator extends Object {
* *
* @var string * @var string
*/ */
private static $default_authenticator = ''; private static $default_authenticator = 'MemberAuthenticator';
/** /**
@ -107,7 +107,9 @@ abstract class Authenticator extends Object {
*/ */
public static function unregister_authenticator($authenticator) { public static function unregister_authenticator($authenticator) {
if(call_user_func(array($authenticator, 'on_unregister')) === true) { if(call_user_func(array($authenticator, 'on_unregister')) === true) {
unset(self::$authenticators[$authenticator]); if(in_array($authenticator, self::$authenticators)) {
unset(self::$authenticators[array_search($authenticator, self::$authenticators)]);
}
}; };
} }

View File

@ -33,7 +33,7 @@ abstract class LoginForm extends Form {
public function getAuthenticator() { public function getAuthenticator() {
if(!class_exists($this->authenticator_class) || !is_subclass_of($this->authenticator_class, 'Authenticator')) { if(!class_exists($this->authenticator_class) || !is_subclass_of($this->authenticator_class, 'Authenticator')) {
user_error('The form uses an invalid authenticator class!', E_USER_ERROR); user_error("The form uses an invalid authenticator class! '{$this->authenticator_class}' is not a subclass of 'Authenticator'", E_USER_ERROR);
return; return;
} }

View File

@ -6,6 +6,8 @@
*/ */
class MemberLoginForm extends LoginForm { class MemberLoginForm extends LoginForm {
protected $authenticator_class = 'MemberAuthenticator';
/** /**
* Constructor * Constructor
* *
@ -22,11 +24,13 @@ class MemberLoginForm extends LoginForm {
* @param bool $checkCurrentUser If set to TRUE, it will be checked if a * @param bool $checkCurrentUser If set to TRUE, it will be checked if a
* the user is currently logged in, and if * the user is currently logged in, and if
* so, only a logout button will be rendered * so, only a logout button will be rendered
* @param string $authenticatorClassName Name of the authenticator class that this form uses.
*/ */
function __construct($controller, $name, $fields = null, $actions = null, function __construct($controller, $name, $fields = null, $actions = null,
$checkCurrentUser = true) { $checkCurrentUser = true) {
$this->authenticator_class = 'MemberAuthenticator'; // This is now set on the class directly to make it easier to create subclasses
// $this->authenticator_class = $authenticatorClassName;
$customCSS = project() . '/css/member_login.css'; $customCSS = project() . '/css/member_login.css';
if(Director::fileExists($customCSS)) { if(Director::fileExists($customCSS)) {
@ -48,10 +52,16 @@ class MemberLoginForm extends LoginForm {
new HiddenField("AuthenticationMethod", null, $this->authenticator_class, $this), new HiddenField("AuthenticationMethod", null, $this->authenticator_class, $this),
new TextField("Email", _t('Member.EMAIL'), new TextField("Email", _t('Member.EMAIL'),
Session::get('SessionForms.MemberLoginForm.Email'), null, $this), Session::get('SessionForms.MemberLoginForm.Email'), null, $this),
new EncryptField("Password", _t('Member.PASSWORD'), null, $this), new EncryptField("Password", _t('Member.PASSWORD'), null, $this)
new CheckboxField("Remember", _t('Member.REMEMBERME', "Remember me next time?"),
Session::get('SessionForms.MemberLoginForm.Remember'), $this)
); );
if(Security::$autologin_enabled) {
$fields->push(new CheckboxField(
"Remember",
_t('Member.REMEMBERME', "Remember me next time?"),
Session::get('SessionForms.MemberLoginForm.Remember'),
$this
));
}
} }
if(!$actions) { if(!$actions) {
$actions = new FieldSet( $actions = new FieldSet(
@ -109,7 +119,7 @@ class MemberLoginForm extends LoginForm {
Session::clear("BackURL"); Session::clear("BackURL");
Director::redirect($backURL); Director::redirect($backURL);
} else { } else {
Director::redirect(Security::default_login_dest()); Director::redirectBack();
} }
} else { } else {
Session::set('SessionForms.MemberLoginForm.Email', $data['Email']); Session::set('SessionForms.MemberLoginForm.Email', $data['Email']);
@ -187,7 +197,7 @@ class MemberLoginForm extends LoginForm {
$member->sendInfo('forgotPassword', array('PasswordResetLink' => $member->sendInfo('forgotPassword', array('PasswordResetLink' =>
Security::getPasswordResetLink($member->AutoLoginHash))); Security::getPasswordResetLink($member->AutoLoginHash)));
Director::redirect('Security/passwordsent/?email=' . urlencode($data['Email'])); Director::redirect('Security/passwordsent/' . urlencode($data['Email']));
} else if($data['Email']) { } else if($data['Email']) {
$this->sessionMessage( $this->sessionMessage(

View File

@ -52,6 +52,14 @@ class Security extends Controller {
*/ */
protected static $useSalt = true; protected static $useSalt = true;
/**
* Showing "Remember me"-checkbox
* on loginform, and saving encrypted credentials to a cookie.
*
* @var bool
*/
public static $autologin_enabled = true;
/** /**
* Location of word list to use for generating passwords * Location of word list to use for generating passwords
* *
@ -207,8 +215,7 @@ class Security extends Controller {
$authenticators = Authenticator::get_authenticators(); $authenticators = Authenticator::get_authenticators();
if(in_array($authenticator, $authenticators)) { if(in_array($authenticator, $authenticators)) {
return call_user_func(array($authenticator, 'get_login_form'), return call_user_func(array($authenticator, 'get_login_form'), $this);
$this);
} }
} }

View File

@ -1,6 +1,6 @@
<% if Markable %><th width="16">&nbsp;</th><% end_if %> <% if Markable %><th width="16">&nbsp;</th><% end_if %>
<th><i>$SummaryTitle</i></th> <th><i>$SummaryTitle</i></th>
<% control SummaryFields %> <% control SummaryFields %>
<th<% if Function %> class="$Function"<% end_if %>>$SummaryValue</th> <th class="field-$Name.HTMLATT<% if Function %> $Function<% end_if %>">$SummaryValue</th>
<% end_control %> <% end_control %>
<% if Can(delete) %><th width="18">&nbsp;</th><% end_if %> <% if Can(delete) %><th width="18">&nbsp;</th><% end_if %>

View File

@ -1,9 +1,9 @@
<div id="$id" class="$CSSClasses TableField field"> <div id="$id" class="$CSSClasses field">
<% if Print %><% else %><% include TableListField_PageControls %><% end_if %> <% if Print %><% else %><% include TableListField_PageControls %><% end_if %>
<table class="data"> <table class="data">
<thead> <thead>
<tr> <tr>
<% if Markable %><th width="16">&nbsp;</th><% end_if %> <% if Markable %><th width="16"><% if MarkableTitle %>$MarkableTitle<% else %>&nbsp;<% end_if %></th><% end_if %>
<% if Print %> <% if Print %>
<% control Headings %> <% control Headings %>
<th class="$Name"> <th class="$Name">

View File

@ -241,6 +241,20 @@ class DataObjectTest extends SapphireTest {
), ),
'Changed fields are correctly detected while ignoring type changes (level=2)' 'Changed fields are correctly detected while ignoring type changes (level=2)'
); );
$newPage = new Page();
$newPage->Title = "New Page Title";
$this->assertEquals(
$newPage->getChangedFields(false, 2),
array(
'Title' => array(
'before' => null,
'after' => 'New Page Title',
'level' => 2
)
),
'Initialised fields are correctly detected as full changes'
);
} }
function testRandomSort() { function testRandomSort() {

View File

@ -11,6 +11,11 @@ class TableListFieldTest extends SapphireTest {
"D" => "Col D", "D" => "Col D",
"E" => "Col E", "E" => "Col E",
)); ));
// A TableListField must be inside a form for its links to be generated
$form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldSet(
$table
), new FieldSet());
$result = $table->FieldHolder(); $result = $table->FieldHolder();
// Do a quick check to ensure that some of the D() and getE() values got through // Do a quick check to ensure that some of the D() and getE() values got through
@ -28,6 +33,11 @@ class TableListFieldTest extends SapphireTest {
"D" => "Col D", "D" => "Col D",
"E" => "Col E", "E" => "Col E",
)); ));
// A TableListField must be inside a form for its links to be generated
$form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldSet(
$table
), new FieldSet());
$items = $table->sourceItems(); $items = $table->sourceItems();
$this->assertNotNull($items); $this->assertNotNull($items);
@ -44,6 +54,11 @@ class TableListFieldTest extends SapphireTest {
"D" => "Col D", "D" => "Col D",
"E" => "Col E", "E" => "Col E",
)); ));
// A TableListField must be inside a form for its links to be generated
$form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldSet(
$table
), new FieldSet());
$table->ShowPagination = true; $table->ShowPagination = true;
$table->PageSize = 2; $table->PageSize = 2;
@ -63,6 +78,11 @@ class TableListFieldTest extends SapphireTest {
"D" => "Col D", "D" => "Col D",
"E" => "Col E", "E" => "Col E",
)); ));
// A TableListField must be inside a form for its links to be generated
$form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldSet(
$table
), new FieldSet());
$table->ShowPagination = true; $table->ShowPagination = true;
$table->PageSize = 2; $table->PageSize = 2;
$_REQUEST['ctf']['Tester']['start'] = 2; $_REQUEST['ctf']['Tester']['start'] = 2;
@ -73,6 +93,46 @@ class TableListFieldTest extends SapphireTest {
$itemMap = $items->toDropdownMap("ID", "A") ; $itemMap = $items->toDropdownMap("ID", "A") ;
$this->assertEquals(array(3 => "a3", 4 => "a4"), $itemMap); $this->assertEquals(array(3 => "a3", 4 => "a4"), $itemMap);
} }
function testCsvExport() {
$table = new TableListField("Tester", "TableListFieldTest_CsvExport", array(
"A" => "Col A",
"B" => "Col B"
));
$form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldSet(
$table
), new FieldSet());
$csvResponse = $table->export();
$csvOutput = $csvResponse->getBody();
$this->assertNotEquals($csvOutput, false);
// Create a temporary file and write the CSV to it.
$csvFileName = tempnam(TEMP_FOLDER, 'csv-export');
$csvFile = fopen($csvFileName, 'w');
fwrite($csvFile, $csvOutput);
fclose($csvFile);
$csvFile = fopen($csvFileName, 'r');
$csvRow = fgetcsv($csvFile);
$this->assertEquals(
$csvRow,
array('Col A', 'Col B')
);
$csvRow = fgetcsv($csvFile);
$this->assertEquals(
$csvRow,
array('"A field, with a comma"', 'A second field')
);
fclose($csvFile);
unlink($csvFileName);
}
} }
class TableListFieldTest_Obj extends DataObject implements TestOnly { class TableListFieldTest_Obj extends DataObject implements TestOnly {
@ -89,5 +149,17 @@ class TableListFieldTest_Obj extends DataObject implements TestOnly {
function getE() { function getE() {
return $this->A . '-e'; return $this->A . '-e';
} }
} }
class TableListFieldTest_CsvExport extends DataObject implements TestOnly {
static $db = array(
"A" => "Varchar",
"B" => "Varchar"
);
}
class TableListFieldTest_TestController extends Controller {
function Link() {
return "TableListFieldTest_TestController/";
}
}

View File

@ -19,4 +19,8 @@ TableListFieldTest_Obj:
A: a5 A: a5
B: b5 B: b5
C: c5 C: c5
TableListFieldTest_CsvExport:
exportone:
A: "\"A field, with a comma\""
B: A second field

View File

@ -10,6 +10,33 @@ class SecurityTest extends FunctionalTest {
protected $autoFollowRedirection = false; protected $autoFollowRedirection = false;
protected $priorAuthenticators = array();
protected $priorDefaultAuthenticator = null;
function setUp() {
// This test assumes that MemberAuthenticator is present and the default
$this->priorAuthenticators = Authenticator::get_authenticators();
$this->priorDefaultAuthenticator = Authenticator::get_default_authenticator();
Authenticator::register('MemberAuthenticator');
Authenticator::set_default_authenticator('MemberAuthenticator');
parent::setUp();
}
function tearDown() {
// Restore selected authenticator
// MemberAuthenticator might not actually be present
if(!in_array('MemberAuthenticator', $this->priorAuthenticators)) {
Authenticator::unregister('MemberAuthenticator');
}
Authenticator::set_default_authenticator($this->priorDefaultAuthenticator);
parent::tearDown();
}
/** /**
* Test that the login form redirects to the change password form after logging in with an expired password * Test that the login form redirects to the change password form after logging in with an expired password
*/ */