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.
1511 lines
42 KiB
1511 lines
42 KiB
<?php
|
|
/*********************************************************************
|
|
class.list.php
|
|
|
|
Custom List utils
|
|
|
|
Jared Hancock <jared@osticket.com>
|
|
Peter Rotich <peter@osticket.com>
|
|
Copyright (c) 2014 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:
|
|
**********************************************************************/
|
|
require_once(INCLUDE_DIR .'class.dynamic_forms.php');
|
|
require_once(INCLUDE_DIR .'class.variable.php');
|
|
|
|
/**
|
|
* Interface for Custom Lists
|
|
*
|
|
* Custom lists are used to represent list of arbitrary data that can be
|
|
* used as dropdown or typeahead selections in dynamic forms. This model
|
|
* defines a list. The individual items are stored in the "Item" model.
|
|
*
|
|
*/
|
|
|
|
interface CustomList {
|
|
|
|
function getId();
|
|
function getName();
|
|
function getPluralName();
|
|
|
|
function getNumItems();
|
|
function getAllItems();
|
|
function getItems($criteria);
|
|
|
|
function getItem($id);
|
|
function addItem($vars, &$errors);
|
|
function isItemUnique($vars);
|
|
|
|
function getForm(); // Config form
|
|
function hasProperties();
|
|
function getConfigurationForm();
|
|
function getSummaryFields();
|
|
|
|
function getSortModes();
|
|
function getSortMode();
|
|
function getListOrderBy();
|
|
|
|
function allowAdd();
|
|
function hasAbbrev();
|
|
|
|
function update($vars, &$errors);
|
|
function delete();
|
|
|
|
static function create($vars, &$errors);
|
|
static function lookup($id);
|
|
}
|
|
|
|
/*
|
|
* Custom list item interface
|
|
*/
|
|
interface CustomListItem {
|
|
function getId();
|
|
function getValue();
|
|
function getAbbrev();
|
|
function getSortOrder();
|
|
|
|
function getList();
|
|
function getListId();
|
|
|
|
function getConfiguration();
|
|
|
|
function hasProperties();
|
|
function isEnabled();
|
|
function isDeletable();
|
|
function isEnableable();
|
|
function isInternal();
|
|
|
|
function enable();
|
|
function disable();
|
|
|
|
function update($vars, &$errors);
|
|
function delete();
|
|
}
|
|
|
|
|
|
/*
|
|
* Base class for Custom List handlers
|
|
*
|
|
* Custom list handler extends custom list and might store data outside the
|
|
* typical dynamic list store.
|
|
*
|
|
*/
|
|
|
|
abstract class CustomListHandler {
|
|
|
|
var $_list;
|
|
|
|
function __construct($list) {
|
|
$this->_list = $list;
|
|
}
|
|
|
|
function __call($name, $args) {
|
|
|
|
$rv = null;
|
|
if ($this->_list && is_callable(array($this->_list, $name)))
|
|
$rv = $args
|
|
? call_user_func_array(array($this->_list, $name), $args)
|
|
: call_user_func(array($this->_list, $name));
|
|
|
|
return $rv;
|
|
}
|
|
|
|
function __get($field) {
|
|
return $this->_list->{$field};
|
|
}
|
|
|
|
function update($vars, &$errors) {
|
|
return $this->_list->update($vars, $errors);
|
|
}
|
|
|
|
abstract function getListOrderBy();
|
|
abstract function getNumItems();
|
|
abstract function getAllItems();
|
|
abstract function getItems($criteria);
|
|
abstract function getItem($id);
|
|
abstract function addItem($vars, &$errors);
|
|
|
|
static protected $registry = array();
|
|
static function forList(/* CustomList */ $list) {
|
|
if ($list->type && ($handler = static::$registry[$list->type]))
|
|
return new $handler($list);
|
|
return $list;
|
|
}
|
|
static function register($type, $handler) {
|
|
static::$registry[$type] = $handler;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Dynamic lists are Custom Lists solely defined by the user.
|
|
*
|
|
*/
|
|
class DynamicList extends VerySimpleModel implements CustomList {
|
|
|
|
static $meta = array(
|
|
'table' => LIST_TABLE,
|
|
'ordering' => array('name'),
|
|
'pk' => array('id'),
|
|
'joins' => array(
|
|
'items' => array(
|
|
'reverse' => 'DynamicListItem.list',
|
|
),
|
|
),
|
|
);
|
|
|
|
// Required fields
|
|
static $fields = array('name', 'name_plural', 'sort_mode', 'notes');
|
|
|
|
// Supported masks
|
|
const MASK_EDIT = 0x0001;
|
|
const MASK_ADD = 0x0002;
|
|
const MASK_DELETE = 0x0004;
|
|
const MASK_ABBREV = 0x0008;
|
|
|
|
var $_items;
|
|
var $_form;
|
|
|
|
function getId() {
|
|
return $this->get('id');
|
|
}
|
|
|
|
function getInfo() {
|
|
return $this->ht;
|
|
}
|
|
|
|
function hasProperties() {
|
|
return ($this->getForm() && $this->getForm()->getFields());
|
|
}
|
|
|
|
function getSortModes() {
|
|
return array(
|
|
'Alpha' => __('Alphabetical'),
|
|
'-Alpha' => __('Alphabetical (Reversed)'),
|
|
'SortCol' => __('Manually Sorted')
|
|
);
|
|
}
|
|
|
|
function getSortMode() {
|
|
return $this->sort_mode;
|
|
}
|
|
|
|
function getListOrderBy() {
|
|
switch ($this->getSortMode()) {
|
|
case 'Alpha': return 'value';
|
|
case '-Alpha': return '-value';
|
|
case 'SortCol': return 'sort';
|
|
}
|
|
}
|
|
|
|
function getName() {
|
|
return $this->getLocal('name');
|
|
}
|
|
|
|
function getPluralName() {
|
|
if ($name = $this->getLocal('name_plural'))
|
|
return $name;
|
|
else
|
|
return $this->getName() . 's';
|
|
}
|
|
|
|
function getItemCount() {
|
|
return DynamicListItem::objects()->filter(array('list_id'=>$this->id))
|
|
->count();
|
|
}
|
|
|
|
function getNumItems() {
|
|
return $this->getItemCount();
|
|
}
|
|
|
|
function getAllItems() {
|
|
return DynamicListItem::objects()->filter(
|
|
array('list_id'=>$this->get('id')))
|
|
->order_by($this->getListOrderBy());
|
|
}
|
|
|
|
function search($q) {
|
|
$items = clone $this->getAllItems();
|
|
return $items->filter(Q::any(array(
|
|
'value__startswith' => $q,
|
|
'extra__contains' => $q,
|
|
'properties__contains' => '"'.$q,
|
|
)));
|
|
}
|
|
|
|
function getItems($limit=false, $offset=false) {
|
|
if (!$this->_items) {
|
|
$this->_items = DynamicListItem::objects()->filter(
|
|
array('list_id'=>$this->get('id'),
|
|
'status__hasbit'=>DynamicListItem::ENABLED))
|
|
->order_by($this->getListOrderBy());
|
|
if ($limit)
|
|
$this->_items->limit($limit);
|
|
if ($offset)
|
|
$this->_items->offset($offset);
|
|
}
|
|
return $this->_items;
|
|
}
|
|
|
|
function getItem($val, $extra=false) {
|
|
|
|
$items = DynamicListItem::objects()->filter(
|
|
array('list_id' => $this->getId()));
|
|
|
|
if (is_int($val))
|
|
$items->filter(array('id' => $val));
|
|
elseif ($extra)
|
|
$items->filter(array('extra' => $val));
|
|
else
|
|
$items->filter(array('value' => $val));
|
|
|
|
|
|
return $items->first();
|
|
}
|
|
|
|
function addItem($vars, &$errors) {
|
|
if (($item=$this->getItem($vars['value'])))
|
|
return $item;
|
|
|
|
$item = DynamicListItem::create(array(
|
|
'status' => 1,
|
|
'list_id' => $this->getId(),
|
|
'sort' => $vars['sort'],
|
|
'value' => $vars['value'],
|
|
'extra' => $vars['extra']
|
|
));
|
|
$this->_items = false;
|
|
|
|
return $item;
|
|
}
|
|
|
|
function isItemUnique($data) {
|
|
try {
|
|
$this->getItems()->filter(array('value'=>$data['value']))->one();
|
|
return false;
|
|
}
|
|
catch (DoesNotExist $e) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
function getConfigurationForm($autocreate=false) {
|
|
if (!$this->_form) {
|
|
$this->_form = DynamicForm::lookup(array('type'=>'L'.$this->getId()));
|
|
if (!$this->_form
|
|
&& $autocreate
|
|
&& $this->createConfigurationForm())
|
|
return $this->getConfigurationForm(false);
|
|
}
|
|
|
|
return $this->_form;
|
|
}
|
|
|
|
function getListItemBasicForm($source=null, $item=false) {
|
|
return new SimpleForm(array(
|
|
'value' => new TextboxField(array(
|
|
'required' => true,
|
|
'label' => __('Value'),
|
|
'configuration' => array(
|
|
'translatable' => $item ? $item->getTranslateTag('value') : false,
|
|
'size' => 60,
|
|
'length' => 0,
|
|
'autofocus' => true,
|
|
),
|
|
)),
|
|
'extra' => new TextboxField(array(
|
|
'label' => __('Abbreviation'),
|
|
'configuration' => array(
|
|
'size' => 60,
|
|
'length' => 0,
|
|
),
|
|
)),
|
|
), $source);
|
|
}
|
|
|
|
// Fields shown on the list items page
|
|
function getSummaryFields() {
|
|
$prop_fields = array();
|
|
foreach ($this->getConfigurationForm()->getFields() as $f) {
|
|
if (in_array($f->get('type'), array('text', 'datetime', 'phone')))
|
|
$prop_fields[] = $f;
|
|
if (strpos($f->get('type'), 'list-') === 0)
|
|
$prop_fields[] = $f;
|
|
|
|
// 4 property columns max
|
|
if (count($prop_fields) == 4)
|
|
break;
|
|
}
|
|
return $prop_fields;
|
|
}
|
|
|
|
function isDeleteable() {
|
|
return !$this->hasMask(static::MASK_DELETE);
|
|
}
|
|
|
|
function isEditable() {
|
|
return !$this->hasMask(static::MASK_EDIT);
|
|
}
|
|
|
|
function allowAdd() {
|
|
return !$this->hasMask(static::MASK_ADD);
|
|
}
|
|
|
|
function hasAbbrev() {
|
|
return !$this->hasMask(static::MASK_ABBREV);
|
|
}
|
|
|
|
protected function hasMask($mask) {
|
|
return 0 !== ($this->get('masks') & $mask);
|
|
}
|
|
|
|
protected function clearMask($mask) {
|
|
return $this->set('masks', $this->get('masks') & ~$mask);
|
|
}
|
|
|
|
protected function setFlag($mask) {
|
|
return $this->set('mask', $this->get('mask') | $mask);
|
|
}
|
|
|
|
private function createConfigurationForm() {
|
|
|
|
$form = DynamicForm::create(array(
|
|
'type' => 'L'.$this->getId(),
|
|
'title' => $this->getName() . ' Properties'
|
|
));
|
|
|
|
return $form->save(true);
|
|
}
|
|
|
|
function getForm($autocreate=true) {
|
|
return $this->getConfigurationForm($autocreate);
|
|
}
|
|
|
|
function getConfiguration() {
|
|
return JsonDataParser::parse($this->configuration);
|
|
}
|
|
|
|
function getTranslateTag($subtag) {
|
|
return _H(sprintf('list.%s.%s', $subtag, $this->id));
|
|
}
|
|
function getLocal($subtag) {
|
|
$tag = $this->getTranslateTag($subtag);
|
|
$T = CustomDataTranslation::translate($tag);
|
|
return $T != $tag ? $T : $this->get($subtag);
|
|
}
|
|
|
|
function update($vars, &$errors) {
|
|
$vars = Format::htmlchars($vars);
|
|
$required = array();
|
|
if ($this->isEditable())
|
|
$required = array('name');
|
|
|
|
foreach (static::$fields as $f) {
|
|
if (in_array($f, $required) && !$vars[$f])
|
|
$errors[$f] = sprintf(__('%s is required'), mb_convert_case($f, MB_CASE_TITLE));
|
|
elseif (isset($vars[$f])) {
|
|
if ($vars[$f] != $this->get($f)) {
|
|
$type = array('type' => 'edited', 'key' => $f);
|
|
Signal::send('object.edited', $this, $type);
|
|
$this->set($f, $vars[$f]);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if ($errors)
|
|
return false;
|
|
|
|
return $this->save(true);
|
|
}
|
|
|
|
function save($refetch=false) {
|
|
if (count($this->dirty))
|
|
$this->set('updated', new SqlFunction('NOW'));
|
|
if (isset($this->dirty['notes']))
|
|
$this->notes = Format::sanitize($this->notes);
|
|
|
|
return parent::save($refetch);
|
|
}
|
|
|
|
function delete() {
|
|
$fields = DynamicFormField::objects()->filter(array(
|
|
'type'=>'list-'.$this->id))->count();
|
|
|
|
// Refuse to delete lists that are in use by fields
|
|
if ($fields != 0)
|
|
return false;
|
|
|
|
if (!parent::delete())
|
|
return false;
|
|
|
|
$type = array('type' => 'deleted');
|
|
Signal::send('object.deleted', $this, $type);
|
|
|
|
if (($form = $this->getForm(false))) {
|
|
$form->delete(false);
|
|
$form->fields->delete();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private function createForm() {
|
|
|
|
$form = DynamicForm::create(array(
|
|
'type' => 'L'.$this->getId(),
|
|
'title' => $this->getName() . ' Properties'
|
|
));
|
|
|
|
return $form->save(true);
|
|
}
|
|
|
|
static function add($vars, &$errors) {
|
|
$vars = Format::htmlchars($vars);
|
|
$required = array('name');
|
|
$ht = array();
|
|
foreach (static::$fields as $f) {
|
|
if (in_array($f, $required) && !$vars[$f])
|
|
$errors[$f] = sprintf(__('%s is required'), mb_convert_case($f, MB_CASE_TITLE));
|
|
elseif(isset($vars[$f]))
|
|
$ht[$f] = $vars[$f];
|
|
}
|
|
|
|
if (!$ht || $errors)
|
|
return false;
|
|
|
|
// Create the list && form
|
|
if (!($list = self::create($ht, $errors, false))
|
|
|| !$list->save(true)
|
|
|| !$list->createConfigurationForm())
|
|
return false;
|
|
|
|
return $list;
|
|
}
|
|
|
|
static function create($ht=false, &$errors=array(), $sanitize=true) {
|
|
if ($ht && $sanitize)
|
|
$ht = Format::htmlchars($ht);
|
|
|
|
if (isset($ht['configuration'])) {
|
|
$ht['configuration'] = JsonDataEncoder::encode($ht['configuration']);
|
|
}
|
|
|
|
$inst = new static($ht);
|
|
$inst->set('created', new SqlFunction('NOW'));
|
|
|
|
if (isset($ht['properties'])) {
|
|
$inst->save();
|
|
$ht['properties']['type'] = 'L'.$inst->getId();
|
|
$form = DynamicForm::create($ht['properties']);
|
|
$form->save();
|
|
}
|
|
|
|
if (isset($ht['items'])) {
|
|
$inst->save();
|
|
foreach ($ht['items'] as $i) {
|
|
$i['list_id'] = $inst->getId();
|
|
$item = DynamicListItem::create($i);
|
|
$item->save();
|
|
}
|
|
}
|
|
|
|
return $inst;
|
|
}
|
|
|
|
static function lookup($id) {
|
|
|
|
if (!($list = parent::lookup($id)))
|
|
return null;
|
|
|
|
if (($config = $list->getConfiguration())) {
|
|
if (($lh=$config['handler']) && class_exists($lh))
|
|
$list = new $lh($list);
|
|
}
|
|
|
|
return $list;
|
|
}
|
|
|
|
static function getSelections() {
|
|
$selections = array();
|
|
foreach (DynamicList::objects() as $list) {
|
|
$selections['list-'.$list->id] =
|
|
array($list->getPluralName(),
|
|
'SelectionField', $list->get('id'));
|
|
}
|
|
return $selections;
|
|
}
|
|
|
|
function importCsv($stream, $defaults=array()) {
|
|
require_once INCLUDE_DIR . 'class.import.php';
|
|
|
|
$form = $this->getConfigurationForm();
|
|
$fields = array(
|
|
'value' => new TextboxField(array(
|
|
'label' => __('Value'),
|
|
'name' => 'value',
|
|
'configuration' => array(
|
|
'length' => 0,
|
|
),
|
|
)),
|
|
'abbrev' => new TextboxField(array(
|
|
'name' => 'extra',
|
|
'label' => __('Abbreviation'),
|
|
'configuration' => array(
|
|
'length' => 0,
|
|
),
|
|
)),
|
|
);
|
|
|
|
$form = $this->getConfigurationForm();
|
|
if ($form && ($custom_fields = $form->getFields())
|
|
&& count($custom_fields)) {
|
|
foreach ($custom_fields as $f)
|
|
if ($f->get('name'))
|
|
$fields[$f->get('name')] = $f;
|
|
}
|
|
|
|
$importer = new CsvImporter($stream);
|
|
$imported = 0;
|
|
try {
|
|
db_autocommit(false);
|
|
$records = $importer->importCsv($fields, $defaults);
|
|
foreach ($records as $data) {
|
|
$errors = array();
|
|
$item = $this->addItem($data, $errors);
|
|
if ($item && $item->setConfiguration($data, $errors))
|
|
$imported++;
|
|
else
|
|
echo sprintf(__('Unable to import item: %s'), print_r($data, true));
|
|
}
|
|
db_autocommit(true);
|
|
}
|
|
catch (Exception $ex) {
|
|
db_rollback();
|
|
return $ex->getMessage();
|
|
}
|
|
return $imported;
|
|
}
|
|
|
|
function importFromPost($stuff, $extra=array()) {
|
|
if (is_array($stuff) && !$stuff['error']) {
|
|
// Properly detect Macintosh style line endings
|
|
ini_set('auto_detect_line_endings', true);
|
|
$stream = fopen($stuff['tmp_name'], 'r');
|
|
}
|
|
elseif ($stuff) {
|
|
$stream = fopen('php://temp', 'w+');
|
|
fwrite($stream, $stuff);
|
|
rewind($stream);
|
|
}
|
|
else {
|
|
return __('Unable to parse submitted items');
|
|
}
|
|
|
|
return self::importCsv($stream, $extra);
|
|
}
|
|
}
|
|
FormField::addFieldTypes(/* @trans */ 'Custom Lists', array('DynamicList', 'getSelections'));
|
|
|
|
/**
|
|
* Represents a single item in a dynamic list
|
|
*
|
|
* Fields:
|
|
* value - (char * 255) Actual list item content
|
|
* extra - (char * 255) Other values that represent the same item in the
|
|
* list, such as an abbreviation. In practice, should be a
|
|
* space-separated list of tokens which should hit this list item in a
|
|
* search
|
|
* sort - (int) If sorting by this field, represents the numeric sort order
|
|
* that this item should come in the dropdown list
|
|
*/
|
|
class DynamicListItem extends VerySimpleModel implements CustomListItem {
|
|
|
|
static $meta = array(
|
|
'table' => LIST_ITEM_TABLE,
|
|
'pk' => array('id'),
|
|
'joins' => array(
|
|
'list' => array(
|
|
'null' => true,
|
|
'constraint' => array('list_id' => 'DynamicList.id'),
|
|
),
|
|
),
|
|
);
|
|
|
|
var $_config;
|
|
var $_form;
|
|
|
|
const ENABLED = 0x0001;
|
|
const INTERNAL = 0x0002;
|
|
|
|
protected function hasStatus($flag) {
|
|
return 0 !== ($this->get('status') & $flag);
|
|
}
|
|
|
|
protected function clearStatus($flag) {
|
|
return $this->set('status', $this->get('status') & ~$flag);
|
|
}
|
|
|
|
protected function setStatus($flag) {
|
|
return $this->set('status', $this->get('status') | $flag);
|
|
}
|
|
|
|
function isInternal() {
|
|
return $this->hasStatus(self::INTERNAL);
|
|
}
|
|
|
|
function isEnableable() {
|
|
return true;
|
|
}
|
|
|
|
function isDisableable() {
|
|
return !$this->isInternal();
|
|
}
|
|
|
|
function isDeletable() {
|
|
return !$this->isInternal();
|
|
}
|
|
|
|
function isEnabled() {
|
|
return $this->hasStatus(self::ENABLED);
|
|
}
|
|
|
|
function enable() {
|
|
$this->setStatus(self::ENABLED);
|
|
}
|
|
function disable() {
|
|
$this->clearStatus(self::ENABLED);
|
|
}
|
|
|
|
function hasProperties() {
|
|
return ($this->getForm() && $this->getForm()->getFields());
|
|
}
|
|
|
|
function getId() {
|
|
return $this->get('id');
|
|
}
|
|
|
|
function getList() {
|
|
return $this->list;
|
|
}
|
|
|
|
function getListId() {
|
|
return $this->get('list_id');
|
|
}
|
|
|
|
function getValue() {
|
|
return $this->getLocal('value');
|
|
}
|
|
|
|
function getAbbrev() {
|
|
return $this->get('extra');
|
|
}
|
|
|
|
function getSortOrder() {
|
|
return $this->get('sort');
|
|
}
|
|
|
|
function getConfiguration() {
|
|
if (!$this->_config) {
|
|
$this->_config = $this->get('properties');
|
|
if (is_string($this->_config))
|
|
$this->_config = JsonDataParser::parse($this->_config);
|
|
elseif (!$this->_config)
|
|
$this->_config = array();
|
|
}
|
|
return $this->_config;
|
|
}
|
|
|
|
function setConfiguration($vars, &$errors=array()) {
|
|
$config = array();
|
|
foreach ($this->getConfigurationForm($vars)->getFields() as $field) {
|
|
$config[$field->get('id')] = $field->to_php($field->getClean());
|
|
$errors = array_merge($errors, $field->errors());
|
|
}
|
|
|
|
if ($errors)
|
|
return false;
|
|
|
|
$this->set('properties', JsonDataEncoder::encode($config));
|
|
|
|
return $this->save();
|
|
}
|
|
|
|
function getConfigurationForm($source=null) {
|
|
if (!$this->_form) {
|
|
$config = $this->getConfiguration();
|
|
$this->_form = $this->list->getForm()->getForm($source);
|
|
if (!$source && $config) {
|
|
$fields = $this->_form->getFields();
|
|
foreach ($fields as $f) {
|
|
$name = $f->get('id');
|
|
if (isset($config[$name]))
|
|
$f->value = $f->to_php($config[$name]);
|
|
else if ($f->get('default'))
|
|
$f->value = $f->get('default');
|
|
}
|
|
}
|
|
}
|
|
|
|
return $this->_form;
|
|
}
|
|
|
|
function getForm() {
|
|
return $this->getConfigurationForm();
|
|
}
|
|
|
|
function getFields() {
|
|
return $this->getForm()->getFields();
|
|
}
|
|
|
|
function getVar($name) {
|
|
$config = $this->getConfiguration();
|
|
$name = mb_strtolower($name);
|
|
foreach ($this->getConfigurationForm()->getFields() as $field) {
|
|
if (mb_strtolower($field->get('name')) == $name)
|
|
return $field->asVar($config[$field->get('id')]);
|
|
}
|
|
}
|
|
|
|
function getFilterData() {
|
|
$data = array();
|
|
foreach ($this->getConfigurationForm()->getFields() as $F) {
|
|
$data['.'.$F->get('id')] = $F->toString($F->value);
|
|
}
|
|
$data['.abb'] = (string) $this->get('extra');
|
|
return $data;
|
|
}
|
|
|
|
function getTranslateTag($subtag) {
|
|
return _H(sprintf('listitem.%s.%s', $subtag, $this->id));
|
|
}
|
|
function getLocal($subtag) {
|
|
$tag = $this->getTranslateTag($subtag);
|
|
$T = CustomDataTranslation::translate($tag);
|
|
return $T != $tag ? $T : $this->get($subtag);
|
|
}
|
|
|
|
function toString() {
|
|
return $this->get('value');
|
|
}
|
|
|
|
function __toString() {
|
|
return $this->toString();
|
|
}
|
|
|
|
function display() {
|
|
|
|
return $this->getValue();
|
|
//TODO: Allow for display mode (edit, preview or both)
|
|
return sprintf('<a class="preview" href="#"
|
|
data-preview="#list/%d/items/%d/preview">%s</a>',
|
|
$this->getListId(),
|
|
$this->getId(),
|
|
$this->getValue()
|
|
);
|
|
}
|
|
|
|
function update($vars, &$errors=array()) {
|
|
|
|
if (!$vars['value']) {
|
|
$errors['value-'.$this->getId()] = __('Value required');
|
|
return false;
|
|
}
|
|
|
|
foreach (array(
|
|
'sort' => 'sort',
|
|
'value' => 'value',
|
|
'abbrev' => 'extra') as $k => $v) {
|
|
if ($k == 'abbrev' && empty($vars[$k])) {
|
|
$vars[$k] = NULL;
|
|
$this->set($v, $vars[$k]);
|
|
} elseif (isset($vars[$k]))
|
|
$this->set($v, $vars[$k]);
|
|
}
|
|
|
|
return $this->save();
|
|
}
|
|
|
|
function delete() {
|
|
# Don't really delete, just unset the list_id to un-associate it with
|
|
# the list
|
|
$this->set('list_id', null);
|
|
return $this->save();
|
|
}
|
|
|
|
static function create($ht=false, &$errors=array()) {
|
|
|
|
if (isset($ht['properties']) && is_array($ht['properties']))
|
|
$ht['properties'] = JsonDataEncoder::encode($ht['properties']);
|
|
|
|
$inst = new static($ht);
|
|
|
|
// Auto-config properties if any
|
|
if ($ht['configuration'] && is_array($ht['configuration'])) {
|
|
$config = $inst->getConfiguration();
|
|
if (($form = $inst->getConfigurationForm())) {
|
|
foreach ($form->getFields() as $f) {
|
|
if (!isset($ht['configuration'][$f->get('name')]))
|
|
continue;
|
|
|
|
$config[$f->get('id')] =
|
|
$ht['configuration'][$f->get('name')];
|
|
}
|
|
}
|
|
|
|
$inst->set('properties', JsonDataEncoder::encode($config));
|
|
}
|
|
|
|
return $inst;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* Ticket status List
|
|
*
|
|
*/
|
|
|
|
class TicketStatusList extends CustomListHandler {
|
|
|
|
// Fields of interest we need to store
|
|
static $config_fields = array('sort_mode', 'notes');
|
|
|
|
var $_items;
|
|
var $_form;
|
|
|
|
function getListOrderBy() {
|
|
switch ($this->getSortMode()) {
|
|
case 'Alpha': return 'name';
|
|
case '-Alpha': return '-name';
|
|
case 'SortCol': return 'sort';
|
|
}
|
|
}
|
|
|
|
function getNumItems() {
|
|
return TicketStatus::objects()->count();
|
|
}
|
|
|
|
function getAllItems() {
|
|
if (!$this->_items)
|
|
$this->_items = TicketStatus::objects()->order_by($this->getListOrderBy());
|
|
|
|
return $this->_items;
|
|
}
|
|
|
|
function search($q) {
|
|
$items = clone $this->getAllItems();
|
|
return $items->filter(Q::any(array(
|
|
'name__startswith' => $q,
|
|
'properties__contains' => '"'.$q,
|
|
)));
|
|
}
|
|
|
|
function getItems($criteria = array()) {
|
|
|
|
// Default to only enabled items
|
|
if (!isset($criteria['enabled']))
|
|
$criteria['enabled'] = true;
|
|
|
|
$filters = array();
|
|
if ($criteria['enabled'])
|
|
$filters['mode__hasbit'] = TicketStatus::ENABLED;
|
|
if ($criteria['states'] && is_array($criteria['states']))
|
|
$filters['state__in'] = $criteria['states'];
|
|
else
|
|
$filters['state__isnull'] = false;
|
|
|
|
$items = TicketStatus::objects();
|
|
if ($filters)
|
|
$items->filter($filters);
|
|
if ($criteria['limit'])
|
|
$items->limit($criteria['limit']);
|
|
if ($criteria['offset'])
|
|
$items->offset($criteria['offset']);
|
|
|
|
$items->order_by($this->getListOrderBy());
|
|
|
|
return $items;
|
|
}
|
|
|
|
function getItem($val) {
|
|
|
|
if (!is_int($val))
|
|
$val = array('name' => $val);
|
|
|
|
return TicketStatus::lookup($val, $this);
|
|
}
|
|
|
|
function addItem($vars, &$errors) {
|
|
$item = TicketStatus::create(array(
|
|
'mode' => 1,
|
|
'flags' => 0,
|
|
'sort' => $vars['sort'],
|
|
'name' => $vars['name'],
|
|
));
|
|
$this->_items = false;
|
|
|
|
return $item;
|
|
}
|
|
|
|
function isItemUnique($data) {
|
|
try {
|
|
$this->getItems()->filter(array('name'=>$data['name']))->one();
|
|
return false;
|
|
}
|
|
catch (DoesNotExist $e) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
|
|
static function getStatuses($criteria=array()) {
|
|
|
|
$statuses = array();
|
|
if (($list = DynamicList::lookup(
|
|
array('type' => 'ticket-status'))))
|
|
$statuses = $list->getItems($criteria);
|
|
|
|
return $statuses;
|
|
}
|
|
|
|
static function __load() {
|
|
require_once(INCLUDE_DIR.'class.i18n.php');
|
|
|
|
$i18n = new Internationalization();
|
|
$tpl = $i18n->getTemplate('list.yaml');
|
|
foreach ($tpl->getData() as $f) {
|
|
if ($f['type'] == 'ticket-status') {
|
|
$list = DynamicList::create($f);
|
|
$list->save();
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$list || !($o=DynamicForm::objects()->filter(
|
|
array('type'=>'L'.$list->getId()))))
|
|
return false;
|
|
|
|
// Create default statuses
|
|
if (($statuses = $i18n->getTemplate('ticket_status.yaml')->getData()))
|
|
foreach ($statuses as $status)
|
|
TicketStatus::__create($status);
|
|
|
|
return $o[0];
|
|
}
|
|
|
|
function getExtraConfigOptions($source=null) {
|
|
$status_choices = array( 0 => __('System Default'));
|
|
if (($statuses=TicketStatusList::getStatuses(
|
|
array( 'enabled' => true, 'states' =>
|
|
array('open')))))
|
|
foreach ($statuses as $s)
|
|
$status_choices[$s->getId()] = $s->getName();
|
|
|
|
return array(
|
|
'allowreopen' => new BooleanField(array(
|
|
'label' =>__('Allow Reopen'),
|
|
'editable' => true,
|
|
'default' => isset($source['allowreopen'])
|
|
? $source['allowreopen']: true,
|
|
'id' => 'allowreopen',
|
|
'name' => 'allowreopen',
|
|
'configuration' => array(
|
|
'desc'=>__('Allow tickets on this status to be reopened by end users'),
|
|
),
|
|
'visibility' => new VisibilityConstraint(
|
|
new Q(array('state__eq'=>'closed')),
|
|
VisibilityConstraint::HIDDEN
|
|
),
|
|
)),
|
|
'reopenstatus' => new ChoiceField(array(
|
|
'label' => __('Reopen Status'),
|
|
'editable' => true,
|
|
'required' => false,
|
|
'default' => isset($source['reopenstatus'])
|
|
? $source['reopenstatus'] : 0,
|
|
'id' => 'reopenstatus',
|
|
'name' => 'reopenstatus',
|
|
'choices' => $status_choices,
|
|
'configuration' => array(
|
|
'widget' => 'dropdown',
|
|
'multiselect' =>false
|
|
),
|
|
'visibility' => new VisibilityConstraint(
|
|
new Q(array('allowreopen__eq'=> true)),
|
|
VisibilityConstraint::HIDDEN
|
|
),
|
|
))
|
|
);
|
|
}
|
|
|
|
function getConfigurationForm($source=null) {
|
|
if (!($form = $this->getForm()))
|
|
return null;
|
|
|
|
$form = $form->getForm($source);
|
|
$fields = $form->getFields();
|
|
foreach ($fields as $k => $f) {
|
|
if ($f->get('name') == 'state' //TODO: check if editable.
|
|
&& ($extras=$this->getExtraConfigOptions($source))) {
|
|
foreach ($extras as $extra) {
|
|
$extra->setForm($form);
|
|
$fields->insert(++$k, $extra);
|
|
}
|
|
}
|
|
|
|
if (!isset($f->ht['editable']))
|
|
$f->ht['editable'] = true;
|
|
}
|
|
|
|
// Enable selection and display of private states
|
|
$form->getField('state')->options['private_too'] = true;
|
|
|
|
return $form;
|
|
}
|
|
|
|
function getListItemBasicForm($source=null, $item=false) {
|
|
return new SimpleForm(array(
|
|
'name' => new TextboxField(array(
|
|
'required' => true,
|
|
'label' => __('Value'),
|
|
'configuration' => array(
|
|
'translatable' => $item ? $item->getTranslateTag('value') : false,
|
|
'size' => 60,
|
|
'length' => 0,
|
|
'autofocus' => true,
|
|
),
|
|
)),
|
|
'extra' => new TextboxField(array(
|
|
'label' => __('Abbreviation'),
|
|
'configuration' => array(
|
|
'size' => 60,
|
|
'length' => 0,
|
|
),
|
|
)),
|
|
), $source);
|
|
}
|
|
|
|
function getSummaryFields() {
|
|
// Like the main one, except the description and state fields are
|
|
// welcome on the screen
|
|
$prop_fields = array();
|
|
foreach ($this->getConfigurationForm()->getFields() as $f) {
|
|
if (in_array($f->get('type'), array('state', 'text', 'datetime', 'phone')))
|
|
$prop_fields[] = $f;
|
|
elseif (strpos($f->get('type'), 'list-') === 0)
|
|
$prop_fields[] = $f;
|
|
elseif ($f->get('name') == 'description')
|
|
$prop_fields[] = $f;
|
|
|
|
// 4 property columns max
|
|
if (count($prop_fields) == 4)
|
|
break;
|
|
}
|
|
return $prop_fields;
|
|
}
|
|
}
|
|
CustomListHandler::register('ticket-status', 'TicketStatusList');
|
|
|
|
class TicketStatus
|
|
extends VerySimpleModel
|
|
implements CustomListItem, TemplateVariable, Searchable {
|
|
|
|
static $meta = array(
|
|
'table' => TICKET_STATUS_TABLE,
|
|
'ordering' => array('name'),
|
|
'pk' => array('id'),
|
|
'joins' => array(
|
|
'tickets' => array(
|
|
'reverse' => 'Ticket.status',
|
|
)
|
|
)
|
|
);
|
|
|
|
var $_list;
|
|
var $_form;
|
|
var $_settings;
|
|
var $_properties;
|
|
|
|
const ENABLED = 0x0001;
|
|
const INTERNAL = 0x0002; // Forbid deletion or name and status change.
|
|
|
|
protected function hasFlag($field, $flag) {
|
|
return 0 !== ($this->get($field) & $flag);
|
|
}
|
|
|
|
protected function clearFlag($field, $flag) {
|
|
return $this->set($field, $this->get($field) & ~$flag);
|
|
}
|
|
|
|
protected function setFlag($field, $flag) {
|
|
return $this->set($field, $this->get($field) | $flag);
|
|
}
|
|
|
|
function hasProperties() {
|
|
return ($this->get('properties'));
|
|
}
|
|
|
|
function isEnabled() {
|
|
return $this->hasFlag('mode', self::ENABLED);
|
|
}
|
|
|
|
function isReopenable() {
|
|
|
|
if (strcasecmp($this->get('state'), 'closed'))
|
|
return true;
|
|
|
|
if (($c=$this->getConfiguration())
|
|
&& $c['allowreopen']
|
|
&& isset($c['reopenstatus']))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
function getReopenStatus() {
|
|
global $cfg;
|
|
|
|
$status = null;
|
|
if ($this->isReopenable()
|
|
&& ($c = $this->getConfiguration())
|
|
&& isset($c['reopenstatus']))
|
|
$status = TicketStatus::lookup(
|
|
$c['reopenstatus'] ?: $cfg->getDefaultTicketStatusId());
|
|
|
|
return ($status
|
|
&& !strcasecmp($status->getState(), 'open'))
|
|
? $status : null;
|
|
}
|
|
|
|
function enable() {
|
|
|
|
// Ticket status without properties cannot be enabled!
|
|
if (!$this->isEnableable())
|
|
return false;
|
|
|
|
return $this->setFlag('mode', self::ENABLED);
|
|
}
|
|
|
|
function disable() {
|
|
return (!$this->isInternal()
|
|
&& !$this->isDefault()
|
|
&& $this->clearFlag('mode', self::ENABLED));
|
|
}
|
|
|
|
function isDefault() {
|
|
global $cfg;
|
|
|
|
return ($cfg
|
|
&& $cfg->getDefaultTicketStatusId() == $this->getId());
|
|
}
|
|
|
|
function isEnableable() {
|
|
return ($this->getState());
|
|
}
|
|
|
|
function isDisableable() {
|
|
return !($this->isInternal() || $this->isDefault());
|
|
}
|
|
|
|
function isDeletable() {
|
|
|
|
return !($this->isInternal()
|
|
|| $this->isDefault()
|
|
|| $this->getNumTickets());
|
|
}
|
|
|
|
function isInternal() {
|
|
return ($this->isDefault()
|
|
|| $this->hasFlag('mode', self::INTERNAL));
|
|
}
|
|
|
|
|
|
function getNumTickets() {
|
|
return $this->tickets->count();
|
|
}
|
|
|
|
function getId() {
|
|
return $this->get('id');
|
|
}
|
|
|
|
function getName() {
|
|
return $this->get('name');
|
|
}
|
|
|
|
function getState() {
|
|
return $this->get('state');
|
|
}
|
|
|
|
function getValue() {
|
|
return $this->getName();
|
|
}
|
|
function getLocalName() {
|
|
return $this->getLocal('value', $this->getName());
|
|
}
|
|
|
|
function getAbbrev() {
|
|
return '';
|
|
}
|
|
|
|
function getSortOrder() {
|
|
return $this->get('sort');
|
|
}
|
|
|
|
private function getProperties() {
|
|
|
|
if (!isset($this->_properties)) {
|
|
$this->_properties = $this->get('properties');
|
|
if (is_string($this->_properties))
|
|
$this->_properties = JsonDataParser::parse($this->_properties);
|
|
elseif (!$this->_properties)
|
|
$this->_properties = array();
|
|
}
|
|
|
|
return $this->_properties;
|
|
}
|
|
|
|
function getTranslateTag($subtag) {
|
|
return _H(sprintf('status.%s.%s', $subtag, $this->id));
|
|
}
|
|
function getLocal($subtag, $default) {
|
|
$tag = $this->getTranslateTag($subtag);
|
|
$T = CustomDataTranslation::translate($tag);
|
|
return $T != $tag ? $T : $default;
|
|
}
|
|
static function getLocalById($id, $subtag, $default) {
|
|
$tag = _H(sprintf('status.%s.%s', $subtag, $id));
|
|
$T = CustomDataTranslation::translate($tag);
|
|
return $T != $tag ? $T : $default;
|
|
}
|
|
|
|
// TemplateVariable interface
|
|
static function getVarScope() {
|
|
$base = array(
|
|
'name' => __('Status label'),
|
|
'state' => __('State name (e.g. open or closed)'),
|
|
);
|
|
return $base;
|
|
}
|
|
|
|
// Searchable interface
|
|
static function getSearchableFields() {
|
|
return array(
|
|
'state' => new TicketStateChoiceField(array(
|
|
'label' => __('State'),
|
|
)),
|
|
'id' => new TicketStatusChoiceField(array(
|
|
'label' => __('Status Name'),
|
|
)),
|
|
);
|
|
}
|
|
|
|
static function supportsCustomData() {
|
|
return false;
|
|
}
|
|
|
|
function getList() {
|
|
if (!isset($this->_list))
|
|
$this->_list = DynamicList::lookup(array('type' => 'ticket-status'));
|
|
return $this->_list;
|
|
}
|
|
|
|
function getListId() {
|
|
if (($list = $this->getList()))
|
|
return $list->getId();
|
|
}
|
|
|
|
function getConfigurationForm($source=null) {
|
|
if (!$this->_form) {
|
|
$config = $this->getConfiguration();
|
|
// Forcefully retain state for internal statuses
|
|
if ($source && $this->isInternal())
|
|
$source['state'] = $this->getState();
|
|
$this->_form = $this->getList()->getConfigurationForm($source);
|
|
if (!$source && $config) {
|
|
$fields = $this->_form->getFields();
|
|
foreach ($fields as $f) {
|
|
$val = $config[$f->get('id')] ?: $config[$f->get('name')];
|
|
if (isset($val))
|
|
$f->value = $f->to_php($val);
|
|
elseif ($f->get('default'))
|
|
$f->value = $f->get('default');
|
|
}
|
|
}
|
|
|
|
if ($this->isInternal()
|
|
&& ($f=$this->_form->getField('state'))) {
|
|
$f->ht['required'] = $f->ht['editable'] = false;
|
|
$f->options['render_mode'] = 'view';
|
|
}
|
|
|
|
}
|
|
|
|
return $this->_form;
|
|
}
|
|
|
|
function getFields() {
|
|
return $this->getConfigurationForm()->getFields();
|
|
}
|
|
|
|
function getConfiguration() {
|
|
|
|
if (!$this->_settings) {
|
|
$this->_settings = $this->getProperties();
|
|
if (!$this->_settings)
|
|
$this->_settings = array();
|
|
|
|
if ($form = $this->getList()->getForm()) {
|
|
foreach ($form->getFields() as $f) {
|
|
$name = mb_strtolower($f->get('name'));
|
|
$id = $f->get('id');
|
|
switch($name) {
|
|
case 'flags':
|
|
foreach (TicketFlagField::$_flags as $k => $v)
|
|
if ($this->hasFlag('flags', $v['flag']))
|
|
$this->_settings[$id][$k] = $v['name'];
|
|
break;
|
|
case 'state':
|
|
$this->_settings[$id][$this->get('state')] = $this->get('state');
|
|
break;
|
|
default:
|
|
if (!$this->_settings[$id] && $this->_settings[$name])
|
|
$this->_settings[$id] = $this->_settings[$name];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $this->_settings;
|
|
}
|
|
|
|
function setConfiguration($vars, &$errors=array()) {
|
|
$properties = array();
|
|
foreach ($this->getConfigurationForm($vars)->getFields() as $f) {
|
|
// Only bother with editable fields
|
|
if (!$f->isEditable()) continue;
|
|
|
|
$val = $f->getClean();
|
|
$errors = array_merge($errors, $f->errors());
|
|
if ($f->errors()) continue;
|
|
$name = mb_strtolower($f->get('name'));
|
|
switch ($name) {
|
|
case 'flags':
|
|
if ($val && is_array($val)) {
|
|
$flags = 0;
|
|
foreach ($val as $k => $v) {
|
|
if (isset(TicketFlagField::$_flags[$k]))
|
|
$flags += TicketFlagField::$_flags[$k]['flag'];
|
|
elseif (!$f->errors())
|
|
$f->addError(__('Unknown or invalid flag'), $name);
|
|
}
|
|
$this->set('flags', $flags);
|
|
} elseif ($val && !$f->errors()) {
|
|
$f->addError(__('Unknown or invalid flag format'), $name);
|
|
}
|
|
break;
|
|
case 'state':
|
|
// Internal statuses cannot change state
|
|
if ($this->isInternal())
|
|
break;
|
|
|
|
if ($val)
|
|
$this->set('state', $val);
|
|
else
|
|
$f->addError(__('Unknown or invalid state'), $name);
|
|
break;
|
|
default: //Custom properties the user might add.
|
|
$properties[$f->get('id')] = $f->to_php($val);
|
|
}
|
|
// Add field specific validation errors (warnings)
|
|
$errors = array_merge($errors, $f->errors());
|
|
}
|
|
|
|
if (count($errors) === 0) {
|
|
if ($properties && is_array($properties))
|
|
$properties = JsonDataEncoder::encode($properties);
|
|
|
|
$this->set('properties', $properties);
|
|
$this->save(true);
|
|
}
|
|
|
|
return count($errors) === 0;
|
|
}
|
|
|
|
function display() {
|
|
|
|
return $this->getLocalName();
|
|
|
|
return sprintf('<a class="preview" href="#"
|
|
data-preview="#list/%d/items/%d/preview">%s</a>',
|
|
$this->getListId(),
|
|
$this->getId(),
|
|
$this->getLocalName()
|
|
);
|
|
}
|
|
|
|
function update($vars, &$errors) {
|
|
$fields = array('name', 'sort');
|
|
foreach($fields as $k) {
|
|
if (isset($vars[$k]))
|
|
$this->set($k, $vars[$k]);
|
|
}
|
|
return $this->save();
|
|
}
|
|
|
|
function delete() {
|
|
|
|
// Statuses with tickets are not deletable
|
|
if (!$this->isDeletable() || !parent::delete())
|
|
return false;
|
|
|
|
Signal::send('object.deleted', $this);
|
|
|
|
return true;
|
|
}
|
|
|
|
function __toString() {
|
|
return $this->getName();
|
|
}
|
|
|
|
static function create($ht=false) {
|
|
if (!is_array($ht))
|
|
return null;
|
|
|
|
if (!isset($ht['mode']))
|
|
$ht['mode'] = 1;
|
|
|
|
$ht['created'] = new SqlFunction('NOW');
|
|
|
|
return new static($ht);
|
|
}
|
|
|
|
static function lookup($var, $list=null) {
|
|
|
|
if (!($item = parent::lookup($var)))
|
|
return null;
|
|
|
|
$item->_list = $list;
|
|
|
|
return $item;
|
|
}
|
|
|
|
|
|
static function __create($ht, &$error=false) {
|
|
global $ost;
|
|
|
|
$ht['properties'] = JsonDataEncoder::encode($ht['properties']);
|
|
if (($status = TicketStatus::create($ht)))
|
|
$status->save(true);
|
|
|
|
return $status;
|
|
}
|
|
|
|
static function status_options() {
|
|
include(STAFFINC_DIR . 'templates/status-options.tmpl.php');
|
|
}
|
|
}
|
|
?>
|