HTMLCheckMatrix support for forcing options on/off
authorebernhardson <ebernhardson@wikimedia.org>
Sat, 27 Apr 2013 01:49:02 +0000 (18:49 -0700)
committerkaldari <kaldari@gmail.com>
Fri, 17 May 2013 00:21:43 +0000 (17:21 -0700)
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

includes/HTMLForm.php
includes/Preferences.php
tests/phpunit/includes/HTMLCheckMatrixTest.php [new file with mode: 0644]

index b7bdcfd..64498fd 100644 (file)
@@ -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(), '&#160;' );
-                               } 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 )
+               ) );
+       }
+}
index 44c87f0..67b6deb 100644 (file)
@@ -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 (file)
index 0000000..4e50e04
--- /dev/null
@@ -0,0 +1,102 @@
+<?php
+
+/**
+ * Unit tests for the HTMLCheckMatrix form field
+ */
+class HtmlCheckMatrixTest extends MediaWikiTestCase {
+       static private $defaultOptions = array(
+               'rows' => 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 )
+               );
+       }
+}