You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
6094 lines
192 KiB
6094 lines
192 KiB
<?php
|
|
/*********************************************************************
|
|
class.forms.php
|
|
|
|
osTicket forms framework
|
|
|
|
Jared Hancock <jared@osticket.com>
|
|
Copyright (c) 2006-2013 osTicket
|
|
http://www.osticket.com
|
|
|
|
Released under the GNU General Public License WITHOUT ANY WARRANTY.
|
|
See LICENSE.TXT for details.
|
|
|
|
vim: expandtab sw=4 ts=4 sts=4:
|
|
**********************************************************************/
|
|
|
|
/**
|
|
* Form template, used for designing the custom form and for entering custom
|
|
* data for a ticket
|
|
*/
|
|
class Form {
|
|
static $renderer = 'GridFluidLayout';
|
|
static $id = 0;
|
|
|
|
var $options = array();
|
|
var $fields = array();
|
|
var $title = '';
|
|
var $instructions = '';
|
|
|
|
var $validators = array();
|
|
|
|
var $_errors = null;
|
|
var $_source = false;
|
|
|
|
function __construct($source=null, $options=array()) {
|
|
|
|
$this->options = $options;
|
|
if (isset($options['title']))
|
|
$this->title = $options['title'];
|
|
if (isset($options['instructions']))
|
|
$this->instructions = $options['instructions'];
|
|
if (isset($options['id']))
|
|
$this->id = $options['id'];
|
|
|
|
// Use POST data if source was not specified
|
|
$this->_source = $source ?: $_POST;
|
|
}
|
|
|
|
function getFormId() {
|
|
return @$this->id ?: static::$id;
|
|
}
|
|
function setId($id) {
|
|
$this->id = $id;
|
|
}
|
|
|
|
function data($source) {
|
|
foreach ($this->fields as $name=>$f)
|
|
if (isset($source[$name]))
|
|
$f->value = $source[$name];
|
|
}
|
|
|
|
function setFields($fields) {
|
|
|
|
if (!is_array($fields) && !$fields instanceof Traversable)
|
|
return;
|
|
|
|
$this->fields = $fields;
|
|
foreach ($fields as $k=>$f) {
|
|
$f->setForm($this);
|
|
if (!$f->get('name') && $k && !is_numeric($k))
|
|
$f->set('name', $k);
|
|
}
|
|
}
|
|
|
|
function getFields() {
|
|
return $this->fields;
|
|
}
|
|
|
|
function getField($name) {
|
|
$fields = $this->getFields();
|
|
foreach($fields as $f)
|
|
if(!strcasecmp($f->get('name'), $name))
|
|
return $f;
|
|
if (isset($fields[$name]))
|
|
return $fields[$name];
|
|
}
|
|
|
|
function hasField($name) {
|
|
return $this->getField($name);
|
|
}
|
|
|
|
function hasAnyEnabledFields() {
|
|
return $this->hasAnyVisibleFields(false);
|
|
}
|
|
|
|
function hasAnyVisibleFields($user=false) {
|
|
$visible = 0;
|
|
$isstaff = $user instanceof Staff;
|
|
foreach ($this->getFields() as $F) {
|
|
if (!$user) {
|
|
// Assume hasAnyEnabledFields
|
|
if ($F->isEnabled())
|
|
$visible++;
|
|
} elseif($isstaff) {
|
|
if ($F->isVisibleToStaff())
|
|
$visible++;
|
|
} elseif ($F->isVisibleToUsers()) {
|
|
$visible++;
|
|
}
|
|
}
|
|
return $visible > 0;
|
|
}
|
|
|
|
function getTitle() { return $this->title; }
|
|
function getInstructions() { return Format::htmldecode($this->instructions); }
|
|
function getSource() { return $this->_source; }
|
|
function setSource($source) { $this->_source = $source; }
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
function isValid($include=false) {
|
|
if (!isset($this->_errors)) {
|
|
$this->_errors = array();
|
|
$this->validate($this->getClean());
|
|
foreach ($this->getFields() as $field)
|
|
if ($field->errors() && (!$include || $include($field)))
|
|
$this->_errors[$field->get('id')] = $field->errors();
|
|
}
|
|
return !$this->_errors;
|
|
}
|
|
|
|
function validate($clean_data) {
|
|
// Validate the whole form so that errors can be added to the
|
|
// individual fields and collected below.
|
|
foreach ($this->validators as $V) {
|
|
$V($this);
|
|
}
|
|
}
|
|
|
|
function getClean($validate=true) {
|
|
if (!$this->_clean) {
|
|
$this->_clean = array();
|
|
foreach ($this->getFields() as $key=>$field) {
|
|
if (!$field->hasData())
|
|
continue;
|
|
|
|
// Prefer indexing by field.id if indexing numerically
|
|
if (is_int($key) && $field->get('id'))
|
|
$key = $field->get('id');
|
|
$this->_clean[$key] = $this->_clean[$field->get('name')]
|
|
= $field->getClean($validate);
|
|
}
|
|
unset($this->_clean[""]);
|
|
}
|
|
return $this->_clean;
|
|
}
|
|
|
|
/*
|
|
* Process the form input and return clean data.
|
|
*
|
|
* It's similar to getClean but forms downstream can use it to return
|
|
* database ready data.
|
|
*/
|
|
function process($validate=true) {
|
|
return $this->getClean($validate);
|
|
}
|
|
|
|
function errors($formOnly=false) {
|
|
return ($formOnly) ? $this->_errors['form'] : $this->_errors;
|
|
}
|
|
|
|
function addError($message, $index=false) {
|
|
|
|
if ($index)
|
|
$this->_errors[$index] = $message;
|
|
else
|
|
$this->_errors['form'][] = $message;
|
|
}
|
|
|
|
function addErrors($errors=array()) {
|
|
foreach ($errors as $k => $v) {
|
|
if (($f=$this->getField($k)))
|
|
$f->addError($v);
|
|
else
|
|
$this->addError($v, $k);
|
|
}
|
|
}
|
|
|
|
function addValidator($function) {
|
|
if (!is_callable($function))
|
|
throw new Exception('Form validator must be callable');
|
|
$this->validators[] = $function;
|
|
}
|
|
|
|
function render($options=array()) {
|
|
if (isset($options['title']))
|
|
$this->title = $options['title'];
|
|
if (isset($options['instructions']))
|
|
$this->instructions = $options['instructions'];
|
|
$form = $this;
|
|
$template = $options['template'] ?: 'dynamic-form.tmpl.php';
|
|
if (isset($options['staff']) && $options['staff'])
|
|
include(STAFFINC_DIR . 'templates/' . $template);
|
|
else
|
|
include(CLIENTINC_DIR . 'templates/' . $template);
|
|
echo $this->getMedia();
|
|
}
|
|
|
|
function getLayout($title=false, $options=array()) {
|
|
$rc = @$options['renderer'] ?: static::$renderer;
|
|
return new $rc($title, $options);
|
|
}
|
|
|
|
function asTable($title=false, $options=array()) {
|
|
return $this->getLayout($title, $options)->asTable($this);
|
|
// XXX: Media can't go in a table
|
|
echo $this->getMedia();
|
|
}
|
|
|
|
function getMedia() {
|
|
static $dedup = array();
|
|
|
|
foreach ($this->getFields() as $f) {
|
|
if (($M = $f->getMedia()) && is_array($M)) {
|
|
foreach ($M as $type=>$files) {
|
|
foreach ($files as $url) {
|
|
$key = strtolower($type.$url);
|
|
if (isset($dedup[$key]))
|
|
continue;
|
|
|
|
self::emitMedia($url, $type);
|
|
|
|
$dedup[$key] = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function emitJavascript($options=array()) {
|
|
|
|
// Check if we need to emit javascript
|
|
if (!($fid=$this->getFormId()))
|
|
return;
|
|
?>
|
|
<script type="text/javascript">
|
|
$(function() {
|
|
<?php
|
|
//XXX: We ONLY want to watch field on this form. We'll only
|
|
// watch form inputs if form_id is specified. Current FORM API
|
|
// doesn't generate the entire form (just fields)
|
|
if ($fid) {
|
|
?>
|
|
$(document).off('change.<?php echo $fid; ?>');
|
|
$(document).on('change.<?php echo $fid; ?>',
|
|
'form#<?php echo $fid; ?> :input',
|
|
function() {
|
|
//Clear any current errors...
|
|
var errors = $('#field'+$(this).attr('id')+'_error');
|
|
if (errors.length)
|
|
errors.slideUp('fast', function (){
|
|
$(this).remove();
|
|
});
|
|
//TODO: Validation input inplace or via ajax call
|
|
// and set any new errors AND visibilty changes
|
|
}
|
|
);
|
|
<?php
|
|
}
|
|
?>
|
|
});
|
|
</script>
|
|
<?php
|
|
}
|
|
|
|
static function emitMedia($url, $type) {
|
|
if ($url[0] == '/')
|
|
$url = ROOT_PATH . substr($url, 1);
|
|
|
|
switch (strtolower($type)) {
|
|
case 'css': ?>
|
|
<link rel="stylesheet" type="text/css" href="<?php echo $url; ?>"/><?php
|
|
break;
|
|
case 'js': ?>
|
|
<script type="text/javascript" src="<?php echo $url; ?>"></script><?php
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* getState
|
|
*
|
|
* Retrieves an array of information which can be passed to the
|
|
* ::loadState method later to recreate the current state of the form
|
|
* fields and values.
|
|
*/
|
|
function getState() {
|
|
$info = array();
|
|
foreach ($this->getFields() as $f) {
|
|
// Skip invisible fields
|
|
if (!$f->isVisible())
|
|
continue;
|
|
|
|
// Skip fields set to default values
|
|
$v = $f->getClean();
|
|
$d = $f->get('default');
|
|
if ($v == $d)
|
|
continue;
|
|
|
|
// Skip empty values
|
|
if (!$v)
|
|
continue;
|
|
|
|
$info[$f->get('name') ?: $f->get('id')] = $f->to_database($v);
|
|
}
|
|
return $info;
|
|
}
|
|
|
|
/**
|
|
* loadState
|
|
*
|
|
* Reset this form to the state previously recorded by the ::getState()
|
|
* method
|
|
*/
|
|
function loadState($state) {
|
|
foreach ($this->getFields() as $f) {
|
|
$name = $f->get('name');
|
|
$f->reset();
|
|
if (isset($state[$name])) {
|
|
$f->value = $f->to_php($state[$name]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Initialize a generic static form
|
|
*/
|
|
static function instantiate() {
|
|
$r = new ReflectionClass(get_called_class());
|
|
return $r->newInstanceArgs(func_get_args());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* SimpleForm
|
|
* Wrapper for inline/static forms.
|
|
*
|
|
*/
|
|
class SimpleForm extends Form {
|
|
function __construct($fields=array(), $source=null, $options=array()) {
|
|
parent::__construct($source, $options);
|
|
if (isset($options['type']))
|
|
$this->type = $options['type'];
|
|
$this->setFields($fields);
|
|
}
|
|
|
|
function getId() {
|
|
return $this->getFormId();
|
|
}
|
|
}
|
|
|
|
class CustomForm extends SimpleForm {
|
|
|
|
function getFields() {
|
|
global $thisstaff, $thisclient;
|
|
|
|
$options = $this->options;
|
|
$user = $options['user'] ?: $thisstaff ?: $thisclient;
|
|
$isedit = ($options['mode'] == 'edit');
|
|
$fields = array();
|
|
foreach (parent::getFields() as $field) {
|
|
if ($isedit && !$field->isEditable($user))
|
|
continue;
|
|
|
|
$fields[] = $field;
|
|
}
|
|
|
|
return $fields;
|
|
}
|
|
}
|
|
|
|
abstract class AbstractForm extends Form {
|
|
function __construct($source=null, $options=array()) {
|
|
parent::__construct($source, $options);
|
|
$this->setFields($this->buildFields());
|
|
}
|
|
/**
|
|
* Fetch the fields defined for this form. This method is only called
|
|
* once.
|
|
*/
|
|
abstract function buildFields();
|
|
}
|
|
|
|
/**
|
|
* Container class to represent the connection between the form fields and the
|
|
* rendered state of the form.
|
|
*/
|
|
interface FormRenderer {
|
|
// Render the form fields into a table
|
|
function asTable($form);
|
|
// Render the form fields into divs
|
|
function asBlock($form);
|
|
}
|
|
|
|
abstract class FormLayout {
|
|
static $default_cell_layout = 'Cell';
|
|
|
|
var $title;
|
|
var $options;
|
|
|
|
function __construct($title=false, $options=array()) {
|
|
$this->title = $title;
|
|
$this->options = $options;
|
|
}
|
|
|
|
function getLayout($field) {
|
|
$layout = $field->get('layout') ?: static::$default_cell_layout;
|
|
if (is_string($layout))
|
|
$layout = new $layout();
|
|
return $layout;
|
|
}
|
|
}
|
|
|
|
class GridFluidLayout
|
|
extends FormLayout
|
|
implements FormRenderer {
|
|
function asTable($form) {
|
|
ob_start();
|
|
?>
|
|
<table class="<?php echo 'grid form' ?>">
|
|
<caption><?php echo Format::htmlchars($this->title ?: $form->getTitle()); ?>
|
|
<div><small><?php echo Format::viewableImages($form->getInstructions()); ?></small></div>
|
|
</caption>
|
|
<tbody><tr><?php for ($i=0; $i<12; $i++) echo '<td style="width:8.3333%"/>'; ?></tr></tbody>
|
|
<?php
|
|
$row_size = 12;
|
|
$cols = $row = 0;
|
|
|
|
//Layout and rendering options
|
|
$options = $this->options;
|
|
|
|
foreach ($form->getFields() as $f) {
|
|
$layout = $this->getLayout($f);
|
|
$size = $layout->getWidth() ?: 12;
|
|
if ($offs = $layout->getOffset()) {
|
|
$size += $offs;
|
|
}
|
|
if ($cols < $size || $layout->isBreakForced()) {
|
|
if ($row) echo '</tr>';
|
|
echo '<tr>';
|
|
$cols = $row_size;
|
|
$row++;
|
|
}
|
|
// Render the cell
|
|
$cols -= $size;
|
|
$attrs = array('colspan' => $size, 'rowspan' => $layout->getHeight(),
|
|
'style' => '"'.$layout->getOption('style').'"');
|
|
if ($offs) { ?>
|
|
<td colspan="<?php echo $offs; ?>"></td> <?php
|
|
}
|
|
?>
|
|
<td class="cell" <?php echo Format::array_implode('=', ' ', array_filter($attrs)); ?>
|
|
data-field-id="<?php echo $f->get('id'); ?>">
|
|
<fieldset class="field <?php if (!$f->isVisible()) echo 'hidden'; ?>"
|
|
id="field<?php echo $f->getWidget()->id; ?>"
|
|
data-field-id="<?php echo $f->get('id'); ?>">
|
|
<?php $label = $f->get('label'); ?>
|
|
<label class="<?php if ($f->isRequired()) echo 'required'; ?>"
|
|
for="<?php echo $f->getWidget()->id; ?>">
|
|
<?php echo $label ? (Format::htmlchars($label).':') : ' '; ?>
|
|
<?php if ($f->isRequired()) { ?>
|
|
<span class="error">*</span>
|
|
</label>
|
|
<?php }
|
|
if ($f->get('hint')) { ?>
|
|
<div class="field-hint-text">
|
|
<?php echo Format::htmlchars($f->get('hint')); ?>
|
|
</div>
|
|
<?php }
|
|
$f->render($options);
|
|
if ($f->errors())
|
|
foreach ($f->errors() as $e)
|
|
echo sprintf('<div class="error">%s</div>', Format::htmlchars($e));
|
|
?>
|
|
</fieldset>
|
|
</td>
|
|
<?php
|
|
}
|
|
if ($row)
|
|
echo '</tr>';
|
|
|
|
echo '</tbody></table>';
|
|
|
|
return ob_get_clean();
|
|
}
|
|
|
|
function asBlock($form) {}
|
|
}
|
|
|
|
/**
|
|
* Basic container for field and form layouts. By default every cell takes
|
|
* a whole output row and does not imply any sort of width.
|
|
*/
|
|
class Cell {
|
|
function isBreakForced() { return true; }
|
|
function getWidth() { return false; }
|
|
function getHeight() { return 1; }
|
|
function getOffset() { return 0; }
|
|
function getOption($prop) { return false; }
|
|
}
|
|
|
|
/**
|
|
* Fluid grid layout, meaning each cell renders to the right of the previous
|
|
* cell (for left-to-right layouts). A width in columns can be specified for
|
|
* each cell along with an offset from the previous cell. A height of columns
|
|
* along with an optional break is supported.
|
|
*/
|
|
class GridFluidCell
|
|
extends Cell {
|
|
var $span;
|
|
var $options;
|
|
|
|
function __construct($span, $options=array()) {
|
|
$this->span = $span;
|
|
$this->options = $options + array(
|
|
'rows' => 1, # rowspan
|
|
'offset' => 0, # skip some columns
|
|
'break' => false, # start on a new row
|
|
);
|
|
}
|
|
|
|
function isBreakForced() { return $this->options['break']; }
|
|
function getWidth() { return $this->span; }
|
|
function getHeight() { return $this->options['rows']; }
|
|
function getOffset() { return $this->options['offset']; }
|
|
function getOption($prop) { return $this->options[$prop]; }
|
|
}
|
|
|
|
require_once(INCLUDE_DIR . "class.json.php");
|
|
|
|
class FormField {
|
|
static $widget = false;
|
|
|
|
var $ht = array(
|
|
'label' => false,
|
|
'required' => false,
|
|
'default' => false,
|
|
'configuration' => array(),
|
|
);
|
|
|
|
var $_form;
|
|
var $_cform;
|
|
var $_clean;
|
|
var $_errors = array();
|
|
var $_widget;
|
|
var $answer;
|
|
var $parent;
|
|
var $presentation_only = false;
|
|
|
|
static $types = array(
|
|
/* @trans */ 'Basic Fields' => array(
|
|
'text' => array( /* @trans */ 'Short Answer', 'TextboxField'),
|
|
'memo' => array( /* @trans */ 'Long Answer', 'TextareaField'),
|
|
'thread' => array( /* @trans */ 'Thread Entry', 'ThreadEntryField', false),
|
|
'datetime' => array(/* @trans */ 'Date and Time', 'DatetimeField'),
|
|
'timezone' => array(/* @trans */ 'Timezone', 'TimezoneField'),
|
|
'phone' => array( /* @trans */ 'Phone Number', 'PhoneField'),
|
|
'bool' => array( /* @trans */ 'Checkbox', 'BooleanField'),
|
|
'choices' => array( /* @trans */ 'Choices', 'ChoiceField'),
|
|
'files' => array( /* @trans */ 'File Upload', 'FileUploadField'),
|
|
'break' => array( /* @trans */ 'Section Break', 'SectionBreakField'),
|
|
'info' => array( /* @trans */ 'Information', 'FreeTextField'),
|
|
),
|
|
);
|
|
static $more_types = array();
|
|
static $uid = null;
|
|
|
|
function _uid() {
|
|
return ++self::$uid;
|
|
}
|
|
|
|
function __construct($options=array()) {
|
|
$this->ht = array_merge($this->ht, $options);
|
|
if (!isset($this->ht['id']))
|
|
$this->ht['id'] = self::_uid();
|
|
}
|
|
|
|
function __clone() {
|
|
$this->_widget = null;
|
|
$this->ht['id'] = self::_uid();
|
|
}
|
|
|
|
static function addFieldTypes($group, $callable) {
|
|
static::$more_types[$group][] = $callable;
|
|
}
|
|
|
|
static function allTypes() {
|
|
if (static::$more_types) {
|
|
foreach (static::$more_types as $group => $entries)
|
|
foreach ($entries as $c)
|
|
static::$types[$group] = array_merge(
|
|
static::$types[$group] ?: array(), call_user_func($c));
|
|
|
|
static::$more_types = array();
|
|
}
|
|
return static::$types;
|
|
}
|
|
|
|
static function getFieldType($type) {
|
|
foreach (static::allTypes() as $group=>$types)
|
|
if (isset($types[$type]))
|
|
return $types[$type];
|
|
}
|
|
|
|
function get($what, $default=null) {
|
|
return array_key_exists($what, $this->ht)
|
|
? $this->ht[$what]
|
|
: $default;
|
|
}
|
|
function set($field, $value) {
|
|
$this->ht[$field] = $value;
|
|
}
|
|
|
|
function getId() {
|
|
return $this->ht['id'];
|
|
}
|
|
|
|
/**
|
|
* getClean
|
|
*
|
|
* Validates and cleans inputs from POST request. This is performed on a
|
|
* field instance, after a DynamicFormSet / DynamicFormSection is
|
|
* submitted via POST, in order to kick off parsing and validation of
|
|
* user-entered data.
|
|
*/
|
|
function getClean($validate=true) {
|
|
if (!isset($this->_clean)) {
|
|
$this->_clean = (isset($this->value))
|
|
// XXX: The widget value may be parsed already if this is
|
|
// linked to dynamic data via ::getAnswer()
|
|
? $this->value : $this->parse($this->getWidget()->value);
|
|
|
|
if ($vs = $this->get('cleaners')) {
|
|
if (is_array($vs)) {
|
|
foreach ($vs as $cleaner)
|
|
if (is_callable($cleaner))
|
|
$this->_clean = call_user_func_array(
|
|
$cleaner, array($this, $this->_clean));
|
|
}
|
|
elseif (is_callable($vs))
|
|
$this->_clean = call_user_func_array(
|
|
$vs, array($this, $this->_clean));
|
|
}
|
|
|
|
if (!isset($this->_clean) && ($d = $this->get('default')))
|
|
$this->_clean = $d;
|
|
|
|
if ($this->isVisible() && $validate)
|
|
$this->validateEntry($this->_clean);
|
|
}
|
|
return $this->_clean;
|
|
}
|
|
|
|
function reset() {
|
|
$this->value = $this->_clean = $this->_widget = null;
|
|
}
|
|
|
|
function getValue() {
|
|
return $this->getWidget()->getValue();
|
|
}
|
|
|
|
function errors() {
|
|
return $this->_errors;
|
|
}
|
|
function addError($message, $index=false) {
|
|
if ($index)
|
|
$this->_errors[$index] = $message;
|
|
else
|
|
$this->_errors[] = $message;
|
|
|
|
// Update parent form errors for the field
|
|
if ($this->_form)
|
|
$this->_form->addError($this->errors(), $this->get('id'));
|
|
}
|
|
|
|
function isValidEntry() {
|
|
$this->validateEntry();
|
|
return count($this->_errors) == 0;
|
|
}
|
|
|
|
/**
|
|
* validateEntry
|
|
*
|
|
* Validates user entry on an instance of the field on a dynamic form.
|
|
* This is called when an instance of this field (like a TextboxField)
|
|
* receives data from the user and that value should be validated.
|
|
*
|
|
* Parameters:
|
|
* $value - (string) input from the user
|
|
*/
|
|
function validateEntry($value) {
|
|
if (!$value && count($this->_errors))
|
|
return;
|
|
|
|
# Validates a user-input into an instance of this field on a dynamic
|
|
# form
|
|
if ($this->get('required') && !$value && $this->hasData())
|
|
$this->_errors[] = $this->getLocal('label')
|
|
? sprintf(__('%s is a required field'), $this->getLocal('label'))
|
|
: __('This is a required field');
|
|
|
|
# Perform declared validators for the field
|
|
if ($vs = $this->get('validators')) {
|
|
if (is_array($vs)) {
|
|
foreach ($vs as $validator)
|
|
if (is_callable($validator))
|
|
$validator($this, $value);
|
|
}
|
|
elseif (is_callable($vs))
|
|
$vs($this, $value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* isVisible
|
|
*
|
|
* If this field has visibility configuration, then it will parse the
|
|
* constraints with the visibility configuration to determine if the
|
|
* field is visible and should be considered for validation
|
|
*/
|
|
function isVisible() {
|
|
if ($this->get('visibility') instanceof VisibilityConstraint) {
|
|
return $this->get('visibility')->isVisible($this);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Check if the user has edit rights
|
|
*
|
|
*/
|
|
|
|
function isEditable($user=null) {
|
|
|
|
// Internal editable flag used by internal forms e.g internal lists
|
|
if (!$user && isset($this->ht['editable']))
|
|
return $this->ht['editable'];
|
|
|
|
if ($user instanceof Staff)
|
|
$flag = DynamicFormField::FLAG_AGENT_EDIT;
|
|
else
|
|
$flag = DynamicFormField::FLAG_CLIENT_EDIT;
|
|
|
|
return (($this->get('flags') & $flag) != 0);
|
|
}
|
|
|
|
|
|
/**
|
|
* isStorable
|
|
*
|
|
* Indicate if this field data is storable locally (default).Some field's data
|
|
* might beed to be stored elsewhere for optimization reasons at the
|
|
* application level.
|
|
*
|
|
*/
|
|
|
|
function isStorable() {
|
|
return (($this->get('flags') & DynamicFormField::FLAG_EXT_STORED) == 0);
|
|
}
|
|
|
|
function isRequired() {
|
|
return $this->get('required');
|
|
}
|
|
|
|
/**
|
|
* parse
|
|
*
|
|
* Used to transform user-submitted data to a PHP value. This value is
|
|
* not yet considered valid. The ::validateEntry() method will be called
|
|
* on the value to determine if the entry is valid. Therefore, if the
|
|
* data is clearly invalid, return something like NULL that can easily
|
|
* be deemed invalid in ::validateEntry(), however, can still produce a
|
|
* useful error message indicating what is wrong with the input.
|
|
*/
|
|
function parse($value) {
|
|
return is_string($value) ? trim($value) : $value;
|
|
}
|
|
|
|
/**
|
|
* to_php
|
|
*
|
|
* Transforms the data from the value stored in the database to a PHP
|
|
* value. The ::to_database() method is used to produce the database
|
|
* valse, so this method is the compliment to ::to_database().
|
|
*
|
|
* Parameters:
|
|
* $value - (string or null) database representation of the field's
|
|
* content
|
|
*/
|
|
function to_php($value) {
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* to_config
|
|
*
|
|
* Transform the data from the value to config form (as determined by
|
|
* field). to_php is used for each field returned from
|
|
* ::getConfigurationOptions(), and when the whole configuration is
|
|
* built, to_config() is called and receives the config array. The array
|
|
* should be returned, perhaps with modifications, and will be JSON
|
|
* encoded and stashed in the database.
|
|
*/
|
|
function to_config($value) {
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* to_database
|
|
*
|
|
* Determines the value to be stored in the database. The database
|
|
* backend for all fields is a text field, so this method should return
|
|
* a text value or NULL to represent the value of the field. The
|
|
* ::to_php() method will convert this value back to PHP.
|
|
*
|
|
* Paremeters:
|
|
* $value - PHP value of the field's content
|
|
*/
|
|
function to_database($value) {
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* toString
|
|
*
|
|
* Converts the PHP value created in ::parse() or ::to_php() to a
|
|
* pretty-printed value to show to the user. This is especially useful
|
|
* for something like dates which are stored considerably different in
|
|
* the database from their respective human-friendly versions.
|
|
* Furthermore, this method allows for internationalization and
|
|
* localization.
|
|
*
|
|
* Parametes:
|
|
* $value - PHP value of the field's content
|
|
*/
|
|
function toString($value) {
|
|
return (string) $value;
|
|
}
|
|
|
|
function __toString() {
|
|
return $this->toString($this->value);
|
|
}
|
|
|
|
/**
|
|
* When data for this field is deleted permanently from some storage
|
|
* backend (like a database), other associated data may need to be
|
|
* cleaned as well. This hook allows fields to participate when the data
|
|
* for a field is cleaned up.
|
|
*/
|
|
function db_cleanup($field=false) {
|
|
}
|
|
|
|
/**
|
|
* Returns an HTML friendly value for the data in the field.
|
|
*/
|
|
function display($value) {
|
|
return Format::htmlchars($this->toString($value ?: $this->value));
|
|
}
|
|
|
|
/**
|
|
* Returns a value suitable for exporting to a foreign system. Mostly
|
|
* useful for things like dates and phone numbers which should be
|
|
* formatted using a standard when exported
|
|
*/
|
|
function export($value) {
|
|
return $this->toString($value);
|
|
}
|
|
|
|
/**
|
|
* Fetch a value suitable for embedding the value of this field in an
|
|
* email template. Reference implementation uses ::to_php();
|
|
*/
|
|
function asVar($value, $id=false) {
|
|
return $this->to_php($value, $id);
|
|
}
|
|
|
|
/**
|
|
* Fetch the var type used with the email templating system's typeahead
|
|
* feature. This helps with variable expansion if supported by this
|
|
* field's ::asVar() method. This method should return a valid classname
|
|
* which implements the `TemplateVariable` interface.
|
|
*/
|
|
function asVarType() {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Describe the difference between the to two values. Note that the
|
|
* values should be passed through ::parse() or to_php() before
|
|
* utilizing this method.
|
|
*/
|
|
function whatChanged($before, $after) {
|
|
if ($before)
|
|
$desc = __('changed from <strong>%1$s</strong> to <strong>%2$s</strong>');
|
|
else
|
|
$desc = __('set to <strong>%2$s</strong>');
|
|
return sprintf($desc, $this->display($before), $this->display($after));
|
|
}
|
|
|
|
/**
|
|
* Convert the field data to something matchable by filtering. The
|
|
* primary use of this is for ticket filtering.
|
|
*/
|
|
function getFilterData() {
|
|
return $this->toString($this->getClean());
|
|
}
|
|
|
|
/**
|
|
* Fetches a value that represents this content in a consistent,
|
|
* searchable format. This is used by the search engine system and
|
|
* backend.
|
|
*/
|
|
function searchable($value) {
|
|
return Format::searchable($this->toString($value));
|
|
}
|
|
|
|
function getKeys($value) {
|
|
return $this->to_database($value);
|
|
}
|
|
|
|
/**
|
|
* Fetches a list of options for searching. The values returned from
|
|
* this method are passed to the widget's `::render()` method so that
|
|
* the widget can be affected by this setting. For instance, date fields
|
|
* might have a 'between' search option which should trigger rendering
|
|
* of two date widgets for search results.
|
|
*/
|
|
function getSearchMethods() {
|
|
return array(
|
|
'set' => __('has a value'),
|
|
'nset' => __('does not have a value'),
|
|
'equal' => __('is'),
|
|
'nequal' => __('is not'),
|
|
'contains' => __('contains'),
|
|
'match' => __('matches'),
|
|
);
|
|
}
|
|
|
|
function getSearchMethodWidgets() {
|
|
return array(
|
|
'set' => null,
|
|
'nset' => null,
|
|
'equal' => array('TextboxField', array('configuration' => array('size' => 40))),
|
|
'nequal' => array('TextboxField', array('configuration' => array('size' => 40))),
|
|
'contains' => array('TextboxField', array('configuration' => array('size' => 40))),
|
|
'match' => array('TextboxField', array(
|
|
'placeholder' => __('Valid regular expression'),
|
|
'configuration' => array('size'=>30),
|
|
'validators' => function($self, $v) {
|
|
if (false === @preg_match($v, ' ')
|
|
&& false === @preg_match("/$v/", ' '))
|
|
$self->addError(__('Cannot compile this regular expression'));
|
|
})),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* This is used by the searching system to build a query for the search
|
|
* engine. The function should return a criteria listing to match
|
|
* content saved by the field by the `::to_database()` function.
|
|
*/
|
|
function getSearchQ($method, $value, $name=false) {
|
|
$criteria = array();
|
|
$Q = new Q();
|
|
$name = $name ?: $this->get('name');
|
|
switch ($method) {
|
|
case 'nset':
|
|
$Q->negate();
|
|
case 'set':
|
|
$criteria[$name . '__isnull'] = false;
|
|
break;
|
|
|
|
case 'nequal':
|
|
$Q->negate();
|
|
case 'equal':
|
|
$criteria[$name] = $value;
|
|
break;
|
|
|
|
case 'contains':
|
|
$criteria[$name . '__contains'] = $value;
|
|
break;
|
|
|
|
case 'match':
|
|
$criteria[$name . '__regex'] = $value;
|
|
break;
|
|
}
|
|
return $Q->add($criteria);
|
|
}
|
|
|
|
function getSearchWidget($method) {
|
|
$methods = $this->getSearchMethodWidgets();
|
|
$info = $methods[$method];
|
|
if (is_array($info)) {
|
|
$class = $info[0];
|
|
return new $class($info[1]);
|
|
}
|
|
return $info;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
// Allow registration with ::addFieldTypes and delayed calling
|
|
$type = static::getFieldType($this->get('type'));
|
|
$clazz = $type[1];
|
|
$inst = new $clazz($this->ht);
|
|
$inst->parent = $parent;
|
|
$inst->setForm($this->_form);
|
|
return $inst;
|
|
}
|
|
|
|
function __call($what, $args) {
|
|
// XXX: Throw exception if $this->parent is not set
|
|
if (!$this->parent)
|
|
throw new Exception(sprintf(__('%s: Call to undefined function'),
|
|
$what));
|
|
// 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);
|
|
}
|
|
|
|
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. Therefore, 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.
|
|
*/
|
|
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-" . SECRET_SALT), -14);
|
|
elseif (is_numeric($this->get('id')))
|
|
return substr(md5(
|
|
session_id() . '-field-id-'.$this->get('id') . '-' . SECRET_SALT), -16);
|
|
|
|
return $default;
|
|
}
|
|
|
|
function getFormNames() {
|
|
|
|
// All possible names - this is important for inline data injection
|
|
$names = array_filter([
|
|
'hash' => $this->getFormName(),
|
|
'name' => $this->get('name'),
|
|
'id' => $this->get('id')]);
|
|
|
|
// Force pseudo-random name for Dynamicforms on POST (Web Tickets)
|
|
if (0 && $_POST
|
|
&& !defined('APICALL')
|
|
&& isset($this->ht['form'])
|
|
&& ($this->ht['form'] instanceof DynamicForm))
|
|
return [$names['hash']];
|
|
|
|
return $names;
|
|
}
|
|
|
|
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()) {
|
|
return;
|
|
}
|
|
|
|
function getMedia() {
|
|
$widget = $this->getWidget();
|
|
return $widget::$media;
|
|
}
|
|
|
|
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
|
|
*/
|
|
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) {
|
|
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);
|
|
if (!$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');
|
|
}
|
|
}
|
|
}
|
|
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;
|
|
$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*'));
|
|
}
|
|
|
|
function getLocal($subtag, $default=false) {
|
|
$tag = $this->getTranslateTag($subtag);
|
|
$T = CustomDataTranslation::translate($tag);
|
|
return $T != $tag ? $T : ($default ?: $this->get($subtag));
|
|
}
|
|
|
|
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() {
|
|
$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 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;
|
|
|
|
$this->_clean = $this->_widget = null;
|
|
return $this->parent->save();
|
|
}
|
|
|
|
|
|
static function init($config) {
|
|
return new Static($config);
|
|
}
|
|
}
|
|
|
|
class TextboxField extends FormField {
|
|
static $widget = 'TextboxWidget';
|
|
|
|
function getConfigurationOptions() {
|
|
return array(
|
|
'size' => new TextboxField(array(
|
|
'id'=>1, 'label'=>__('Size'), 'required'=>false, 'default'=>16,
|
|
'validator' => 'number')),
|
|
'length' => new TextboxField(array(
|
|
'id'=>2, 'label'=>__('Max Length'), 'required'=>false, 'default'=>30,
|
|
'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, ' ')) {
|
|
$value = $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')
|
|
),
|
|
)),
|
|
);
|
|
}
|
|
|
|
function validateEntry($value) {
|
|
//check to see if value is the string '0'
|
|
$value = ($value === '0') ? '0' : Format::htmlchars($this->toString($value ?: $this->value));
|
|
parent::validateEntry($value);
|
|
$config = $this->getConfiguration();
|
|
$validators = array(
|
|
'' => '',
|
|
'noop' => array(
|
|
function($a, &$b) { return true; }
|
|
),
|
|
'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')),
|
|
'phone' => array(array('Validator', 'is_phone'),
|
|
__('Enter a valid phone number')),
|
|
'ip' => array(array('Validator', 'is_ip'),
|
|
__('Enter a valid IP address')),
|
|
'number' => array(array('Validator', 'is_numeric'),
|
|
__('Enter a number')),
|
|
'password' => array(array('Validator', 'check_passwd'),
|
|
__('Invalid Password')),
|
|
'regex' => array(
|
|
function($v) use ($config) {
|
|
$regex = $config['regex'];
|
|
return @preg_match($regex, $v);
|
|
}, __('Value does not match required pattern')
|
|
),
|
|
);
|
|
// 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';
|
|
$func = $validators[$valid];
|
|
$error = $err = null;
|
|
// If validator is number and the value is 0 set to 0 (int) for is_numeric
|
|
if ($valid == 'number' && $value == '0')
|
|
$value = 0;
|
|
if ($config['validator-error'])
|
|
$error = $this->getLocal('validator-error', $config['validator-error']);
|
|
if (is_array($func) && is_callable($func[0]))
|
|
if (!call_user_func_array($func[0], array($value, &$err)))
|
|
$this->_errors[] = $error ?: $err ?: $func[1];
|
|
}
|
|
|
|
function parse($value) {
|
|
return Format::strip_emoticons(Format::striptags($value));
|
|
}
|
|
|
|
function display($value) {
|
|
return ($value === '0') ? '0' : Format::htmlchars($this->toString($value ?: $this->value), true);
|
|
}
|
|
}
|
|
|
|
class PasswordField extends TextboxField {
|
|
static $widget = 'PasswordWidget';
|
|
|
|
function __construct($options=array()) {
|
|
parent::__construct($options);
|
|
if (!isset($options['validator']))
|
|
$this->set('validator', 'password');
|
|
}
|
|
|
|
function parse($value) {
|
|
// Don't trim the value
|
|
return $value;
|
|
}
|
|
|
|
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');
|
|
}
|
|
|
|
function to_php($value) {
|
|
return Crypto::decrypt($value, SECRET_SALT, 'pwfield');
|
|
}
|
|
}
|
|
|
|
class TextareaField extends FormField {
|
|
static $widget = 'TextareaWidget';
|
|
|
|
function getConfigurationOptions() {
|
|
return array(
|
|
'cols' => new TextboxField(array(
|
|
'id'=>1, 'label'=>__('Width').' '.__('(chars)'), 'required'=>true, 'default'=>40)),
|
|
'rows' => new TextboxField(array(
|
|
'id'=>2, 'label'=>__('Height').' '.__('(rows)'), 'required'=>false, 'default'=>4)),
|
|
'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]))
|
|
return;
|
|
|
|
$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, true));
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
}
|
|
|
|
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')),
|
|
)),
|
|
);
|
|
}
|
|
|
|
function validateEntry($value) {
|
|
parent::validateEntry($value);
|
|
$config = $this->getConfiguration();
|
|
# 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']) {
|
|
if (!is_numeric($ext))
|
|
$this->_errors[] = __("Enter a valid phone extension");
|
|
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;
|
|
}
|
|
|
|
function toString($value) {
|
|
$config = $this->getConfiguration();
|
|
list($phone, $ext) = explode("X", $value, 2);
|
|
switch ($config['format']) {
|
|
case 'us':
|
|
$phone = Format::phone($phone);
|
|
break;
|
|
}
|
|
if ($ext)
|
|
$phone.=" x$ext";
|
|
return $phone;
|
|
}
|
|
}
|
|
|
|
class BooleanField extends FormField {
|
|
static $widget = 'CheckboxWidget';
|
|
|
|
function getConfigurationOptions() {
|
|
return array(
|
|
'desc' => new TextareaField(array(
|
|
'id'=>1, 'label'=>__('Description'), 'required'=>false, 'default'=>'',
|
|
'hint'=>__('Text shown inline with the widget'),
|
|
'configuration'=>array('rows'=>2)))
|
|
);
|
|
}
|
|
|
|
function to_database($value) {
|
|
return ($value) ? '1' : '0';
|
|
}
|
|
|
|
function parse($value) {
|
|
return $this->to_php($value);
|
|
}
|
|
function to_php($value) {
|
|
return $value ? true : false;
|
|
}
|
|
|
|
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 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,
|
|
'nset' => null,
|
|
);
|
|
}
|
|
|
|
function getSearchQ($method, $value, $name=false) {
|
|
$name = $name ?: $this->get('name');
|
|
switch ($method) {
|
|
case 'set':
|
|
return new Q(array($name => '1'));
|
|
case 'nset':
|
|
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,
|
|
));
|
|
}
|
|
}
|
|
|
|
class ChoiceField extends FormField {
|
|
static $widget = 'ChoicesWidget';
|
|
var $_choices;
|
|
|
|
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))
|
|
$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
|
|
if (is_string($value) && strpos($value, ',')) {
|
|
$values = array();
|
|
$choices = $this->getChoices();
|
|
$vals = array_map('trim', explode(',', $value));
|
|
foreach ($vals as $V) {
|
|
if (isset($choices[$V]))
|
|
$values[$V] = $choices[$V];
|
|
}
|
|
if (array_filter($values))
|
|
$value = $values;
|
|
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 getChanges() {
|
|
$new = $this->to_database($this->getValue());
|
|
$old = $this->to_database($this->answer ? $this->answer->getValue()
|
|
: $this->get('default'));
|
|
// Compare old and new
|
|
return ($old == $new)
|
|
? false
|
|
: array($old, $new);
|
|
}
|
|
|
|
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);
|
|
$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));
|
|
}
|
|
return $desc;
|
|
}
|
|
|
|
/*
|
|
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)) {
|
|
$selection = $value;
|
|
} elseif (isset($choices[$value]))
|
|
$selection[$value] = $choices[$value];
|
|
elseif (($v=$this->get('default')) && isset($choices[$v]))
|
|
$selection[$v] = $choices[$v];
|
|
|
|
return $selection;
|
|
}
|
|
|
|
function getChoices($verbose=false, $options=array()) {
|
|
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);
|
|
}
|
|
// 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,
|
|
'nset' => 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':
|
|
return Q::not(array("{$name}__regex" => $val));
|
|
case 'includes':
|
|
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);
|
|
}
|
|
}
|
|
|
|
function supportsQuickFilter() {
|
|
return true;
|
|
}
|
|
|
|
function getQuickFilterChoices() {
|
|
return $this->getChoices();
|
|
}
|
|
|
|
function applyQuickFilter($query, $qf_value, $name=false) {
|
|
global $thisstaff;
|
|
|
|
//special assignment quick filters
|
|
switch (true) {
|
|
case ($qf_value == 'assigned'):
|
|
case ($qf_value == '!assigned'):
|
|
$result = AssigneeChoiceField::getSearchQ($qf_value, $qf_value);
|
|
return $query->filter($result);
|
|
case (strpos($qf_value, 's') !== false):
|
|
case (strpos($qf_value, 't') !== false):
|
|
case ($qf_value == 'M'):
|
|
case ($qf_value == 'T'):
|
|
$value = array($qf_value => $qf_value);
|
|
$result = AssigneeChoiceField::getSearchQ('includes', $value);
|
|
return $query->filter($result);
|
|
break;
|
|
}
|
|
|
|
return $query->filter(array(
|
|
$name ?: $this->get('name') => $qf_value,
|
|
));
|
|
}
|
|
}
|
|
|
|
class NumericField extends FormField {
|
|
|
|
function getSearchMethods() {
|
|
return array(
|
|
'equal' => __('Equal'),
|
|
'greater' => __('Greater Than'),
|
|
'less' => __('Less Than'),
|
|
);
|
|
}
|
|
|
|
function getSearchMethodWidgets() {
|
|
return array(
|
|
'equal' => array('TextboxField', array(
|
|
'configuration' => array(
|
|
'validator' => 'number',
|
|
'size' => 6
|
|
),
|
|
)),
|
|
'greater' => array('TextboxField', array(
|
|
'configuration' => array(
|
|
'validator' => 'number',
|
|
'size' => 6
|
|
),
|
|
)),
|
|
'less' => array('TextboxField', array(
|
|
'configuration' => array(
|
|
'validator' => 'number',
|
|
'size' => 6
|
|
),
|
|
)),
|
|
);
|
|
}
|
|
|
|
function getSearchQ($method, $value, $name=false) {
|
|
switch ($method) {
|
|
case 'equal':
|
|
return new Q(array(
|
|
"{$name}__exact" => intval($value)
|
|
));
|
|
break;
|
|
case 'greater':
|
|
return Q::any(array(
|
|
"{$name}__gt" => intval($value)
|
|
));
|
|
break;
|
|
case 'less':
|
|
return Q::any(array(
|
|
"{$name}__lt" => intval($value)
|
|
));
|
|
break;
|
|
default:
|
|
return parent::getSearchQ($method, $value, $name);
|
|
}
|
|
}
|
|
}
|
|
|
|
class DatetimeField extends FormField {
|
|
static $widget = 'DatetimePickerWidget';
|
|
|
|
var $min = null;
|
|
var $max = null;
|
|
|
|
static function intervals($count=2, $i='') {
|
|
$intervals = array(
|
|
'i' => _N('minute', 'minutes', $count),
|
|
'h' => _N('hour', 'hours', $count),
|
|
'd' => _N('day','days', $count),
|
|
'w' => _N('week', 'weeks', $count),
|
|
'm' => _N('month', 'months', $count),
|
|
);
|
|
return $i ? $intervals[$i] : $intervals;
|
|
}
|
|
|
|
static function periods($period='') {
|
|
$periods = array(
|
|
'td' => __('Today'),
|
|
'yd' => __('Yesterday'),
|
|
'tw' => __('This Week'),
|
|
'tm' => __('This Month'),
|
|
'tq' => __('This Quarter'),
|
|
'ty' => __('This Year'),
|
|
'lw' => __('Last Week'),
|
|
'lm' => __('Last Month'),
|
|
'lq' => __('Last Quarter'),
|
|
'ly' => __('Last Year'),
|
|
);
|
|
return $period ? $periods[$period] : $periods;
|
|
}
|
|
|
|
// Get php DatateTime object of the field - null if value is empty
|
|
function getDateTime($value=null) {
|
|
return Format::parseDateTime($value ?: $this->value);
|
|
}
|
|
|
|
// Get effective timezone for the field
|
|
function getTimeZone() {
|
|
global $cfg;
|
|
|
|
$config = $this->getConfiguration();
|
|
$timezone = new DateTimeZone($config['timezone'] ?:
|
|
$cfg->getTimezone());
|
|
|
|
return $timezone;
|
|
}
|
|
|
|
function getMinDateTime() {
|
|
|
|
if (!isset($this->min)) {
|
|
$config = $this->getConfiguration();
|
|
$this->min = $config['min']
|
|
? Format::parseDateTime($config['min']) : false;
|
|
}
|
|
|
|
return $this->min;
|
|
}
|
|
|
|
function getMaxDateTime() {
|
|
|
|
if (!isset($this->max)) {
|
|
$config = $this->getConfiguration();
|
|
$this->max = $config['max']
|
|
? Format::parseDateTime($config['max']) : false;
|
|
}
|
|
|
|
return $this->max;
|
|
}
|
|
|
|
function getPastPresentLabels() {
|
|
return array(__('Create Date'), __('Reopen Date'),
|
|
__('Close Date'), __('Last Update'));
|
|
}
|
|
|
|
function to_database($value) {
|
|
// Store time in format given by Date Picker (DateTime::W3C)
|
|
return $value;
|
|
}
|
|
|
|
function to_php($value) {
|
|
|
|
if (!is_numeric($value) && strtotime($value) <= 0)
|
|
return 0;
|
|
|
|
return $value;
|
|
}
|
|
|
|
function display($value) {
|
|
global $cfg;
|
|
|
|
if (!$value || !($datetime = Format::parseDateTime($value)))
|
|
return '';
|
|
|
|
$config = $this->getConfiguration();
|
|
$format = $config['format'] ?: false;
|
|
if ($config['gmt'])
|
|
return $this->format((int) $datetime->format('U'), $format);
|
|
|
|
// Force timezone if field has one.
|
|
if ($config['timezone']) {
|
|
$timezone = new DateTimezone($config['timezone']);
|
|
$datetime->setTimezone($timezone);
|
|
}
|
|
|
|
$value = $this->format($datetime->format('U'),
|
|
$datetime->getTimezone()->getName(),
|
|
$format);
|
|
// No need to show timezone
|
|
if (!$config['time'] || $format)
|
|
return $value;
|
|
|
|
// Display is NOT timezone aware show entry's timezone.
|
|
return sprintf('%s (%s)',
|
|
$value, $datetime->format('T'));
|
|
}
|
|
|
|
function from_query($row, $name=false) {
|
|
$value = parent::from_query($row, $name);
|
|
$timestamp = is_int($value) ? $value : (int) strtotime($value);
|
|
return ($timestamp > 0) ? $timestamp : '';
|
|
}
|
|
|
|
function format($timestamp, $timezone=false, $format=false) {
|
|
|
|
if (!$timestamp || $timestamp <= 0)
|
|
return '';
|
|
|
|
$config = $this->getConfiguration();
|
|
if ($config['time'])
|
|
$formatted = Format::datetime($timestamp, false, $format, $timezone);
|
|
else
|
|
$formatted = Format::date($timestamp, false, $format, $timezone);
|
|
|
|
return $formatted;
|
|
}
|
|
|
|
function toString($value) {
|
|
if (is_array($value))
|
|
return '';
|
|
|
|
$timestamp = is_int($value) ? $value : (int) strtotime($value);
|
|
if ($timestamp <= 0)
|
|
return '';
|
|
|
|
return $this->format($timestamp);
|
|
}
|
|
|
|
function asVar($value, $id=false) {
|
|
global $cfg;
|
|
|
|
if (!$value)
|
|
return null;
|
|
|
|
$datetime = $this->getDateTime($value);
|
|
$config = $this->getConfiguration();
|
|
if (!$config['gmt'] || !$config['time'])
|
|
$timezone = $datetime->getTimezone()->getName();
|
|
else
|
|
$timezone = false;
|
|
|
|
return new FormattedDate($value, array(
|
|
'timezone' => $timezone,
|
|
'format' => $config['time'] ? 'long' : 'short'
|
|
)
|
|
);
|
|
}
|
|
|
|
function asVarType() {
|
|
return 'FormattedDate';
|
|
}
|
|
|
|
function getConfigurationOptions() {
|
|
return array(
|
|
'time' => new BooleanField(array(
|
|
'id'=>1, 'label'=>__('Time'), 'required'=>false, 'default'=>false,
|
|
'configuration'=>array(
|
|
'desc'=>__('Show time selection with date picker')))),
|
|
'timezone' => new TimezoneField(array(
|
|
'id'=>2, 'label'=>__('Timezone'), 'required'=>false,
|
|
'hint'=>__('Timezone of the date time selection'),
|
|
'configuration' => array('autodetect'=>false,
|
|
'prompt' => __("User's timezone")),
|
|
'visibility' => new VisibilityConstraint(
|
|
new Q(array('time__eq'=> true)),
|
|
VisibilityConstraint::HIDDEN
|
|
),
|
|
)),
|
|
'gmt' => new BooleanField(array(
|
|
'id'=>3, 'label'=>__('Timezone Aware'), 'required'=>false,
|
|
'configuration'=>array(
|
|
'desc'=>__("Show date/time relative to user's timezone")))),
|
|
'min' => new DatetimeField(array(
|
|
'id'=>4, 'label'=>__('Earliest'), 'required'=>false,
|
|
'hint'=>__('Earliest date selectable'))),
|
|
'max' => new DatetimeField(array(
|
|
'id'=>5, 'label'=>__('Latest'), 'required'=>false,
|
|
'default'=>null, 'hint'=>__('Latest date selectable'))),
|
|
'future' => new BooleanField(array(
|
|
'id'=>6, 'label'=>__('Allow Future Dates'), 'required'=>false,
|
|
'default'=>true, 'configuration'=>array(
|
|
'desc'=>__('Allow entries into the future' /* Used in the date field */)),
|
|
)),
|
|
);
|
|
}
|
|
|
|
function validateEntry($value) {
|
|
global $cfg;
|
|
|
|
$config = $this->getConfiguration();
|
|
parent::validateEntry($value);
|
|
if (!$value || !($datetime = Format::parseDateTime($value)))
|
|
return;
|
|
|
|
// Get configured min/max (if any)
|
|
$min = $this->getMinDateTime();
|
|
$max = $this->getMaxDateTime();
|
|
|
|
if (!$datetime) {
|
|
$this->_errors[] = __('Enter a valid date');
|
|
} elseif ($min and $datetime < $min) {
|
|
$this->_errors[] = sprintf('%s (%s)',
|
|
__('Selected date is earlier than permitted'),
|
|
Format::date($min->getTimestamp(), false, false,
|
|
$min->getTimezone()->getName() ?: 'UTC')
|
|
);
|
|
} elseif ($max and $datetime > $max) {
|
|
$this->_errors[] = sprintf('%s (%s)',
|
|
__('Selected date is later than permitted'),
|
|
Format::date($max->getTimestamp(), false, false,
|
|
$max->getTimezone()->getName() ?: 'UTC')
|
|
);
|
|
}
|
|
}
|
|
|
|
// SearchableField interface ------------------------------
|
|
function getSearchMethods() {
|
|
return array(
|
|
'set' => __('has a value'),
|
|
'nset' => __('does not have a value'),
|
|
'equal' => __('on'),
|
|
'nequal' => __('not on'),
|
|
'before' => __('before'),
|
|
'after' => __('after'),
|
|
'between' => __('between'),
|
|
'period' => __('period'),
|
|
'ndaysago' => __('in the last n days'),
|
|
'ndays' => __('in the next n days'),
|
|
'future' => __('in the future'),
|
|
'past' => __('in the past'),
|
|
'distfut' => __('more than n days from now'),
|
|
'distpast' => __('more than n days ago'),
|
|
);
|
|
}
|
|
|
|
function getSearchMethodWidgets() {
|
|
$config_notime = $config = $this->getConfiguration();
|
|
$config_notime['time'] = false;
|
|
$nday_form = function($x=5) {
|
|
return array(
|
|
'until' => new TextboxField(array(
|
|
'configuration' => array('validator'=>'number', 'size'=>4))
|
|
),
|
|
'int' => new ChoiceField(array(
|
|
'default' => 'd',
|
|
'choices' => self::intervals($x),
|
|
)),
|
|
);
|
|
};
|
|
return array(
|
|
'set' => null,
|
|
'nset' => null,
|
|
'past' => null,
|
|
'future' => null,
|
|
'equal' => array('DatetimeField', array(
|
|
'configuration' => $config_notime,
|
|
)),
|
|
'nequal' => array('DatetimeField', array(
|
|
'configuration' => $config_notime,
|
|
)),
|
|
'before' => array('DatetimeField', array(
|
|
'configuration' => $config,
|
|
)),
|
|
'after' => array('DatetimeField', array(
|
|
'configuration' => $config,
|
|
)),
|
|
'between' => array('InlineformField', array(
|
|
'form' => array(
|
|
'left' => new DatetimeField($config + array('required' => true)),
|
|
'text' => new FreeTextField(array(
|
|
'configuration' => array('content' => __('and')))
|
|
),
|
|
'right' => new DatetimeField($config + array('required' => true)),
|
|
),
|
|
'configuration' => array(
|
|
'error' => '',
|
|
),
|
|
)),
|
|
'period' => array('ChoiceField', array(
|
|
'choices' => self::periods(),
|
|
)),
|
|
'ndaysago' => array('InlineformField', array('form'=>$nday_form())),
|
|
'ndays' => array('InlineformField', array('form'=>$nday_form())),
|
|
'distfut' => array('InlineformField', array('form'=>$nday_form())),
|
|
'distpast' => array('InlineformField', array('form'=>$nday_form())),
|
|
);
|
|
}
|
|
|
|
function getSearchQ($method, $value, $name=false) {
|
|
global $cfg;
|
|
|
|
static $intervals = array(
|
|
'm' => 'MONTH',
|
|
'w' => 'WEEK',
|
|
'd' => 'DAY',
|
|
'h' => 'HOUR',
|
|
'i' => 'MINUTE',
|
|
);
|
|
$name = $name ?: $this->get('name');
|
|
$now = SqlFunction::NOW();
|
|
$config = $this->getConfiguration();
|
|
if (is_int($value))
|
|
$value = DateTime::createFromFormat('U', !$config['gmt'] ? Misc::gmtime($value) : $value) ?: $value;
|
|
elseif (is_string($value) && strlen($value) > 2)
|
|
$value = Format::parseDateTime($value) ?: $value;
|
|
|
|
switch ($method) {
|
|
case 'equal':
|
|
$l = clone $value;
|
|
$r = $value->add(new DateInterval('P1D'));
|
|
return new Q(array(
|
|
"{$name}__gte" => $l,
|
|
"{$name}__lt" => $r
|
|
));
|
|
case 'nequal':
|
|
$l = clone $value;
|
|
$r = $value->add(new DateInterval('P1D'));
|
|
return Q::any(array(
|
|
"{$name}__lt" => $l,
|
|
"{$name}__gte" => $r,
|
|
));
|
|
case 'future':
|
|
$value = $now;
|
|
case 'after':
|
|
return new Q(array("{$name}__gte" => $value));
|
|
case 'past':
|
|
$value = $now;
|
|
case 'before':
|
|
return new Q(array("{$name}__lt" => $value));
|
|
case 'between':
|
|
$left = Format::parseDateTime($value['left']);
|
|
$right = Format::parseDateTime($value['right']);
|
|
if (!$left || !$right)
|
|
return null;
|
|
|
|
// TODO: allow time selection for between
|
|
$left = $left->setTime(00, 00, 00);
|
|
$right = $right->setTime(23, 59, 59);
|
|
// Convert time to db timezone
|
|
$dbtz = new DateTimeZone($cfg->getDbTimezone());
|
|
$left->setTimezone($dbtz);
|
|
$right->setTimezone($dbtz);
|
|
return new Q(array(
|
|
"{$name}__gte" => $left->format('Y-m-d H:i:s'),
|
|
"{$name}__lte" => $right->format('Y-m-d H:i:s'),
|
|
));
|
|
case 'ndaysago':
|
|
$int = $intervals[$value['int'] ?: 'd'] ?: 'DAY';
|
|
$interval = new SqlInterval($int, $value['until']);
|
|
return new Q(array(
|
|
"{$name}__range" => array($now->minus($interval), $now),
|
|
));
|
|
case 'ndays':
|
|
$int = $intervals[$value['int'] ?: 'd'] ?: 'DAY';
|
|
$interval = new SqlInterval($int, $value['until']);
|
|
return new Q(array(
|
|
"{$name}__range" => array($now, $now->plus($interval)),
|
|
));
|
|
// Distant past and future ranges
|
|
case 'distpast':
|
|
$int = $intervals[$value['int'] ?: 'd'] ?: 'DAY';
|
|
$interval = new SqlInterval($int, $value['until']);
|
|
return new Q(array(
|
|
"{$name}__lte" => $now->minus($interval),
|
|
));
|
|
case 'distfut':
|
|
$int = $intervals[$value['int'] ?: 'd'] ?: 'DAY';
|
|
$interval = new SqlInterval($int, $value['until']);
|
|
return new Q(array(
|
|
"{$name}__gte" => $now->plus($interval),
|
|
));
|
|
case 'period':
|
|
// User's effective timezone
|
|
$tz = new DateTimeZone($cfg->getTimezone());
|
|
// Get the period range boundaries in user's tz
|
|
$period = Misc::date_range($value, Misc::gmtime('now'), $tz);
|
|
// Convert boundaries to db time
|
|
$dbtz = new DateTimeZone($cfg->getDbTimezone());
|
|
$start = $period->start->setTimezone($dbtz);
|
|
$end = $period->end->setTimezone($dbtz);
|
|
// Set the range
|
|
return new Q(array(
|
|
"{$name}__range" => array(
|
|
$start->format('Y-m-d H:i:s'),
|
|
$end->format('Y-m-d H:i:s')
|
|
)
|
|
));
|
|
break;
|
|
default:
|
|
return parent::getSearchQ($method, $value, $name);
|
|
}
|
|
}
|
|
|
|
function describeSearchMethod($method) {
|
|
switch ($method) {
|
|
case 'before':
|
|
return __('%1$s before %2$s' /* occurs before a date and time */);
|
|
case 'after':
|
|
return __('%1$s after %2$s' /* occurs after a date and time */);
|
|
case 'ndays':
|
|
return __('%1$s in the next %2$s' /* occurs within a window (like 3 days) */);
|
|
case 'ndaysago':
|
|
return __('%1$s in the last %2$s' /* occurs within a recent window (like 3 days) */);
|
|
case 'distfut':
|
|
return __('%1$s after %2$s from now' /* occurs after a window (like 3 days) */);
|
|
case 'distpast':
|
|
return __('%1$s before %2$s ago' /* occurs previous to a window (like 3 days) */);
|
|
case 'between':
|
|
return __('%1$s between %2$s and %3$s');
|
|
case 'future':
|
|
return __('%1$s is in the future');
|
|
case 'past':
|
|
return __('%1$s is in the past');
|
|
case 'period':
|
|
return __('%1$s is %2$s');
|
|
default:
|
|
return parent::describeSearchMethod($method);
|
|
}
|
|
}
|
|
|
|
function describeSearch($method, $value, $name=false) {
|
|
|
|
$name = $name ?: $this->get('name');
|
|
$desc = $this->describeSearchMethod($method);
|
|
switch ($method) {
|
|
case 'between':
|
|
return sprintf($desc, $name,
|
|
$this->toString($value['left']),
|
|
$this->toString($value['right']));
|
|
case 'ndays':
|
|
case 'ndaysago':
|
|
case 'distfut':
|
|
case 'distpast':
|
|
$interval = sprintf('%s %s', $value['until'],
|
|
self::intervals($value['until'], $value['int']));
|
|
return sprintf($desc, $name, $interval);
|
|
break;
|
|
case 'future':
|
|
case 'past':
|
|
return sprintf($desc, $name);
|
|
case 'before':
|
|
case 'after':
|
|
return sprintf($desc, $name, $this->toString($value));
|
|
case 'period':
|
|
return sprintf($desc, $name, self::periods($value) ?: $value);
|
|
default:
|
|
return parent::describeSearch($method, $value, $name);
|
|
}
|
|
}
|
|
|
|
function supportsQuickFilter() {
|
|
return true;
|
|
}
|
|
|
|
function getQuickFilterChoices() {
|
|
return array(
|
|
'h' => __('Today'),
|
|
'm' => __('Tomorrow'),
|
|
'g' => __('Yesterday'),
|
|
'l7' => __('Last 7 days'),
|
|
'l30' => __('Last 30 days'),
|
|
'n7' => __('Next 7 days'),
|
|
'n30' => __('Next 30 days'),
|
|
/* Ugh. These boundaries are so difficult in SQL
|
|
'w' => __('This Week'),
|
|
'm' => __('This Month'),
|
|
'lw' => __('Last Week'),
|
|
'lm' => __('Last Month'),
|
|
'nw' => __('Next Week'),
|
|
'nm' => __('Next Month'),
|
|
*/
|
|
);
|
|
}
|
|
|
|
function applyQuickFilter($query, $qf_value, $name=false) {
|
|
$name = $name ?: $this->get('name');
|
|
$now = SqlFunction::NOW();
|
|
$midnight = Misc::dbtime(time() - (time() % 86400));
|
|
switch ($qf_value) {
|
|
case 'l7':
|
|
return $query->filter([
|
|
"{$name}__range" => array($now->minus(SqlInterval::DAY(7)), $now),
|
|
]);
|
|
case 'l30':
|
|
return $query->filter([
|
|
"{$name}__range" => array($now->minus(SqlInterval::DAY(30)), $now),
|
|
]);
|
|
case 'n7':
|
|
return $query->filter([
|
|
"{$name}__range" => array($now, $now->plus(SqlInterval::DAY(7))),
|
|
]);
|
|
case 'n30':
|
|
return $query->filter([
|
|
"{$name}__range" => array($now, $now->plus(SqlInterval::DAY(30))),
|
|
]);
|
|
case 'g':
|
|
$midnight -= 86400;
|
|
// Fall through to the today case
|
|
case 'm':
|
|
if ($qf_value === 'm') $midnight += 86400;
|
|
// Fall through to the today case
|
|
case 'h':
|
|
$midnight = DateTime::createFromFormat('U', $midnight);
|
|
return $query->filter([
|
|
"{$name}__range" => array($midnight,
|
|
SqlExpression::plus($midnight, SqlInterval::DAY(1))),
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* TimeField for time selection
|
|
*
|
|
*/
|
|
class TimeField extends FormField {
|
|
static $widget = 'TimePickerWidget';
|
|
|
|
var $start = null;
|
|
var $end = null;
|
|
|
|
function getTimeZone() {
|
|
global $cfg;
|
|
$config = $this->getConfiguration();
|
|
return new DateTimeZone($config['timezone'] ?: $cfg->getTimezone());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This is kind-of a special field that doesn't have any data. It's used as
|
|
* a field to provide a horizontal section break in the display of a form
|
|
*/
|
|
class SectionBreakField extends FormField {
|
|
static $widget = 'SectionBreakWidget';
|
|
|
|
function hasData() {
|
|
return false;
|
|
}
|
|
|
|
function isBlockLevel() {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
class ThreadEntryField extends FormField {
|
|
static $widget = 'ThreadEntryWidget';
|
|
|
|
function isChangeable() {
|
|
return false;
|
|
}
|
|
function isBlockLevel() {
|
|
return true;
|
|
}
|
|
function isPresentationOnly() {
|
|
return true;
|
|
}
|
|
function getMedia() {
|
|
$config = $this->getConfiguration();
|
|
$media = parent::getMedia() ?: array();
|
|
if ($config['attachments'])
|
|
$media = array_merge_recursive($media, FileUploadWidget::$media);
|
|
return $media;
|
|
}
|
|
|
|
function getConfiguration() {
|
|
global $cfg;
|
|
$config = parent::getConfiguration();
|
|
$config['html'] = (bool) ($cfg && $cfg->isRichTextEnabled());
|
|
return $config;
|
|
}
|
|
|
|
function getConfigurationOptions() {
|
|
global $cfg;
|
|
|
|
$attachments = new FileUploadField();
|
|
$fileupload_config = $attachments->getConfigurationOptions();
|
|
if ($cfg->getAllowedFileTypes())
|
|
$fileupload_config['extensions']->set('default', $cfg->getAllowedFileTypes());
|
|
|
|
foreach ($fileupload_config as $C) {
|
|
$C->set('visibility', new VisibilityConstraint(new Q(array(
|
|
'attachments__eq'=>true,
|
|
)), VisibilityConstraint::HIDDEN));
|
|
}
|
|
return array(
|
|
'attachments' => new BooleanField(array(
|
|
'label'=>__('Enable Attachments'),
|
|
'default'=>$cfg->allowAttachments(),
|
|
'configuration'=>array(
|
|
'desc'=>__('Enables attachments, regardless of channel'),
|
|
),
|
|
'validators' => function($self, $value) {
|
|
if (!ini_get('file_uploads'))
|
|
$self->addError(__('The "file_uploads" directive is disabled in php.ini'));
|
|
}
|
|
)),
|
|
)
|
|
+ $fileupload_config;
|
|
}
|
|
|
|
function isAttachmentsEnabled() {
|
|
$config = $this->getConfiguration();
|
|
return $config['attachments'];
|
|
}
|
|
|
|
function getWidget($widgetClass=false) {
|
|
if ($hint = $this->getLocal('hint'))
|
|
$this->set('placeholder', Format::striptags($hint));
|
|
$this->set('hint', null);
|
|
$widget = parent::getWidget($widgetClass);
|
|
return $widget;
|
|
}
|
|
}
|
|
|
|
class TopicField extends ChoiceField {
|
|
var $topics;
|
|
var $_choices;
|
|
|
|
function getTopics() {
|
|
global $thisstaff;
|
|
|
|
if (!isset($this->topics))
|
|
$this->topics = $thisstaff->getTopicNames();
|
|
|
|
return $this->topics;
|
|
}
|
|
|
|
function getTopic($id) {
|
|
if ($this->getTopics() &&
|
|
isset($this->topics[$id]))
|
|
return Topic::lookup($id);
|
|
}
|
|
|
|
function getWidget($widgetClass=false) {
|
|
$default = $this->get('default');
|
|
$widget = parent::getWidget($widgetClass);
|
|
if ($widget->value instanceof Topic)
|
|
$widget->value = $widget->value->getId();
|
|
elseif (!isset($widget->value) && $default)
|
|
$widget->value = $default;
|
|
return $widget;
|
|
}
|
|
|
|
function hasIdValue() {
|
|
return true;
|
|
}
|
|
|
|
function getChoices($verbose=false, $options=array()) {
|
|
if (!isset($this->_choices)) {
|
|
$this->_choices = $this->getTopics();
|
|
}
|
|
|
|
return $this->_choices;
|
|
}
|
|
|
|
function parse($id) {
|
|
return $this->to_php(null, $id);
|
|
}
|
|
|
|
function to_php($value, $id=false) {
|
|
if ($value instanceof Topic)
|
|
return $value;
|
|
if (is_array($id)) {
|
|
reset($id);
|
|
$id = key($id);
|
|
}
|
|
elseif (is_array($value))
|
|
list($value, $id) = $value;
|
|
elseif ($id === false)
|
|
$id = $value;
|
|
|
|
return $this->getTopic($id);
|
|
}
|
|
|
|
function to_database($topic) {
|
|
return ($topic instanceof Topic)
|
|
? array($topic->getName(), $topic->getId())
|
|
: $topic;
|
|
}
|
|
|
|
function display($topic, &$styles=null) {
|
|
if (!$topic instanceof Topic)
|
|
return parent::display($topic);
|
|
|
|
return Format::htmlchars($topic->getName());
|
|
}
|
|
|
|
function toString($value) {
|
|
if (!($value instanceof Topic) && is_numeric($value))
|
|
$value = $this->getTopic($value);
|
|
|
|
return ($value instanceof Topic) ? $value->getName() : $value;
|
|
}
|
|
|
|
function whatChanged($before, $after) {
|
|
return FormField::whatChanged($before, $after);
|
|
}
|
|
|
|
function searchable($value) {
|
|
return null;
|
|
}
|
|
|
|
function getKeys($value) {
|
|
return ($value instanceof Topic) ? array($value->getId()) : null;
|
|
}
|
|
|
|
function asVar($value, $id=false) {
|
|
return $this->to_php($value, $id);
|
|
}
|
|
|
|
function getConfiguration() {
|
|
global $cfg;
|
|
|
|
$config = parent::getConfiguration();
|
|
if (!isset($config['default']))
|
|
$config['default'] = $cfg->getDefaultTopicId();
|
|
return $config;
|
|
}
|
|
}
|
|
|
|
class SLAField extends ChoiceField {
|
|
var $slas;
|
|
var $_choices;
|
|
|
|
function getSLAs() {
|
|
if (!isset($this->slas))
|
|
$this->slas = SLA::objects();
|
|
|
|
return $this->slas;
|
|
}
|
|
|
|
function getSLA($id) {
|
|
if ($this->getSLAs() &&
|
|
($s=$this->slas->findFirst(array('id' => $id))))
|
|
return $s;
|
|
|
|
return SLA::lookup($id);
|
|
}
|
|
|
|
function getWidget($widgetClass=false) {
|
|
$default = $this->get('default');
|
|
$widget = parent::getWidget($widgetClass);
|
|
if ($widget->value instanceof SLA)
|
|
$widget->value = $widget->value->getId();
|
|
elseif (!isset($widget->value) && $default)
|
|
$widget->value = $default;
|
|
return $widget;
|
|
}
|
|
|
|
function hasIdValue() {
|
|
return true;
|
|
}
|
|
|
|
function getChoices($verbose=false, $options=array()) {
|
|
if (!isset($this->_choices)) {
|
|
$choices = array();
|
|
foreach ($this->getSLAs() as $s)
|
|
$choices[$s->getId()] = $s->getName();
|
|
$this->_choices = $choices;
|
|
}
|
|
|
|
return $this->_choices;
|
|
}
|
|
|
|
function parse($id) {
|
|
return $this->to_php(null, $id);
|
|
}
|
|
|
|
function to_php($value, $id=false) {
|
|
if ($value instanceof SLA)
|
|
return $value;
|
|
if (is_array($id)) {
|
|
reset($id);
|
|
$id = key($id);
|
|
}
|
|
elseif (is_array($value))
|
|
list($value, $id) = $value;
|
|
elseif ($id === false)
|
|
$id = $value;
|
|
|
|
return $this->getSLA($id);
|
|
}
|
|
|
|
function to_database($sla) {
|
|
return ($sla instanceof SLA)
|
|
? array($sla->getName(), $sla->getId())
|
|
: $sla;
|
|
}
|
|
|
|
function display($sla, &$styles=null) {
|
|
if (!$sla instanceof SLA)
|
|
return parent::display($sla);
|
|
|
|
return Format::htmlchars($sla->getName());
|
|
}
|
|
|
|
function toString($value) {
|
|
if (!($value instanceof SLA) && is_numeric($value))
|
|
$value = $this->getSLA($value);
|
|
return ($value instanceof SLA) ? $value->getName() : $value;
|
|
}
|
|
|
|
function whatChanged($before, $after) {
|
|
return FormField::whatChanged($before, $after);
|
|
}
|
|
|
|
function searchable($value) {
|
|
return null;
|
|
}
|
|
|
|
function getKeys($value) {
|
|
return ($value instanceof SLA) ? array($value->getId()) : null;
|
|
}
|
|
|
|
function asVar($value, $id=false) {
|
|
return $this->to_php($value, $id);
|
|
}
|
|
|
|
function getConfiguration() {
|
|
global $cfg;
|
|
|
|
$config = parent::getConfiguration();
|
|
if (!isset($config['default']))
|
|
$config['default'] = $cfg->getDefaultSLAId();
|
|
return $config;
|
|
}
|
|
}
|
|
|
|
class PriorityField extends ChoiceField {
|
|
|
|
var $priorities;
|
|
var $_choices;
|
|
|
|
function getPriorities() {
|
|
if (!isset($this->priorities))
|
|
$this->priorities = Priority::objects();
|
|
|
|
return $this->priorities;
|
|
}
|
|
|
|
function getPriority($id) {
|
|
|
|
if ($this->getPriorities() &&
|
|
($p=$this->priorities->findFirst(array('priority_id' =>
|
|
$id))))
|
|
return $p;
|
|
|
|
return Priority::lookup($id);
|
|
}
|
|
|
|
function getWidget($widgetClass=false) {
|
|
$widget = parent::getWidget($widgetClass);
|
|
if ($widget->value instanceof Priority)
|
|
$widget->value = $widget->value->getId();
|
|
return $widget;
|
|
}
|
|
|
|
function hasIdValue() {
|
|
return true;
|
|
}
|
|
|
|
function getChoices($verbose=false, $options=array()) {
|
|
if (!isset($this->_choices)) {
|
|
$choices = array();
|
|
foreach ($this->getPriorities() as $p)
|
|
$choices[$p->getId()] = $p->getDesc();
|
|
$this->_choices = $choices;
|
|
}
|
|
|
|
return $this->_choices;
|
|
}
|
|
|
|
function parse($id) {
|
|
return $this->to_php(null, $id);
|
|
}
|
|
|
|
function to_php($value, $id=false) {
|
|
if ($value instanceof Priority)
|
|
return $value;
|
|
if (is_array($id)) {
|
|
reset($id);
|
|
$id = key($id);
|
|
}
|
|
elseif (is_array($value))
|
|
list($value, $id) = $value;
|
|
elseif ($id === false)
|
|
$id = $value;
|
|
|
|
return $this->getPriority($id);
|
|
}
|
|
|
|
function to_database($value) {
|
|
if ($value instanceof Priority)
|
|
return array($value->getDesc(), $value->getId());
|
|
|
|
if (is_array($value))
|
|
return array(current($value), key($value));
|
|
|
|
return $value;
|
|
}
|
|
|
|
function display($prio, &$styles=null) {
|
|
if (!$prio instanceof Priority)
|
|
return parent::display($prio);
|
|
if (is_array($styles))
|
|
$styles += array(
|
|
'background-color' => $prio->getColor()
|
|
);
|
|
return Format::htmlchars($prio->getDesc());
|
|
}
|
|
|
|
function toString($value) {
|
|
return ($value instanceof Priority) ? $value->getDesc() : $value;
|
|
}
|
|
|
|
function whatChanged($before, $after) {
|
|
return FormField::whatChanged($before, $after);
|
|
}
|
|
|
|
function searchable($value) {
|
|
// Priority isn't searchable this way
|
|
return null;
|
|
}
|
|
|
|
function getKeys($value) {
|
|
return ($value instanceof Priority) ? array($value->getId()) : null;
|
|
}
|
|
|
|
function asVar($value, $id=false) {
|
|
return $this->to_php($value, $id);
|
|
}
|
|
|
|
function getConfigurationOptions() {
|
|
$choices = $this->getChoices();
|
|
return array(
|
|
'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),
|
|
)),
|
|
'default' => new ChoiceField(array(
|
|
'id'=>3, 'label'=>__('Default'), 'required'=>false, 'default'=>'',
|
|
'choices' => $choices,
|
|
'hint'=>__('Default selection for this field'),
|
|
'configuration'=>array('size'=>20, 'length'=>40),
|
|
)),
|
|
);
|
|
}
|
|
|
|
function getConfiguration() {
|
|
global $cfg;
|
|
|
|
$config = parent::getConfiguration();
|
|
if (!isset($config['default']))
|
|
$config['default'] = $cfg->getDefaultPriorityId();
|
|
return $config;
|
|
}
|
|
|
|
function applyOrderBy($query, $reverse=false, $name=false) {
|
|
if ($query->model == 'Ticket' && $name == 'cdata__priority') {
|
|
// Order by the priority urgency field
|
|
$col = 'cdata__:priority__priority_urgency';
|
|
$reverse = !$reverse;
|
|
}
|
|
else {
|
|
$col = $name ?: CustomQueue::getOrmPath($this->get('name'), $query);
|
|
}
|
|
if ($reverse)
|
|
$col = "-$col";
|
|
return $query->order_by($col);
|
|
}
|
|
}
|
|
FormField::addFieldTypes(/*@trans*/ 'Dynamic Fields', function() {
|
|
return array(
|
|
'priority' => array(__('Priority Level'), 'PriorityField'),
|
|
);
|
|
});
|
|
|
|
|
|
class TimezoneField extends ChoiceField {
|
|
static $widget = 'TimezoneWidget';
|
|
|
|
function hasIdValue() {
|
|
return false;
|
|
}
|
|
|
|
function getChoices($verbose=false, $options=array()) {
|
|
global $cfg;
|
|
|
|
$choices = array();
|
|
foreach (DateTimeZone::listIdentifiers() as $zone)
|
|
$choices[$zone] = str_replace('/',' / ',$zone);
|
|
|
|
return $choices;
|
|
}
|
|
|
|
function whatChanged($before, $after) {
|
|
return FormField::whatChanged($before, $after);
|
|
}
|
|
|
|
function searchable($value) {
|
|
return null;
|
|
}
|
|
|
|
function getConfigurationOptions() {
|
|
return array(
|
|
'autodetect' => new BooleanField(array(
|
|
'id'=>1, 'label'=>__('Auto Detect'), 'required'=>false, 'default'=>true,
|
|
'configuration'=>array(
|
|
'desc'=>__('Add Auto Detect Button'))
|
|
)),
|
|
'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),
|
|
)),
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
class DepartmentField extends ChoiceField {
|
|
function getWidget($widgetClass=false) {
|
|
$widget = $this->_widget ?: parent::getWidget($widgetClass);
|
|
if (is_object($widget->value))
|
|
$widget->value = $widget->value->getId();
|
|
return $widget;
|
|
}
|
|
|
|
function hasIdValue() {
|
|
return true;
|
|
}
|
|
|
|
function getChoices($verbose=false, $options=array()) {
|
|
global $cfg, $thisstaff;
|
|
|
|
$config = $this->getConfiguration();
|
|
$staff = $config['staff'] ?: $thisstaff;
|
|
$selected = self::getWidget();
|
|
if($selected && $selected->value) {
|
|
if(is_array($selected->value)) {
|
|
foreach ($selected->value as $k => $v) {
|
|
$current_id = $k;
|
|
$current_name = $v;
|
|
}
|
|
}
|
|
else {
|
|
$current_id = $selected->value;
|
|
$current_name = Dept::getNameById($current_id);
|
|
}
|
|
}
|
|
|
|
$choices = array();
|
|
|
|
//get all depts unfiltered
|
|
$depts = $config['hideDisabled'] ? Dept::getDepartments(array('activeonly' => true)) :
|
|
Dept::getDepartments(null, true, Dept::DISPLAY_DISABLED);
|
|
|
|
//get staff depts based on permissions
|
|
if ($staff) {
|
|
$active = $staff->getDepartmentNames(true);
|
|
|
|
if ($staff->hasPerm(Dept::PERM_DEPT))
|
|
return $depts;
|
|
}
|
|
//filter custom department fields when there is no staff
|
|
else {
|
|
$userDepts = Dept::getDepartments(array('publiconly' => true, 'activeonly' => true));
|
|
|
|
return $userDepts;
|
|
}
|
|
|
|
//add selected dept to list
|
|
if($current_id)
|
|
$active[$current_id] = $current_name;
|
|
else
|
|
return $active;
|
|
|
|
foreach ($depts as $id => $name) {
|
|
$choices[$id] = $name;
|
|
if(!array_key_exists($id, $active) && $current_id)
|
|
unset($choices[$id]);
|
|
}
|
|
|
|
return $choices;
|
|
}
|
|
|
|
function parse($id) {
|
|
return $this->to_php(null, $id);
|
|
}
|
|
|
|
function getValue() {
|
|
if (($value = parent::getValue()) && ($id=$this->getClean()))
|
|
return $value[$id];
|
|
}
|
|
|
|
function to_php($value, $id=false) {
|
|
if ($id) {
|
|
if (is_array($id)) {
|
|
reset($id);
|
|
$id = key($id);
|
|
}
|
|
return $id;
|
|
} else {
|
|
return $value;
|
|
}
|
|
}
|
|
|
|
function to_database($dept) {
|
|
if ($dept instanceof Dept)
|
|
return array($dept->getName(), $dept->getId());
|
|
|
|
if (!is_array($dept)) {
|
|
$choices = $this->getChoices();
|
|
if (in_array($dept, $choices)) {
|
|
$deptId = array_search($dept, $choices);
|
|
$dept = array($dept, $deptId);
|
|
}
|
|
}
|
|
|
|
return $dept ?: array();
|
|
}
|
|
|
|
function toString($value) {
|
|
if (!is_array($value))
|
|
$value = $this->getChoice($value);
|
|
if (is_array($value))
|
|
return implode(', ', $value);
|
|
return (string) $value;
|
|
}
|
|
|
|
function getChoice($value) {
|
|
$choices = $this->getChoices();
|
|
$selection = array();
|
|
if ($value && is_array($value)) {
|
|
$selection = $value;
|
|
} elseif (isset($choices[$value])) {
|
|
$selection[] = $choices[$value];
|
|
}
|
|
|
|
return $selection;
|
|
}
|
|
|
|
function searchable($value) {
|
|
return null;
|
|
}
|
|
|
|
function getConfigurationOptions() {
|
|
return array(
|
|
'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),
|
|
)),
|
|
);
|
|
}
|
|
}
|
|
FormField::addFieldTypes(/*@trans*/ 'Dynamic Fields', function() {
|
|
return array(
|
|
'department' => array(__('Department'), 'DepartmentField'),
|
|
);
|
|
});
|
|
|
|
class AssigneeField extends ChoiceField {
|
|
var $_choices = null;
|
|
var $_criteria = null;
|
|
|
|
function getWidget($widgetClass=false) {
|
|
$widget = parent::getWidget($widgetClass);
|
|
$value = $widget->value;
|
|
if (is_object($value)) {
|
|
$id = $value->getId();
|
|
if ($value instanceof Staff)
|
|
$widget->value = 's'.$id;
|
|
elseif ($value instanceof Team)
|
|
$widget->value = 't'.$id;
|
|
}
|
|
return $widget;
|
|
}
|
|
|
|
function getCriteria() {
|
|
if (!isset($this->_criteria)) {
|
|
$this->_criteria = array('available' => true, 'namesOnly' => true);
|
|
if (($c=parent::getCriteria()))
|
|
$this->_criteria = array_merge($this->_criteria, $c);
|
|
}
|
|
|
|
return $this->_criteria;
|
|
}
|
|
|
|
function hasIdValue() {
|
|
return true;
|
|
}
|
|
|
|
function setChoices($choices) {
|
|
$this->_choices = $choices;
|
|
}
|
|
|
|
function display($value) {
|
|
if ($this->getAnswer() && is_string($this->getAnswer()->value)) {
|
|
$v = JsonDataParser::parse($this->getAnswer()->value);
|
|
if (is_array($v))
|
|
$value = $v[key($v)];
|
|
}
|
|
return $value;
|
|
}
|
|
|
|
function getAssignees($options=array()) {
|
|
global $thisstaff;
|
|
|
|
$config = $this->getConfiguration();
|
|
$criteria = $this->getCriteria();
|
|
$dept = $config['dept'] ?: null;
|
|
$staff = $config['staff'] ?: $thisstaff;
|
|
$assignees = array();
|
|
switch (strtolower($config['target'])) {
|
|
case 'agents':
|
|
if ($dept)
|
|
foreach ($dept->getAssignees(array('staff' => $staff)) as $a)
|
|
$assignees['s'.$a->getId()] = $a;
|
|
else
|
|
foreach (Staff::getStaffMembers(array('staff' => $staff)) as $id => $name)
|
|
$assignees['s'.$id] = $name;
|
|
break;
|
|
case 'teams':
|
|
foreach (Team::getActiveTeams() ?: array() as $id => $name)
|
|
$assignees['t'.$id] = $name;
|
|
break;
|
|
default:
|
|
// both agents and teams
|
|
$assignees = array(
|
|
__('Agents') => new ArrayObject(),
|
|
__('Teams') => new ArrayObject());
|
|
$A = current($assignees);
|
|
$criteria = $this->getCriteria();
|
|
$agents = array();
|
|
if ($dept)
|
|
foreach ($dept->getAssignees(array('staff' => $staff)) as $a)
|
|
$A['s'.$a->getId()] = $a;
|
|
else
|
|
foreach (Staff::getStaffMembers(array('staff' => $staff)) as $a => $name)
|
|
$A['s'.$a] = $name;
|
|
|
|
next($assignees);
|
|
$T = current($assignees);
|
|
if (($teams = Team::getActiveTeams()))
|
|
foreach ($teams as $id => $name)
|
|
$T['t'.$id] = $name;
|
|
break;
|
|
}
|
|
|
|
return $assignees;
|
|
}
|
|
|
|
function getChoices($verbose=false, $options=array()) {
|
|
if (!isset($this->_choices))
|
|
$this->_choices = $this->getAssignees($options);
|
|
|
|
return $this->_choices;
|
|
}
|
|
|
|
function getChoice($value) {
|
|
$choices = $this->getChoices();
|
|
$selection = array();
|
|
if ($value && is_object($value)) {
|
|
$keys = null;
|
|
if ($value instanceof Staff)
|
|
$keys = array('Agents', 's'.$value->getId());
|
|
elseif ($value instanceof Team)
|
|
$keys = array('Teams', 't'.$value->getId());
|
|
if ($keys && isset($choices[$keys[0]]))
|
|
$selection = $choices[$keys[0]][$keys[1]];
|
|
|
|
if (!empty($selection))
|
|
return $selection;
|
|
}
|
|
|
|
return parent::getChoice($value);
|
|
}
|
|
|
|
function getQuickFilterChoices() {
|
|
$choices = $this->getChoices();
|
|
$names = array();
|
|
foreach ($choices as $value) {
|
|
foreach ($value as $key => $value)
|
|
$names[$key] = is_object($value) ? $value->name : $value;
|
|
}
|
|
|
|
return $names;
|
|
}
|
|
|
|
function getValue() {
|
|
if (($value = parent::getValue()) && ($id=$this->getClean())) {
|
|
$name = (is_object($value[key($value)]) && get_class($value[key($value)]) == 'AgentsName') ?
|
|
$value[key($value)]->name : $value[key($value)];
|
|
$key = (($value[key($value)] instanceof AgentsName) ? 's' : 't').substr(key($value), 1);
|
|
return array(array($key => $name), substr(key($value), 1));
|
|
} else
|
|
return null;
|
|
}
|
|
|
|
function parse($id) {
|
|
return $this->to_php(null, $id);
|
|
}
|
|
|
|
function to_php($value, $id=false) {
|
|
if (is_string($value))
|
|
$value = JsonDataParser::parse($value);
|
|
|
|
$type = '';
|
|
if (is_array($id)) {
|
|
reset($id);
|
|
$id = key($id);
|
|
$type = $id[0];
|
|
$id = substr($id, 1);
|
|
}
|
|
if (is_array($value)) {
|
|
$type = key($value)[0];
|
|
if (!$id)
|
|
$id = key($value)[1];
|
|
}
|
|
|
|
switch ($type) {
|
|
case 's':
|
|
return Staff::lookup($id);
|
|
case 't':
|
|
return Team::lookup($id);
|
|
case 'd':
|
|
return Dept::lookup($id);
|
|
default:
|
|
return $id;
|
|
}
|
|
}
|
|
|
|
function to_database($value) {
|
|
if (is_object($value)) {
|
|
$id = $value->getId();
|
|
if ($value instanceof Staff) {
|
|
$key = 's'.$id;
|
|
$name = $value->getName()->name;
|
|
} elseif ($value instanceof Team) {
|
|
$key = 't'.$id;
|
|
$name = $value->getName();
|
|
}
|
|
|
|
return array(JsonDataEncoder::encode(array($key => $name)));
|
|
}
|
|
if (is_array($value)) {
|
|
return array(JsonDataEncoder::encode($value[0]));
|
|
}
|
|
return $value;
|
|
}
|
|
|
|
function toString($value) {
|
|
return (string) $value;
|
|
}
|
|
|
|
function searchable($value) {
|
|
return null;
|
|
}
|
|
|
|
function getKeys($value) {
|
|
$value = $this->to_database($value);
|
|
if (is_array($value))
|
|
return $value[0];
|
|
|
|
return (string) $value;
|
|
}
|
|
|
|
function asVar($value, $id=false) {
|
|
$v = $this->to_php($value, $id);
|
|
return $v ? $v->getName() : null;
|
|
}
|
|
|
|
function getChanges() {
|
|
$new = $this->to_database($this->getValue());
|
|
$old = $this->to_database($this->answer ? $this->answer->getValue()
|
|
: $this->get('default'));
|
|
// Compare old and new
|
|
return ($old == $new)
|
|
? false
|
|
: array($old[0], $new[0]);
|
|
}
|
|
|
|
function whatChanged($before, $after) {
|
|
if ($before)
|
|
$before = array($before->getName());
|
|
if ($after)
|
|
$after = array($after->getName());
|
|
|
|
return parent::whatChanged($before, $after);
|
|
}
|
|
|
|
function getConfigurationOptions() {
|
|
return array(
|
|
'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),
|
|
)),
|
|
);
|
|
}
|
|
}
|
|
FormField::addFieldTypes(/*@trans*/ 'Dynamic Fields', function() {
|
|
return array(
|
|
'assignee' => array(__('Assignee'), 'AssigneeField'),
|
|
);
|
|
});
|
|
|
|
|
|
class TicketStateField extends ChoiceField {
|
|
|
|
static $_states = array(
|
|
'open' => array(
|
|
'name' => /* @trans, @context "ticket state name" */ 'Open',
|
|
'verb' => /* @trans, @context "ticket state action" */ 'Open'
|
|
),
|
|
'closed' => array(
|
|
'name' => /* @trans, @context "ticket state name" */ 'Closed',
|
|
'verb' => /* @trans, @context "ticket state action" */ 'Close'
|
|
)
|
|
);
|
|
// Private states
|
|
static $_privatestates = array(
|
|
'archived' => array(
|
|
'name' => /* @trans, @context "ticket state name" */ 'Archived',
|
|
'verb' => /* @trans, @context "ticket state action" */ 'Archive'
|
|
),
|
|
'deleted' => array(
|
|
'name' => /* @trans, @context "ticket state name" */ 'Deleted',
|
|
'verb' => /* @trans, @context "ticket state action" */ 'Delete'
|
|
)
|
|
);
|
|
|
|
function hasIdValue() {
|
|
return true;
|
|
}
|
|
|
|
function isChangeable() {
|
|
return false;
|
|
}
|
|
|
|
function getChoices($verbose=false, $options=array()) {
|
|
static $_choices;
|
|
|
|
$states = static::$_states;
|
|
if ($this->options['private_too'])
|
|
$states += static::$_privatestates;
|
|
|
|
if (!isset($_choices)) {
|
|
// Translate and cache the choices
|
|
foreach ($states as $k => $v)
|
|
$_choices[$k] = _P('ticket state name', $v['name']);
|
|
|
|
$this->ht['default'] = '';
|
|
}
|
|
|
|
return $_choices;
|
|
}
|
|
|
|
function getChoice($state) {
|
|
|
|
if ($state && is_array($state))
|
|
$state = key($state);
|
|
|
|
if (isset(static::$_states[$state]))
|
|
return _P('ticket state name', static::$_states[$state]['name']);
|
|
|
|
if (isset(static::$_privatestates[$state]))
|
|
return _P('ticket state name', static::$_privatestates[$state]['name']);
|
|
|
|
return $state;
|
|
}
|
|
|
|
function getConfigurationOptions() {
|
|
return array(
|
|
'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),
|
|
)),
|
|
);
|
|
}
|
|
|
|
static function getVerb($state) {
|
|
|
|
if (isset(static::$_states[$state]))
|
|
return _P('ticket state action', static::$_states[$state]['verb']);
|
|
|
|
if (isset(static::$_privatestates[$state]))
|
|
return _P('ticket state action', static::$_privatestates[$state]['verb']);
|
|
}
|
|
}
|
|
FormField::addFieldTypes('Dynamic Fields', function() {
|
|
return array(
|
|
'state' => array('Ticket State', 'TicketStateField', false),
|
|
);
|
|
});
|
|
|
|
class TicketFlagField extends ChoiceField {
|
|
|
|
// Supported flags (TODO: move to configurable custom list)
|
|
static $_flags = array(
|
|
'onhold' => array(
|
|
'flag' => 1,
|
|
'name' => 'Onhold',
|
|
'states' => array('open'),
|
|
),
|
|
'overdue' => array(
|
|
'flag' => 2,
|
|
'name' => 'Overdue',
|
|
'states' => array('open'),
|
|
),
|
|
'answered' => array(
|
|
'flag' => 4,
|
|
'name' => 'Answered',
|
|
'states' => array('open'),
|
|
)
|
|
);
|
|
|
|
var $_choices;
|
|
|
|
function hasIdValue() {
|
|
return true;
|
|
}
|
|
|
|
function isChangeable() {
|
|
return true;
|
|
}
|
|
|
|
function getChoices($verbose=false, $options=array()) {
|
|
$this->ht['default'] = '';
|
|
|
|
if (!$this->_choices) {
|
|
foreach (static::$_flags as $k => $v)
|
|
$this->_choices[$k] = $v['name'];
|
|
}
|
|
|
|
return $this->_choices;
|
|
}
|
|
|
|
function getConfigurationOptions() {
|
|
return array(
|
|
'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),
|
|
)),
|
|
);
|
|
}
|
|
}
|
|
|
|
FormField::addFieldTypes('Dynamic Fields', function() {
|
|
return array(
|
|
'flags' => array('Ticket Flags', 'TicketFlagField', false),
|
|
);
|
|
});
|
|
|
|
class FileUploadField extends FormField {
|
|
static $widget = 'FileUploadWidget';
|
|
|
|
protected $attachments;
|
|
protected $files;
|
|
|
|
static function getFileTypes() {
|
|
static $filetypes;
|
|
|
|
if (!isset($filetypes)) {
|
|
if (function_exists('apcu_fetch')) {
|
|
$key = md5(SECRET_SALT . GIT_VERSION . 'filetypes');
|
|
$filetypes = apcu_fetch($key);
|
|
}
|
|
if (!$filetypes)
|
|
$filetypes = YamlDataParser::load(INCLUDE_DIR . '/config/filetype.yaml');
|
|
if ($key)
|
|
apcu_store($key, $filetypes, 7200);
|
|
}
|
|
return $filetypes;
|
|
}
|
|
|
|
function getConfigurationOptions() {
|
|
// Compute size selections
|
|
$sizes = array('262144' => '— '.__('Small').' —');
|
|
$next = 512 << 10;
|
|
$max = strtoupper(ini_get('upload_max_filesize'));
|
|
$limit = (int) $max;
|
|
if (!$limit) $limit = 2 << 20; # 2M default value
|
|
elseif (strpos($max, 'K')) $limit <<= 10;
|
|
elseif (strpos($max, 'M')) $limit <<= 20;
|
|
elseif (strpos($max, 'G')) $limit <<= 30;
|
|
while ($next <= $limit) {
|
|
// Select the closest, larger value (in case the
|
|
// current value is between two)
|
|
$sizes[$next] = Format::file_size($next);
|
|
$next *= 2;
|
|
}
|
|
// Add extra option if top-limit in php.ini doesn't fall
|
|
// at a power of two
|
|
if ($next < $limit * 2)
|
|
$sizes[$limit] = Format::file_size($limit);
|
|
|
|
$types = array();
|
|
foreach (self::getFileTypes() as $type=>$info) {
|
|
$types[$type] = $info['description'];
|
|
}
|
|
|
|
global $cfg;
|
|
return array(
|
|
'size' => new ChoiceField(array(
|
|
'label'=>__('Maximum File Size'),
|
|
'hint'=>__('Choose maximum size of a single file uploaded to this field'),
|
|
'default'=>$cfg->getMaxFileSize(),
|
|
'choices'=>$sizes
|
|
)),
|
|
'mimetypes' => new ChoiceField(array(
|
|
'label'=>__('Restrict by File Type'),
|
|
'hint'=>__('Optionally, choose acceptable file types.'),
|
|
'required'=>false,
|
|
'choices'=>$types,
|
|
'configuration'=>array('multiselect'=>true,'prompt'=>__('No restrictions'))
|
|
)),
|
|
'extensions' => new TextareaField(array(
|
|
'label'=>__('Additional File Type Filters'),
|
|
'hint'=>__('Optionally, enter comma-separated list of additional file types, by extension. (e.g .doc, .pdf).'),
|
|
'configuration'=>array('html'=>false, 'rows'=>2),
|
|
)),
|
|
'max' => new TextboxField(array(
|
|
'label'=>__('Maximum Files'),
|
|
'hint'=>__('Users cannot upload more than this many files.'),
|
|
'default'=>false,
|
|
'required'=>false,
|
|
'validator'=>'number',
|
|
'configuration'=>array('size'=>8, 'length'=>4, 'placeholder'=>__('No limit')),
|
|
))
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Called from the ajax handler for async uploads via web clients.
|
|
*/
|
|
function ajaxUpload($bypass=false) {
|
|
$config = $this->getConfiguration();
|
|
|
|
$files = AttachmentFile::format($_FILES['upload'],
|
|
// For numeric fields assume configuration exists
|
|
!is_numeric($this->get('id')));
|
|
if (count($files) != 1)
|
|
Http::response(400, 'Send one file at a time');
|
|
$file = array_shift($files);
|
|
$file['name'] = urldecode($file['name']);
|
|
|
|
if (!$this->isValidFile($file))
|
|
Http::response(413, 'Invalid File');
|
|
|
|
if (!$bypass && !$this->isValidFileType($file['name'], $file['type']))
|
|
Http::response(415, 'File type is not allowed');
|
|
|
|
$config = $this->getConfiguration();
|
|
if (!$bypass && $file['size'] > $config['size'])
|
|
Http::response(413, 'File is too large');
|
|
|
|
if (!($F = AttachmentFile::upload($file)))
|
|
Http::response(500, 'Unable to store file: '. $file['error']);
|
|
|
|
$id = $F->getId();
|
|
|
|
// This file is allowed for attachment in this session
|
|
$_SESSION[':uploadedFiles'][$id] = $F->getName();
|
|
|
|
return $id;
|
|
}
|
|
|
|
/**
|
|
* Called from FileUploadWidget::getValue() when manual upload is used
|
|
* for browsers which do not support the HTML5 way of uploading async.
|
|
*/
|
|
function uploadFile($file) {
|
|
if (!$this->isValidFileType($file['name'], $file['type']))
|
|
throw new FileUploadError(__('File type is not allowed'));
|
|
|
|
if (!$this->isValidFile($file))
|
|
throw new FileUploadError(__('Invalid File'));
|
|
|
|
$config = $this->getConfiguration();
|
|
if ($file['size'] > $config['size'])
|
|
throw new FileUploadError(__('File size is too large'));
|
|
|
|
return AttachmentFile::upload($file);
|
|
}
|
|
|
|
/**
|
|
* Called from API and email routines and such to handle attachments
|
|
* sent other than via web upload
|
|
*/
|
|
function uploadAttachment(&$file) {
|
|
if (!$this->isValidFileType($file['name'], $file['type']))
|
|
throw new FileUploadError(__('File type is not allowed'));
|
|
|
|
if (!isset($file['data']) && isset($file['data_cbk'])
|
|
&& is_callable($file['data_cbk']))
|
|
$file['data'] = $file['data_cbk']();
|
|
|
|
if (!isset($file['size']) && isset($file['data'])) {
|
|
// bootstrap.php include a compat version of mb_strlen
|
|
if (extension_loaded('mbstring'))
|
|
$file['size'] = mb_strlen($file['data'], '8bit');
|
|
else
|
|
$file['size'] = strlen($file['data']);
|
|
}
|
|
|
|
$config = $this->getConfiguration();
|
|
if ($file['size'] > $config['size'])
|
|
throw new FileUploadError(__('File size is too large'));
|
|
|
|
if (!$F = AttachmentFile::create($file))
|
|
throw new FileUploadError(__('Unable to save file'));
|
|
|
|
return $F;
|
|
}
|
|
|
|
function isValidFile($file) {
|
|
|
|
// Check invalid image hacks
|
|
if ($file['tmp_name']
|
|
&& stripos($file['type'], 'image/') === 0
|
|
&& function_exists('exif_imagetype')
|
|
&& !exif_imagetype($file['tmp_name']))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
function isValidFileType($name, $type=false) {
|
|
$config = $this->getConfiguration();
|
|
|
|
// Check MIME type - file ext. shouldn't be solely trusted.
|
|
if ($type && $config['__mimetypes']
|
|
&& in_array($type, $config['__mimetypes'], true))
|
|
return true;
|
|
|
|
// Return true if all file types are allowed (.*)
|
|
if (!$config['__extensions'] || in_array('.*', $config['__extensions']))
|
|
return true;
|
|
|
|
$allowed = $config['__extensions'];
|
|
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
|
|
|
|
return ($ext && is_array($allowed) && in_array(".$ext", $allowed));
|
|
}
|
|
|
|
function getAttachments() {
|
|
if (!isset($this->attachments) && ($a = $this->getAnswer())
|
|
&& ($e = $a->getEntry()) && ($e->get('id'))
|
|
) {
|
|
$this->attachments = GenericAttachments::forIdAndType(
|
|
// Combine the field and entry ids to make the key
|
|
sprintf('%u', abs(crc32('E'.$this->get('id').$e->get('id')))),
|
|
'E');
|
|
}
|
|
return $this->attachments ?: array();
|
|
}
|
|
|
|
function setAttachments(GenericAttachments $att) {
|
|
$this->attachments = $att;
|
|
}
|
|
|
|
function getFiles() {
|
|
if (!isset($this->files)) {
|
|
$files = array();
|
|
foreach ($this->getAttachments() as $a) {
|
|
if ($a && ($f=$a->getFile()))
|
|
$files[] = $f;
|
|
}
|
|
|
|
foreach ($this->getClean(false) ?: array() as $key => $value)
|
|
$files[] = array('id' => $key, 'name' => $value);
|
|
|
|
$this->files = $files;
|
|
}
|
|
return $this->files;
|
|
}
|
|
|
|
function getConfiguration() {
|
|
$config = parent::getConfiguration();
|
|
$_types = self::getFileTypes();
|
|
$mimetypes = array();
|
|
$extensions = array();
|
|
if (isset($config['mimetypes']) && is_array($config['mimetypes'])) {
|
|
foreach ($config['mimetypes'] as $type=>$desc) {
|
|
foreach ($_types[$type]['types'] as $mime=>$exts) {
|
|
$mimetypes[$mime] = true;
|
|
if (is_array($exts))
|
|
foreach ($exts as $ext)
|
|
$extensions['.'.$ext] = true;
|
|
}
|
|
}
|
|
}
|
|
if (strpos($config['extensions'], '.*') !== false)
|
|
$config['extensions'] = '';
|
|
|
|
if (is_string($config['extensions'])) {
|
|
foreach (preg_split('/\s+/', str_replace(',',' ', $config['extensions'])) as $ext) {
|
|
if (!$ext) {
|
|
continue;
|
|
}
|
|
elseif (strpos($ext, '/')) {
|
|
$mimetypes[$ext] = true;
|
|
}
|
|
else {
|
|
if ($ext[0] != '.')
|
|
$ext = '.' . $ext;
|
|
|
|
// Ensure that the extension is lower-cased for comparison latr
|
|
$ext = strtolower($ext);
|
|
|
|
// Add this to the MIME types list so it can be exported to
|
|
// the @accept attribute
|
|
if (!isset($extensions[$ext]))
|
|
$mimetypes[$ext] = true;
|
|
|
|
$extensions[$ext] = true;
|
|
}
|
|
}
|
|
$config['__extensions'] = array_keys($extensions);
|
|
}
|
|
elseif (is_array($config['extensions'])) {
|
|
$config['__extensions'] = $config['extensions'];
|
|
}
|
|
|
|
// 'mimetypes' is the array represented from the user interface,
|
|
// '__mimetypes' is a complete list of supported MIME types.
|
|
$config['__mimetypes'] = array_keys($mimetypes);
|
|
return $config;
|
|
}
|
|
|
|
// When the field is saved to database, encode the ID listing as a json
|
|
// array. Then, inspect the difference between the files actually
|
|
// attached to this field
|
|
function to_database($value) {
|
|
$this->getAttachments();
|
|
if (isset($this->attachments) && $this->attachments) {
|
|
$this->attachments->keepOnlyFileIds($value);
|
|
}
|
|
return JsonDataEncoder::encode($value);
|
|
}
|
|
|
|
function parse($value) {
|
|
return $value;
|
|
}
|
|
|
|
function to_php($value) {
|
|
return is_array($value) ? $value : JsonDataParser::decode($value);
|
|
}
|
|
|
|
function display($value) {
|
|
$links = array();
|
|
foreach ($this->getAttachments() as $a) {
|
|
$links[] = sprintf('<a class="no-pjax" href="%s"><i class="icon-paperclip icon-flip-horizontal"></i> %s</a>',
|
|
Format::htmlchars($a->file->getDownloadUrl()),
|
|
Format::htmlchars($a->getFilename()));
|
|
}
|
|
return implode('<br/>', $links);
|
|
}
|
|
|
|
function toString($value) {
|
|
$files = array();
|
|
foreach ($this->getFiles() as $f) {
|
|
$files[] = $f->name;
|
|
}
|
|
return implode(', ', $files);
|
|
}
|
|
|
|
function db_cleanup($field=false) {
|
|
if ($this->getAttachments()) {
|
|
$this->attachments->deleteAll();
|
|
}
|
|
}
|
|
|
|
function asVar($value, $id=false) {
|
|
if (($attachments = $this->getAttachments()))
|
|
$attachments = $attachments->all();
|
|
|
|
return new FileFieldAttachments($attachments ?: array());
|
|
}
|
|
function asVarType() {
|
|
return 'FileFieldAttachments';
|
|
}
|
|
|
|
function whatChanged($before, $after) {
|
|
$B = (array) $before;
|
|
$A = (array) $after;
|
|
$added = array_diff($A, $B);
|
|
$deleted = array_diff($B, $A);
|
|
$added = Format::htmlchars(array_values($added));
|
|
$deleted = Format::htmlchars(array_values($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));
|
|
}
|
|
return $desc;
|
|
}
|
|
}
|
|
|
|
class FileFieldAttachments {
|
|
var $attachments;
|
|
|
|
function __construct($attachments) {
|
|
$this->attachments = $attachments;
|
|
}
|
|
|
|
function __toString() {
|
|
$files = array();
|
|
foreach ($this->getAttachments() as $a) {
|
|
$files[] = $a->getFilename();
|
|
}
|
|
return implode(', ', $files);
|
|
}
|
|
|
|
function getAttachments() {
|
|
return $this->attachments ?: array();
|
|
}
|
|
|
|
function getVar($tag) {
|
|
switch ($tag) {
|
|
case 'names':
|
|
return $this->__toString();
|
|
case 'files':
|
|
throw new OOBContent(OOBContent::FILES, $this->getAttachments());
|
|
}
|
|
}
|
|
|
|
static function getVarScope() {
|
|
return array(
|
|
'names' => __('List of file names'),
|
|
'files' => __('Attached files'),
|
|
);
|
|
}
|
|
}
|
|
|
|
class ColorChoiceField extends FormField {
|
|
static $widget = 'ColorPickerWidget';
|
|
}
|
|
|
|
class InlineFormData extends ArrayObject {
|
|
var $_form;
|
|
|
|
function __construct($form, array $data=array()) {
|
|
parent::__construct($data);
|
|
$this->_form = $form;
|
|
}
|
|
|
|
function getVar($tag) {
|
|
foreach ($this->_form->getFields() as $f) {
|
|
if ($f->get('name') == $tag)
|
|
return $this[$f->get('id')];
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
class InlineFormField extends FormField {
|
|
static $widget = 'InlineFormWidget';
|
|
|
|
var $_iform = null;
|
|
|
|
function validateEntry($value) {
|
|
if (!$this->getInlineForm()->isValid()) {
|
|
$config = $this->getConfiguration();
|
|
$this->_errors[] = isset($config['error'])
|
|
? $config['error'] : __('Correct any errors below and try again.');
|
|
}
|
|
}
|
|
|
|
function parse($value) {
|
|
// The InlineFieldWidget returns an array of cleaned data
|
|
return $value;
|
|
}
|
|
|
|
function to_database($value) {
|
|
return JsonDataEncoder::encode($value);
|
|
}
|
|
|
|
function to_php($value) {
|
|
$data = JsonDataParser::decode($value);
|
|
// The InlineFormData helps with the variable replacer API
|
|
return new InlineFormData($this->getInlineForm(), $data);
|
|
}
|
|
|
|
function display($data) {
|
|
$form = $this->getInlineForm();
|
|
ob_start(); ?>
|
|
<div><?php
|
|
foreach ($form->getFields() as $field) { ?>
|
|
<span style="display:inline-block;padding:0 5px;vertical-align:top">
|
|
<strong><?php echo Format::htmlchars($field->get('label')); ?></strong>
|
|
<div><?php
|
|
$value = $data[$field->get('id')];
|
|
echo $field->display($value); ?></div>
|
|
</span><?php
|
|
} ?>
|
|
</div><?php
|
|
return ob_get_clean();
|
|
}
|
|
|
|
function getInlineForm($data=false) {
|
|
$form = $this->get('form');
|
|
if (is_array($form)) {
|
|
$form = new SimpleForm($form, $data ?: $this->value ?: $this->getSource());
|
|
// Ensure unique, but predictable form and field IDs
|
|
$form->setId(sprintf('%u', crc32($this->get('name')) >> 1));
|
|
}
|
|
return $form;
|
|
}
|
|
}
|
|
|
|
class InlineDynamicFormField extends FormField {
|
|
function getInlineForm($data=false) {
|
|
if (!isset($this->_iform) || $data) {
|
|
$config = $this->getConfiguration();
|
|
$this->_iform = DynamicForm::lookup($config['form']);
|
|
if ($data)
|
|
$this->_iform = $this->_iform->getForm($data);
|
|
}
|
|
return $this->_iform;
|
|
}
|
|
|
|
function getConfigurationOptions() {
|
|
$forms = DynamicForm::objects()->filter(array('type'=>'G'))
|
|
->values_flat('id', 'title');
|
|
$choices = array();
|
|
foreach ($forms as $row) {
|
|
list($id, $title) = $row;
|
|
$choices[$id] = $title;
|
|
}
|
|
return array(
|
|
'form' => new ChoiceField(array(
|
|
'id'=>2, 'label'=>'Inline Form', 'required'=>true,
|
|
'default'=>'', 'choices'=>$choices
|
|
)),
|
|
);
|
|
}
|
|
}
|
|
|
|
class InlineFormWidget extends Widget {
|
|
function render($mode=false) {
|
|
$form = $this->field->getInlineForm();
|
|
if (!$form)
|
|
return;
|
|
// Handle first-step edits -- load data from $this->value
|
|
if ($form instanceof DynamicForm && !$form->getSource())
|
|
$form = $form->getForm($this->value);
|
|
$inc = ($mode == 'client') ? CLIENTINC_DIR : STAFFINC_DIR;
|
|
include $inc . 'templates/inline-form.tmpl.php';
|
|
}
|
|
|
|
function getValue() {
|
|
$data = $this->field->getSource();
|
|
if (!$data)
|
|
return null;
|
|
$form = $this->field->getInlineForm($data);
|
|
if (!$form)
|
|
return null;
|
|
return $form->getClean();
|
|
}
|
|
}
|
|
|
|
class Widget {
|
|
static $media = null;
|
|
|
|
function __construct($field) {
|
|
$this->field = $field;
|
|
$this->name = $field->getFormName();
|
|
$this->id = '_' . $this->name;
|
|
}
|
|
|
|
function parseValue() {
|
|
$this->value = $this->getValue();
|
|
if (!isset($this->value) && is_object($this->field->getAnswer()))
|
|
$this->value = $this->field->getAnswer()->getValue();
|
|
if (!isset($this->value) && isset($this->field->value))
|
|
$this->value = $this->field->value;
|
|
}
|
|
|
|
function getValue() {
|
|
$data = $this->field->getSource();
|
|
foreach ($this->field->getFormNames() as $name)
|
|
if (isset($data[$name]))
|
|
return $data[$name];
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* getJsValueGetter
|
|
*
|
|
* Used with the dependent fields feature, this function should return a
|
|
* single javascript expression which can be used in a larger expression
|
|
* (<> == true, where <> is the result of this function). The %s token
|
|
* will be replaced with a jQuery variable representing this widget.
|
|
*/
|
|
function getJsValueGetter($id='%s') {
|
|
return sprintf('%s.val()', $id);
|
|
}
|
|
|
|
/**
|
|
* getJsComparator
|
|
*
|
|
* Used with the dependent fields to get comparison expression
|
|
*
|
|
*/
|
|
function getJsComparator($value, $id) {
|
|
|
|
if (strpos($value, '|') !== false)
|
|
return sprintf('$.inArray(%s, %s) !== -1',
|
|
$this->getJsValueGetter($id),
|
|
JsonDataEncoder::encode(explode('|', $value)));
|
|
|
|
return sprintf('%s == %s',
|
|
$this->getJsValueGetter($id),
|
|
JsonDataEncoder::encode($value));
|
|
}
|
|
}
|
|
|
|
class TextboxWidget extends Widget {
|
|
static $input_type = 'text';
|
|
|
|
function render($options=array(), $extraConfig=false) {
|
|
$config = $this->field->getConfiguration();
|
|
if (is_array($extraConfig)) {
|
|
foreach ($extraConfig as $k=>$v)
|
|
if (!isset($config[$k]) || !$config[$k])
|
|
$config[$k] = $v;
|
|
}
|
|
|
|
// Input attributes
|
|
$attrs = array();
|
|
foreach ($config as $k => $v) {
|
|
switch ($k) {
|
|
case 'autocomplete':
|
|
if (is_numeric($v))
|
|
$v = $v ? 'on' : 'off';
|
|
$attrs[$k] = '"'.$v.'"';
|
|
break;
|
|
case 'disabled';
|
|
$attrs[$k] = '"disabled"';
|
|
break;
|
|
case 'translatable':
|
|
if ($v)
|
|
$attrs['data-translate-tag'] = '"'.$v.'"';
|
|
break;
|
|
case 'size':
|
|
case 'maxlength':
|
|
if ($v && is_numeric($v))
|
|
$attrs[$k] = '"'.$v.'"';
|
|
break;
|
|
case 'class':
|
|
case 'classes':
|
|
$attrs['class'] = '"'.$v.'"';
|
|
break;
|
|
case 'inputmode':
|
|
case 'pattern':
|
|
$attrs[$k] = '"'.$v.'"';
|
|
break;
|
|
}
|
|
}
|
|
// autofocus
|
|
$autofocus = '';
|
|
if (isset($config['autofocus']))
|
|
$autofocus = 'autofocus';
|
|
// placeholder
|
|
$attrs['placeholder'] = sprintf('"%s"',
|
|
Format::htmlchars($this->field->getLocal('placeholder',
|
|
$config['placeholder'])));
|
|
$type = static::$input_type;
|
|
$types = array(
|
|
'email' => 'email',
|
|
'phone' => 'tel',
|
|
);
|
|
if ($type == 'text' && isset($types[$config['validator']]))
|
|
$type = $types[$config['validator']];
|
|
?>
|
|
<input type="<?php echo $type; ?>"
|
|
id="<?php echo $this->id; ?>"
|
|
<?php echo $autofocus .' '.Format::array_implode('=', ' ',
|
|
array_filter($attrs)); ?>
|
|
name="<?php echo $this->name; ?>"
|
|
value="<?php echo Format::htmlchars($this->value, true); ?>"/>
|
|
<?php
|
|
}
|
|
}
|
|
|
|
|
|
class TextboxSelectionWidget extends TextboxWidget {
|
|
//TODO: Support multi-input e.g comma separated inputs
|
|
function render($options=array(), $extraConfig=array()) {
|
|
|
|
if ($this->value && is_array($this->value))
|
|
$this->value = current($this->value);
|
|
|
|
parent::render($options);
|
|
}
|
|
|
|
function getValue() {
|
|
|
|
$value = parent::getValue();
|
|
if ($value && ($item=$this->field->lookupChoice((string) $value)))
|
|
$value = $item;
|
|
|
|
return $value;
|
|
}
|
|
}
|
|
|
|
class PasswordWidget extends TextboxWidget {
|
|
static $input_type = 'password';
|
|
|
|
function render($mode=false, $extra=false) {
|
|
$extra = array();
|
|
if ($this->field->value) {
|
|
$extra['placeholder'] = '••••••••••••';
|
|
}
|
|
return parent::render($mode, $extra);
|
|
}
|
|
|
|
function parseValue() {
|
|
parent::parseValue();
|
|
// Show empty box unless failed POST
|
|
if ($_SERVER['REQUEST_METHOD'] != 'POST'
|
|
|| !$this->field->getForm()->isValid())
|
|
$this->value = '';
|
|
}
|
|
}
|
|
|
|
class TextareaWidget extends Widget {
|
|
function render($options=array()) {
|
|
$config = $this->field->getConfiguration();
|
|
// process textarea attributes
|
|
$attrs = array();
|
|
foreach ($config as $k => $v) {
|
|
switch ($k) {
|
|
case 'rows':
|
|
case 'cols':
|
|
case 'length':
|
|
case 'maxlength':
|
|
if ($v && is_numeric($v))
|
|
$attrs[$k] = '"'.$v.'"';;
|
|
break;
|
|
case 'context':
|
|
$attrs['data-root-context'] = '"'.$v.'"';
|
|
break;
|
|
case 'class':
|
|
// This might conflict with html attr below
|
|
$attrs[$k] = '"'.$v.'"';
|
|
break;
|
|
case 'html':
|
|
if ($v) {
|
|
$class = array('richtext', 'no-bar');
|
|
$class[] = @$config['size'] ?: 'small';
|
|
$attrs['class'] = '"'.implode(' ', $class).'"';
|
|
$this->value = Format::viewableImages($this->value);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
// placeholder
|
|
$attrs['placeholder'] = sprintf('"%s"',
|
|
Format::htmlchars($this->field->getLocal('placeholder',
|
|
$config['placeholder'])));
|
|
?>
|
|
<span style="display:inline-block;width:100%">
|
|
<textarea <?php echo Format::array_implode('=', ' ',
|
|
array_filter($attrs)); ?>
|
|
id="<?php echo $this->id; ?>"
|
|
name="<?php echo $this->name; ?>"><?php
|
|
echo Format::htmlchars($this->value, true);
|
|
?></textarea>
|
|
</span>
|
|
<?php
|
|
}
|
|
|
|
function parseValue() {
|
|
parent::parseValue();
|
|
if (isset($this->value)) {
|
|
$value = $this->value;
|
|
$config = $this->field->getConfiguration();
|
|
// Trim empty spaces based on text input type.
|
|
// Preserve original input if not empty.
|
|
if ($config['html'])
|
|
$this->value = trim($value, " <>br/\t\n\r") ? $value : '';
|
|
else
|
|
$this->value = trim($value) ? $value : '';
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
class PhoneNumberWidget extends Widget {
|
|
function render($options=array()) {
|
|
$config = $this->field->getConfiguration();
|
|
list($phone, $ext) = explode("X", $this->value);
|
|
?>
|
|
<input id="<?php echo $this->id; ?>" type="tel" name="<?php echo $this->name; ?>" value="<?php
|
|
echo Format::htmlchars($phone); ?>"/><?php
|
|
// Allow display of extension field even if disabled if the phone
|
|
// number being edited has an extension
|
|
if ($ext || $config['ext']) { ?> <?php echo __('Ext'); ?>:
|
|
<input type="text" name="<?php
|
|
echo $this->name; ?>-ext" value="<?php echo Format::htmlchars($ext);
|
|
?>" size="5"/>
|
|
<?php }
|
|
}
|
|
|
|
function getValue() {
|
|
$data = $this->field->getSource();
|
|
$base = parent::getValue();
|
|
if ($base === null)
|
|
return $base;
|
|
$ext = $data["{$this->name}-ext"];
|
|
// NOTE: 'X' is significant. Don't change it
|
|
if ($ext) $ext = 'X'.$ext;
|
|
return $base . $ext;
|
|
}
|
|
}
|
|
|
|
class ChoicesWidget extends Widget {
|
|
function render($options=array()) {
|
|
$mode = null;
|
|
if (isset($options['mode']))
|
|
$mode = $options['mode'];
|
|
elseif (isset($this->field->options['render_mode']))
|
|
$mode = $this->field->options['render_mode'];
|
|
|
|
if ($mode == 'view') {
|
|
$val = (string) $this->field;
|
|
echo sprintf('<span id="field_%s" %s >%s</span>', $this->id,
|
|
$val ? '': 'class="faded"',
|
|
$val ?: __('None'));
|
|
return;
|
|
}
|
|
|
|
$config = $this->field->getConfiguration();
|
|
if ($mode == 'search') {
|
|
$config['multiselect'] = true;
|
|
}
|
|
|
|
// Determine the value for the default (the one listed if nothing is
|
|
// selected)
|
|
$choices = $this->field->getChoices(true, $options);
|
|
$prompt = ($config['prompt'])
|
|
? $this->field->getLocal('prompt', $config['prompt'])
|
|
: __('Select'
|
|
/* Used as a default prompt for a custom drop-down list */);
|
|
|
|
$have_def = false;
|
|
// We don't consider the 'default' when rendering in 'search' mode
|
|
if (!strcasecmp($mode, 'search')) {
|
|
$def_val = $prompt;
|
|
} else {
|
|
$showdefault = true;
|
|
if ($mode != 'create')
|
|
$showdefault = false;
|
|
$def_key = $this->field->get('default');
|
|
if (!$def_key && isset($config['default']))
|
|
$def_key = $config['default'];
|
|
if (is_array($def_key))
|
|
$def_key = key($def_key);
|
|
$have_def = isset($choices[$def_key]);
|
|
$def_val = ($have_def && !$showdefault) ? $choices[$def_key] : $prompt;
|
|
}
|
|
|
|
$values = $this->value;
|
|
if (!is_array($values) && isset($values)) {
|
|
$values = array($values => $this->field->getChoice($values));
|
|
}
|
|
|
|
if (!is_array($values))
|
|
$values = $have_def ? array($def_key => $choices[$def_key]) : array();
|
|
|
|
if (isset($config['classes']))
|
|
$classes = 'class="'.$config['classes'].'"';
|
|
?>
|
|
<select name="<?php echo $this->name; ?>[]"
|
|
<?php echo implode(' ', array_filter(array($classes))); ?>
|
|
id="<?php echo $this->id; ?>"
|
|
<?php if (isset($config['data']))
|
|
foreach ($config['data'] as $D=>$V)
|
|
echo ' data-'.$D.'="'.Format::htmlchars($V).'"';
|
|
?>
|
|
data-placeholder="<?php echo Format::htmlchars($prompt); ?>"
|
|
<?php if ($config['multiselect'])
|
|
echo ' multiple="multiple"'; ?>>
|
|
<?php if ($showdefault || (!$have_def && !$config['multiselect'])) { ?>
|
|
<option value="<?php echo $showdefault ? '' : $def_key; ?>">— <?php
|
|
echo $def_val; ?> —</option>
|
|
<?php
|
|
}
|
|
$this->emitChoices($choices, $values, $have_def, $def_key); ?>
|
|
</select>
|
|
<?php
|
|
if ($config['multiselect']) {
|
|
?>
|
|
<script type="text/javascript">
|
|
$(function() {
|
|
$("#<?php echo $this->id; ?>")
|
|
.select2({'minimumResultsForSearch':10, 'width': '350px'});
|
|
});
|
|
</script>
|
|
<?php
|
|
}
|
|
}
|
|
|
|
function emitChoices($choices, $values=array(), $have_def=false, $def_key=null) {
|
|
reset($choices);
|
|
if (is_array(current($choices)) || current($choices) instanceof Traversable)
|
|
return $this->emitComplexChoices($choices, $values, $have_def, $def_key);
|
|
|
|
foreach ($choices as $key => $name) {
|
|
if (!$have_def && $key === $def_key)
|
|
continue; ?>
|
|
<option value="<?php echo $key; ?>" <?php
|
|
if (isset($values[$key])) echo 'selected="selected"';
|
|
?>><?php echo Format::htmlchars($name); ?></option>
|
|
<?php
|
|
}
|
|
}
|
|
|
|
function emitComplexChoices($choices, $values=array(), $have_def=false, $def_key=null) {
|
|
foreach ($choices as $label => $group) {
|
|
if (!count($group)) continue;
|
|
?>
|
|
<optgroup label="<?php echo $label; ?>"><?php
|
|
foreach ($group as $key => $name) {
|
|
if (!$have_def && $key == $def_key)
|
|
continue; ?>
|
|
<option value="<?php echo $key; ?>" <?php
|
|
if (isset($values[$key])) echo 'selected="selected"';
|
|
?>><?php echo Format::htmlchars($name); ?></option>
|
|
<?php } ?>
|
|
</optgroup><?php
|
|
}
|
|
}
|
|
|
|
function getValue() {
|
|
|
|
if (!($value = parent::getValue()))
|
|
return null;
|
|
|
|
if ($value && !is_array($value))
|
|
$value = array($value);
|
|
|
|
// Assume multiselect
|
|
$values = array();
|
|
$choices = $this->field->getChoices();
|
|
|
|
if ($choices && is_array($value)) {
|
|
// Complex choices
|
|
if (is_array(current($choices))
|
|
|| current($choices) instanceof Traversable) {
|
|
foreach ($choices as $label => $group) {
|
|
foreach ($group as $k => $v)
|
|
if (in_array($k, $value))
|
|
$values[$k] = $v;
|
|
}
|
|
} else {
|
|
foreach($value as $k => $v) {
|
|
if (isset($choices[$v]))
|
|
$values[$v] = $choices[$v];
|
|
elseif (($i=$this->field->lookupChoice($v)))
|
|
$values += $i;
|
|
elseif (!$k && $v)
|
|
return $v;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $values;
|
|
}
|
|
|
|
function getJsValueGetter($id='%s') {
|
|
return sprintf('%s.find(":selected").val()', $id);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* A widget for the ChoiceField which will render a list of radio boxes or
|
|
* checkboxes depending on the value of $config['multiple']. Complex choices
|
|
* are also supported and will be rendered as divs.
|
|
*/
|
|
class BoxChoicesWidget extends Widget {
|
|
function render($options=array()) {
|
|
$this->emitChoices($this->field->getChoices());
|
|
}
|
|
|
|
function emitChoices($choices) {
|
|
static $uid = 1;
|
|
|
|
if (!isset($this->value))
|
|
$this->value = $this->field->get('default');
|
|
$config = $this->field->getConfiguration();
|
|
$type = $config['multiple'] ? 'checkbox' : 'radio';
|
|
|
|
$classes = array('checkbox');
|
|
if (isset($config['classes']))
|
|
$classes = array_merge($classes, (array) $config['classes']);
|
|
|
|
foreach ($choices as $k => $v) {
|
|
if (is_array($v)) {
|
|
$this->renderSectionBreak($k);
|
|
$this->emitChoices($v);
|
|
continue;
|
|
}
|
|
$id = sprintf("%s-%s", $this->id, $uid++);
|
|
?>
|
|
<label class="<?php echo implode(' ', $classes); ?>"
|
|
for="<?php echo $id; ?>">
|
|
<input id="<?php echo $id; ?>" type="<?php echo $type; ?>"
|
|
name="<?php echo $this->name; ?>[]" <?php
|
|
if ($this->value[$k]) echo 'checked="checked"'; ?> value="<?php
|
|
echo Format::htmlchars($k); ?>"/>
|
|
<?php
|
|
if ($v) {
|
|
echo Format::viewableImages($v);
|
|
} ?>
|
|
</label>
|
|
<?php }
|
|
}
|
|
|
|
function renderSectionBreak($label) { ?>
|
|
<div><?php echo Format::htmlchars($label); ?></div>
|
|
<?php
|
|
}
|
|
|
|
function getValue() {
|
|
$data = $this->field->getSource();
|
|
if (count($data)) {
|
|
if (!isset($data[$this->name]))
|
|
return array();
|
|
return $this->collectValues($data[$this->name], $this->field->getChoices());
|
|
}
|
|
return parent::getValue();
|
|
}
|
|
|
|
function collectValues($data, $choices) {
|
|
$value = array();
|
|
foreach ($choices as $k => $v) {
|
|
if (is_array($v))
|
|
$value = array_merge($value, $this->collectValues($data, $v));
|
|
elseif (@in_array($k, $data))
|
|
$value[$k] = $v;
|
|
}
|
|
return $value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An extension to the BoxChoicesWidget which will render complex choices in
|
|
* tabs.
|
|
*/
|
|
class TabbedBoxChoicesWidget extends BoxChoicesWidget {
|
|
function render($options=array()) {
|
|
$tabs = array();
|
|
foreach ($this->field->getChoices() as $label=>$group) {
|
|
if (is_array($group)) {
|
|
$tabs[$label] = $group;
|
|
}
|
|
else {
|
|
$this->emitChoices(array($label=>$group));
|
|
}
|
|
}
|
|
if ($tabs) {
|
|
?>
|
|
<div>
|
|
<ul class="alt tabs">
|
|
<?php $i = 0;
|
|
foreach ($tabs as $label => $group) {
|
|
$active = $i++ == 0; ?>
|
|
<li <?php if ($active) echo 'class="active"';
|
|
?>><a href="#<?php echo sprintf('%s-%s', $this->name, Format::slugify($label));
|
|
?>"><?php echo Format::htmlchars($label); ?></a></li>
|
|
<?php } ?>
|
|
</ul>
|
|
<?php $i = 0;
|
|
foreach ($tabs as $label => $group) {
|
|
$first = $i++ == 0; ?>
|
|
<div class="tab_content <?php if (!$first) echo 'hidden'; ?>" id="<?php
|
|
echo sprintf('%s-%s', $this->name, Format::slugify($label));?>">
|
|
<?php $this->emitChoices($group); ?>
|
|
</div>
|
|
<?php } ?>
|
|
</div>
|
|
<?php }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* TimezoneWidget extends ChoicesWidget to add auto-detect and select2 search
|
|
* options
|
|
*
|
|
**/
|
|
class TimezoneWidget extends ChoicesWidget {
|
|
|
|
function render($options=array()) {
|
|
parent::render($options);
|
|
$config = $this->field->getConfiguration();
|
|
if (@$config['autodetect']) {
|
|
?>
|
|
<button type="button" class="action-button" onclick="javascript:
|
|
$('head').append($('<script>').attr('src', '<?php
|
|
echo ROOT_PATH; ?>js/jstz.min.js'));
|
|
var recheck = setInterval(function() {
|
|
if (window.jstz !== undefined) {
|
|
clearInterval(recheck);
|
|
var zone = jstz.determine();
|
|
$('#<?php echo $this->id; ?>').val(zone.name()).trigger('change');
|
|
|
|
}
|
|
}, 100);
|
|
return false;"
|
|
style="vertical-align:middle">
|
|
<i class="icon-map-marker"></i> <?php echo __('Auto Detect'); ?>
|
|
</button>
|
|
<?php
|
|
} ?>
|
|
<script type="text/javascript">
|
|
$(function() {
|
|
$('#<?php echo $this->id; ?>').select2({
|
|
allowClear: true,
|
|
width: '300px'
|
|
});
|
|
});
|
|
</script>
|
|
<?php
|
|
}
|
|
}
|
|
|
|
class CheckboxWidget extends Widget {
|
|
function __construct($field) {
|
|
parent::__construct($field);
|
|
$this->name = '_field-checkboxes';
|
|
}
|
|
|
|
function render($options=array()) {
|
|
$config = $this->field->getConfiguration();
|
|
if (!isset($this->value))
|
|
$this->value = $this->field->get('default');
|
|
$classes = array('checkbox');
|
|
if (isset($config['classes']))
|
|
$classes = array_merge($classes, (array) $config['classes']);
|
|
?>
|
|
<label class="<?php echo implode(' ', $classes); ?>">
|
|
<input id="<?php echo $this->id; ?>"
|
|
type="checkbox" name="<?php echo $this->name; ?>[]" <?php
|
|
if ($this->value) echo 'checked="checked"'; ?> value="<?php
|
|
echo $this->field->get('id'); ?>"/>
|
|
<?php
|
|
if ($config['desc']) {
|
|
echo Format::viewableImages($config['desc']);
|
|
} ?>
|
|
</label>
|
|
<?php
|
|
}
|
|
|
|
function getValue() {
|
|
$data = $this->field->getSource();
|
|
if (is_array($data)) {
|
|
if (isset($data[$this->name]))
|
|
return @in_array($this->field->get('id'),
|
|
$data[$this->name]);
|
|
// initial value set on source
|
|
if (isset($data[$this->field->get('id')]))
|
|
return $data[$this->field->get('id')];
|
|
}
|
|
|
|
if (!$data && isset($this->value))
|
|
return $this->value;
|
|
|
|
|
|
return parent::getValue();
|
|
}
|
|
|
|
function getJsValueGetter($id='%s') {
|
|
return sprintf('%s.is(":checked")', $id);
|
|
}
|
|
|
|
}
|
|
|
|
class DatetimePickerWidget extends Widget {
|
|
|
|
function render($options=array()) {
|
|
global $cfg;
|
|
|
|
$config = $this->field->getConfiguration();
|
|
$timezone = $this->field->getTimezone();
|
|
$dateFormat = $cfg->getDateFormat(true);
|
|
$timeFormat = $cfg->getTimeFormat(true);
|
|
if (!isset($this->value) && ($default=$this->field->get('default')))
|
|
$this->value = $default;
|
|
|
|
if ($this->value == 0)
|
|
$this->value = '';
|
|
|
|
if ($this->value) {
|
|
$datetime = Format::parseDateTime($this->value);
|
|
if ($config['time'])
|
|
// Convert to user's timezone for update.
|
|
$datetime->setTimezone($timezone);
|
|
|
|
// Get formatted date
|
|
$this->value = Format::date($datetime->getTimestamp(), false,
|
|
false, $timezone ? $timezone->getName() : 'UTC');
|
|
// Get formatted time
|
|
if ($config['time']) {
|
|
$this->value .=' '.Format::time($datetime->getTimestamp(),
|
|
false, $timeFormat, $timezone ?
|
|
$timezone->getName() : 'UTC');
|
|
}
|
|
|
|
} else {
|
|
// For timezone display purposes
|
|
$datetime = new DateTime('now');
|
|
$datetime->setTimezone($timezone);
|
|
}
|
|
?>
|
|
<input type="text" name="<?php echo $this->name; ?>"
|
|
id="<?php echo $this->id; ?>" style="display:inline-block;width:auto"
|
|
value="<?php echo $this->value; ?>"
|
|
size="<?php $config['time'] ? 20 : 12; ?>"
|
|
autocomplete="off" class="dp" />
|
|
<?php
|
|
// Timezone hint
|
|
// Show timzone hit by default but allow field to turn it off.
|
|
$showtimezone = true;
|
|
if (isset($config['showtimezone']))
|
|
$showtimezone = $config['showtimezone'];
|
|
|
|
if ($datetime && $showtimezone) {
|
|
echo sprintf(' <span class="faded">(<a href="#"
|
|
data-placement="top" data-toggle="tooltip"
|
|
title="%s">%s</a>)</span>',
|
|
$datetime->getTimezone()->getName(),
|
|
$datetime->format('T'));
|
|
}
|
|
?>
|
|
<script type="text/javascript">
|
|
$(function() {
|
|
$('input[name="<?php echo $this->name; ?>"]').<?php echo
|
|
$config['time'] ? 'datetimepicker':'datepicker';?>({
|
|
<?php
|
|
if ($dt=$this->field->getMinDateTime())
|
|
echo sprintf("minDate: new Date(%s),\n", $dt->format('U')*1000);
|
|
if ($dt=$this->field->getMaxDateTime())
|
|
echo sprintf("maxDate: new Date(%s),\n", $dt->format('U')*1000);
|
|
elseif (!$config['future'])
|
|
echo "maxDate: new Date().getTime(),\n";
|
|
|
|
// Set time options
|
|
if ($config['time']) {
|
|
// Set Timezone
|
|
echo sprintf("timezone: %s,\n",
|
|
($datetime->getOffset()/60));
|
|
echo sprintf("
|
|
controlType: 'select',\n
|
|
timeInput: true,\n
|
|
timeFormat: \"%s\",\n",
|
|
Format::dtfmt_php2js($timeFormat));
|
|
}
|
|
?>
|
|
numberOfMonths: 2,
|
|
showButtonPanel: true,
|
|
buttonImage: './images/cal.png',
|
|
showOn:'both',
|
|
dateFormat: '<?php echo
|
|
Format::dtfmt_php2js($dateFormat); ?>'
|
|
});
|
|
});
|
|
</script>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Function: getValue
|
|
* Combines the datepicker date value and the time dropdown selected
|
|
* time value into a single date and time string value in DateTime::W3C
|
|
*/
|
|
function getValue() {
|
|
global $cfg;
|
|
|
|
if ($value = parent::getValue()) {
|
|
if (($dt = Format::parseDateTime($value))) {
|
|
// Effective timezone for the selection
|
|
if (($timezone = $this->field->getTimezone()))
|
|
$dt->setTimezone($timezone);
|
|
// Format date time to universal format
|
|
$value = $dt->format('Y-m-d H:i:s T');
|
|
}
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
}
|
|
|
|
class TimePickerWidget extends Widget {
|
|
|
|
function render($options=array()) {
|
|
$config = $this->field->getConfiguration();
|
|
if (!isset($this->value) && ($default=$this->field->get('default')))
|
|
$this->value = $default;
|
|
|
|
// For timezone display purposes only - for now
|
|
$datetime = new DateTime('now');
|
|
// Selection timezone
|
|
$datetime->setTimezone($this->field->getTimeZone());
|
|
|
|
if ($this->value) {
|
|
// TODO: Reformat time here to match settings
|
|
}
|
|
|
|
?>
|
|
<input type="text" name="<?php echo $this->name; ?>"
|
|
id="<?php echo $this->id; ?>" style="display:inline-block;width:auto"
|
|
value="<?php echo $this->value; ?>"
|
|
size="10"
|
|
autocomplete="off" />
|
|
<?php
|
|
// Timezone hint
|
|
// Show timzone hit by default but allow field to turn it off.
|
|
$showtimezone = true;
|
|
if (isset($config['showtimezone']))
|
|
$showtimezone = $config['showtimezone'];
|
|
|
|
if ($showtimezone) {
|
|
echo sprintf(' <span class="faded">(<a href="#"
|
|
data-placement="top" data-toggle="tooltip"
|
|
title="%s">%s</a>)</span>',
|
|
$datetime->getTimezone()->getName(),
|
|
$datetime->format('T'));
|
|
}
|
|
?>
|
|
<script type="text/javascript">
|
|
$(function() {
|
|
$('input[name="<?php echo $this->name; ?>"]').timepicker({
|
|
<?php
|
|
// Set time options
|
|
echo sprintf("
|
|
controlType: 'select',\n
|
|
timeInput: true,\n
|
|
timeFormat: \"%s\",\n",
|
|
"hh:mm tt");
|
|
echo sprintf("timezone: %s\n",
|
|
($datetime->getOffset()/60));
|
|
?>
|
|
});
|
|
});
|
|
</script>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Function: getValue
|
|
*/
|
|
function getValue() {
|
|
global $cfg;
|
|
|
|
if ($value = parent::getValue()) {
|
|
// TODO: Return ISO format.
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
}
|
|
class SectionBreakWidget extends Widget {
|
|
function render($options=array()) {
|
|
?><div class="form-header section-break"><h3><?php
|
|
echo Format::htmlchars($this->field->getLocal('label'));
|
|
?></h3><em><?php echo Format::display($this->field->getLocal('hint'));
|
|
?></em></div>
|
|
<?php
|
|
}
|
|
}
|
|
|
|
class ThreadEntryWidget extends Widget {
|
|
function render($options=array()) {
|
|
|
|
$config = $this->field->getConfiguration();
|
|
$object_id = false;
|
|
if ($options['client']) {
|
|
$namespace = $options['draft-namespace']
|
|
?: 'ticket.client';
|
|
$object_id = substr(session_id(), -12);
|
|
} else {
|
|
$namespace = $options['draft-namespace'] ?: 'ticket.staff';
|
|
}
|
|
|
|
list($draft, $attrs) = Draft::getDraftAndDataAttrs($namespace, $object_id, $this->value);
|
|
?>
|
|
<textarea style="width:100%;" name="<?php echo $this->name; ?>"
|
|
placeholder="<?php echo Format::htmlchars($this->field->get('placeholder')); ?>"
|
|
class="<?php if ($config['html']) echo 'richtext';
|
|
?> draft draft-delete" <?php echo $attrs; ?>
|
|
cols="21" rows="8" style="width:80%;"><?php echo
|
|
ThreadEntryBody::clean($this->value ?: $draft); ?></textarea>
|
|
<?php
|
|
if (!$config['attachments'])
|
|
return;
|
|
|
|
$attachments = $this->getAttachments($config);
|
|
print $attachments->render($options);
|
|
foreach ($attachments->getMedia() as $type=>$urls) {
|
|
foreach ($urls as $url)
|
|
Form::emitMedia($url, $type);
|
|
}
|
|
}
|
|
|
|
function getAttachments($config=false) {
|
|
if (!$config)
|
|
$config = $this->field->getConfiguration();
|
|
|
|
$field = new FileUploadField(array(
|
|
'id'=>'attach',
|
|
'name'=>'attach:' . $this->field->get('id'),
|
|
'configuration'=>$config)
|
|
);
|
|
$field->setForm($this->field->getForm());
|
|
return $field;
|
|
}
|
|
|
|
function parseValue() {
|
|
parent::parseValue();
|
|
if (isset($this->value)) {
|
|
$value = $this->value;
|
|
$config = $this->field->getConfiguration();
|
|
// Trim spaces based on text input type.
|
|
// Preserve original input if not empty.
|
|
if ($config['html'])
|
|
$this->value = trim($value, " <>br/\t\n\r") ? $value : '';
|
|
else
|
|
$this->value = trim($value) ? $value : '';
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
class FileUploadWidget extends Widget {
|
|
static $media = array(
|
|
'css' => array(
|
|
'/css/filedrop.css',
|
|
),
|
|
);
|
|
|
|
function render($options=array()) {
|
|
$config = $this->field->getConfiguration();
|
|
$name = $this->field->getFormName();
|
|
$id = substr(md5(spl_object_hash($this)), 10);
|
|
$mimetypes = array_filter($config['__mimetypes'],
|
|
function($t) { return strpos($t, '/') !== false; }
|
|
);
|
|
$maxfilesize = ($config['size'] ?: 1048576) / 1048576;
|
|
$files = array();
|
|
$new = $this->field->getClean(false);
|
|
|
|
foreach ($this->field->getAttachments() as $att) {
|
|
unset($new[$att->file_id]);
|
|
$files[] = array(
|
|
'id' => $att->file->getId(),
|
|
'name' => $att->getFilename(),
|
|
'type' => $att->file->getType(),
|
|
'size' => $att->file->getSize(),
|
|
'download_url' => $att->file->getDownloadUrl(),
|
|
);
|
|
}
|
|
|
|
// Add in newly added files not yet saved (if redisplaying after an
|
|
// error)
|
|
if ($new) {
|
|
$F = AttachmentFile::objects()
|
|
->filter(array('id__in' => array_keys($new)))
|
|
->all();
|
|
|
|
foreach ($F as $f) {
|
|
$f->tmp_name = $new[$f->getId()];
|
|
$files[] = array(
|
|
'id' => $f->getId(),
|
|
'name' => $f->getName(),
|
|
'type' => $f->getType(),
|
|
'size' => $f->getSize(),
|
|
'download_url' => $f->getDownloadUrl(),
|
|
);
|
|
}
|
|
}
|
|
|
|
//see if the attachment is saved in the session for this specific field
|
|
if ($sessionAttachment = $_SESSION[':form-data'][$this->field->get('name')]) {
|
|
$F = AttachmentFile::objects()
|
|
->filter(array('id__in' => array_keys($sessionAttachment)))
|
|
->all();
|
|
|
|
foreach ($F as $f) {
|
|
$f->tmp_name = $sessionAttachment[$f->getId()];
|
|
$files[] = array(
|
|
'id' => $f->getId(),
|
|
'name' => $f->getName(),
|
|
'type' => $f->getType(),
|
|
'size' => $f->getSize(),
|
|
'download_url' => $f->getDownloadUrl(),
|
|
);
|
|
}
|
|
}
|
|
|
|
// Set default $field_id
|
|
$field_id = $this->field->get('id');
|
|
// Get Form Type
|
|
$type = $this->field->getForm()->type;
|
|
// Determine if for Ticket/Task/Custom
|
|
if ($type) {
|
|
if ($type == 'T')
|
|
$field_id = 'ticket/attach';
|
|
elseif ($type == 'A')
|
|
$field_id = 'task/attach';
|
|
}
|
|
|
|
?><div id="<?php echo $id;
|
|
?>" class="filedrop"><div class="files"></div>
|
|
<div class="dropzone"><i class="icon-upload"></i>
|
|
<?php echo sprintf(
|
|
__('Drop files here or %s choose them %s'),
|
|
'<a href="#" class="manual">', '</a>'); ?>
|
|
<input type="file" multiple="multiple"
|
|
id="file-<?php echo $id; ?>" style="display:none;"
|
|
accept="<?php echo implode(',', $config['__mimetypes']); ?>"/>
|
|
</div></div>
|
|
<script type="text/javascript">
|
|
$(function(){$('#<?php echo $id; ?> .dropzone').filedropbox({
|
|
url: 'ajax.php/form/upload/<?php echo $field_id; ?>',
|
|
link: $('#<?php echo $id; ?>').find('a.manual'),
|
|
paramname: 'upload[]',
|
|
fallback_id: 'file-<?php echo $id; ?>',
|
|
allowedfileextensions: <?php echo JsonDataEncoder::encode(
|
|
$config['__extensions'] ?: array()); ?>,
|
|
allowedfiletypes: <?php echo JsonDataEncoder::encode(
|
|
$mimetypes); ?>,
|
|
maxfiles: <?php echo $config['max'] ?: 20; ?>,
|
|
maxfilesize: <?php echo str_replace(',', '.', $maxfilesize); ?>,
|
|
name: '<?php echo $name; ?>[]',
|
|
files: <?php echo JsonDataEncoder::encode($files); ?>
|
|
});});
|
|
</script>
|
|
<?php
|
|
}
|
|
|
|
function getValue() {
|
|
$ids = array();
|
|
// Handle manual uploads (IE<10)
|
|
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_FILES[$this->name])) {
|
|
foreach (AttachmentFile::format($_FILES[$this->name]) as $file) {
|
|
try {
|
|
$F = $this->field->uploadFile($file);
|
|
$ids[$F->getId()] = $F->getName();
|
|
}
|
|
catch (FileUploadError $ex) {}
|
|
}
|
|
return $ids;
|
|
}
|
|
|
|
// Files uploaded here MUST have been uploaded by this user and
|
|
// identified in the session
|
|
//
|
|
// If no value was sent, assume an empty list
|
|
if (!($files = parent::getValue()))
|
|
return array();
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
|
$_files = array();
|
|
foreach ($files as $info) {
|
|
if (@list($id, $name) = explode(',', $info, 2))
|
|
$_files[$id] = $name;
|
|
}
|
|
$files = $_files;
|
|
}
|
|
|
|
$allowed = array();
|
|
// Files already attached to the field are allowed
|
|
foreach ($this->field->getFiles() as $F) {
|
|
// FIXME: This will need special porting in v1.10
|
|
$allowed[$F->id] = $F->getName();
|
|
}
|
|
|
|
// New files uploaded in this session are allowed
|
|
if (isset($_SESSION[':uploadedFiles']))
|
|
$allowed += $_SESSION[':uploadedFiles'];
|
|
|
|
// Canned attachments initiated by this session
|
|
if (isset($_SESSION[':cannedFiles']))
|
|
$allowed += $_SESSION[':cannedFiles'];
|
|
|
|
// Parse the files and make sure it's allowed.
|
|
foreach ($files as $id => $name) {
|
|
if (!isset($allowed[$id]))
|
|
continue;
|
|
|
|
// Keep the values as the IDs
|
|
$ids[$id] = $name ?: $allowed[$id];
|
|
}
|
|
|
|
return $ids;
|
|
}
|
|
}
|
|
|
|
class FileUploadError extends Exception {}
|
|
|
|
class FreeTextField extends FormField {
|
|
static $widget = 'FreeTextWidget';
|
|
protected $attachments;
|
|
|
|
function getConfigurationOptions() {
|
|
return array(
|
|
'content' => new TextareaField(array(
|
|
'configuration' => array('html' => true, 'size'=>'large'),
|
|
'label'=>__('Content'), 'required'=>true, 'default'=>'',
|
|
'hint'=>__('Free text shown in the form, such as a disclaimer'),
|
|
)),
|
|
'attachments' => new FileUploadField(array(
|
|
'id'=>'attach',
|
|
'label' => __('Attachments'),
|
|
'name'=>'files',
|
|
'configuration' => array('extensions'=>'')
|
|
)),
|
|
);
|
|
}
|
|
|
|
function hasData() {
|
|
return false;
|
|
}
|
|
|
|
function isBlockLevel() {
|
|
return true;
|
|
}
|
|
|
|
/* utils */
|
|
|
|
function to_config($config) {
|
|
if ($config && isset($config['attachments']))
|
|
$keepers = $config['attachments'];
|
|
$this->getAttachments()->keepOnlyFileIds($keepers);
|
|
|
|
return $config;
|
|
}
|
|
|
|
function db_cleanup($field=false) {
|
|
|
|
if ($field && $this->getFiles())
|
|
$this->getAttachments()->deleteAll();
|
|
}
|
|
|
|
function getAttachments() {
|
|
if (!isset($this->attachments))
|
|
$this->attachments = GenericAttachments::forIdAndType($this->get('id'), 'I');
|
|
|
|
return $this->attachments ?: array();
|
|
}
|
|
|
|
function getFiles() {
|
|
if (!isset($this->files)) {
|
|
$files = array();
|
|
if (($attachments=$this->getAttachments()))
|
|
foreach ($attachments->all() as $a)
|
|
$files[] = $a->getFile();
|
|
$this->files = $files;
|
|
}
|
|
return $this->files;
|
|
}
|
|
}
|
|
|
|
class FreeTextWidget extends Widget {
|
|
function render($options=array()) {
|
|
$config = $this->field->getConfiguration();
|
|
$class = $config['classes'] ?: 'thread-body bleed';
|
|
?><div class="<?php echo $class; ?>"><?php
|
|
if ($label = $this->field->getLocal('label')) { ?>
|
|
<h3><?php
|
|
echo Format::htmlchars($label);
|
|
?></h3><?php
|
|
}
|
|
if ($hint = $this->field->getLocal('hint')) { ?>
|
|
<em><?php
|
|
echo Format::display($hint);
|
|
?></em><?php
|
|
} ?>
|
|
<div><?php
|
|
echo Format::viewableImages($config['content']); ?></div>
|
|
</div>
|
|
<?php
|
|
if (($attachments = $this->field->getAttachments()) && count($attachments)) { ?>
|
|
<section class="freetext-files">
|
|
<div class="title"><?php echo __('Related Resources'); ?></div>
|
|
<?php foreach ($attachments->all() as $attach) {
|
|
$filename = Format::htmlchars($attach->getFilename());
|
|
?>
|
|
<div class="file">
|
|
<a href="<?php echo $attach->file->getDownloadUrl(); ?>"
|
|
target="_blank" download="<?php echo $filename; ?>"
|
|
class="truncate no-pjax">
|
|
<i class="icon-file"></i>
|
|
<?php echo $filename; ?>
|
|
</a>
|
|
</div>
|
|
<?php } ?>
|
|
</section>
|
|
<?php }
|
|
}
|
|
}
|
|
|
|
class ColorPickerWidget extends Widget {
|
|
static $media = array(
|
|
'css' => array(
|
|
'css/spectrum.css',
|
|
),
|
|
'js' => array(
|
|
'js/spectrum.js',
|
|
),
|
|
);
|
|
|
|
function render($options=array()) {
|
|
?><input type="color"
|
|
id="<?php echo $this->id; ?>"
|
|
name="<?php echo $this->name; ?>"
|
|
value="<?php echo Format::htmlchars($this->value); ?>"/><?php
|
|
}
|
|
}
|
|
|
|
class VisibilityConstraint {
|
|
static $operators = array(
|
|
'eq' => 1,
|
|
'neq' => 1,
|
|
);
|
|
|
|
const HIDDEN = 0x0001;
|
|
const VISIBLE = 0x0002;
|
|
|
|
var $initial;
|
|
var $constraint;
|
|
|
|
function __construct($constraint, $initial=self::VISIBLE) {
|
|
$this->constraint = $constraint;
|
|
$this->initial = $initial;
|
|
}
|
|
|
|
function emitJavascript($field) {
|
|
|
|
if (!$this->constraint->constraints)
|
|
return;
|
|
|
|
$func = 'recheck_'.$field->getWidget()->id;
|
|
$form = $field->getForm();
|
|
?>
|
|
<script type="text/javascript">
|
|
!(function() {
|
|
var <?php echo $func; ?> = function() {
|
|
var target = $('#field<?php echo $field->getWidget()->id; ?>');
|
|
<?php $fields = $this->getAllFields($this->constraint);
|
|
foreach ($fields as $f) {
|
|
if (!($field = $form->getField($f)))
|
|
continue;
|
|
echo sprintf('var %1$s = x = $("#%1$s");',
|
|
$field->getWidget()->id);
|
|
}
|
|
$expression = $this->compileQ($this->constraint, $form);
|
|
?>
|
|
if (<?php echo $expression; ?>) {
|
|
target.slideDown('fast', function (){
|
|
$(this).trigger('show');
|
|
});
|
|
} else {
|
|
target.slideUp('fast', function (){
|
|
$(this).trigger('hide');
|
|
});
|
|
}
|
|
};
|
|
|
|
<?php foreach ($fields as $f) {
|
|
if (!($field=$form->getField($f)))
|
|
continue;
|
|
$w = $field->getWidget();
|
|
?>
|
|
$('#<?php echo $w->id; ?>').on('change', <?php echo $func; ?>);
|
|
$('#field<?php echo $w->id; ?>').on('show hide', <?php
|
|
echo $func; ?>);
|
|
<?php } ?>
|
|
})();
|
|
</script><?php
|
|
}
|
|
|
|
/**
|
|
* Determines if the field was visible when the form was submitted
|
|
*/
|
|
function isVisible($field) {
|
|
|
|
// Assume initial visibility if constraint is not provided.
|
|
if (!$this->constraint->constraints)
|
|
return $this->initial == self::VISIBLE;
|
|
|
|
|
|
return $this->compileQPhp($this->constraint, $field);
|
|
}
|
|
|
|
static function splitFieldAndOp($field) {
|
|
if (false !== ($last = strrpos($field, '__'))) {
|
|
$op = substr($field, $last + 2);
|
|
if (isset(static::$operators[$op]))
|
|
$field = substr($field, 0, strrpos($field, '__'));
|
|
}
|
|
return array($field, $op);
|
|
}
|
|
|
|
function compileQPhp(Q $Q, $field) {
|
|
if (!($form = $field->getForm())) {
|
|
return $this->initial == self::VISIBLE;
|
|
}
|
|
$expr = array();
|
|
foreach ($Q->constraints as $c=>$value) {
|
|
if ($value instanceof Q) {
|
|
$expr[] = $this->compileQPhp($value, $field);
|
|
}
|
|
else {
|
|
@list($f, $op) = self::splitFieldAndOp($c);
|
|
$field = $form->getField($f);
|
|
$wval = $field ? $field->getClean() : null;
|
|
$values = explode('|', $value);
|
|
switch ($op) {
|
|
case 'neq':
|
|
$expr[] = ($wval && !in_array($wval, $values) && $field->isVisible());
|
|
break;
|
|
case 'eq':
|
|
case null:
|
|
$expr[] = (in_array($wval, $values) && $field->isVisible());
|
|
}
|
|
}
|
|
}
|
|
$glue = $Q->isOred()
|
|
? function($a, $b) { return $a || $b; }
|
|
: function($a, $b) { return $a && $b; };
|
|
$initial = !$Q->isOred();
|
|
$expression = array_reduce($expr, $glue, $initial);
|
|
if ($Q->isNegated)
|
|
$expression = !$expression;
|
|
return $expression;
|
|
}
|
|
|
|
function getAllFields(Q $Q, &$fields=array()) {
|
|
foreach ($Q->constraints as $c=>$value) {
|
|
if ($c instanceof Q) {
|
|
$this->getAllFields($c, $fields);
|
|
}
|
|
else {
|
|
@list($f) = self::splitFieldAndOp($c);
|
|
$fields[$f] = true;
|
|
}
|
|
}
|
|
return array_keys($fields);
|
|
}
|
|
|
|
function compileQ($Q, $form) {
|
|
$expr = array();
|
|
foreach ($Q->constraints as $c=>$value) {
|
|
if ($value instanceof Q) {
|
|
$expr[] = $this->compileQ($value, $form);
|
|
}
|
|
else {
|
|
list($f, $op) = self::splitFieldAndOp($c);
|
|
if (!($field=$form->getField($f))) continue;
|
|
$widget = $field->getWidget();
|
|
$id = $widget->id;
|
|
switch ($op) {
|
|
case 'neq':
|
|
$expr[] = sprintf('(%s.is(":visible") && !(%s))',
|
|
$id, $widget->getJsComparator($value, $id));
|
|
break;
|
|
case 'eq':
|
|
case null:
|
|
$expr[] = sprintf('(%s.is(":visible") && (%s))',
|
|
$id, $widget->getJsComparator($value, $id));
|
|
}
|
|
}
|
|
}
|
|
$glue = $Q->isOred() ? ' || ' : ' && ';
|
|
$expression = implode($glue, $expr);
|
|
if (count($expr) > 1)
|
|
$expression = '('.$expression.')';
|
|
if ($Q->isNegated)
|
|
$expression = '!'.$expression;
|
|
return $expression;
|
|
}
|
|
}
|
|
|
|
class AssignmentForm extends Form {
|
|
|
|
static $id = 'assign';
|
|
var $_assignee = null;
|
|
var $_assignees = null;
|
|
|
|
|
|
function getFields() {
|
|
|
|
if ($this->fields)
|
|
return $this->fields;
|
|
|
|
$fields = array(
|
|
'assignee' => new AssigneeField(array(
|
|
'id'=>1, 'label' => __('Assignee'),
|
|
'flags' => hexdec(0X450F3), 'required' => true,
|
|
'validator-error' => __('Assignee selection required'),
|
|
'configuration' => array(
|
|
'criteria' => array(
|
|
'available' => true,
|
|
),
|
|
),
|
|
)
|
|
),
|
|
'refer' => new BooleanField(array(
|
|
'id'=>2, 'label'=>'', 'required'=>false,
|
|
'default'=>false,
|
|
'configuration'=>array(
|
|
'desc' => 'Maintain referral access to current assignees')
|
|
)
|
|
),
|
|
'comments' => new TextareaField(array(
|
|
'id' => 3, 'label'=> '', 'required'=>false, 'default'=>'',
|
|
'configuration' => array(
|
|
'html' => true,
|
|
'size' => 'small',
|
|
'placeholder' => __('Optional reason for the assignment'),
|
|
),
|
|
)
|
|
),
|
|
);
|
|
|
|
|
|
if (isset($this->_assignees))
|
|
$fields['assignee']->setChoices($this->_assignees);
|
|
|
|
$this->setFields($fields);
|
|
|
|
return $this->fields;
|
|
}
|
|
|
|
function getField($name) {
|
|
|
|
if (($fields = $this->getFields())
|
|
&& isset($fields[$name]))
|
|
return $fields[$name];
|
|
}
|
|
|
|
function isValid($include=false) {
|
|
|
|
if (!parent::isValid($include) || !($f=$this->getField('assignee')))
|
|
return false;
|
|
|
|
// Do additional assignment validation
|
|
if (!($assignee = $this->getAssignee())) {
|
|
$f->addError(__('Unknown assignee'));
|
|
} elseif ($assignee instanceof Staff) {
|
|
// Make sure the agent is available
|
|
if (!$assignee->isAvailable())
|
|
$f->addError(__('Agent is unavailable for assignment'));
|
|
} elseif ($assignee instanceof Team) {
|
|
// Make sure the team is active and has members
|
|
if (!$assignee->isActive())
|
|
$f->addError(__('Team is disabled'));
|
|
elseif (!$assignee->getNumMembers())
|
|
$f->addError(__('Team does not have members'));
|
|
}
|
|
|
|
return !$this->errors();
|
|
}
|
|
|
|
function render($options=array()) {
|
|
|
|
switch(strtolower($options['template'])) {
|
|
case 'simple':
|
|
$inc = STAFFINC_DIR . 'templates/dynamic-form-simple.tmpl.php';
|
|
break;
|
|
default:
|
|
throw new Exception(sprintf(__('%s: Unknown template style %s'),
|
|
'FormUtils', $options['template']));
|
|
}
|
|
|
|
$form = $this;
|
|
include $inc;
|
|
}
|
|
|
|
function setAssignees($assignees) {
|
|
$this->_assignees = $assignees;
|
|
$this->_fields = array();
|
|
}
|
|
|
|
function getAssignees() {
|
|
return $this->_assignees;
|
|
}
|
|
|
|
function getAssignee() {
|
|
|
|
if (!isset($this->_assignee))
|
|
$this->_assignee = $this->getField('assignee')->getClean();
|
|
|
|
return $this->_assignee;
|
|
}
|
|
|
|
function getComments() {
|
|
return $this->getField('comments')->getClean();
|
|
}
|
|
|
|
function refer() {
|
|
return $this->getField('refer')->getClean();
|
|
}
|
|
}
|
|
|
|
class ClaimForm extends AssignmentForm {
|
|
|
|
var $_fields;
|
|
|
|
function setFields($fields) {
|
|
$this->_fields = $fields;
|
|
parent::setFields($fields);
|
|
}
|
|
|
|
function getFields() {
|
|
|
|
if ($this->_fields)
|
|
return $this->_fields;
|
|
|
|
$fields = parent::getFields();
|
|
|
|
// Disable && hide assignee field selection
|
|
if (isset($fields['assignee'])) {
|
|
$visibility = new VisibilityConstraint(
|
|
new Q(array()), VisibilityConstraint::HIDDEN);
|
|
|
|
$fields['assignee']->set('visibility', $visibility);
|
|
}
|
|
|
|
// Change coments placeholder to reflect claim
|
|
if (isset($fields['comments'])) {
|
|
$fields['comments']->configure('placeholder',
|
|
__('Optional reason for the claim'));
|
|
}
|
|
|
|
|
|
$this->setFields($fields);
|
|
|
|
return $this->fields;
|
|
}
|
|
|
|
}
|
|
|
|
class ReleaseForm extends Form {
|
|
static $id = 'unassign';
|
|
|
|
function getFields() {
|
|
if ($this->fields)
|
|
return $this->fields;
|
|
|
|
$fields = array(
|
|
'comments' => new TextareaField(array(
|
|
'id' => 1, 'label'=> '', 'required'=>false, 'default'=>'',
|
|
'configuration' => array(
|
|
'html' => true,
|
|
'size' => 'small',
|
|
'placeholder' => __('Optional reason for releasing assignment'),
|
|
),
|
|
)
|
|
),
|
|
);
|
|
|
|
|
|
$this->setFields($fields);
|
|
|
|
return $this->fields;
|
|
}
|
|
|
|
function getField($name) {
|
|
if (($fields = $this->getFields())
|
|
&& isset($fields[$name]))
|
|
return $fields[$name];
|
|
}
|
|
|
|
function isValid($include=false) {
|
|
if (!parent::isValid($include))
|
|
return false;
|
|
|
|
return !$this->errors();
|
|
}
|
|
|
|
function getComments() {
|
|
return $this->getField('comments')->getClean();
|
|
}
|
|
}
|
|
|
|
class MarkAsForm extends Form {
|
|
static $id = 'markAs';
|
|
|
|
function getFields() {
|
|
if ($this->fields)
|
|
return $this->fields;
|
|
|
|
$fields = array(
|
|
'comments' => new TextareaField(array(
|
|
'id' => 1, 'label'=> '', 'required'=>false, 'default'=>'',
|
|
'configuration' => array(
|
|
'html' => true,
|
|
'size' => 'small',
|
|
'placeholder' => __('Optional reason for marking ticket as (un)answered'),
|
|
),
|
|
)
|
|
),
|
|
);
|
|
|
|
|
|
$this->setFields($fields);
|
|
|
|
return $this->fields;
|
|
}
|
|
|
|
function getField($name) {
|
|
if (($fields = $this->getFields())
|
|
&& isset($fields[$name]))
|
|
return $fields[$name];
|
|
}
|
|
|
|
function isValid($include=false) {
|
|
if (!parent::isValid($include))
|
|
return false;
|
|
|
|
return !$this->errors();
|
|
}
|
|
|
|
function getComments() {
|
|
return $this->getField('comments')->getClean();
|
|
}
|
|
}
|
|
|
|
class ReferralForm extends Form {
|
|
|
|
static $id = 'refer';
|
|
var $_target = null;
|
|
var $_choices = null;
|
|
var $_prompt = '';
|
|
|
|
function getFields() {
|
|
|
|
if ($this->fields)
|
|
return $this->fields;
|
|
|
|
$fields = array(
|
|
'target' => new ChoiceField(array(
|
|
'id'=>1,
|
|
'label' => __('Referee'),
|
|
'flags' => hexdec(0X450F3),
|
|
'required' => true,
|
|
'validator-error' => __('Selection required'),
|
|
'choices' => array(
|
|
'agent' => __('Agent'),
|
|
'team' => __('Team'),
|
|
'dept' => __('Department'),
|
|
),
|
|
)
|
|
),
|
|
'agent' => new AgentSelectionField(array(
|
|
'id'=>2,
|
|
'label' => '',
|
|
'flags' => hexdec(0X450F3),
|
|
'required' => true,
|
|
'validator-error' => __('Agent selection required'),
|
|
'configuration'=>array('prompt'=>__('Select Agent')),
|
|
'visibility' => new VisibilityConstraint(
|
|
new Q(array('target__eq'=>'agent')),
|
|
VisibilityConstraint::HIDDEN
|
|
),
|
|
)
|
|
),
|
|
'team' => new ChoiceField(array(
|
|
'id'=>3,
|
|
'label' => '',
|
|
'flags' => hexdec(0X450F3),
|
|
'required' => true,
|
|
'validator-error' => __('Team selection required'),
|
|
'configuration'=>array('prompt'=>__('Select Team')),
|
|
'visibility' => new VisibilityConstraint(
|
|
new Q(array('target__eq'=>'team')),
|
|
VisibilityConstraint::HIDDEN
|
|
),
|
|
)
|
|
),
|
|
'dept' => new DepartmentField(array(
|
|
'id'=>4,
|
|
'label' => '',
|
|
'flags' => hexdec(0X450F3),
|
|
'required' => true,
|
|
'validator-error' => __('Dept. selection required'),
|
|
'configuration'=>array('prompt'=>__('Select Department')),
|
|
'visibility' => new VisibilityConstraint(
|
|
new Q(array('target__eq'=>'dept')),
|
|
VisibilityConstraint::HIDDEN
|
|
),
|
|
)
|
|
),
|
|
'comments' => new TextareaField(array(
|
|
'id' => 5,
|
|
'label'=> '',
|
|
'required'=>false,
|
|
'default'=>'',
|
|
'configuration' => array(
|
|
'html' => true,
|
|
'size' => 'small',
|
|
'placeholder' => __('Optional reason for the referral'),
|
|
),
|
|
)
|
|
),
|
|
);
|
|
|
|
$this->setFields($fields);
|
|
|
|
return $this->fields;
|
|
}
|
|
|
|
function getField($name) {
|
|
|
|
if (($fields = $this->getFields())
|
|
&& isset($fields[$name]))
|
|
return $fields[$name];
|
|
}
|
|
|
|
|
|
|
|
function isValid($include=false) {
|
|
|
|
if (!parent::isValid($include) || !($f=$this->getField('target')))
|
|
return false;
|
|
|
|
// Do additional assignment validation
|
|
$referee = $this->getReferee();
|
|
switch (true) {
|
|
case $referee instanceof Staff:
|
|
// Make sure the agent is available
|
|
if (!$referee->isAvailable())
|
|
$f->addError(__('Agent is unavailable for assignment'));
|
|
break;
|
|
case $referee instanceof Team:
|
|
// Make sure the team is active and has members
|
|
if (!$referee->isActive())
|
|
$f->addError(__('Team is disabled'));
|
|
elseif (!$referee->getNumMembers())
|
|
$f->addError(__('Team does not have members'));
|
|
break;
|
|
case $referee instanceof Dept:
|
|
break;
|
|
default:
|
|
$f->addError(__('Unknown selection'));
|
|
}
|
|
|
|
return !$this->errors();
|
|
}
|
|
|
|
function render($options=array()) {
|
|
|
|
switch(strtolower($options['template'])) {
|
|
case 'simple':
|
|
$inc = STAFFINC_DIR . 'templates/dynamic-form-simple.tmpl.php';
|
|
break;
|
|
default:
|
|
throw new Exception(sprintf(__('%s: Unknown template style %s'),
|
|
'FormUtils', $options['template']));
|
|
}
|
|
|
|
$form = $this;
|
|
include $inc;
|
|
}
|
|
|
|
function setChoices($field, $choices, $prompt='') {
|
|
|
|
if (!($f= $this->getField($field)))
|
|
return;
|
|
|
|
$f->set('choices', $choices);
|
|
|
|
return $f;
|
|
}
|
|
|
|
function getReferee() {
|
|
|
|
$target = $this->getField('target')->getClean();
|
|
if (!$target || !($f=$this->getField($target)))
|
|
return null;
|
|
|
|
$id = $f->getClean();
|
|
switch($target) {
|
|
case 'agent':
|
|
return Staff::lookup($id);
|
|
case 'team':
|
|
return Team::lookup($id);
|
|
case 'dept':
|
|
return Dept::lookup($id);
|
|
}
|
|
}
|
|
|
|
function getComments() {
|
|
return $this->getField('comments')->getClean();
|
|
}
|
|
}
|
|
|
|
|
|
class TransferForm extends Form {
|
|
|
|
static $id = 'transfer';
|
|
var $_dept = null;
|
|
|
|
function __construct($source=null, $options=array()) {
|
|
parent::__construct($source, $options);
|
|
}
|
|
|
|
function getFields() {
|
|
|
|
if ($this->fields)
|
|
return $this->fields;
|
|
|
|
$fields = array(
|
|
'dept' => new DepartmentField(array(
|
|
'id'=>1,
|
|
'label' => __('Department'),
|
|
'flags' => hexdec(0X450F3),
|
|
'required' => true,
|
|
'validator-error' => __('Department selection is required'),
|
|
)
|
|
),
|
|
'refer' => new BooleanField(array(
|
|
'id'=>2, 'label'=>'', 'required'=>false, 'default'=>false,
|
|
'configuration'=>array(
|
|
'desc' => 'Maintain referral access to current department')
|
|
)),
|
|
'comments' => new TextareaField(array(
|
|
'id' => 3,
|
|
'label'=> '',
|
|
'required'=>false,
|
|
'default'=>'',
|
|
'configuration' => array(
|
|
'html' => true,
|
|
'size' => 'small',
|
|
'placeholder' => __('Optional reason for the transfer'),
|
|
),
|
|
)
|
|
),
|
|
);
|
|
|
|
$this->setFields($fields);
|
|
|
|
return $this->fields;
|
|
}
|
|
|
|
function isValid($include=false) {
|
|
|
|
if (!parent::isValid($include))
|
|
return false;
|
|
|
|
// Do additional validations
|
|
if (!($dept = $this->getDept()))
|
|
$this->getField('dept')->addError(
|
|
__('Unknown department'));
|
|
|
|
return !$this->errors();
|
|
}
|
|
|
|
function render($options=array()) {
|
|
|
|
switch(strtolower($options['template'])) {
|
|
case 'simple':
|
|
$inc = STAFFINC_DIR . 'templates/dynamic-form-simple.tmpl.php';
|
|
break;
|
|
default:
|
|
throw new Exception(sprintf(__('%s: Unknown template style %s'),
|
|
get_class(), $options['template']));
|
|
}
|
|
|
|
$form = $this;
|
|
include $inc;
|
|
|
|
}
|
|
|
|
function refer() {
|
|
return $this->getField('refer')->getClean();
|
|
}
|
|
|
|
function getDept() {
|
|
|
|
if (!isset($this->_dept)) {
|
|
if (($id = $this->getField('dept')->getClean()))
|
|
$this->_dept = Dept::lookup($id);
|
|
}
|
|
|
|
return $this->_dept;
|
|
}
|
|
|
|
function hideDisabled() {
|
|
global $thisstaff;
|
|
|
|
if ($f = $this->getField('dept')) {
|
|
$f->configure('staff', $thisstaff);
|
|
$f->configure('hideDisabled', true);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* FieldUnchanged
|
|
*
|
|
* Thrown in the to_database() method to indicate the value should not be
|
|
* saved in the database (it wasn't changed in the request)
|
|
*/
|
|
class FieldUnchanged extends Exception {}
|
|
?>
|