Newer
Older
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
*/
function getFilterData() {
$vars = array();
foreach ($this->getFields() as $f) {
$tag = 'field.'.$f->get('id');
if ($d = $f->getFilterData()) {
if (is_array($d)) {
foreach ($d as $k=>$v) {
if (is_string($k))
$vars["$tag$k"] = $v;
else
$vars[$tag] = $v;
}
}
else {
$vars[$tag] = $d;
}
}
}
return $vars;
}
function getSaved() {
$info = array();
foreach ($this->getAnswers() as $a) {
$field = $a->getField();
$info[$field->get('id')]
= $info[$field->get('name')] = $a->getValue();
}
return $info;
}
function forTicket($ticket_id, $force=false) {
if (!isset($entries[$ticket_id]) || $force) {
$stuff = DynamicFormEntry::objects()
->filter(array('object_id'=>$ticket_id, 'object_type'=>'T'));
// If forced, don't cache the result
if ($force)
return $stuff;
$entries[$ticket_id] = &$stuff;
}
function setTicketId($ticket_id) {
$this->object_type = 'T';
$this->object_id = $ticket_id;
}
function setClientId($user_id) {
$this->object_type = 'U';
$this->object_id = $user_id;
}
function setObjectId($object_id) {
$this->object_id = $object_id;
}
function forObject($object_id, $object_type) {
->filter(array('object_id'=>$object_id, 'object_type'=>$object_type));
function render($staff=true, $title=false, $options=array()) {
return $this->getForm()->render($staff, $title, $options);
* Adds fields that have been added to the linked form (field set) since
* this entry was originally created. If fields are added to the form,
* the method will automatically add the fields and null answers to the
* entry.
// Track deletions
foreach ($this->getAnswers() as $answer)
$answer->deleted = true;
foreach ($this->getForm()->getDynamicFields() as $field) {
$found = false;
foreach ($this->getAnswers() as $answer) {
if ($answer->get('field_id') == $field->get('id')) {
$answer->deleted = false; $found = true; break;
if (!$found && ($fImpl = $field->getImpl($field))
&& $field->isEnabled()
&& !$fImpl->isPresentationOnly()) {
$a = DynamicFormEntryAnswer::create(
array('field_id'=>$field->get('id'), 'entry_id'=>$this->id));
$a->field = $field;
// Add to list of answers
$this->_values[] = $a;
$this->_fields[$field->get('id')] = $fImpl;
// Omit fields without data and non-storable fields.
if (!$field->hasData() || !$field->isStorable())
continue;
$a->save();
}
// Sort the form the way it is declared to be sorted
if ($this->_fields) {
uasort($this->_fields,
function($a, $b) {
return $a->get('sort') - $b->get('sort');
});
function save($refetch=false) {
if (count($this->dirty))
$this->set('updated', new SqlFunction('NOW'));
if (!parent::save($refetch || count($this->dirty)))
return false;
foreach ($this->getFields() as $field) {
if (!($a = $field->getAnswer()) || !$field->isStorable())
// Set the entry ID here so that $field->getClean() can use the
// entry-id if necessary
$a->set('entry_id', $this->get('id'));
$val = $field->to_database($field->getClean());
if (is_array($val)) {
$a->set('value', $val[0]);
$a->set('value_id', $val[1]);
}
else
$a->set('value', $val);
// Don't save answers for presentation-only fields
if ($field->hasData() && !$field->isPresentationOnly())
$a->save();
function delete() {
foreach ($this->getAnswers() as $a)
$a->delete();
return parent::delete();
}
static function create($ht=false) {
$inst = parent::create($ht);
$inst->set('created', new SqlFunction('NOW'));
foreach ($inst->getForm()->getDynamicFields() as $f) {
$a = DynamicFormEntryAnswer::create(
array('field_id'=>$f->get('id')));
$a->field = $f;
$inst->_values[] = $a;
}
return $inst;
}
}
/**
* Represents a single answer to a single field on a dynamic form. The
* data / answer to the field is linked back to the form and field which was
* originally used for the submission.
*/
class DynamicFormEntryAnswer extends VerySimpleModel {
static $meta = array(
'table' => FORM_ANSWER_TABLE,
'ordering' => array('field__sort'),
'pk' => array('entry_id', 'field_id'),
'select_related' => array('field'),
'fields' => array('entry_id', 'field_id', 'value', 'value_id'),
'joins' => array(
'field' => array(
'constraint' => array('field_id' => 'DynamicFormField.id'),
),
'entry' => array(
'constraint' => array('entry_id' => 'DynamicFormEntry.id'),
),
),
);
var $deleted = false;
var $_value;
function getEntry() {
return $this->entry;
}
function getForm() {
if (!$this->form)
$this->form = $this->getEntry()->getForm();
return $this->form;
}
function getField() {
if (!isset($this->_field)) {
$this->_field = $this->field->getImpl($this->field);
$this->_field->setAnswer($this);
return $this->_field;
if (!isset($this->_value) && isset($this->value)) {
//XXX: We're settting the value here to avoid infinite loop
$this->_value = false;
$this->_value = $this->getField()->to_php(
$this->get('value'), $this->get('value_id'));
function getLocal($tag) {
return $this->field->getLocal($tag);
}
function getIdValue() {
return $this->get('value_id');
}
function isDeleted() {
return $this->deleted;
}
function toString() {
return $this->getField()->toString($this->getValue());
}
function display() {
return $this->getField()->display($this->getValue());
}
function getSearchable($include_label=false) {
if ($include_label)
$label = Format::searchable($this->getField()->getLabel()) . " ";
return sprintf("%s%s", $label,
$this->getField()->searchable($this->getValue())
);
}
function getSearchKeys() {
$val = $this->getField()->to_php(
$this->get('value'), $this->get('value_id'));
if (is_array($val))
return array_keys($val);
elseif (is_object($val) && method_exists($val, 'getId'))
return array($val->getId());
return (is_object($this->getValue()))
? $this->getValue() : $this->toString();
}
function getVar($tag) {
if (is_object($this->getValue()) && method_exists($this->getValue(), 'getVar'))
return $this->getValue()->getVar($tag);
function __toString() {
$v = $this->toString();
return is_string($v) ? $v : (string) $this->getValue();
}
class SelectionField extends FormField {
static $widget = 'ChoicesWidget';
function getListId() {
list(,$list_id) = explode('-', $this->get('type'));
return $list_id ?: $this->get('list_id');
if (!$this->_list)
$this->_list = DynamicList::lookup($this->getListId());
function getWidget() {
$config = $this->getConfiguration();
$widgetClass = false;
if ($config['widget'] == 'typeahead' && $config['multiselect'] == false)
$widgetClass = 'TypeaheadSelectionWidget';
elseif ($config['widget'] == 'textbox')
$widgetClass = 'TextboxSelectionWidget';
return parent::getWidget($widgetClass);
}
if (!($list=$this->getList()))
return null;
$config = $this->getConfiguration();
$choices = $this->getChoices();
$selection = array();
if ($value && is_array($value)) {
foreach ($value as $k=>$v) {
if (($i=$list->getItem((int) $k)))
elseif (isset($choices[$k]))
$selection[$k] = $choices[$k];
} elseif($value) {
//Assume invalid textbox input to be validated
$selection[] = $value;
// Don't return an empty array
return $selection ?: null;
if (is_array($value)) {
reset($value);
}
if ($value && is_array($value))
$value = JsonDataEncoder::encode($value);
return $value;
function to_php($value, $id=false) {
if (is_string($value))
$value = JsonDataParser::parse($value) ?: $value;
if (!is_array($value)) {
$choices = $this->getChoices();
foreach (explode(',', $value) as $V) {
if (isset($choices[$V]))
$values[$V] = $choices[$V];
}
if ($id && isset($choices[$id]))
$values[$id] = $choices[$id];
if ($values)
return $values;
// else return $value unchanged
// Don't set the ID here as multiselect prevents using exactly one
// ID value. Instead, stick with the JSON value only.
return $this->getList()->getForm();
$fields = new ListObject(array(
new TextboxField(array(
// XXX: i18n: Change to a better word when the UI changes
'label' => '['.__('Abbrev').']',
'id' => 'abb',
))
));
$form = $this->getList()->getForm();
if ($form && ($F = $form->getFields()))
$fields->extend($F);
return $fields;
function toString($items) {
return ($items && is_array($items))
? implode(', ', $items) : (string) $items;
function validateEntry($entry) {
parent::validateEntry($entry);
if (!$this->errors()) {
$config = $this->getConfiguration();
if ($config['widget'] == 'textbox') {
if ($entry && (
!($k=key($entry))
|| !($i=$this->getList()->getItem((int) $k))
)) {
$config = $this->getConfiguration();
$this->_errors[] = $this->getLocal('validator-error', $config['validator-error'])
?: __('Unknown or invalid input');
}
&& ($entered = $this->getWidget()->getEnteredValue())
&& !in_array($entered, $entry))
$this->_errors[] = __('Select a value from the list');
}
function getConfigurationOptions() {
return array(
'multiselect' => new BooleanField(array(
'id'=>2,
'label'=>__(/* Type of widget allowing multiple selections */ 'Multiselect'),
'required'=>false, 'default'=>false,
'configuration'=>array(
'desc'=>__('Allow multiple selections')),
)),
'id'=>1,
'label'=>__('Widget'),
'required'=>false, 'default' => 'dropdown',
'typeahead' => __('Typeahead'),
'textbox' => __('Text Input'),
),
'configuration'=>array(
'multiselect' => false,
),
'visibility' => new VisibilityConstraint(
new Q(array('multiselect__eq'=>false)),
VisibilityConstraint::HIDDEN
),
'hint'=>__('Typeahead will work better for large lists')
'validator-error' => new TextboxField(array(
'id'=>5, 'label'=>__('Validation Error'), 'default'=>'',
'configuration'=>array('size'=>40, 'length'=>80,
'translatable'=>$this->getTranslateTag('validator-error')
),
'visibility' => new VisibilityConstraint(
new Q(array('widget__eq'=>'textbox')),
VisibilityConstraint::HIDDEN
),
'hint'=>__('Message shown to user if the item entered is not in the list')
)),
'id'=>3,
'label'=>__('Prompt'), 'required'=>false, 'default'=>'',
'hint'=>__('Leading text shown before a value is selected'),
'configuration'=>array('size'=>40, 'length'=>40,
'translatable'=>$this->getTranslateTag('prompt'),
),
'default' => new SelectionField(array(
'id'=>4, 'label'=>__('Default'), 'required'=>false, 'default'=>'',
'list_id'=>$this->getListId(),
'configuration' => array('prompt'=>__('Select a Default')),
)),
function getConfiguration() {
$config = parent::getConfiguration();
if ($config['widget'])
$config['typeahead'] = $config['widget'] == 'typeahead';
// Drop down list does not support multiple selections
if ($config['typeahead'])
$config['multiselect'] = false;
return $config;
}
function getChoices($verbose=false) {
if (!$this->_choices || $verbose) {
foreach ($this->getList()->getItems() as $i)
$choices[$i->getId()] = $i->getValue();
// Retired old selections
$values = ($a=$this->getAnswer()) ? $a->getValue() : array();
if ($values && is_array($values)) {
foreach ($values as $k => $v) {
if ($verbose) $v .= ' '.__('(retired)');
if ($verbose) // Don't cache
return $choices;
$this->_choices = $choices;
function getChoice($value) {
$choices = $this->getChoices();
if ($value && is_array($value)) {
$selection = $value;
} elseif (isset($choices[$value]))
$selection[] = $choices[$value];
elseif ($this->get('default'))
$selection[] = $choices[$this->get('default')];
return $selection;
}
// Start with the filter data for the list item as the [0] index
$data = array(parent::getFilterData());
if (($v = $this->getClean())) {
// Add in the properties for all selected list items in sub
// labeled by their field id
foreach ($v as $id=>$L) {
if (!($li = DynamicListItem::lookup($id)))
continue;
foreach ($li->getFilterData() as $prop=>$value) {
if (!isset($data[$prop]))
$data[$prop] = $value;
else
$data[$prop] .= " $value";
}
}
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
function getSearchMethods() {
return array(
'set' => __('has a value'),
'notset' => __('does not have a value'),
'includes' => __('includes'),
'!includes' => __('does not include'),
);
}
function getSearchMethodWidgets() {
return array(
'set' => null,
'notset' => 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');
switch ($method) {
case '!includes':
return Q::not(array("{$name}__intersect" => array_keys($value)));
return new Q(array("{$name}__intersect" => array_keys($value)));
default:
return parent::getSearchQ($method, $value, $name);
}
}
class TypeaheadSelectionWidget extends ChoicesWidget {
function render($options=array()) {
if ($options['mode'] == 'search')
return parent::render($options);
$name = $this->getEnteredValue();
$config = $this->field->getConfiguration();
if (is_array($this->value)) {
$name = $name ?: current($this->value);
$value = key($this->value);
else {
// Pull configured default (if configured)
$def_key = $this->field->get('default');
if (!$def_key && $config['default'])
$def_key = $config['default'];
if (is_array($def_key))
$name = current($def_key);
}
$source = array();
foreach ($this->field->getList()->getItems() as $i)
$source[] = array(
'info' => sprintf('%s%s',
(($extra= $i->getAbbrev()) ? " — $extra" : '')),
<input type="text" size="30" name="<?php echo $this->name; ?>_name"
id="<?php echo $this->name; ?>" value="<?php echo Format::htmlchars($name); ?>"
placeholder="<?php echo $config['prompt'];
?>" autocomplete="off" />
<input type="hidden" name="<?php echo $this->name;
?>[<?php echo $value; ?>]" id="<?php echo $this->name;
?>_id" value="<?php echo Format::htmlchars($name); ?>"/>
<script type="text/javascript">
$(function() {
$('input#<?php echo $this->name; ?>').typeahead({
source: <?php echo JsonDataEncoder::encode($source); ?>,
$('input#<?php echo $this->name; ?>_name').val(item['value'])
$('input#<?php echo $this->name; ?>_id')
.attr('name', '<?php echo $this->name; ?>[' + item['id'] + ']')
.val(item['value']);
}
});
});
</script>
</span>
<?php
}
function parsedValue() {
return array($this->getValue() => $this->getEnteredValue());
}
function getValue() {
$data = $this->field->getSource();
if (isset($data[$this->name]))
return $data[$this->name];
return parent::getValue();
}
function getEnteredValue() {
// Used to verify typeahead fields
$data = $this->field->getSource();
if (isset($data[$this->name.'_name'])) {
// Drop the extra part, if any
$v = $data[$this->name.'_name'];
$v = substr($v, 0, strrpos($v, ' — '));
return trim($v);
}
return parent::getValue();
}