Newer
Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?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');
/**
* 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 getForm(); // Config form
function hasProperties();
function getSortModes();
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 getConfiguration();
function getConfigurationForm($source=null);
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);
}
/**
* 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'),
);
// 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 $_config;
function __construct() {
call_user_func_array(array('parent', '__construct'), func_get_args());
$this->_config = new Config('list.'.$this->getId());
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;
}
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
case 'Alpha': return 'value';
case '-Alpha': return '-value';
case 'SortCol': return 'sort';
}
}
function getName() {
return $this->get('name');
}
function getPluralName() {
if ($name = $this->get('name_plural'))
return $name;
else
return $this->get('name') . '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 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) {
$criteria = array('list_id' => $this->getId());
if (is_int($val))
$criteria['id'] = $val;
else
$criteria['value'] = $val;
return DynamicListItem::lookup($criteria);
}
function addItem($vars, &$errors) {
$item = DynamicListItem::create(array(
'list_id' => $this->getId(),
'sort' => $vars['sort'],
'value' => $vars['value'],
'extra' => $vars['abbrev']
));
$item->save();
$this->_items = false;
return $item;
}
function getConfigurationForm($autocreate=false) {
$this->_form = DynamicForm::lookup(array('type'=>'L'.$this->getId()));
if (!$this->_form
&& $autocreate
&& $this->createConfigurationForm())
return $this->getConfigurationForm(false);
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
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->_config->get('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();
if ($fields == 0)
return parent::delete();
else
// Refuse to delete lists that are in use by fields
return false;
}
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()) {
$inst = parent::create($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['configuration'])) {
$inst->save();
$c = new Config('list.'.$inst->getId());
$c->set('configuration', JsonDataEncoder::encode($ht['configuration']));
}
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(),
SelectionField, $list->get('id'));
}
return $selections;
}
}
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 getId() {
return $this->get('id');
}
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(&$errors=array()) {
$config = array();
foreach ($this->getConfigurationForm($_POST)->getFields() as $field) {
$config[$field->get('id')] = $field->to_php($field->getClean());
$errors = array_merge($errors, $field->errors());
}
if (count($errors) === 0)
$this->set('properties', JsonDataEncoder::encode($config));
return count($errors) === 0;
}
function getConfigurationForm($source=null) {
$config = $this->getConfiguration();
array('type'=>'L'.$this->get('list_id')))->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 getVar($name) {
$config = $this->getConfiguration();
$name = mb_strtolower($name);
foreach ($this->getConfigurationForm()->getFields() as $field) {
if (mb_strtolower($field->get('name')) == $name)
return $config[$field->get('id')];
}
}
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 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 = parent::create($ht);
$inst->save(true);
// 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 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);
'flags' => 0,
'sort' => $vars['sort'],
'name' => $vars['value'],
));
$item->save();
$this->_items = false;
return $item;
}
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];
}
class TicketStatus extends VerySimpleModel implements CustomListItem {
static $meta = array(
'table' => TICKET_STATUS_TABLE,
'ordering' => array('name'),
'pk' => array('id'),
'joins' => array(
'tickets' => array(
'reverse' => 'TicketModel.status',
)
)
var $_list;
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);
}
protected function hasProperties() {
return ($this->get('properties'));
}
if (!$this->_form && $this->_list) {
$this->_form = DynamicForm::lookup(
array('type'=>'L'.$this->_list->getId()));
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
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'),
),
'visibility' => new VisibilityConstraint(
new Q(array('state__eq'=>'closed')),
VisibilityConstraint::HIDDEN
),
)),
'reopenstatus' => new ChoiceField(array(
'label' => __('Reopen Status'),
'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;
$config = $this->getConfiguration();
$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 (!$source && $config) {
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 $form;
}
function isEnabled() {
return $this->hasFlag('mode', self::ENABLED);
}
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
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->clearFlag('mode', self::ENABLED));
}
function isDefault() {
global $cfg;
return ($cfg
&& $cfg->getDefaultTicketStatusId() == $this->getId());
}
return ($this->getState());
function isDisableable() {
return !($this->isInternal() || $this->isDefault());
}
return !($this->isInternal()
|| $this->isDefault()
|| $this->getNumTickets());
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');
}