Skip to content
Snippets Groups Projects
class.forms.php 174 KiB
Newer Older
  • Learn to ignore specific revisions
  •     function describeSearchMethod($method) {
            switch ($method) {
            case 'set':
                return __('%s has a value');
            case 'nset':
                return __('%s does not have a value');
            case 'equal':
                return __('%s is %s' /* describes an equality */);
            case 'nequal':
                return __('%s is not %s' /* describes an inequality */);
            case 'contains':
                return __('%s contains "%s"');
            case 'match':
                return __('%s matches pattern %s');
    
            case 'includes':
                return __('%s in (%s)');
            case '!includes':
                return __('%s not in (%s)');
    
        function describeSearch($method, $value, $name=false) {
    
            $name = $name ?: $this->get('name');
    
            $desc = $this->describeSearchMethod($method);
    
            switch ($method) {
                case 'set':
                case 'nset':
                    return sprintf($desc, $name);
                default:
                     return sprintf($desc, $name, $this->toString($value));
            }
    
        function addToQuery($query, $name=false) {
            return $query->values($name ?: $this->get('name'));
        }
    
        /**
         * Similary to to_php() and parse(), except a row from a queryset is
         * passed. The value returned should be what would be retured from
         * parse() or to_php()
         */
        function from_query($row, $name=false) {
            return $row[$name ?: $this->get('name')];
        }
    
    
        /**
         * If the field can be used in a quick filter. To be used, it should
         * also implement getQuickFilterChoices() which should return a list of
         * choices to appear in a quick filter drop-down
         */
        function supportsQuickFilter() {
            return false;
        }
    
        /**
         * Fetch a keyed array of quick filter choices. The keys should be
         * passed later to ::applyQuickFilter() to apply the quick filter to a
         * query. The values should be localized titles for the choices.
         */
        function getQuickFilterChoices() {
            return array();
        }
    
        /**
         * Apply a quick filter selection of this field to the query. The
         * modified query should be returned. Optionally, the orm path / field
         * name can be passed.
         */
        function applyQuickFilter($query, $choice, $name=false) {
            return $query;
        }
    
    
    Jared Hancock's avatar
    Jared Hancock committed
        function getLabel() { return $this->get('label'); }
    
    
        function getSortKeys($path) {
            return array($path);
        }
    
        function getOrmPath($name=false, $query=null) {
            return CustomQueue::getOrmPath($name ?:$this->get('name'), $query);
        }
    
    
        function applyOrderBy($query, $reverse=false, $name=false) {
    
            $col = sprintf('%s%s',
                    $reverse ? '-' : '',
                    $this->getOrmPath($name, $query));
    
            return $query->order_by($col);
        }
    
    
    Jared Hancock's avatar
    Jared Hancock committed
        /**
         * getImpl
         *
         * Magic method that will return an implementation instance of this
         * field based on the simple text value of the 'type' value of this
         * field instance. The list of registered fields is determined by the
         * global get_dynamic_field_types() function. The data from this model
         * will be used to initialize the returned instance.
         *
         * For instance, if the value of this field is 'text', a TextField
         * instance will be returned.
         */
    
        function getImpl($parent=null) {
    
    Jared Hancock's avatar
    Jared Hancock committed
            // Allow registration with ::addFieldTypes and delayed calling
    
            $type = static::getFieldType($this->get('type'));
            $clazz = $type[1];
    
    Jared Hancock's avatar
    Jared Hancock committed
            $inst = new $clazz($this->ht);
            $inst->parent = $parent;
    
            $inst->setForm($this->_form);
    
    Jared Hancock's avatar
    Jared Hancock committed
            return $inst;
    
        function __call($what, $args) {
            // XXX: Throw exception if $this->parent is not set
    
                throw new Exception(sprintf(__('%s: Call to undefined function'),
                    $what));
    
    Jared Hancock's avatar
    Jared Hancock committed
            // BEWARE: DynamicFormField has a __call() which will create a new
            //      FormField instance and invoke __call() on it or bounce
            //      immediately back
    
            return call_user_func_array(
                array($this->parent, $what), $args);
        }
    
    
    Jared Hancock's avatar
    Jared Hancock committed
        function getAnswer() { return $this->answer; }
    
        function setAnswer($ans) { $this->answer = $ans; }
    
        function setValue($value) {
            $this->reset();
            $this->getWidget()->value = $value;
        }
    
    
        /**
         * Fetch a pseudo-random id for this form field. It is used when
         * rendering the widget in the @name attribute emitted in the resulting
         * HTML. The form element is based on the form id, field id and name,
         * and the current user's session id. Therefor, the same form fields
         * will yield differing names for different users. This is used to ward
         * off bot attacks as it makes it very difficult to predict and
         * correlate the form names to the data they represent.
         */
    
    Jared Hancock's avatar
    Jared Hancock committed
        function getFormName() {
    
            $default = $this->get('name') ?: $this->get('id');
    
            if ($this->_form && is_numeric($fid = $this->_form->getFormId()))
    
                return substr(md5(
                    session_id() . '-form-field-id-' . $fid . $default), -14);
            elseif (is_numeric($this->get('id')))
    
                return substr(md5(
                    session_id() . '-field-id-'.$this->get('id')), -16);
    
        function setForm($form) {
            $this->_form = $form;
        }
        function getForm() {
            return $this->_form;
        }
        /**
         * Returns the data source for this field. If created from a form, the
         * data source from the form is returned. Otherwise, if the request is a
         * POST, then _POST is returned.
         */
        function getSource() {
            if ($this->_form)
                return $this->_form->getSource();
            elseif ($_SERVER['REQUEST_METHOD'] == 'POST')
                return $_POST;
            else
                return array();
        }
    
    
        function render($options=array()) {
            $rv = $this->getWidget()->render($options);
    
            if ($v = $this->get('visibility')) {
                $v->emitJavascript($this);
            }
            return $rv;
    
        function renderExtras($options=array()) {
    
        function getMedia() {
            $widget = $this->getWidget();
            return $widget::$media;
        }
    
    
    Jared Hancock's avatar
    Jared Hancock committed
        function getConfigurationOptions() {
            return array();
        }
    
        /**
         * getConfiguration
         *
         * Loads configuration information from database into hashtable format.
         * Also, the defaults from ::getConfigurationOptions() are integrated
         * into the database-backed options, so that if options have not yet
         * been set or a new option has been added and not saved for this field,
         * the default value will be reflected in the returned configuration.
         */
        function getConfiguration() {
            if (!$this->_config) {
                $this->_config = $this->get('configuration');
                if (is_string($this->_config))
                    $this->_config = JsonDataParser::parse($this->_config);
                elseif (!$this->_config)
                    $this->_config = array();
                foreach ($this->getConfigurationOptions() as $name=>$field)
                    if (!isset($this->_config[$name]))
                        $this->_config[$name] = $field->get('default');
            }
            return $this->_config;
        }
    
    
        /**
         * If the [Config] button should be shown to allow for the configuration
         * of this field
         */
    
    Jared Hancock's avatar
    Jared Hancock committed
        function isConfigurable() {
            return true;
        }
    
    
        /**
         * Field type is changeable in the admin interface
         */
        function isChangeable() {
            return true;
        }
    
        /**
         * Field does not contain data that should be saved to the database. Ie.
         * non data fields like section headers
         */
        function hasData() {
            return true;
        }
    
        /**
         * Returns true if the field/widget should be rendered as an entire
         * block in the target form.
         */
        function isBlockLevel() {
            return false;
        }
    
        /**
         * Fields should not be saved with the dynamic data. It is assumed that
         * some static processing will store the data elsewhere.
         */
        function isPresentationOnly() {
    
            return $this->presentation_only;
    
        /**
         * Indicates if the field places data in the `value_id` column. This
         * is currently used by the materialized view system
         */
        function hasIdValue() {
            return false;
        }
    
    
        /**
         * Indicates if the field has subfields accessible via getSubFields()
         * method. Useful for filter integration. Should connect with
         * getFilterData()
         */
        function hasSubFields() {
            return false;
        }
        function getSubFields() {
            return null;
        }
    
    
        function getConfigurationForm($source=null) {
    
    Jared Hancock's avatar
    Jared Hancock committed
            if (!$this->_cform) {
    
                $type = static::getFieldType($this->get('type'));
                $clazz = $type[1];
    
                $T = new $clazz($this->ht);
    
                $config = $this->getConfiguration();
    
                $this->_cform = new SimpleForm($T->getConfigurationOptions(), $source);
    
                    foreach ($this->_cform->getFields() as $name=>$f) {
    
                        if ($config && isset($config[$name]))
    
                            $f->value = $config[$name];
                        elseif ($f->get('default'))
                            $f->value = $f->get('default');
                    }
                }
    
    Jared Hancock's avatar
    Jared Hancock committed
            }
            return $this->_cform;
        }
    
    
        function configure($prop, $value) {
            $this->getConfiguration();
            $this->_config[$prop] = $value;
        }
    
    
        function getWidget($widgetClass=false) {
    
            if (!static::$widget)
    
                throw new Exception(__('Widget not defined for this field'));
    
            if (!isset($this->_widget)) {
    
                $wc = $widgetClass ?: $this->get('widget') ?: static::$widget;
    
    Jared Hancock's avatar
    Jared Hancock committed
                $this->_widget = new $wc($this);
    
                $this->_widget->parseValue();
            }
            return $this->_widget;
    
    
        function getSelectName() {
            $name = $this->get('name') ?: 'field_'.$this->get('id');
            if ($this->hasIdValue())
                $name .= '_id';
    
            return $name;
        }
    
    
        function getTranslateTag($subtag) {
    
            return _H(sprintf('field.%s.%s%s', $subtag, $this->get('id'),
                $this->get('form_id') ? '' : '*internal*'));
    
    Peter Rotich's avatar
    Peter Rotich committed
    
    
        function getLocal($subtag, $default=false) {
            $tag = $this->getTranslateTag($subtag);
            $T = CustomDataTranslation::translate($tag);
            return $T != $tag ? $T : ($default ?: $this->get($subtag));
        }
    
    Peter Rotich's avatar
    Peter Rotich committed
    
        function getEditForm($source=null) {
    
            $fields = array(
                    'field' => $this,
                    'comments' => new TextareaField(array(
                            'id' => 2,
                            'label'=> '',
                            'required' => false,
                            'default' => '',
                            'configuration' => array(
                                'html' => true,
                                'size' => 'small',
                                'placeholder' => __('Optional reason for the update'),
                                )
                            ))
                    );
    
            return new SimpleForm($fields, $source);
        }
    
        function getChanges() {
            $a = $this->to_database($this->getClean());
            $b = $this->to_database($this->answer ? $this->answer->getValue() : $this->get('default'));
            return ($a != $b) ? array($b, $a) : false;
        }
    
    
        function save() {
    
            if (!($changes=$this->getChanges()))
                return true;
    
            if (!($a = $this->answer))
                return false;
    
            $val = $changes[1];
            if (is_array($val)) {
                $a->set('value', $val[0]);
                $a->set('value_id', $val[1]);
            } else {
                $a->set('value', $val);
            }
    
            if (!$a->save(true))
                return false;
    
            return $this->parent->save();
        }
    
    
        static function init($config) {
            return new Static($config);
        }
    
    Jared Hancock's avatar
    Jared Hancock committed
    }
    
    class TextboxField extends FormField {
    
        static $widget = 'TextboxWidget';
    
    Jared Hancock's avatar
    Jared Hancock committed
    
        function getConfigurationOptions() {
            return array(
                'size'  =>  new TextboxField(array(
    
                    'id'=>1, 'label'=>__('Size'), 'required'=>false, 'default'=>16,
    
    Jared Hancock's avatar
    Jared Hancock committed
                        'validator' => 'number')),
                'length' => new TextboxField(array(
    
                    'id'=>2, 'label'=>__('Max Length'), 'required'=>false, 'default'=>30,
    
    Jared Hancock's avatar
    Jared Hancock committed
                        'validator' => 'number')),
                'validator' => new ChoiceField(array(
    
                    'id'=>3, 'label'=>__('Validator'), 'required'=>false, 'default'=>'',
                    'choices' => array('phone'=>__('Phone Number'),'email'=>__('Email Address'),
    
                        'ip'=>__('IP Address'), 'number'=>__('Number'),
                        'regex'=>__('Custom (Regular Expression)'), ''=>__('None')))),
                'regex' => new TextboxField(array(
                    'id'=>6, 'label'=>__('Regular Expression'), 'required'=>true,
                    'configuration'=>array('size'=>40, 'length'=>100),
                    'visibility' => new VisibilityConstraint(
                        new Q(array('validator__eq'=>'regex')),
                        VisibilityConstraint::HIDDEN
                    ),
                    'cleaners' => function ($self, $value) {
                        $wrapped = "/".$value."/iu";
                        if (false === @preg_match($value, ' ')
                                && false !== @preg_match($wrapped, ' ')) {
    
                        if ($value == '//iu')
                            return '';
    
    
                        return $value;
                    },
                    'validators' => function($self, $v) {
                        if (false === @preg_match($v, ' '))
                            $self->addError(__('Cannot compile this regular expression'));
                    })),
    
                'validator-error' => new TextboxField(array(
    
                    'id'=>4, 'label'=>__('Validation Error'), 'default'=>'',
    
                    'configuration'=>array('size'=>40, 'length'=>60,
                        'translatable'=>$this->getTranslateTag('validator-error')
                    ),
    
                    'hint'=>__('Message shown to user if the input does not match the validator'))),
    
                'placeholder' => new TextboxField(array(
    
                    'id'=>5, 'label'=>__('Placeholder'), 'required'=>false, 'default'=>'',
                    'hint'=>__('Text shown in before any input from the user'),
    
                    'configuration'=>array('size'=>40, 'length'=>40,
                        'translatable'=>$this->getTranslateTag('placeholder')
                    ),
    
    Jared Hancock's avatar
    Jared Hancock committed
            );
        }
    
        function validateEntry($value) {
    
            //check to see if value is the string '0'
            $value = ($value == '0') ? '&#48' : Format::htmlchars($this->toString($value ?: $this->value));
    
    Jared Hancock's avatar
    Jared Hancock committed
            parent::validateEntry($value);
    
            $config = $this->getConfiguration();
    
    Jared Hancock's avatar
    Jared Hancock committed
            $validators = array(
    
                '' => '',
                'formula' => array(array('Validator', 'is_formula'),
    
                    __('Content cannot start with the following characters: = - + @')),
    
                'email' =>  array(array('Validator', 'is_valid_email'),
    
                    __('Enter a valid email address')),
    
    Jared Hancock's avatar
    Jared Hancock committed
                'phone' =>  array(array('Validator', 'is_phone'),
    
                    __('Enter a valid phone number')),
    
    Jared Hancock's avatar
    Jared Hancock committed
                'ip' =>     array(array('Validator', 'is_ip'),
    
                    __('Enter a valid IP address')),
    
                'number' => array('is_numeric', __('Enter a number')),
                'regex' => array(
                    function($v) use ($config) {
                        $regex = $config['regex'];
                        return @preg_match($regex, $v);
                    }, __('Value does not match required pattern')
                ),
    
    Jared Hancock's avatar
    Jared Hancock committed
            );
            // Support configuration forms, as well as GUI-based form fields
            $valid = $this->get('validator');
            if (!$valid) {
                $valid = $config['validator'];
            }
    
            if (!$value || !isset($validators[$valid]))
                return;
    
            // If no validators are set and not an instanceof AdvancedSearchForm
            // force formula validation
            if (!$valid && !($this->getForm() instanceof AdvancedSearchForm))
                $valid = 'formula';
    
    Jared Hancock's avatar
    Jared Hancock committed
            $func = $validators[$valid];
    
            $error = $func[1];
            if ($config['validator-error'])
    
                $error = $this->getLocal('validator-error', $config['validator-error']);
    
    Jared Hancock's avatar
    Jared Hancock committed
            if (is_array($func) && is_callable($func[0]))
                if (!call_user_func($func[0], $value))
    
                    $this->_errors[] = $error;
    
    
        function parse($value) {
            return Format::striptags($value);
        }
    
    
        function display($value) {
    
            return ($value === '0') ? '0' : Format::htmlchars($this->toString($value ?: $this->value));
    
    Jared Hancock's avatar
    Jared Hancock committed
    class PasswordField extends TextboxField {
        static $widget = 'PasswordWidget';
    
    
        function parse($value) {
            // Don't trim the value
            return $value;
        }
    
    
    Jared Hancock's avatar
    Jared Hancock committed
        function to_database($value) {
    
            // If not set in UI, don't save the empty value
            if (!$value)
                throw new FieldUnchanged();
            return Crypto::encrypt($value, SECRET_SALT, 'pwfield');
    
    Jared Hancock's avatar
    Jared Hancock committed
        }
    
        function to_php($value) {
    
            return Crypto::decrypt($value, SECRET_SALT, 'pwfield');
    
    Jared Hancock's avatar
    Jared Hancock committed
    class TextareaField extends FormField {
    
        static $widget = 'TextareaWidget';
    
    
    Jared Hancock's avatar
    Jared Hancock committed
        function getConfigurationOptions() {
            return array(
                'cols'  =>  new TextboxField(array(
    
                    'id'=>1, 'label'=>__('Width').' '.__('(chars)'), 'required'=>true, 'default'=>40)),
    
    Jared Hancock's avatar
    Jared Hancock committed
                'rows'  =>  new TextboxField(array(
    
                    'id'=>2, 'label'=>__('Height').' '.__('(rows)'), 'required'=>false, 'default'=>4)),
    
    Jared Hancock's avatar
    Jared Hancock committed
                'length' => new TextboxField(array(
    
                    'id'=>3, 'label'=>__('Max Length'), 'required'=>false, 'default'=>0)),
    
                'html' => new BooleanField(array(
    
                    'id'=>4, 'label'=>__('HTML'), 'required'=>false, 'default'=>true,
                    'configuration'=>array('desc'=>__('Allow HTML input in this box')))),
    
                'placeholder' => new TextboxField(array(
    
                    'id'=>5, 'label'=>__('Placeholder'), 'required'=>false, 'default'=>'',
                    'hint'=>__('Text shown in before any input from the user'),
    
                    'configuration'=>array('size'=>40, 'length'=>40,
                        'translatable'=>$this->getTranslateTag('placeholder')),
    
        function validateEntry($value) {
            parent::validateEntry($value);
    
            if (!$value)
                return;
    
            $config = $this->getConfiguration();
            $validators = array(
    
                '' =>       array(array('Validator', 'is_formula'),
                    __('Content cannot start with the following characters: = - + @')),
    
                'choices' => array(
                    function($val) {
                        $val = str_replace('"', '', JsonDataEncoder::encode($val));
    
                        $regex = "/^(?! )[A-z0-9 _-]+:{1}[^\n]+$/";
    
                        foreach (explode('\r\n', $val) as $v) {
                            if (!preg_match($regex, $v))
                                return false;
                        }
                        return true;
                    }, __('Each choice requires a key and has to be on a new line. (eg. key:value)')
                ),
            );
            // Support configuration forms, as well as GUI-based form fields
    
            if (!($valid = $this->get('validator')) && isset($config['validator']))
    
                $valid = $config['validator'];
    
            if (!isset($validators[$valid]))
    
            $func = $validators[$valid];
            $error = $func[1];
            if ($config['validator-error'])
                $error = $this->getLocal('validator-error', $config['validator-error']);
            if (is_array($func) && is_callable($func[0]))
                if (!call_user_func($func[0], $value))
                    $this->_errors[] = $error;
        }
    
    
        function display($value) {
            $config = $this->getConfiguration();
            if ($config['html'])
                return Format::safe_html($value);
            else
    
                return nl2br(Format::htmlchars($value));
    
        function searchable($value) {
    
            $body = new HtmlThreadEntryBody($value);
            return $body->getSearchable();
    
        function export($value) {
            return (!$value) ? $value : Format::html2text($value);
        }
    
    
        function parse($value) {
            $config = $this->getConfiguration();
            if ($config['html'])
                return Format::sanitize($value);
            else
                return $value;
        }
    
    
    Jared Hancock's avatar
    Jared Hancock committed
    }
    
    class PhoneField extends FormField {
    
        static $widget = 'PhoneNumberWidget';
    
    
        function getConfigurationOptions() {
            return array(
                'ext' => new BooleanField(array(
    
                    'label'=>__('Extension'), 'default'=>true,
    
                    'configuration'=>array(
    
                        'desc'=>__('Add a separate field for the extension'),
    
                    ),
                )),
                'digits' => new TextboxField(array(
    
                    'label'=>__('Minimum length'), 'default'=>7,
                    'hint'=>__('Fewest digits allowed in a valid phone number'),
    
                    'configuration'=>array('validator'=>'number', 'size'=>5),
                )),
                'format' => new ChoiceField(array(
    
                    'label'=>__('Display format'), 'default'=>'us',
                    'choices'=>array(''=>'-- '.__('Unformatted').' --',
                        'us'=>__('United States')),
    
    Jared Hancock's avatar
    Jared Hancock committed
        function validateEntry($value) {
            parent::validateEntry($value);
    
            $config = $this->getConfiguration();
    
    Jared Hancock's avatar
    Jared Hancock committed
            # Run validator against $this->value for email type
            list($phone, $ext) = explode("X", $value, 2);
    
            if ($phone && (
                    !is_numeric($phone) ||
                    strlen($phone) < $config['digits']))
    
                $this->_errors[] = __("Enter a valid phone number");
    
            if ($ext && $config['ext']) {
    
    Jared Hancock's avatar
    Jared Hancock committed
                if (!is_numeric($ext))
    
                    $this->_errors[] = __("Enter a valid phone extension");
    
    Jared Hancock's avatar
    Jared Hancock committed
                elseif (!$phone)
    
                    $this->_errors[] = __("Enter a phone number for the extension");
    
        function parse($value) {
            // NOTE: Value may have a legitimate 'X' to separate the number and
            // extension parts. Don't remove the 'X'
    
            $val = preg_replace('/[^\dX]/', '', $value);
            // Pass completely-incorrect string for validation error
            return $val ?: $value;
    
    Jared Hancock's avatar
    Jared Hancock committed
        function toString($value) {
    
            $config = $this->getConfiguration();
    
    Jared Hancock's avatar
    Jared Hancock committed
            list($phone, $ext) = explode("X", $value, 2);
    
            switch ($config['format']) {
            case 'us':
                $phone = Format::phone($phone);
                break;
            }
    
    Jared Hancock's avatar
    Jared Hancock committed
            if ($ext)
                $phone.=" x$ext";
            return $phone;
        }
    }
    
    class BooleanField extends FormField {
    
        static $widget = 'CheckboxWidget';
    
    Jared Hancock's avatar
    Jared Hancock committed
    
        function getConfigurationOptions() {
            return array(
                'desc' => new TextareaField(array(
    
                    'id'=>1, 'label'=>__('Description'), 'required'=>false, 'default'=>'',
                    'hint'=>__('Text shown inline with the widget'),
    
    Jared Hancock's avatar
    Jared Hancock committed
                    'configuration'=>array('rows'=>2)))
            );
        }
    
        function to_database($value) {
            return ($value) ? '1' : '0';
        }
    
    
        function parse($value) {
            return $this->to_php($value);
        }
    
    Jared Hancock's avatar
    Jared Hancock committed
        function to_php($value) {
    
            return $value ? true : false;
    
    Jared Hancock's avatar
    Jared Hancock committed
        }
    
        function toString($value) {
    
            return ($value) ? __('Yes') : __('No');
    
        function getClean($validate=true) {
            if (!isset($this->_clean)) {
                $this->_clean = (isset($this->value))
                    ? $this->value : $this->getValue();
    
                if ($this->isVisible() && $validate)
                    $this->validateEntry($this->_clean);
            }
            return $this->_clean;
        }
    
        function getChanges() {
            $new = $this->getValue();
            $old = $this->answer ? $this->answer->getValue() : $this->get('default');
    
            return ($old != $new) ? array($this->to_database($old), $this->to_database($new)) : false;
        }
    
    
        function getSearchMethods() {
            return array(
                'set' =>        __('checked'),
    
                'nset' =>    __('unchecked'),
    
        function describeSearchMethod($method) {
    
            $methods = $this->get('descsearchmethods');
            if (isset($methods[$method]))
                return $methods[$method];
    
            return parent::describeSearchMethod($method);
        }
    
    
        function getSearchMethodWidgets() {
            return array(
                'set' => null,
    
    
        function getSearchQ($method, $value, $name=false) {
            $name = $name ?: $this->get('name');
            switch ($method) {
            case 'set':
                return new Q(array($name => '1'));
    
                return new Q(array($name => '0'));
            default:
                return parent::getSearchQ($method, $value, $name);
            }
        }
    
    
        function supportsQuickFilter() {
            return true;
        }
    
        function getQuickFilterChoices() {
            return array(
                true => __('Checked'),
                false => __('Not Checked'),
            );
        }
    
        function applyQuickFilter($query, $qf_value, $name=false) {
            return $query->filter(array(
                $name ?: $this->get('name') => (int) $qf_value,
            ));
        }
    
    Jared Hancock's avatar
    Jared Hancock committed
    }
    
    class ChoiceField extends FormField {
    
        static $widget = 'ChoicesWidget';
    
    Peter Rotich's avatar
    Peter Rotich committed
        var $_choices;
    
    Jared Hancock's avatar
    Jared Hancock committed
    
        function getConfigurationOptions() {
            return array(
                'choices'  =>  new TextareaField(array(
    
                    'id'=>1, 'label'=>__('Choices'), 'required'=>false, 'default'=>'',
    
                    'hint'=>__('List choices, one per line. To protect against spelling changes, specify key:value names to preserve entries if the list item names change.</br><b>Note:</b> If you have more than two choices, use a List instead.'),
                    'validator'=>'choices',
    
                    'configuration'=>array('html'=>false)
                )),
    
                'default' => new TextboxField(array(
    
                    'id'=>3, 'label'=>__('Default'), 'required'=>false, 'default'=>'',
                    'hint'=>__('(Enter a key). Value selected from the list initially'),
    
                    'configuration'=>array('size'=>20, 'length'=>40),
                )),
                'prompt' => new TextboxField(array(
    
                    'id'=>2, 'label'=>__('Prompt'), 'required'=>false, 'default'=>'',
                    'hint'=>__('Leading text shown before a value is selected'),
    
                    'configuration'=>array('size'=>40, 'length'=>40,
                        'translatable'=>$this->getTranslateTag('prompt'),
                    ),
    
                'multiselect' => new BooleanField(array(
                    'id'=>1, 'label'=>'Multiselect', 'required'=>false, 'default'=>false,
                    'configuration'=>array(
                        'desc'=>'Allow multiple selections')
                )),
    
        function parse($value) {
    
            return $this->to_php($value ?: null);
    
        }
    
        function to_database($value) {
    
            if (!is_array($value)) {
                $choices = $this->getChoices();
                if (isset($choices[$value]))
                    $value = array($value => $choices[$value]);
            }
            if (is_array($value))
    
    Peter Rotich's avatar
    Peter Rotich committed
                $value = JsonDataEncoder::encode($value);
    
    
            return $value;
        }
    
        function to_php($value) {
    
            if (is_string($value))
    
                $value = JsonDataParser::parse($value) ?: $value;
    
            // CDATA table may be built with comma-separated key,value,key,value
    
    Peter Rotich's avatar
    Peter Rotich committed
            if (is_string($value) && strpos($value, ',')) {
    
                $values = array();
                $choices = $this->getChoices();
    
    Peter Rotich's avatar
    Peter Rotich committed
                $vals = explode(',', $value);
                foreach ($vals as $V) {
    
                    if (isset($choices[$V]))
                        $values[$V] = $choices[$V];
    
                if (array_filter($values))
                    $value = $values;
    
    Peter Rotich's avatar
    Peter Rotich committed
                elseif($vals)
                    list($value) = $vals;
    
    
            $config = $this->getConfiguration();
            if (!$config['multiselect'] && is_array($value) && count($value) < 2) {
                reset($value);
    
                $value = key($value);
    
            }
            return $value;
    
        function toString($value) {
    
            if (!is_array($value))
                $value = $this->getChoice($value);
            if (is_array($value))
                return implode(', ', $value);
            return (string) $value;
    
        function getKeys($value) {
            if (!is_array($value))
                $value = $this->getChoice($value);
            if (is_array($value))
                return implode(', ', array_keys($value));
            return (string) $value;
        }
    
    
        function asVar($value, $id=false) {
            $value = $this->to_php($value);
            return $this->toString($this->getChoice($value));
        }
    
    
        function whatChanged($before, $after) {
    
            $B = (array) $before;
            $A = (array) $after;
            $added = array_diff($A, $B);
            $deleted = array_diff($B, $A);
    
            $added = array_map(array($this, 'display'), $added);
            $deleted = array_map(array($this, 'display'), $deleted);
    
    JediKev's avatar
    JediKev committed
            $added = array_filter($added);
            $deleted = array_filter($deleted);
    
    
            if ($added && $deleted) {
                $desc = sprintf(
                    __('added <strong>%1$s</strong> and removed <strong>%2$s</strong>'),
                    implode(', ', $added), implode(', ', $deleted));
            }
            elseif ($added) {
                $desc = sprintf(
                    __('added <strong>%1$s</strong>'),
                    implode(', ', $added));
            }
            elseif ($deleted) {
                $desc = sprintf(
                    __('removed <strong>%1$s</strong>'),
                    implode(', ', $deleted));
            }
            else {
                $desc = sprintf(
    
                    __('changed from <strong>%1$s</strong> to <strong>%2$s</strong>'),
                    $this->display($before), $this->display($after));
    
    Peter Rotich's avatar
    Peter Rotich committed
        /*
         Return criteria to which the choice should be filtered by
         */
        function getCriteria() {
            $config = $this->getConfiguration();
            $criteria = array();
            if (isset($config['criteria']))
                $criteria = $config['criteria'];
    
            return $criteria;
        }
    
    
        function getChoice($value) {
    
            $choices = $this->getChoices();
    
            $selection = array();
            if ($value && is_array($value)) {
    
    Peter Rotich's avatar
    Peter Rotich committed
                $selection = $value;
    
            } elseif (isset($choices[$value]))
    
    JediKev's avatar
    JediKev committed
                $selection[$value] = $choices[$value];
            elseif (($v=$this->get('default')) && isset($choices[$v]))
                $selection[$v] = $choices[$v];
    
    Peter Rotich's avatar
    Peter Rotich committed
        function getChoices($verbose=false) {
            if ($this->_choices === null || $verbose) {
    
                // Allow choices to be set in this->ht (for configurationOptions)
                $this->_choices = $this->get('choices');
                if (!$this->_choices) {
                    $this->_choices = array();
                    $config = $this->getConfiguration();
                    $choices = explode("\n", $config['choices']);
                    foreach ($choices as $choice) {
                        // Allow choices to be key: value
    
                        list($key, $val) = explode(':', $choice, 2);
    
                        if ($val == null)
                            $val = $key;
                        $this->_choices[trim($key)] = trim($val);
                    }
    
    Peter Rotich's avatar
    Peter Rotich committed
                    // Add old selections if nolonger available
                    // This is necessary so choices made previously can be
                    // retained
                    $values = ($a=$this->getAnswer()) ? $a->getValue() : array();
                    if ($values && is_array($values)) {
                        foreach ($values as $k => $v) {
                            if (!isset($this->_choices[$k])) {
                                if ($verbose) $v .= ' (retired)';
                                $this->_choices[$k] = $v;
                            }
                        }
                    }
    
                }
            }
            return $this->_choices;
    
        function lookupChoice($value) {
            return null;
        }
    
        function getSearchMethods() {
            return array(
                'set' =>        __('has a value'),
    
                'nset' =>     __('does not have a value'),
    
                'includes' =>   __('includes'),
    
                '!includes' =>  __('does not include'),
    
        function getSearchMethodWidgets() {
            return array(
                'set' => null,
    
                'includes' => array('ChoiceField', array(
                    'choices' => $this->getChoices(),
                    'configuration' => array('multiselect' => true),
                )),
    
                '!includes' => array('ChoiceField', array(
                    'choices' => $this->getChoices(),
                    'configuration' => array('multiselect' => true),
                )),
    
    
        function getSearchQ($method, $value, $name=false) {
            $name = $name ?: $this->get('name');
    
            $val = $value;
            if ($value && is_array($value))
                $val = '"?'.implode('("|,|$)|"?', array_keys($value)).'("|,|$)';
    
            switch ($method) {
            case '!includes':
    
    JediKev's avatar
    JediKev committed
                return Q::not(array("{$name}__regex" => $val));
    
    JediKev's avatar
    JediKev committed
                return new Q(array("{$name}__regex" => $val));
    
            default:
                return parent::getSearchQ($method, $value, $name);
            }
        }
    
    
        function describeSearchMethod($method) {
            switch ($method) {
            case 'includes':
                return __('%s includes %s' /* includes -> if a list includes a selection */);
    
            case '!includes':
    
                return __('%s does not include %s' /* includes -> if a list includes a selection */);
            default:
                return parent::describeSearchMethod($method);
            }