From: ebernhardson Date: Sat, 27 Apr 2013 01:49:02 +0000 (-0700) Subject: HTMLCheckMatrix support for forcing options on/off X-Git-Tag: 1.31.0-rc.0~19648^2 X-Git-Url: http://git.cyclocoop.org/%22.htmlspecialchars%28%24url_syndic%29.%22?a=commitdiff_plain;h=3998d1e6371f403775dd131004ceb9aca5fa45fc;p=lhc%2Fweb%2Fwiklou.git HTMLCheckMatrix support for forcing options on/off Bug: 47743 Renamed remove-options parameter of HTMLCheckMatrix to force-options-on and added additional force-options-off. Minor refactor of PreferencesForm::filterDataForSubmit to move class specific code into their respective classes. Change-Id: I61a6b2bcce3102e2350088912ee77620a9f678f9 --- diff --git a/includes/HTMLForm.php b/includes/HTMLForm.php index b7bdcfd20c..64498fd14e 100644 --- a/includes/HTMLForm.php +++ b/includes/HTMLForm.php @@ -1807,9 +1807,33 @@ class HTMLCheckField extends HTMLFormField { * A checkbox matrix * Operates similarly to HTMLMultiSelectField, but instead of using an array of * options, uses an array of rows and an array of columns to dynamically - * construct a matrix of options. + * construct a matrix of options. The tags used to identify a particular cell + * are of the form "columnName-rowName" + * + * Options: + * columns: Required list of columns in the matrix. + * rows: Required list of rows in the matrix. + * force-options-on: Accepts array of column-row tags to be displayed as enabled + * but unavailable to change + * force-options-off: Accepts array of column-row tags to be displayed as disabled + * but unavailable to change. */ -class HTMLCheckMatrix extends HTMLFormField { +class HTMLCheckMatrix extends HTMLFormField implements HTMLNestedFilterable { + + static private $requiredParams = array( + // Required by underlying HTMLFormField + 'fieldname', + // Required by HTMLCheckMatrix + 'rows', 'columns' + ); + + public function __construct( $params ) { + $missing = array_diff( self::$requiredParams, array_keys( $params ) ); + if ( $missing ) { + throw HTMLFormFieldRequiredOptionsException::create( $this, $missing ); + } + parent::__construct( $params ); + } function validate( $value, $alldata ) { $rows = $this->mParams['rows']; @@ -1873,23 +1897,25 @@ class HTMLCheckMatrix extends HTMLFormField { foreach ( $rows as $rowLabel => $rowTag ) { $rowContents = Html::rawElement( 'td', array(), $rowLabel ); foreach ( $columns as $columnTag ) { - // Knock out any options that are not wanted - if ( isset( $this->mParams['remove-options'] ) - && in_array( "$columnTag-$rowTag", $this->mParams['remove-options'] ) ) - { - $rowContents .= Html::rawElement( 'td', array(), ' ' ); - } else { - // Construct the checkbox - $thisAttribs = array( - 'id' => "{$this->mID}-$columnTag-$rowTag", - 'value' => $columnTag . '-' . $rowTag - ); - $checkbox = Xml::check( - $this->mName . '[]', - in_array( $columnTag . '-' . $rowTag, (array)$value, true ), - $attribs + $thisAttribs ); - $rowContents .= Html::rawElement( 'td', array(), $checkbox ); + $thisTag = "$columnTag-$rowTag"; + // Construct the checkbox + $thisAttribs = array( + 'id' => "{$this->mID}-$thisTag", + 'value' => $thisTag, + ); + $checked = in_array( $thisTag, (array)$value, true); + if ( $this->isTagForcedOff( $thisTag ) ) { + $checked = false; + $thisAttribs['disabled'] = 1; + } elseif ( $this->isTagForcedOn( $thisTag ) ) { + $checked = true; + $thisAttribs['disabled'] = 1; } + $rowContents .= Html::rawElement( + 'td', + array(), + Xml::check( "{$this->mName}[]", $checked, $attribs + $thisAttribs ) + ); } $tableContents .= Html::rawElement( 'tr', array(), "\n$rowContents\n" ); } @@ -1901,6 +1927,16 @@ class HTMLCheckMatrix extends HTMLFormField { return $html; } + protected function isTagForcedOff( $tag ) { + return isset( $this->mParams['force-options-off'] ) + && in_array( $tag, $this->mParams['force-options-off'] ); + } + + protected function isTagForcedOn( $tag ) { + return isset( $this->mParams['force-options-on'] ) + && in_array( $tag, $this->mParams['force-options-on'] ); + } + /** * Get the complete table row for the input, including help text, * labels, and whatever. @@ -1965,6 +2001,27 @@ class HTMLCheckMatrix extends HTMLFormField { return array(); } } + + function filterDataForSubmit( $data ) { + $columns = HTMLFormField::flattenOptions( $this->mParams['columns'] ); + $rows = HTMLFormField::flattenOptions( $this->mParams['rows'] ); + $res = array(); + foreach ( $columns as $column ) { + foreach ( $rows as $row ) { + // Make sure option hasn't been forced + $thisTag = "$column-$row"; + if ( $this->isTagForcedOff( $thisTag ) ) { + $res[$thisTag] = false; + } elseif ($this->isTagForcedOn( $thisTag ) ) { + $res[$thisTag] = true; + } else { + $res[$thisTag] = in_array( $thisTag, $data ); + } + } + } + + return $res; + } } /** @@ -2106,7 +2163,7 @@ class HTMLSelectOrOtherField extends HTMLTextField { /** * Multi-select field */ -class HTMLMultiSelectField extends HTMLFormField { +class HTMLMultiSelectField extends HTMLFormField implements HTMLNestedFilterable { function validate( $value, $alldata ) { $p = parent::validate( $value, $alldata ); @@ -2199,6 +2256,17 @@ class HTMLMultiSelectField extends HTMLFormField { } } + function filterDataForSubmit( $data ) { + $options = HTMLFormField::flattenOptions( $this->mParams['options'] ); + + $res = array(); + foreach ( $options as $opt ) { + $res["$opt"] = in_array( $opt, $data ); + } + + return $res; + } + protected function needsLabel() { return false; } @@ -2638,3 +2706,22 @@ class HTMLApiField extends HTMLFormField { return ''; } } + +interface HTMLNestedFilterable { + /** + * Support for seperating multi-option preferences into multiple preferences + * Due to lack of array support. + * @param $data array + */ + function filterDataForSubmit( $data ); +} + +class HTMLFormFieldRequiredOptionsException extends MWException { + static public function create( HTMLFormField $field, array $missing ) { + return new self( sprintf( + "Form type `%s` expected the following parameters to be set: %s", + get_class( $field ), + implode( ', ', $missing ) + ) ); + } +} diff --git a/includes/Preferences.php b/includes/Preferences.php index 44c87f016c..67b6deb720 100644 --- a/includes/Preferences.php +++ b/includes/Preferences.php @@ -1558,40 +1558,19 @@ class PreferencesForm extends HTMLForm { } /** + * Separate multi-option preferences into multiple preferences, since we + * have to store them separately * @param $data array * @return array */ function filterDataForSubmit( $data ) { - // Support for separating multi-option preferences into multiple preferences - // Due to lack of array support. foreach ( $this->mFlatFields as $fieldname => $field ) { - $info = $field->mParams; - - if ( $field instanceof HTMLMultiSelectField ) { - $options = HTMLFormField::flattenOptions( $info['options'] ); + if ( $field instanceof HTMLNestedFilterable ) { + $info = $field->mParams; $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $fieldname; - - foreach ( $options as $opt ) { - $data["$prefix$opt"] = in_array( $opt, $data[$fieldname] ); + foreach ( $field->filterDataForSubmit( $data[$fieldname] ) as $key => $value ) { + $data["$prefix-$key"] = $value; } - - unset( $data[$fieldname] ); - - } elseif ( $field instanceof HTMLCheckMatrix ) { - $columns = HTMLFormField::flattenOptions( $info['columns'] ); - $rows = HTMLFormField::flattenOptions( $info['rows'] ); - $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $fieldname; - foreach ( $columns as $column ) { - foreach ( $rows as $row ) { - // Make sure option hasn't been removed - if ( !isset( $info['remove-options'] ) - || !in_array( "$column-$row", $info['remove-options'] ) ) - { - $data["$prefix-$column-$row"] = in_array( "$column-$row", $data[$fieldname] ); - } - } - } - unset( $data[$fieldname] ); } } diff --git a/tests/phpunit/includes/HTMLCheckMatrixTest.php b/tests/phpunit/includes/HTMLCheckMatrixTest.php new file mode 100644 index 0000000000..4e50e04894 --- /dev/null +++ b/tests/phpunit/includes/HTMLCheckMatrixTest.php @@ -0,0 +1,102 @@ + array( 'r1', 'r2' ), + 'columns' => array( 'c1', 'c2' ), + 'fieldname' => 'test', + ); + + public function testPlainInstantiation() { + try { + $form = new HTMLCheckMatrix( array() ); + } catch ( MWException $e ) { + $this->assertInstanceOf( 'HTMLFormFieldRequiredOptionsException', $e ); + return; + } + + $this->fail('Expected MWException indicating missing parameters but none was thrown.'); + } + + public function testInstantiationWithMinimumRequiredParameters() { + $form = new HTMLCheckMatrix( self::$defaultOptions ); + $this->assertTrue(true); // form instantiation must throw exception on failure + } + + public function testValidateCallsUserDefinedValidationCallback() { + $called = false; + $field = new HTMLCheckMatrix( self::$defaultOptions + array( + 'validation-callback' => function() use ( &$called ) { + $called = true; + return false; + }, + ) ); + $this->assertEquals( false, $this->validate( $field, array() ) ); + $this->assertTrue( $called ); + } + + public function testValidateRequiresArrayInput() { + $field = new HTMLCheckMatrix( self::$defaultOptions ); + $this->assertEquals( false, $this->validate( $field, null ) ); + $this->assertEquals( false, $this->validate( $field, true ) ); + $this->assertEquals( false, $this->validate( $field, 'abc' ) ); + $this->assertEquals( false, $this->validate( $field, new stdClass ) ); + $this->assertEquals( true, $this->validate( $field, array() ) ); + } + + public function testValidateAllowsOnlyKnownTags() { + $field = new HTMLCheckMatrix( self::$defaultOptions ); + $this->assertInternalType('string', $this->validate( $field, array( 'foo' ) ) ); + } + + public function testValidateAcceptsPartialTagList() { + $field = new HTMLCheckMatrix( self::$defaultOptions ); + $this->assertTrue( $this->validate( $field, array() ) ); + $this->assertTrue( $this->validate( $field, array( 'c1-r1' ) ) ); + $this->assertTrue( $this->validate( $field, array( 'c1-r1', 'c1-r2', 'c2-r1', 'c2-r2' ) ) ); + } + + /** + * This form object actually has no visibility into what happens later on, but essentially + * if the data submitted by the user passes validate the following is run: + * foreach ( $field->filterDataForSubmit( $data ) as $k => $v ) { + * $user->setOption( $k, $v ); + * } + */ + public function testValuesForcedOnRemainOn() { + $field = new HTMLCheckMatrix( self::$defaultOptions + array( + 'force-options-on' => array( 'c2-r1' ), + ) ); + $expected = array( + 'c1-r1' => false, + 'c1-r2' => false, + 'c2-r1' => true, + 'c2-r2' => false, + ); + $this->assertEquals($expected, $field->filterDataForSubmit( array() ) ); + } + + public function testValuesForcedOffRemainOff() { + $field = new HTMLCheckMatrix( self::$defaultOptions + array( + 'force-options-off' => array( 'c1-r2', 'c2-r2' ), + ) ); + $expected = array( + 'c1-r1' => true, + 'c1-r2' => false, + 'c2-r1' => true, + 'c2-r2' => false, + ); + // array_keys on the result simulates submitting all fields checked + $this->assertEquals($expected, $field->filterDataForSubmit( array_keys($expected) ) ); + } + + protected function validate( HTMLFormField $field, $submitted ) { + return $field->validate( + $submitted, + array( self::$defaultOptions['fieldname'] => $submitted ) + ); + } +}