Newer
Older
return $this->_form;
}
function getDynamicFields() {
return $this->form->fields;
function getMedia() {
return $this->getForm()->getMedia();
}
// Get all dynamic fields associated with the form
// even when stored elsewhere -- important during validation
foreach ($this->getDynamicFields() as $f) {
$f = $f->getImpl($f);
$this->_fields[$f->get('id')] = $f;
$f->isnew = true;
// Include any other answers included in this entry, which may
// be for fields which have since been deleted
foreach ($this->getAnswers() as $a) {
$f = $a->getField();
$id = $f->get('id');
if (!isset($this->_fields[$id])) {
// This field is not currently on the associated form
$a->deleted = true;
}
$this->_fields[$id] = $f;
// This field has an answer, so it isn't new (to this entry)
$f->isnew = false;
function filterFields($filter) {
$this->getFields();
foreach ($this->_fields as $i=>$f) {
if ($filter($f))
unset($this->_fields[$i]);
}
}
function getSource() {
return $this->_source ?: (isset($this->id) ? false : $_POST);
}
function setSource($source) {
$this->_source = $source;
// Ensure the field is connected to this data source
foreach ($this->getFields() as $F)
if (!$F->getForm())
$F->setForm($this);
function getField($name) {
foreach ($this->getFields() as $field)
if (!strcasecmp($field->get('name'), $name))
return $field;
return null;
}
/**
* Validate the form and indicate if there no errors.
*
* Parameters:
* $filter - (callback) function to receive each field and return
* boolean true if the field's errors are significant
* $options - options to pass to form and fields.
*
function isValid($filter=false, $options=array()) {
$form = $this->getForm(false, $options);
$form->isValid($filter);
$this->_errors = $form->errors();
function isValidForClient() {
$filter = function($f) {
return $f->isVisibleToUsers();
return $this->isValid($filter);
}
function isValidForStaff() {
$filter = function($f) {
return $f->isVisibleToStaff();
return $this->isValid($filter);
}
return $this->getForm()->getClean();
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
/**
* Compile a list of data used by the filtering system to match dynamic
* content in this entry. This returs an array of `field.<id>` =>
* <value> pairs where the <id> is the field id and the <value> is the
* toString() value for the entered data.
*
* If the field returns an array for its ::getFilterData() method, the
* data will be added in the array with the keys prefixed with
* `field.<id>`. This is useful for properties on custom lists, for
* instance, which can contain properties usefule for matching and
* filtering.
*/
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 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);
function getChanges() {
$fields = array();
foreach ($this->getAnswers() as $a) {
$field = $a->getField();
if (!$field->hasData() || $field->isPresentationOnly())
continue;
$after = $field->to_database($field->getClean());
$before = $field->to_database($a->getValue());
if ($before == $after)
continue;
$fields[$field->get('id')] = array($before, $after);
}
return $fields;
}
* 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.
foreach ($this->getFields() as $field) {
if ($field->isnew && $field->isEnabled()
&& !$field->isPresentationOnly()
&& $field->hasData()
&& $field->isStorable()
) {
$a = new DynamicFormEntryAnswer(
array('field_id'=>$field->get('id'), 'entry'=>$this));
// 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');
});
/**
* Save the form entry and all associated answers.
*
* Returns:
* (mixed) FALSE if updated failed, otherwise the number of dirty answers
* which were save is returned (which may be ZERO).
*/
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->getAnswers() as $a) {
$field = $a->getField();
// Don't save answers for presentation-only fields or fields
// which are stored elsewhere
if (!$field->hasData() || !$field->isStorable()
|| $field->isPresentationOnly()
) {
// Set the entry here so that $field->getClean() can use the
$field->setForm($this);
$val = $field->to_database($field->getClean());
}
catch (FieldUnchanged $e) {
// Don't update the answer.
continue;
}
if (is_array($val)) {
$a->set('value', $val[0]);
$a->set('value_id', $val[1]);
}
if ($a->dirty)
$dirty++;
if (!parent::delete())
return false;
foreach ($this->getAnswers() as $a)
$a->delete();
static function create($ht=false, $data=null) {
$inst = new static($ht);
$inst->set('created', new SqlFunction('NOW'));
foreach ($inst->getDynamicFields() as $field) {
if (!($impl = $field->getImpl($field)))
continue;
if (!$impl->hasData() || !$impl->isStorable())
$a = new DynamicFormEntryAnswer(
array('field'=>$field, 'entry'=>$inst));
}
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() {
return $this->getEntry()->getForm();
if (!isset($this->_field)) {
$this->_field = $this->field->getImpl($this->field);
$this->_field->setAnswer($this);
return $this->_field;
//XXX: We're settting the value here to avoid infinite loop
$this->_value = false;
if (isset($this->value))
$this->_value = $this->getField()->to_php(
$this->get('value'), $this->get('value_id'));
function setValue($value, $id=false) {
$this->getField()->reset();
$this->_value = null;
$this->set('value', $value);
if ($id !== false)
$this->set('value_id', $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() {
return implode(',', (array) $this->getField()->getKeys($this->getValue()));
return $this->getField()->asVar(
$this->get('value'), $this->get('value_id')
);
}
function getVar($tag) {
if (is_object($var = $this->asVar()) && method_exists($var, 'getVar'))
return $var->getVar($tag);
function __toString() {
$v = $this->toString();
return is_string($v) ? $v : (string) $this->getValue();
function delete() {
if (!parent::delete())
return false;
// Allow the field to cleanup anything else in the database
$this->getField()->db_cleanup();
return true;
}
if ($this->dirty)
unset($this->_value);
return parent::save($refetch);
}
}
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($widgetClass=false) {
$config = $this->getConfiguration();
if ($config['widget'] == 'typeahead' && $config['multiselect'] == false)
$widgetClass = 'TypeaheadSelectionWidget';
elseif ($config['widget'] == 'textbox')
$widgetClass = 'TextboxSelectionWidget';
return parent::getWidget($widgetClass);
}
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
function display($value) {
global $thisstaff;
if (!is_array($value)
|| !$thisstaff // Only agents can preview for now
|| !($list=$this->getList()))
return parent::display($value);
$display = array();
foreach ($value as $k => $v) {
if (is_numeric($k)
&& ($i=$list->getItem((int) $k))
&& $i->hasProperties())
$display[] = $i->display();
else // Perhaps deleted entry
$display[] = $v;
}
return implode(',', $display);
}
if (!($list=$this->getList()))
return null;
$config = $this->getConfiguration();
$choices = $this->getChoices();
$selection = array();
if ($value && !is_array($value))
$value = array($value);
if ($value && is_array($value)) {
foreach ($value as $k=>$v) {
if ($k && ($i=$list->getItem((int) $k)))
elseif (isset($choices[$k]))
$selection[$k] = $choices[$k];
elseif (isset($choices[$v]))
$selection[$v] = $choices[$v];
elseif (($i=$list->getItem($v, true)))
$selection[$i->getId()] = $i->getValue();
} 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.
function getKeys($value) {
if (!is_array($value))
$value = $this->getChoice($value);
if (is_array($value))
return implode(', ', array_keys($value));
return (string) $value;
}
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
// PHP 5.4 Move this to a trait
function whatChanged($before, $after) {
$before = (array) $before;
$after = (array) $after;
$added = array_diff($after, $before);
$deleted = array_diff($before, $after);
$added = array_map(array($this, 'display'), $added);
$deleted = array_map(array($this, 'display'), $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 to <strong>%1$s</strong>'),
$this->display($after));
}
return $desc;
}
function asVar($value, $id=false) {
$values = $this->to_php($value, $id);
if (is_array($values)) {
return new PlaceholderList($this->getList()->getAllItems()
->filter(array('id__in' => array_keys($values)))
);
}
}
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;
return 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)
&& $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;
}
function lookupChoice($value) {
// See if it's in the choices.
$choices = $this->getChoices();
if ($choices && ($i=array_search($value, $choices)))
return array($i=>$choices[$i]);
// Query the store by value or extra (abbrv.)
if (!($list=$this->getList()))
return null;
if ($i = $list->getItem($value))
return array($i->getId() => $i->getValue());
if ($i = $list->getItem($value, true))
return array($i->getId() => $i->getValue());
return null;
}
// 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";
}
}
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
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;
?>_id" id="<?php echo $this->name;
?>_id" value="<?php echo Format::htmlchars($value); ?>"/>
<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();
$name = $this->field->get('name');
if (isset($data["{$this->name}_id"]) && is_numeric($data["{$this->name}_id"])) {
return array($data["{$this->name}_id"] => $data["{$this->name}_name"]);
}
elseif (isset($data[$name])) {
return $data[$name];
}
// Attempt to lookup typed value (usually from a default)
elseif ($val = $this->getEnteredValue()) {
return $this->field->lookupChoice($val);
}
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'];
$pos = strrpos($v, ' — ');
if ($pos !== false)
$v = substr($v, 0, $pos);
return trim($v);
}
return parent::getValue();
}