diff --git a/core/model/DataObject.php b/core/model/DataObject.php index ab4778cab..57a2a40fd 100755 --- a/core/model/DataObject.php +++ b/core/model/DataObject.php @@ -3419,6 +3419,21 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity return $entities; } + /** + * Returns true if the given method/parameter has a value + * (Uses the DBField::hasValue if the parameter is a database field) + * @param string $funcName The function name. + * @param array $args The arguments. + * @return boolean + */ + function hasValue($funcName, $args = null) { + $field = $this->dbObject($funcName); + if ( $field ) { + return $field->hasValue(); + } else { + return parent::hasValue($funcName, $args); + } + } } ?> diff --git a/core/model/fieldtypes/StringField.php b/core/model/fieldtypes/StringField.php new file mode 100644 index 000000000..c59dee366 --- /dev/null +++ b/core/model/fieldtypes/StringField.php @@ -0,0 +1,79 @@ +false). See {@link StringField::setOptions()} for information on the available options + * @return unknown_type + */ + function __construct($name = null, $options = array()) { + // Workaround: The singleton pattern calls this constructor with true/1 as the second parameter, so we must ignore it + if(is_array($options)){ + $this->setOptions($options); + } + parent::__construct($name); + } + + /** + * Update the optional parameters for this field. + * @param $options array of options + * The options allowed are: + * + * @return unknown_type + */ + function setOptions(array $options = array()) { + if(array_key_exists("nullifyEmpty", $options)) { + $this->nullifyEmpty = $options["nullifyEmpty"] ? true : false; + } + } + + /** + * Set whether this field stores empty strings rather than converting them to null + * @param $value boolean True if empty strings are to be converted to null + * @return + */ + function setNullifyEmpty($value) { + $this->nullifyEmpty == $value ? true : false; + } + /** + * Get whether this field stores empty strings rather than converting them to null + * @return bool True if empty strings are to be converted to null + */ + function getNullifyEmpty() { + return $this->nullifyEmpty; + } + + /** + * (non-PHPdoc) + * @see core/model/fieldtypes/DBField#hasValue() + */ + function hasValue() { + return ($this->value || $this->value == '0') || ( !$this->nullifyEmpty && $this->value === ''); + } + + /** + * (non-PHPdoc) + * @see core/model/fieldtypes/DBField#prepValueForDB($value) + */ + function prepValueForDB($value) { + if ( !$this->nullifyEmpty && $value === '' ) { + return "'" . Convert::raw2sql($value) . "'"; + } else { + return parent::prepValueForDB($value); + } + } + +} diff --git a/core/model/fieldtypes/Text.php b/core/model/fieldtypes/Text.php index c5aa45783..580e37d97 100644 --- a/core/model/fieldtypes/Text.php +++ b/core/model/fieldtypes/Text.php @@ -4,21 +4,21 @@ * @package sapphire * @subpackage model */ -class Text extends DBField { +class Text extends StringField { static $casting = array( "AbsoluteLinks" => "HTMLText", ); + /** + * (non-PHPdoc) + * @see DBField::requireField() + */ function requireField() { $parts=Array('datatype'=>'mediumtext', 'character set'=>'utf8', 'collate'=>'utf8_general_ci', 'arrayValue'=>$this->arrayValue); $values=Array('type'=>'text', 'parts'=>$parts); DB::requireField($this->tableName, $this->name, $values, $this->default); } - function hasValue() { - return ($this->value || $this->value == '0'); - } - /** * Limit this field's content by a number of words. * CAUTION: This is not XML safe. Please use @@ -42,13 +42,25 @@ class Text extends DBField { return $ret; } + /** + * Return the value of the field stripped of html tags + * @return string + */ function NoHTML() { return strip_tags($this->value); } + /** + * Return the value of the field with XML tags escaped. + * @return string + */ function EscapeXML() { return str_replace(array('&','<','>','"'), array('&','<','>','"'), $this->value); } + /** + * Return the value of the field with relative links converted to absolute urls. + * @return string + */ function AbsoluteLinks() { return HTTP::absoluteURLs($this->value); } @@ -292,13 +304,26 @@ class Text extends DBField { } } + /** + * (non-PHPdoc) + * @see DBField::scaffoldFormField() + */ public function scaffoldFormField($title = null, $params = null) { - return new TextareaField($this->name, $title); + if($this->nullifyEmpty) { + // We can have an empty field so we need to let the user specifically set null value in the field. + return new NullableField(new TextareaField($this->name, $title)); + } else { + return new TextareaField($this->name, $title); + } } + /** + * (non-PHPdoc) + * @see DBField::scaffoldSearchField() + */ public function scaffoldSearchField($title = null, $params = null) { return new TextField($this->name, $title); } } -?> \ No newline at end of file +?> diff --git a/core/model/fieldtypes/Varchar.php b/core/model/fieldtypes/Varchar.php index 7833d9338..195669710 100644 --- a/core/model/fieldtypes/Varchar.php +++ b/core/model/fieldtypes/Varchar.php @@ -4,30 +4,48 @@ * @package sapphire * @subpackage model */ -class Varchar extends DBField { +class Varchar extends StringField { protected $size; - - function __construct($name, $size = 50) { + + /** + * Construct a new short text field + * @param $name string The name of the field + * @param $size int The maximum size of the field, in terms of characters + * @param $options array Optional parameters, e.g. array("nullifyEmpty"=>false). See {@link StringField::setOptions()} for information on the available options + * @return unknown_type + */ + function __construct($name, $size = 50, $options = array()) { $this->size = $size ? $size : 50; - parent::__construct($name); + parent::__construct($name, $options); } + /** + * (non-PHPdoc) + * @see DBField::requireField() + */ function requireField() { - $parts=Array('datatype'=>'varchar', 'precision'=>$this->size, 'character set'=>'utf8', 'collate'=>'utf8_general_ci', 'arrayValue'=>$this->arrayValue); - $values=Array('type'=>'varchar', 'parts'=>$parts); + $parts = array( + 'datatype'=>'varchar', + 'precision'=>$this->size, + 'character set'=>'utf8', + 'collate'=>'utf8_general_ci', + 'arrayValue'=>$this->arrayValue + ); + + $values = array( + 'type' => 'varchar', + 'parts' => $parts + ); + DB::requireField($this->tableName, $this->name, $values); } - function hasValue() { - return ($this->value || $this->value == '0'); - } - /** * Return the first letter of the string followed by a . */ function Initial() { - if($this->value) return $this->value[0] . '.'; + if($this->hasValue()) return $this->value[0] . '.'; } /** @@ -37,15 +55,38 @@ class Varchar extends DBField { if(ereg('^[a-zA-Z]+://', $this->value)) return $this->value; else return "http://" . $this->value; } - + + /** + * Return the value of the field in rich text format + * @return string + */ function RTF() { return str_replace("\n", '\par ', $this->value); } - + + /** + * Returns the value of the string, limited to the specified number of characters + * @param $limit int Character limit + * @param $add string Extra string to add to the end of the limited string + * @return string + */ function LimitCharacters($limit = 20, $add = "...") { $value = trim($this->value); return (strlen($value) > $limit) ? substr($value, 0, $limit) . $add : $value; } + + /** + * (non-PHPdoc) + * @see DBField::scaffoldFormField() + */ + public function scaffoldFormField($title = null, $params = null) { + if ( !$this->nullifyEmpty ) { + // We can have an empty field so we need to let the user specifically set null value in the field. + return new NullableField(new TextField($this->name, $title)); + } else { + return parent::scaffoldFormField($title); + } + } } -?> \ No newline at end of file +?> diff --git a/forms/NullableField.php b/forms/NullableField.php new file mode 100644 index 000000000..65bfa3f06 --- /dev/null +++ b/forms/NullableField.php @@ -0,0 +1,118 @@ +Value() or $field->dataValue() + * + * You can specify the label to use for the "is null" checkbox. If you want to use I8N for this label then specify it like this: + * $field->setIsNullLabel(_T(SOME_MODULE_ISNULL_LABEL, "Is Null"); + * + * @author Pete Bacon Darwin + * + */ +class NullableField extends FormField { + /** + * The field that holds the value of this field + * @var FormField + */ + protected $valueField; + + /** + * The label to show next to the is null check box. + * @var string + */ + protected $isNullLabel; + + + /** + * Create a new nullable field + * @param $valueField + * @return NullableField + */ + function __construct(FormField $valueField, $isNullLabel = null) { + $this->valueField = $valueField; + $this->isNullLabel = $isNullLabel; + if ( is_null($this->isNullLabel) ) { + // Set a default label if one is not provided. + $this->isNullLabel = _t('NullableField.IsNullLabel', 'Is Null', PR_HIGH); + } + parent::__construct($valueField->Name(), $valueField->Title(), $valueField->Value(), $valueField->getForm(), $valueField->RightTitle()); + $this->readonly = $valueField->isReadonly(); + } + + /** + * Get the label used for the Is Null checkbox. + * @return string + */ + function getIsNullLabel() { + return $this->isNullLabel; + } + /** + * Set the label used for the Is Null checkbox. + * @param $isNulLabel string + * @return + */ + function setIsNullLabel(string $isNulLabel){ + $this->isNullLabel = $isNulLabel; + } + + /** + * Get the id used for the Is Null check box. + * @return string + */ + function getIsNullId() { + return $this->Name() . "_IsNull"; + } + + /** + * (non-PHPdoc) + * @see sapphire/forms/FormField#Field() + */ + function Field() { + if ( $this->isReadonly()) { + $nullableCheckbox = new CheckboxField_Readonly($this->getIsNullId()); + } else { + $nullableCheckbox = new CheckboxField($this->getIsNullId()); + } + $nullableCheckbox->setValue(is_null($this->dataValue())); + return $this->valueField->Field() . ' ' . $nullableCheckbox->Field() . ' ' . $this->getIsNullLabel().''; + } + + /** + * Value is sometimes an array, and sometimes a single value, so we need to handle both cases + */ + function setValue($value, $data = null) { + if ( is_array($data) && array_key_exists($this->getIsNullId(), $data) && $data[$this->getIsNullId()] ) { + $value = null; + } + $this->valueField->setValue($value); + parent::setValue($value); + } + + /** + * (non-PHPdoc) + * @see forms/FormField#setName($name) + */ + function setName($name) { + // We need to pass through the name change to the underlying value field. + $this->valueField->setName($name); + parent::setName($name); + } + + /** + * (non-PHPdoc) + * @see sapphire/forms/FormField#debug() + */ + function debug() { + $result = "$this->class ($this->name: $this->title : $this->message) = "; + $result .= (is_null($this->value)) ? "<>" : $this->value; + return result; + } +} diff --git a/tests/fieldtypes/DBFieldTest.php b/tests/fieldtypes/DBFieldTest.php index b7fd540c1..6e25c0b62 100644 --- a/tests/fieldtypes/DBFieldTest.php +++ b/tests/fieldtypes/DBFieldTest.php @@ -1,5 +1,12 @@ assertEquals("'test'", singleton('Varchar')->prepValueForDB('test')); $this->assertEquals("'123'", singleton('Varchar')->prepValueForDB(123)); + /* AllowEmpty Varchar behaviour */ + $varcharField = new Varchar("testfield", 50, array("nullifyEmpty"=>false)); + $this->assertSame("'0'", $varcharField->prepValueForDB(0)); + $this->assertSame("null", $varcharField->prepValueForDB(null)); + $this->assertSame("null", $varcharField->prepValueForDB(false)); + $this->assertSame("''", $varcharField->prepValueForDB('')); + $this->assertSame("'0'", $varcharField->prepValueForDB('0')); + $this->assertSame("'1'", $varcharField->prepValueForDB(1)); + $this->assertSame("'1'", $varcharField->prepValueForDB(true)); + $this->assertSame("'1'", $varcharField->prepValueForDB('1')); + $this->assertSame("'00000'", $varcharField->prepValueForDB('00000')); + $this->assertSame("'0'", $varcharField->prepValueForDB(0000)); + $this->assertSame("'test'", $varcharField->prepValueForDB('test')); + $this->assertSame("'123'", $varcharField->prepValueForDB(123)); + unset($varcharField); + /* Text behaviour */ $this->assertEquals("'0'", singleton('Text')->prepValueForDB(0)); $this->assertEquals("null", singleton('Text')->prepValueForDB(null)); @@ -70,6 +93,22 @@ class DBFieldTest extends SapphireTest { $this->assertEquals("'0'", singleton('Text')->prepValueForDB(0000)); $this->assertEquals("'test'", singleton('Text')->prepValueForDB('test')); $this->assertEquals("'123'", singleton('Text')->prepValueForDB(123)); + + /* AllowEmpty Text behaviour */ + $textField = new Text("testfield", array("nullifyEmpty"=>false)); + $this->assertSame("'0'", $textField->prepValueForDB(0)); + $this->assertSame("null", $textField->prepValueForDB(null)); + $this->assertSame("null", $textField->prepValueForDB(false)); + $this->assertSame("''", $textField->prepValueForDB('')); + $this->assertSame("'0'", $textField->prepValueForDB('0')); + $this->assertSame("'1'", $textField->prepValueForDB(1)); + $this->assertSame("'1'", $textField->prepValueForDB(true)); + $this->assertSame("'1'", $textField->prepValueForDB('1')); + $this->assertSame("'00000'", $textField->prepValueForDB('00000')); + $this->assertSame("'0'", $textField->prepValueForDB(0000)); + $this->assertSame("'test'", $textField->prepValueForDB('test')); + $this->assertSame("'123'", $textField->prepValueForDB(123)); + unset($textField); /* Time behaviour */ $time = singleton('Time'); @@ -91,9 +130,45 @@ class DBFieldTest extends SapphireTest { $this->assertEquals("00:00:00", $time->getValue()); $time->setValue('00:00:00'); $this->assertEquals("00:00:00", $time->getValue()); - } + function testHasValue() { + $varcharField = new Varchar("testfield"); + $this->assertTrue($varcharField->getNullifyEmpty()); + $varcharField->setValue('abc'); + $this->assertTrue($varcharField->hasValue()); + $varcharField->setValue(''); + $this->assertFalse($varcharField->hasValue()); + $varcharField->setValue(null); + $this->assertFalse($varcharField->hasValue()); + + $varcharField = new Varchar("testfield", 50, array('nullifyEmpty'=>false)); + $this->assertFalse($varcharField->getNullifyEmpty()); + $varcharField->setValue('abc'); + $this->assertTrue($varcharField->hasValue()); + $varcharField->setValue(''); + $this->assertTrue($varcharField->hasValue()); + $varcharField->setValue(null); + $this->assertFalse($varcharField->hasValue()); + + $textField = new Text("testfield"); + $this->assertTrue($textField->getNullifyEmpty()); + $textField->setValue('abc'); + $this->assertTrue($textField->hasValue()); + $textField->setValue(''); + $this->assertFalse($textField->hasValue()); + $textField->setValue(null); + $this->assertFalse($textField->hasValue()); + + $textField = new Text("testfield", array('nullifyEmpty'=>false)); + $this->assertFalse($textField->getNullifyEmpty()); + $textField->setValue('abc'); + $this->assertTrue($textField->hasValue()); + $textField->setValue(''); + $this->assertTrue($textField->hasValue()); + $textField->setValue(null); + $this->assertFalse($textField->hasValue()); + } } -?> \ No newline at end of file +?> diff --git a/tests/forms/FormFieldTest.php b/tests/forms/FormFieldTest.php index 02b58a558..dbdf8c7ef 100644 --- a/tests/forms/FormFieldTest.php +++ b/tests/forms/FormFieldTest.php @@ -34,8 +34,11 @@ class FormFieldTest extends SapphireTest { if($constructor->getNumberOfRequiredParameters() > 1) continue; if($fieldClass == 'CompositeField' || is_subclass_of($fieldClass, 'CompositeField')) continue; - $instance = new $fieldClass("{$fieldClass}_instance"); - + if ( $fieldClass = 'NullableField' ) { + $instance = new $fieldClass(new TextField("{$fieldClass}_instance")); + } else { + $instance = new $fieldClass("{$fieldClass}_instance"); + } $isReadonlyBefore = $instance->isReadonly(); $readonlyInstance = $instance->performReadonlyTransformation(); $this->assertEquals( @@ -64,8 +67,12 @@ class FormFieldTest extends SapphireTest { if($constructor->getNumberOfRequiredParameters() > 1) continue; if($fieldClass == 'CompositeField' || is_subclass_of($fieldClass, 'CompositeField')) continue; - $instance = new $fieldClass("{$fieldClass}_instance"); - + if ( $fieldClass = 'NullableField' ) { + $instance = new $fieldClass(new TextField("{$fieldClass}_instance")); + } else { + $instance = new $fieldClass("{$fieldClass}_instance"); + } + $isDisabledBefore = $instance->isDisabled(); $disabledInstance = $instance->performDisabledTransformation(); $this->assertEquals( diff --git a/tests/forms/NullableFieldTests.php b/tests/forms/NullableFieldTests.php new file mode 100644 index 000000000..83f13fecf --- /dev/null +++ b/tests/forms/NullableFieldTests.php @@ -0,0 +1,124 @@ +assertEquals("Field1", $a->Name()); + $this->assertEquals("Field 1", $a->Title()); + $this->assertSame("abc", $a->Value()); + $this->assertSame("abc", $a->dataValue()); + $field = $a->Field(); + $this->assertTag(array( + 'tag'=>'input', + 'id'=>'Field1', + 'attributes'=>array('type'=>'text', 'name'=>'Field1', 'value'=>'abc'), + ), $field); + $this->assertTag(array( + 'tag'=>'input', + 'id'=>'Field1_IsNull', + 'attributes'=>array('type'=>'checkbox', 'name'=>'Field1_IsNull', 'value'=>'1'), + ), $field); + } + /** + * Test that the NullableField works when it wraps a TextField containing an empty string + */ + function testWithEmpty() { + $a = new NullableField(new TextField("Field1", "Field 1", "")); + $this->assertEquals("Field1", $a->Name()); + $this->assertEquals("Field 1", $a->Title()); + $this->assertSame("", $a->Value()); + $this->assertSame("", $a->dataValue()); + $field = $a->Field(); + $this->assertTag(array( + 'tag'=>'input', + 'id'=>'Field1', + 'attributes'=>array('type'=>'text', 'name'=>'Field1', 'value'=>''), + ), $field); + $this->assertTag(array( + 'tag'=>'input', + 'id'=>'Field1_IsNull', + 'attributes'=>array('type'=>'checkbox', 'name'=>'Field1_IsNull', 'value'=>'1'), + ), $field); + } + /** + * Test that the NullableField works when it wraps a TextField containing a null string + */ + function testWithNull() { + $a = new NullableField(new TextField("Field1", "Field 1", null)); + $this->assertEquals("Field1", $a->Name()); + $this->assertEquals("Field 1", $a->Title()); + $this->assertSame(null, $a->Value()); + $this->assertSame(null, $a->dataValue()); + $field = $a->Field(); + $this->assertTag(array( + 'tag'=>'input', + 'id'=>'Field1', + 'attributes'=>array('type'=>'text', 'name'=>'Field1', 'value'=>''), + ), $field); + $this->assertTag(array( + 'tag'=>'input', + 'id'=>'Field1_IsNull', + 'attributes'=>array('type'=>'checkbox', 'name'=>'Field1_IsNull', 'value'=>'1', 'checked'=>'checked'), + ), $field); + unset($a); + } + + /** + * Test NullableField::setValue works when passed simple values + */ + function testSetValueSimple() { + $a = new NullableField(new TextField("Field1", "Field 1")); + $a->setValue("abc"); + $this->assertSame("abc", $a->dataValue()); + + $a = new NullableField(new TextField("Field1", "Field 1")); + $a->setValue(null); + $this->assertSame(null, $a->dataValue()); + + $a = new NullableField(new TextField("Field1", "Field 1", "abc")); + $a->setValue(null); + $this->assertSame(null, $a->dataValue()); + + $a = new NullableField(new TextField("Field1", "Field 1", "abc")); + $a->setValue("xyz"); + $this->assertSame("xyz", $a->dataValue()); + + $a = new NullableField(new TextField("Field1", "Field 1")); + $a->setValue(""); + $this->assertSame("", $a->dataValue()); + + $a = new NullableField(new TextField("Field1", "Field 1", "abc")); + $a->setValue(""); + $this->assertSame("", $a->dataValue()); + } + + /** + * Test NullableField::setValue works when passed an array values, + * which happens when the form submits. + */ + function testSetValueArray() { + $a = new NullableField(new TextField("Field1", "Field 1")); + $a->setValue("abc", array("Field1_IsNull"=>false)); + $this->assertSame("abc", $a->dataValue()); + + $a = new NullableField(new TextField("Field1", "Field 1")); + $a->setValue("", array("Field1_IsNull"=>false)); + $this->assertSame("", $a->dataValue()); + + $a = new NullableField(new TextField("Field1", "Field 1")); + $a->setValue("", array("Field1_IsNull"=>true)); + $this->assertSame(null, $a->dataValue()); + } + +} +