Newer
Older
$this->_form = new CustomForm($fields, $source, $options);
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();
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
1139
1140
/**
* 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()
) {
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()
) {
continue;
}
// 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 = parent::create($ht);
$inst->set('created', new SqlFunction('NOW'));
foreach ($inst->getDynamicFields() as $field) {
if (!($impl = $field->getImpl($field)))
continue;
if (!$impl->hasData() || !$impl->isStorable())
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;
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() {
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;
}
}
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);
}
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;
}
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
// 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))
$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";
}
}
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
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();
}