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:
+ *
- "nullifyEmpty"
+ * This is a boolean flag.
+ * True (the default) means that empty strings are automatically converted to nulls to be stored in the database.
+ * Set it to false to ensure that nulls and empty strings are kept intact in the database.
+ *
+ * @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());
+ }
+
+}
+