Newer
Older
<?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 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 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 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;
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;
}
case 'Alpha': return 'value';
case '-Alpha': return '-value';
case 'SortCol': return 'sort';
}
}
function getName() {
if ($name = $this->getLocal('name_plural'))
}
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()));
elseif ($extra)
$items->filter(array('extra' => $val));
$items->filter(array('value' => $val));
}
function addItem($vars, &$errors) {
if (($item=$this->getItem($vars['value'])))
return $item;
'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) {
$this->_form = DynamicForm::lookup(array('type'=>'L'.$this->getId()));
if (!$this->_form
&& $autocreate
&& $this->createConfigurationForm())
return $this->getConfigurationForm(false);
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() {
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;
}
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
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);
}
$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]))
$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())
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) {
$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))
|| !$list->save(true)
|| !$list->createConfigurationForm())
return false;
return $list;
}
static function create($ht=false, &$errors=array()) {
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();
}
}
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(),
function importCsv($stream, $defaults=array()) {
require_once INCLUDE_DIR . 'class.import.php';
$form = $this->getConfigurationForm();
'value' => new TextboxField(array(
'label' => __('Value'),
'name' => 'value',
'configuration' => array(
'length' => 0,
),
)),
'abbrev' => new TextboxField(array(
'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));
catch (Exception $ex) {
db_rollback();
return $ex->getMessage();
}
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);
}
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');
}
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()) {
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) {
$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');
}
}
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');
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 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');
foreach (array(
'sort' => 'sort',
'value' => 'value',
'abbrev' => 'extra') as $k => $v) {
if (isset($vars[$k]))
$this->set($v, $vars[$k]);
}
}
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;
}
class TicketStatusList extends CustomListHandler {
// Fields of interest we need to store
static $config_fields = array('sort_mode', 'notes');
function getListOrderBy() {
switch ($this->getSortMode()) {
case 'Alpha': return 'name';
case '-Alpha': return '-name';
case 'SortCol': return 'sort';
}
}
function getAllItems() {
if (!$this->_items)
$this->_items = TicketStatus::objects()->order_by($this->getListOrderBy());
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(
'flags' => 0,
'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);
static function __load() {
require_once(INCLUDE_DIR.'class.i18n.php');
$i18n = new Internationalization();
if ($f['type'] == 'ticket-status') {
$list = DynamicList::create($f);
$list->save();
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'),
'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'),
),