Newer
Older
<?php
/*********************************************************************
class.queue.php
Custom (ticket) queues for osTicket
Jared Hancock <jared@osticket.com>
Peter Rotich <peter@osticket.com>
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:
**********************************************************************/
class CustomQueue extends VerySimpleModel {
'table' => QUEUE_TABLE,
'pk' => array('id'),
'ordering' => array('sort'),
'select_related' => array('parent', 'default_sort'),
'children' => array(
'reverse' => 'CustomQueue.parent',
'constrain' => ['children__id__gt' => 0],
),
'constrain' => array('staff_id' =>'QueueColumnGlue.staff_id'),
'sorts' => array(
'reverse' => 'QueueSortGlue.queue',
'broker' => 'QueueSortListBroker',
),
'default_sort' => array(
'constraint' => array('sort_id' => 'QueueSort.id'),
'null' => true,
),
'exports' => array(
'reverse' => 'QueueExport.queue',
),
'parent' => array(
'constraint' => array(
'parent_id' => 'CustomQueue.id',
),
'null' => true,
),
'staff' => array(
'constraint' => array(
'staff_id' => 'Staff.staff_id',
)
const FLAG_PUBLIC = 0x0001; // Shows up in e'eryone's saved searches
const FLAG_QUEUE = 0x0002; // Shows up in queue navigation
const FLAG_DISABLED = 0x0004; // NOT enabled
const FLAG_INHERIT_CRITERIA = 0x0008; // Include criteria from parent
const FLAG_INHERIT_COLUMNS = 0x0010; // Inherit column layout from parent
const FLAG_INHERIT_SORTING = 0x0020; // Inherit advanced sorting from parent
const FLAG_INHERIT_DEF_SORT = 0x0040; // Inherit default selected sort
const FLAG_INHERIT_EXPORT = 0x0080; // Inherit export fields from parent
const FLAG_INHERIT_EVERYTHING = 0x158; // Maskf or all INHERIT flags
return parent::objects()->filter(array(
'flags__hasbit' => static::FLAG_QUEUE
));
}
function __onload() {
// Ensure valid state
if ($this->hasFlag(self::FLAG_INHERIT_COLUMNS) && !$this->parent_id)
$this->clearFlag(self::FLAG_INHERIT_COLUMNS);
if ($this->hasFlag(self::FLAG_INHERIT_EXPORT) && !$this->parent_id)
$this->clearFlag(self::FLAG_INHERIT_EXPORT);
function getId() {
return $this->id;
}
function getName() {
return $this->title;
}
function getHref() {
// TODO: Get base page from getRoot();
$root = $this->getRoot();
return 'tickets.php?queue='.$this->getId();
}
function getRoot() {
switch ($this->root) {
case 'T':
default:
return 'Ticket';
}
}
function getPath() {
return $this->path ?: $this->buildPath();
}
function criteriaRequired() {
return true;
}
function getCriteria($include_parent=false) {
if (!isset($this->criteria)) {
$this->criteria = is_string($this->config)
? JsonDataParser::decode($this->config)
: $this->config;
// Auto-upgrade v1.10 saved-search criteria to new format
// But support new style with `conditions` support
if ($old && is_array($this->criteria)
&& !isset($this->criteria['conditions'])
) {
// TODO: Upgrade old ORM path names
// Parse criteria out of JSON if any.
$this->criteria = self::isolateCriteria($this->criteria,
$this->getRoot());
}
}
$criteria = $this->criteria ?: array();
// Support new style with `conditions` support
if (isset($criteria['criteria']))
$criteria = $criteria['criteria'];
if ($include_parent && $this->parent_id && $this->parent) {
$criteria = array_merge($this->parent->getCriteria(true),
$criteria);
}
return $criteria;
}
function describeCriteria($criteria=false){
$all = $this->getSupportedMatches($this->getRoot());
$items = array();
$criteria = $criteria ?: $this->getCriteria(true);
foreach ($criteria as $C) {
list($path, $method, $value) = $C;
if ($path === ':keywords') {
$items[] = Format::htmlchars("\"{$value}\"");
continue;
}
if (!isset($all[$path]))
continue;
list($label, $field) = $all[$path];
$items[] = $field->describeSearch($method, $value, $label);
}
return implode("\nAND ", $items);
}
/**
* Fetch an AdvancedSearchForm instance for use in displaying or
* configuring this search in the user interface.
*
* Parameters:
* $search - <array> Request parameters ($_POST) used to update the
* search beyond the current configuration of the search criteria
* $searchables - search fields - default to current if not provided
function getForm($source=null, $searchable=null) {
$fields = array();
$validator = false;
if (!isset($searchable)) {
$searchable = $this->getCurrentSearchFields($source);
$validator = true;
$fields = array(
':keywords' => new TextboxField(array(
'id' => 3001,
'configuration' => array(
'size' => 40,
'length' => 400,
'autofocus' => true,
'classes' => 'full-width headline',
'placeholder' => __('Keywords — Optional'),
),
)),
);
foreach ($searchable as $path=>$field)
$fields = array_merge($fields, static::getSearchField($field, $path));
$form = new AdvancedSearchForm($fields, $source);
// Field selection validator
if ($this->criteriaRequired()) {
$form->addValidator(function($form) {
if (!$form->getNumFieldsSelected())
$form->addError(__('No fields selected for searching'));
});
}
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
// Load state from current configuraiton
if (!$source) {
foreach ($this->getCriteria() as $I) {
list($path, $method, $value) = $I;
if ($path == ':keywords' && $method === null) {
if ($F = $form->getField($path))
$F->value = $value;
continue;
}
if (!($F = $form->getField("{$path}+search")))
continue;
$F->value = true;
if (!($F = $form->getField("{$path}+method")))
continue;
$F->value = $method;
if ($value && ($F = $form->getField("{$path}+{$method}")))
$F->value = $value;
}
}
return $form;
}
/**
* Fetch a bucket of fields for a custom search. The fields should be
* added to a form before display. One searchable field may encompass 10
* or more actual fields because fields are expanded to support multiple
* search methods along with the fields for each search method. This
* method returns all the FormField instances for all the searchable
* model fields currently in use.
*
* Parameters:
* $source - <array> data from a request. $source['fields'] is expected
* to contain a list extra fields by ORM path, of newly added
* fields not yet saved in this object's getCriteria().
*/
function getCurrentSearchFields($source=array(), $criteria=array()) {
static $basic = array(
'Ticket' => array(
'status__state',
'dept_id',
'assignee',
'topic_id',
'created',
'est_duedate',
)
);
$all = $this->getSupportedMatches();
$core = array();
// Include basic fields for new searches
if (!isset($this->id))
foreach ($basic[$this->getRoot()] as $path)
if (isset($all[$path]))
$core[$path] = $all[$path];
// Add others from current configuration
foreach ($criteria ?: $this->getCriteria() as $C) {
list($path) = $C;
if (isset($all[$path]))
$core[$path] = $all[$path];
}
if (isset($source['fields']))
foreach ($source['fields'] as $path)
if (isset($all[$path]))
$core[$path] = $all[$path];
return $core;
}
/**
* Fetch all supported ORM fields filterable by this search object.
*/
function getSupportedFilters() {
return static::getFilterableFields($this->getRoot());
}
/**
* Get get supplemental matches for public queues.
*
*/
function getSupplementalMatches() {
return array();
}
function getSupplementalCriteria() {
return array();
}
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
/**
* Fetch all supported ORM fields searchable by this search object. The
* returned list represents searchable fields, keyed by the ORM path.
* Use ::getCurrentSearchFields() or ::getSearchField() to retrieve for
* use in the user interface.
*/
function getSupportedMatches() {
return static::getSearchableFields($this->getRoot());
}
/**
* Trace ORM fields from a base object and retrieve a complete list of
* fields which can be used in an ORM query based on the base object.
* The base object must implement Searchable interface and extend from
* VerySimpleModel. Then all joins from the object are also inspected,
* and any which implement the Searchable interface are traversed and
* automatically added to the list. The resulting list is cached based
* on the $base class, so multiple calls for the same $base return
* quickly.
*
* Parameters:
* $base - Class, name of a class implementing Searchable
* $recurse - int, number of levels to recurse, default is 2
* $cache - bool, cache results for future class for the same base
* $customData - bool, include all custom data fields for all general
* forms
*/
static function getSearchableFields($base, $recurse=2,
$customData=true, $exclude=array()
) {
static $cache = array(), $otherFields;
// Early exit if already cached
$fields = &$cache[$base];
if ($fields)
return $fields;
if (!in_array('Searchable', class_implements($base)))
return array();
$fields = $fields ?: array();
foreach ($base::getSearchableFields() as $path=>$F) {
if (is_array($F)) {
list($label, $field) = $F;
}
else {
$label = $F->getLocal('label');
$field = $F;
}
$fields[$path] = array($label, $field);
}
if ($customData && $base::supportsCustomData()) {
if (!isset($otherFields)) {
$otherFields = array();
$dfs = DynamicFormField::objects()
->filter(array('form__type' => 'G'))
->select_related('form');
foreach ($dfs as $field) {
$otherFields[$field->getId()] = array($field->form,
$field->getImpl());
}
}
foreach ($otherFields as $id=>$F) {
list($form, $field) = $F;
$label = sprintf("%s / %s",
$form->getTitle(), $field->getLocal('label'));
$fields["entries__answers!{$id}__value"] = array(
$label, $field);
}
}
if ($recurse) {
$exclude[$base] = 1;
foreach ($base::getMeta('joins') as $path=>$j) {
$fc = $j['fkey'][0];
if (isset($exclude[$fc]) || $j['list'])
continue;
foreach (static::getSearchableFields($fc, $recurse-1,
true, $exclude)
as $path2=>$F) {
list($label, $field) = $F;
$fields["{$path}__{$path2}"] = array(
sprintf("%s / %s", $fc, $label),
$field);
}
}
}
// Sort the field listing by the (localized) label name
if (function_exists('collator_create')) {
$coll = Collator::create(Internationalization::getCurrentLanguage());
$keys = array_map(function($a) use ($coll) {
return $coll->getSortKey($a[0]); #nolint
}, $fields);
}
else {
// Fall back to 8-bit string sorting
$keys = array_map(function($a) { return $a[0]; }, $fields);
}
array_multisort($keys, $fields);
return $fields;
}
/**
* Fetch all searchable fileds, for the base object which support quick filters.
*/
function getFilterableFields($object) {
$filters = array();
foreach (static::getSearchableFields($object) as $p => $f) {
list($label, $field) = $f;
if ($field && $field->supportsQuickFilter())
$filters[$p] = $f;
}
return $filters;
}
/**
* Fetch the FormField instances used when for configuring a searchable
* field in the user interface. This is the glue between a field
* representing a searchable model field and the configuration of that
* search in the user interface.
*
* Parameters:
* $F - <array<string, FormField>> the label and the FormField instance
* representing the configurable search
* $name - <string> ORM path for the search
*/
static function getSearchField($F, $name) {
list($label, $field) = $F;
$pieces = array();
$pieces["{$name}+search"] = new BooleanField(array(
'id' => sprintf('%u', crc32($name)) >> 1,
'configuration' => array(
'desc' => $label ?: $field->getLocal('label'),
'classes' => 'inline',
),
));
$methods = $field->getSearchMethods();
//remove future options for datetime fields that can't be in the future
if (in_array($field->getLabel(), DateTimeField::getPastPresentLabels()))
unset($methods['ndays'], $methods['future'], $methods['distfut']);
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
$pieces["{$name}+method"] = new ChoiceField(array(
'choices' => $methods,
'default' => key($methods),
'visibility' => new VisibilityConstraint(new Q(array(
"{$name}+search__eq" => true,
)), VisibilityConstraint::HIDDEN),
));
$offs = 0;
foreach ($field->getSearchMethodWidgets() as $m=>$w) {
if (!$w)
continue;
list($class, $args) = $w;
$args['required'] = true;
$args['__searchval__'] = true;
$args['visibility'] = new VisibilityConstraint(new Q(array(
"{$name}+method__eq" => $m,
)), VisibilityConstraint::HIDDEN);
$pieces["{$name}+{$m}"] = new $class($args);
}
return $pieces;
}
function getField($path) {
$searchable = $this->getSupportedMatches();
return $searchable[$path];
}
// Remove this and adjust advanced-search-criteria template to use the
// getCriteria() list and getField()
function getSearchFields($form=false) {
$form = $form ?: $this->getForm();
$searchable = $this->getCurrentSearchFields();
$info = array();
foreach ($form->getFields() as $f) {
if (substr($f->get('name'), -7) == '+search') {
$name = substr($f->get('name'), 0, -7);
$value = null;
// Determine the search method and fetch the original field
if (($M = $form->getField("{$name}+method"))
&& ($method = $M->getClean())
&& (list(,$field) = $searchable[$name])
) {
// Request the field to generate a search Q for the
// search method and given value
if ($value = $form->getField("{$name}+{$method}"))
$value = $value->getClean();
}
$info[$name] = array(
'field' => $field,
'method' => $method,
'value' => $value,
'active' => $f->getClean(),
);
}
}
return $info;
}
/**
* Take the criteria from the SavedSearch fields setup and isolate the
* field name being search, the method used for searhing, and the method-
* specific data entered in the UI.
*/
static function isolateCriteria($criteria, $base='Ticket') {
$items = array();
$searchable = static::getSearchableFields($base);
foreach ($criteria as $k=>$v) {
if (substr($k, -7) === '+method') {
list($name,) = explode('+', $k, 2);
if (!isset($searchable[$name]))
continue;
// Require checkbox to be checked too
if (!$criteria["{$name}+search"])
continue;
// Lookup the field to search this condition
list($label, $field) = $searchable[$name];
// Get the search method
$method = is_array($v) ? key($v) : $v;
// Not all search methods require a value
$value = $criteria["{$name}+{$method}"];
$items[] = array($name, $method, $value);
}
}
if (isset($criteria[':keywords'])
&& ($kw = $criteria[':keywords'])
) {
$items[] = array(':keywords', null, $kw);
}
return $items;
}
function getConditions() {
if (!isset($this->_conditions)) {
$this->getCriteria();
$conds = array();
if (is_array($this->criteria)
&& isset($this->criteria['conditions'])
) {
$conds = $this->criteria['conditions'];
}
foreach ($conds as $C)
if ($T = QueueColumnCondition::fromJson($C))
$this->_conditions[] = $T;
}
return $this->_conditions;
}
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
function getExportableFields() {
$cdata = $fields = array();
foreach (TicketForm::getInstance()->getFields() as $f) {
// Ignore core fields
if (in_array($f->get('name'), array('priority')))
continue;
// Ignore non-data fields
elseif (!$f->hasData() || $f->isPresentationOnly())
continue;
$name = $f->get('name') ?: 'field_'.$f->get('id');
$key = 'cdata.'.$name;
$cdata[$key] = $f->getLocal('label');
}
// Standard export fields if none is provided.
$fields = array(
'number' => __('Ticket Number'),
'created' => __('Date Created'),
'cdata.subject' => __('Subject'),
'user.name' => __('From'),
'user.default_email.address' => __('From Email'),
'cdata.:priority.priority_desc' => __('Priority'),
'dept::getLocalName' => __('Department'),
'topic::getName' => __('Help Topic'),
'source' => __('Source'),
'status::getName' =>__('Current Status'),
'lastupdate' => __('Last Updated'),
'est_duedate' => __('SLA Due Date'),
'duedate' => __('Due Date'),
'isoverdue' => __('Overdue'),
'isanswered' => __('Answered'),
'staff::getName' => __('Agent Assigned'),
'team::getName' => __('Team Assigned'),
'thread_count' => __('Thread Count'),
'reopen_count' => __('Reopen Count'),
'attachment_count' => __('Attachment Count'),
) + $cdata;
return $fields;
}
function getExportFields($inherit=true) {
&& $this->hasFlag(self::FLAG_INHERIT_EXPORT)
&& $this->parent
) {
$fields = $this->parent->getExportFields();
}
elseif (count($this->exports)) {
foreach ($this->exports as $f)
$fields[$f->path] = $f->getHeading();
if (!count($fields))
$fields = $this->getExportableFields();
function getStandardColumns() {
return $this->getColumns();
}
function getColumns($use_template=false) {
if ($this->columns_id
&& ($q = CustomQueue::lookup($this->columns_id))
) {
// Use columns from cited queue
return $q->getColumns();
}
elseif ($this->parent_id
&& $this->hasFlag(self::FLAG_INHERIT_COLUMNS)
&& $this->parent
) {
$columns = $this->parent->getColumns();
foreach ($columns as $c)
$c->setQueue($this);
return $columns;
elseif (count($this->columns)) {
return $this->columns;
// Use the columns of the "Open" queue as a default template
if ($use_template && ($template = CustomQueue::lookup(1)))
return $template->getColumns();
// Last resort — use standard columns
foreach (array(
QueueColumn::placeholder(array(
"heading" => "Number",
"primary" => 'number',
"width" => 85,
"filter" => "link:ticketP",
"annotations" => '[{"c":"TicketSourceDecoration","p":"b"}]',
"conditions" => '[{"crit":["isanswered","set",null],"prop":{"font-weight":"bold"}}]',
)),
QueueColumn::placeholder(array(
"heading" => "Created",
"primary" => 'created',
"filter" => 'date:full',
"truncate" =>'wrap',
"width" => 120,
QueueColumn::placeholder(array(
"heading" => "Subject",
"primary" => 'cdata__subject',
"width" => 250,
"filter" => "link:ticket",
"annotations" => '[{"c":"TicketThreadCount","p":">"},{"c":"ThreadAttachmentCount","p":"a"},{"c":"OverdueFlagDecoration","p":"<"}]',
"conditions" => '[{"crit":["isanswered","set",null],"prop":{"font-weight":"bold"}}]',
"truncate" => 'ellipsis',
)),
QueueColumn::placeholder(array(
"heading" => "From",
"primary" => 'user__name',
"width" => 150,
"bits" => QueueColumn::FLAG_SORTABLE,
QueueColumn::placeholder(array(
"heading" => "Priority",
"primary" => 'cdata__priority',
"width" => 120,
"bits" => QueueColumn::FLAG_SORTABLE,
QueueColumn::placeholder(array(
"heading" => "Assignee",
"primary" => 'assignee',
"width" => 100,
"bits" => QueueColumn::FLAG_SORTABLE,
) as $col)
$this->addColumn($col);
return $this->getColumns();
}
function addColumn(QueueColumn $col) {
$this->columns->add($col);
$col->queue = $this;
}
function getSortOptions() {
if ($this->inheritSorting() && $this->parent) {
return $this->parent->getSortOptions();
}
return $this->sorts;
}
function getDefaultSortId() {
if ($this->isDefaultSortInherited() && $this->parent
&& ($sort_id = $this->parent->getDefaultSortId())
) {
return $sort_id;
}
return $this->sort_id;
}
function getDefaultSort() {
if ($this->isDefaultSortInherited() && $this->parent
&& ($sort = $this->parent->getDefaultSort())
) {
return $sort;
}
return $this->default_sort;
}
return $this->hasFlag(self::FLAG_DISABLED)
? __('Disabled') : __('Active');
function getChildren() {
return $this->children;
}
function getPublicChildren() {
return $this->children->findAll(array(
));
}
function getMyChildren() {
global $thisstaff;
if (!$thisstaff instanceof Staff)
return array();
return $this->children->findAll(array(
'staff_id' => $thisstaff->getId(),
Q::not(array(
'flags__hasbit' => self::FLAG_PUBLIC
))
));
}
if (!($query=$this->getBasicQuery()))
return false;
if (!($fields=$this->getExportFields()))
$filename = sprintf('%s Tickets-%s.csv',
$this->getName(),
strftime('%Y%m%d'));
// See if we have cached export preference
if (isset($_SESSION['Export:Q'.$this->getId()])) {
$opts = $_SESSION['Export:Q'.$this->getId()];
if (isset($opts['fields']))
$fields = array_intersect_key($fields,
array_flip($opts['fields']));
if (isset($opts['filename'])
&& ($parts = pathinfo($opts['filename']))) {
$filename = $opts['filename'];
if (strcasecmp($parts['extension'], 'csv') !=0)
$filename ="$filename.csv";
}
if (isset($opts['delimiter']))
$options['delimiter'] = $opts['delimiter'];
}
return Export::saveTickets($query, $fields, $filename, 'csv',
$options);
/**
* Add critiera to a query based on the constraints configured for this
* queue. The criteria of the parent queue is also automatically added
* if the queue is configured to inherit the criteria.
*/
function getBasicQuery() {
if ($this->parent && $this->inheritCriteria()) {
$query = $this->parent->getBasicQuery();
}
else {
$root = $this->getRoot();
$query = $root::objects();
}
return $this->mangleQuerySet($query);
}
/**
* Retrieve a QuerySet instance based on the type of object (root) of
* this Q, which is automatically configured with the data and criteria
* of the queue and its columns.
*
* Returns:
* <QuerySet> instance
*/
function getQuery($form=false, $quick_filter=null) {
$query = $this->getBasicQuery($form);
// Apply quick filter
if (isset($quick_filter)
&& ($qf = $this->getQuickFilterField($quick_filter))
) {
$filter = @self::getOrmPath($this->getQuickFilter(), $query);
$query = $qf->applyQuickFilter($query, $quick_filter,
}
// Apply column, annotations and conditions additions
foreach ($this->getColumns() as $C) {
$query = $C->mangleQuery($query, $this->getRoot());
function getQuickFilter() {
if ($this->filter == '::' && $this->parent) {
return $this->parent->getQuickFilter();
}
return $this->filter;
}
function getQuickFilterField($value=null) {
if ($this->filter == '::') {
if ($this->parent) {
return $this->parent->getQuickFilterField($value);
}
}
elseif ($this->filter
&& ($fields = self::getSearchableFields($this->getRoot()))
&& (list(,$f) = @$fields[$this->filter])
&& $f->supportsQuickFilter()
) {
$f->value = $value;
return $f;
}
}
/**
* Get a description of a field in a search. Expects an entry from the
* array retrieved in ::getSearchFields()
*/
function describeField($info, $name=false) {
$name = $name ?: $info['field']->get('label');
return $info['field']->describeSearch($info['method'], $info['value'], $name);
}
function mangleQuerySet(QuerySet $qs, $form=false) {
$qs = clone $qs;
$searchable = $this->getSupportedMatches();
// Figure out fields to search on
foreach ($this->getCriteria() as $I) {
list($name, $method, $value) = $I;
// Consider keyword searching
if ($name === ':keywords') {
global $ost;
$qs = $ost->searcher->find($value, $qs, false);
}
else {
// XXX: Move getOrmPath to be more of a utility
// Ensure the special join is created to support custom data joins
$name = @static::getOrmPath($name, $qs);
if (preg_match('/__answers!\d+__/', $name)) {
$qs->annotate(array($name => SqlAggregate::MAX($name)));
}
// Fetch a criteria Q for the query
if (list(,$field) = $searchable[$name])
if ($q = $field->getSearchQ($method, $value, $name))
$qs = $qs->filter($q);
}
}
return $qs;
}
function applyDefaultSort($qs) {
// Apply default sort
if ($sorter = $this->getDefaultSort()) {
$qs = $sorter->applySort($qs, false, $this->getRoot());
}
return $qs;
}
function checkAccess(Staff $agent) {
return $this->isPublic() || $this->checkOwnership($agent);
}
function checkOwnership(Staff $agent) {
return ($agent->getId() == $this->staff_id &&
!$this->isAQueue());
}
function isOwner(Staff $agent) {
return $agent && $this->isPrivate() && $this->checkOwnership($agent);
function ignoreVisibilityConstraints(Staff $agent) {
// For saved searches (not queues), some staff can have a permission to
return (!$this->isASubQueue()
&& $this->isOwner($agent)
&& $agent->canSearchEverything());
}
function inheritCriteria() {
return $this->flags & self::FLAG_INHERIT_CRITERIA;
}
function inheritColumns() {
return $this->hasFlag(self::FLAG_INHERIT_COLUMNS);
}
function useStandardColumns() {
return !count($this->columns);
}
function inheritExport() {
return ($this->hasFlag(self::FLAG_INHERIT_EXPORT) ||
!count($this->exports));
}
function inheritSorting() {
return $this->hasFlag(self::FLAG_INHERIT_SORTING);
}
function isDefaultSortInherited() {
return $this->hasFlag(self::FLAG_INHERIT_DEF_SORT);
}
function buildPath() {
if (!$this->id)
return;
$path = $this->parent ? $this->parent->buildPath() : '';
return rtrim($path, "/") . "/{$this->id}/";
}
function getFullName() {
$base = $this->getName();
if ($this->parent)
$base = sprintf("%s / %s", $this->parent->getFullName(), $base);
return $base;
}
function isASubQueue() {
return $this->parent ? $this->parent->isASubQueue() :
$this->isAQueue();
}
function isAQueue() {
return $this->hasFlag(self::FLAG_QUEUE);
}
function isPrivate() {
return !$this->isAQueue() && !$this->isPublic() &&
$this->staff_id;
}
function isPublic() {
return $this->hasFlag(self::FLAG_PUBLIC);
}
protected function hasFlag($flag) {
return ($this->flags & $flag) !== 0;
}
protected function clearFlag($flag) {
return $this->flags &= ~$flag;
}
protected function setFlag($flag, $value=true) {
return $value
? $this->flags |= $flag
: $this->clearFlag($flag);
}
function disable() {
$this->setFlag(self::FLAG_DISABLED);
}
function enable() {
$this->clearFlag(self::FLAG_DISABLED);
}
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
function updateExports($fields, $save=true) {
if (!$fields)
return false;
$order = array_keys($fields);
// Filter exportable fields
if (!($fields = array_intersect_key($this->getExportableFields(), $fields)))
return false;
$new = $fields;
foreach ($this->exports as $f) {
$key = $f->getPath();
if (!isset($fields[$key])) {
$this->exports->remove($f);
continue;
}
$info = $fields[$key];
if (is_array($info))
$heading = $info['heading'];
else
$heading = $info;
$f->set('heading', $heading);
$f->set('sort', array_search($key, $order)+1);
unset($new[$key]);
}
foreach ($new as $k => $field) {
if (is_array($field))
$heading = $field['heading'];
else
$heading = $field;
$f = QueueExport::create(array(
'path' => $k,
'heading' => $heading,
'sort' => array_search($k, $order)+1));
$this->exports->add($f);
}
$this->exports->sort(function($f) { return $f->sort; });
if (!count($this->exports) && $this->parent)
$this->hasFlag(self::FLAG_INHERIT_EXPORT);
if ($save)
$this->exports->saveAll();
return true;
}
function update($vars, &$errors=array()) {
// Set basic search information
if (!$vars['queue-name'])
$errors['queue-name'] = __('A title is required');
'title' => $vars['queue-name'],
'staff_id' => $this->staff_id)))
&& $q->getId() != $this->id
)
$errors['queue-name'] = __('Saved queue with same name exists');
$this->title = $vars['queue-name'];
$this->parent_id = @$vars['parent_id'] ?: 0;
if ($this->parent_id && !$this->parent)
$errors['parent_id'] = __('Select a valid queue');
// Try to avoid infinite recursion determining ancestry
if ($this->parent_id && isset($this->id)) {
$P = $this;
while ($P = $P->parent)
if ($P->parent_id == $this->id)
$errors['parent_id'] = __('Cannot be a descendent of itself');
}
if ($vars['sort_id']) {
if ($vars['filter'] === '::') {
if (!$this->parent)
$errors['filter'] = __('No parent selected');
}
elseif ($vars['filter'] && !array_key_exists($vars['filter'],
static::getSearchableFields($this->getRoot()))
) {
$errors['filter'] = __('Select an item from the list');
}
}
// Set basic queue information
$this->path = $this->buildPath();
$this->setFlag(self::FLAG_INHERIT_CRITERIA,
$this->parent_id > 0 && isset($vars['inherit']));
$this->setFlag(self::FLAG_INHERIT_COLUMNS,
isset($vars['inherit-columns']));
$this->parent_id > 0 && isset($vars['inherit-exports']));
$this->setFlag(self::FLAG_INHERIT_SORTING,
$this->parent_id > 0 && isset($vars['inherit-sorting']));
// Update queue columns (but without save)
if (!isset($vars['columns']) && $this->parent) {
// No columns -- imply column inheritance
$this->setFlag(self::FLAG_INHERIT_COLUMNS);
}
if ($this->getId()
&& isset($vars['columns'])
&& !$this->hasFlag(self::FLAG_INHERIT_COLUMNS)) {
if ($this->columns->update($vars['columns'], $errors, array(
'queue_id' => $this->getId(),
'staff_id' => $this->staff_id)))
$this->columns->reset();
// Update export fields for the queue
if (isset($vars['exports']) &&
!$this->hasFlag(self::FLAG_INHERIT_EXPORT)) {
$this->updateExports($vars['exports'], false);
if (!count($this->exports) && $this->parent)
$this->hasFlag(self::FLAG_INHERIT_EXPORT);
// Update advanced sorting options for the queue
if (isset($vars['sorts']) && !$this->hasFlag(self::FLAG_INHERIT_SORTING)) {
foreach ($this->sorts as $sort) {
$key = $sort->sort_id;
$idx = array_search($key, $vars['sorts']);
if (false === $idx) {
else {
$sort->set('sort', $idx);
unset($new[$idx]);
}
}
// Add new columns
foreach ($new as $id) {
if (!$sort = QueueSort::lookup($id))
continue;
$glue = new QueueSortGlue(array(
'sort_id' => $id,
'sort' => array_search($id, $order),
));
}
// Re-sort the in-memory columns array
$this->sorts->sort(function($c) { return $c->sort; });
}
if (!count($this->sorts) && $this->parent) {
// No sorting -- imply sorting inheritance
$this->setFlag(self::FLAG_INHERIT_SORTING);
}
// Configure default sorting
$this->setFlag(self::FLAG_INHERIT_DEF_SORT,
$this->parent && $vars['sort_id'] === '::');
if ($vars['sort_id']) {
if ($vars['sort_id'] === '::') {
if (!$this->parent)
$errors['sort_id'] = __('No parent selected');
}
elseif ($qs = QueueSort::lookup($vars['sort_id'])) {
$this->sort_id = $vars['sort_id'];
}
else {
$errors['sort_id'] = __('Select an item from the list');
}
}
list($this->_conditions, $conditions)
= QueueColumn::getConditionsFromPost($vars, $this->id, $this->getRoot());
// TODO: Move this to SavedSearch::update() and adjust
// AjaxSearch::_saveSearch()
$form = $form ?: $this->getForm($vars);
if (!$vars) {
$errors['criteria'] = __('No criteria specified');
}
elseif (!$form->isValid()) {
$errors['criteria'] = __('Validation errors exist on criteria');
}
else {
$this->criteria = static::isolateCriteria($form->getClean(),
$this->getRoot());
$this->config = JsonDataEncoder::encode([
'conditions' => $conditions,
]);
// Clear currently set criteria.and conditions.
$this->criteria = $this->_conditions = null;
return 0 === count($errors);
}
function psave() {
return parent::save();
}
$nopath = !isset($this->path);
$path_changed = isset($this->dirty['parent_id']);
if ($this->dirty)
$this->updated = SqlFunction::NOW();
if (!($rv = parent::save($refetch || $this->dirty)))
$this->path = $this->buildPath();
$this->save();
}
if ($path_changed) {
$this->children->reset();
$move_children = function($q) use (&$move_children) {
foreach ($q->children as $qq) {
$qq->path = $qq->buildPath();
$qq->save();
$move_children($qq);
}
};
$move_children($this);
}
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
/**
* Fetch a tree-organized listing of the queues. Each queue is listed in
* the tree exactly once, and every visible queue is represented. The
* returned structure is an array where the items are two-item arrays
* where the first item is a CustomQueue object an the second is a list
* of the children using the same pattern (two-item arrays of a CustomQueue
* and its children). Visually:
*
* [ [ $queue, [ [ $child, [] ], [ $child, [] ] ], [ $queue, ... ] ]
*
* Parameters:
* $staff - <Staff> staff object which should be used to determine
* visible queues.
* $pid - <int> parent_id of root queue. Default is zero (top-level)
*/
static function getHierarchicalQueues(Staff $staff, $pid=0) {
$all = static::objects()
->filter(Q::any(array(
'flags__hasbit' => self::FLAG_PUBLIC,
'flags__hasbit' => static::FLAG_QUEUE,
'staff_id' => $staff->getId(),
)))
->exclude(['flags__hasbit' => self::FLAG_DISABLED])
->asArray();
// Find all the queues with a given parent
$for_parent = function($pid) use ($all, &$for_parent) {
$results = [];
foreach (new \ArrayIterator($all) as $q) {
if ($q->parent_id == $pid)
$results[] = [ $q, $for_parent($q->getId()) ];
}
return $results;
};
return $for_parent($pid);
}
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
static function getOrmPath($name, $query=null) {
// Special case for custom data `__answers!id__value`. Only add the
// join and constraint on the query the first pass, when the query
// being mangled is received.
$path = array();
if ($query && preg_match('/^(.+?)__(answers!(\d+))/', $name, $path)) {
// Add a join to the model of the queryset where the custom data
// is forked from — duplicate the 'answers' join and add the
// constraint to the query based on the field_id
// $path[1] - part before the answers (user__org__entries)
// $path[2] - answers!xx join part
// $path[3] - the `xx` part of the answers!xx join component
$root = $query->model;
$meta = $root::getMeta()->getByPath($path[1]);
$joins = $meta['joins'];
if (!isset($joins[$path[2]])) {
$meta->addJoin($path[2], $joins['answers']);
}
// Ensure that the query join through answers!xx is only for the
// records which match field_id=xx
$query->constrain(array("{$path[1]}__{$path[2]}" =>
array("{$path[1]}__{$path[2]}__field_id" => (int) $path[3])
));
// Leave $name unchanged
}
return $name;
}
static function create($vars=false) {
$queue = new static($vars);
$queue->created = SqlFunction::NOW();
if (!isset($vars['flags']))
$queue->setFlag(self::FLAG_QUEUE);
static function __create($vars) {
$q = static::create($vars);
$glue = new QueueColumnGlue($info);
$glue->queue_id = $q->getId();
$glue->save();
}
if (isset($vars['sorts'])) {
foreach ($vars['sorts'] as $info) {
$glue = new QueueSortGlue($info);
$glue->queue_id = $q->getId();
$glue->save();
}
}
abstract class QueueColumnAnnotation {
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
static $icon = false;
static $desc = '';
var $config;
function __construct($config) {
$this->config = $config;
}
static function fromJson($config) {
$class = $config['c'];
if (class_exists($class))
return new $class($config);
}
static function getDescription() {
return __(static::$desc);
}
static function getIcon() {
return static::$icon;
}
static function getPositions() {
return array(
"<" => __('Start'),
"b" => __('Before'),
"a" => __('After'),
">" => __('End'),
);
}
function decorate($text, $dec) {
static $positions = array(
'<' => '<span class="pull-left">%2$s</span>%1$s',
'>' => '<span class="pull-right">%2$s</span>%1$s',
'a' => '%1$s%2$s',
'b' => '%2$s%1$s',
if (!isset($positions[$pos]))
return $text;
return sprintf($positions[$pos], $text, $dec);
}
// Render the annotation with the database record $row. $text is the
// text of the cell before annotations were applied.
function render($row, $cell) {
if ($decoration = $this->getDecoration($row, $cell))
return $this->decorate($cell, $decoration);
return $cell;
}
// Add the annotation to a QuerySet
abstract function annotate($query);
// Fetch some HTML to render the decoration on the page. This function
// can return boolean FALSE to indicate no decoration should be applied
abstract function getDecoration($row, $text);
function getPosition() {
return strtolower($this->config['p']) ?: 'a';
}
function getClassName() {
return @$this->config['c'] ?: get_class();
}
static function getAnnotations($root) {
// Ticket annotations
static $annotations;
if (!isset($annotations[$root])) {
foreach (get_declared_classes() as $class)
if (is_subclass_of($class, get_called_class()))
$annotations[$root][] = $class;
}
return $annotations[$root];
}
/**
* Estimate the width of the rendered annotation in pixels
*/
function getWidth($row) {
return $this->isVisible($row) ? 25 : 0;
}
function isVisible($row) {
return true;
}
class TicketThreadCount
static $icon = 'comments-alt';
static $qname = '_thread_count';
static $desc = /* @trans */ 'Thread Count';
function annotate($query) {
return $query->annotate(array(
static::$qname => TicketThread::objects()
->filter(array('ticket__ticket_id' => new SqlField('ticket_id', 1)))
->exclude(array('entries__flags__hasbit' => ThreadEntry::FLAG_HIDDEN))
->aggregate(array('count' => SqlAggregate::COUNT('entries__id')))
));
}
function getDecoration($row, $text) {
$threadcount = $row[static::$qname];
if ($threadcount > 1) {
return sprintf(
'<small class="faded-more"><i class="icon-comments-alt"></i> %s</small>',
$threadcount
);
}
}
function isVisible($row) {
return $row[static::$qname] > 1;
}
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
class TicketReopenCount
extends QueueColumnAnnotation {
static $icon = 'folder-open-alt';
static $qname = '_reopen_count';
static $desc = /* @trans */ 'Reopen Count';
function annotate($query) {
return $query->annotate(array(
static::$qname => TicketThread::objects()
->filter(array('ticket__ticket_id' => new SqlField('ticket_id', 1)))
->filter(array('events__annulled' => 0, 'events__state' => 'reopened'))
->aggregate(array('count' => SqlAggregate::COUNT('events__id')))
));
}
function getDecoration($row, $text) {
$reopencount = $row[static::$qname];
if ($reopencount) {
return sprintf(
' <small class="faded-more"><i class="icon-%s"></i> %s</small>',
static::$icon,
$reopencount > 1 ? $reopencount : ''
);
}
}
function isVisible($row) {
return $row[static::$qname];
}
}
class ThreadAttachmentCount
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
static $icon = 'paperclip';
static $qname = '_att_count';
static $desc = /* @trans */ 'Attachment Count';
function annotate($query) {
// TODO: Convert to Thread attachments
return $query->annotate(array(
static::$qname => TicketThread::objects()
->filter(array('ticket__ticket_id' => new SqlField('ticket_id', 1)))
->filter(array('entries__attachments__inline' => 0))
->aggregate(array('count' => SqlAggregate::COUNT('entries__attachments__id')))
));
}
function getDecoration($row, $text) {
$count = $row[static::$qname];
if ($count) {
return sprintf(
'<i class="small icon-paperclip icon-flip-horizontal" data-toggle="tooltip" title="%s"></i>',
$count);
}
}
function isVisible($row) {
return $row[static::$qname] > 0;
}
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
class ThreadCollaboratorCount
extends QueueColumnAnnotation {
static $icon = 'group';
static $qname = '_collabs';
static $desc = /* @trans */ 'Collaborator Count';
function annotate($query) {
return $query->annotate(array(
static::$qname => TicketThread::objects()
->filter(array('ticket__ticket_id' => new SqlField('ticket_id', 1)))
->aggregate(array('count' => SqlAggregate::COUNT('collaborators__id')))
));
}
function getDecoration($row, $text) {
$count = $row[static::$qname];
if ($count) {
return sprintf(
'<span class="pull-right faded-more" data-toggle="tooltip" title="%d"><i class="icon-group"></i></span>',
$count);
}
}
function isVisible($row) {
return $row[static::$qname] > 0;
}
class OverdueFlagDecoration
static $icon = 'exclamation';
static $desc = /* @trans */ 'Overdue Icon';
function annotate($query) {
return $query->values('isoverdue');
}
function getDecoration($row, $text) {
if ($row['isoverdue'])
return '<span class="Icon overdueTicket"></span>';
function isVisible($row) {
return $row['isoverdue'];
}
}
class TicketSourceDecoration
static $icon = 'phone';
static $desc = /* @trans */ 'Ticket Source';
function annotate($query) {
return $query->values('source');
}
function getDecoration($row, $text) {
return sprintf('<span class="Icon %sTicket"></span>',
strtolower($row['source']));
class LockDecoration
extends QueueColumnAnnotation {
static $icon = "lock";
static $desc = /* @trans */ 'Locked';
function annotate($query) {
global $thisstaff;
return $query
->annotate(array(
'_locked' => new SqlExpr(new Q(array(
'lock__expire__gt' => SqlFunction::NOW(),
Q::not(array('lock__staff_id' => $thisstaff->getId())),
));
}
function getDecoration($row, $text) {
if ($row['_locked'])
return sprintf('<span class="Icon lockedTicket"></span>');
}
function isVisible($row) {
return $row['_locked'];
}
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
class AssigneeAvatarDecoration
extends QueueColumnAnnotation {
static $icon = "user";
static $desc = /* @trans */ 'Assignee Avatar';
function annotate($query) {
return $query->values('staff_id', 'team_id');
}
function getDecoration($row, $text) {
if ($row['staff_id'] && ($staff = Staff::lookup($row['staff_id'])))
return sprintf('<span class="avatar">%s</span>',
$staff->getAvatar(16));
elseif ($row['team_id'] && ($team = Team::lookup($row['team_id']))) {
$avatars = [];
foreach ($team->getMembers() as $T)
$avatars[] = $T->getAvatar(16);
return sprintf('<span class="avatar group %s">%s</span>',
count($avatars), implode('', $avatars));
}
}
function isVisible($row) {
return $row['staff_id'] + $row['team_id'] > 0;
}
function getWidth($row) {
if (!$this->isVisible($row))
return 0;
// If assigned to a team with no members, return 0 width
$width = 10;
if ($row['team_id'] && ($team = Team::lookup($row['team_id'])))
$width += (count($team->getMembers()) - 1) * 10;
return $width ? $width + 10 : $width;
}
}
class UserAvatarDecoration
extends QueueColumnAnnotation {
static $icon = "user";
static $desc = /* @trans */ 'User Avatar';
function annotate($query) {
return $query->values('user_id');
}
function getDecoration($row, $text) {
if ($row['user_id'] && ($user = User::lookup($row['user_id'])))
return sprintf('<span class="avatar">%s</span>',
$user->getAvatar(16));
}
function isVisible($row) {
return $row['user_id'] > 0;
}
}
class DataSourceField
extends ChoiceField {
function getChoices($verbose=false) {
$config = $this->getConfiguration();
$root = $config['root'];
$fields = array();
foreach (CustomQueue::getSearchableFields($root) as $path=>$f) {
list($label,) = $f;
$fields[$path] = $label;
}
return $fields;
}
}
class QueueColumnCondition {
var $config;
var $properties = array();
function __construct($config, $queue=null) {
if (is_array($config['prop']))
$this->properties = $config['prop'];
}
function getProperties() {
return $this->properties;
}
// Add the annotation to a QuerySet
function annotate($query) {
if (!($Q = $this->getSearchQ($query)))
return $query;
// Add an annotation to the query
return $query->annotate(array(
$this->getAnnotationName() => new SqlExpr(array($Q))
));
}
function getField($name=null) {
// FIXME
#$root = $this->getColumn()->getRoot();
$searchable = CustomQueue::getSearchableFields($root);
if (!isset($name))
list($name) = $this->config['crit'];
// Lookup the field to search this condition
if (isset($searchable[$name])) {
return $searchable[$name];
}
function getFieldName() {
list($name) = $this->config['crit'];
return $name;
}
function getCriteria() {
return $this->config['crit'];
}
list($name, $method, $value) = $this->config['crit'];
// XXX: Move getOrmPath to be more of a utility
// Ensure the special join is created to support custom data joins
$name = @CustomQueue::getOrmPath($name, $query);
$name2 = null;
if (preg_match('/__answers!\d+__/', $name)) {
// Ensure that only one record is returned from the join through
// the entry and answers joins
$name2 = $this->getAnnotationName().'2';
$query->annotate(array($name2 => SqlAggregate::MAX($name)));
}
if (list(,$field) = $this->getField($name))
return $field->getSearchQ($method, $value, $name2 ?: $name);
/**
* Take the criteria from the SavedSearch fields setup and isolate the
* field name being search, the method used for searhing, and the method-
* specific data entered in the UI.
*/
static function isolateCriteria($criteria, $base='Ticket') {
$searchable = CustomQueue::getSearchableFields($base);
foreach ($criteria as $k=>$v) {
if (substr($k, -7) === '+method') {
list($name,) = explode('+', $k, 2);
if (!isset($searchable[$name]))
continue;
// Lookup the field to search this condition
list($label, $field) = $searchable[$name];
// Get the search method and value
$method = $v;
// Not all search methods require a value
$value = $criteria["{$name}+{$method}"];
return array($name, $method, $value);
function render($row, $text, &$styles=array()) {
if ($V = $row[$this->getAnnotationName()]) {
foreach ($this->getProperties() as $css=>$value) {
$field = QueueColumnConditionProperty::getField($css);
$field->value = $value;
$V = $field->getClean();
if (is_array($V))
$V = current($V);
$styles[$css] = $V;
}
}
return $text;
}
function getAnnotationName() {
// This should be predictable based on the criteria so that the
// query can deduplicate the same annotations used in different
// conditions
if (!isset($this->annotation_name)) {
$this->annotation_name = $this->getShortHash();
}
return $this->annotation_name;
function __toString() {
list($name, $method, $value) = $this->config['crit'];
if (is_array($value))
$value = implode('+', $value);
return "{$name} {$method} {$value}";
}
function getHash($binary=false) {
return sha1($this->__toString(), $binary);
}
function getShortHash() {
return substr(base64_encode($this->getHash(true)), 0, 7);
}
static function getUid() {
return static::$uid++;
}
static function fromJson($config, $queue=null) {
$config = JsonDataParser::decode($config);
if (!is_array($config))
throw new BadMethodCallException('$config must be string or array');
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
}
}
class QueueColumnConditionProperty
extends ChoiceField {
static $properties = array(
'background-color' => 'ColorChoiceField',
'color' => 'ColorChoiceField',
'font-family' => array(
'monospace', 'serif', 'sans-serif', 'cursive', 'fantasy',
),
'font-size' => array(
'small', 'medium', 'large', 'smaller', 'larger',
),
'font-style' => array(
'normal', 'italic', 'oblique',
),
'font-weight' => array(
'lighter', 'normal', 'bold', 'bolder',
),
'text-decoration' => array(
'none', 'underline',
),
'text-transform' => array(
'uppercase', 'lowercase', 'captalize',
),
);
function __construct($property) {
$this->property = $property;
}
static function getProperties() {
return array_keys(static::$properties);
}
static function getField($prop) {
$choices = static::$properties[$prop];
if (!isset($choices))
return null;
if (is_array($choices))
return new ChoiceField(array(
'choices' => array_combine($choices, $choices),
));
elseif (class_exists($choices))
return new $choices(array('name' => $prop));
function getChoices($verbose=false) {
if (isset($this->property))
return static::$properties[$this->property];
$keys = array_keys(static::$properties);
return array_combine($keys, $keys);
}
}
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
class LazyDisplayWrapper {
function __construct($field, $value) {
$this->field = $field;
$this->value = $value;
$this->safe = false;
}
/**
* Allow a filter to change the value of this to a "safe" value which
* will not be automatically encoded with htmlchars()
*/
function changeTo($what, $safe=false) {
$this->field = null;
$this->value = $what;
$this->safe = $safe;
}
function __toString() {
return $this->display();
}
function display(&$styles=array()) {
if (isset($this->field))
return $this->field->display(
$this->field->to_php($this->value), $styles);
if ($this->safe)
return $this->value;
return Format::htmlchars($this->value);
}
}
* A column of a custom queue. Columns have many customizable features
* including:
* * Data Source (primary and secondary)
* * Heading
* * Link (to an object like the ticket)
* * Size and truncate settings
* * annotations (like counts and flags)
* * Conditions (which change the formatting like bold text)
*
* Columns are stored in a separate table from the queue itself, but other
* breakout items for the annotations and conditions, for instance, are stored
* as JSON text in the QueueColumn model.
*/
class QueueColumn
extends VerySimpleModel {
static $meta = array(
var $_annotations;
var $_conditions;
var $_queue; // Apparent queue if being inherited
function getId() {
return $this->id;
}
if ($this->filter
&& ($F = QueueColumnFilter::getInstance($this->filter)))
return $F;
}
function getName() {
return $this->name;
}
// These getters fetch data from the annotated overlay from the
// queue_column table
return $this->_queue ?: $this->queue;
}
/**
* If a column is inherited into a child queue and there are conditions
* added to that queue, then the column will need to be linked at
* run-time to the child queue rather than the parent.
*/
function setQueue(CustomQueue $queue) {
$this->_queue = $queue;
function getWidth() {
return $this->width ?: 100;
}
function getHeading() {
return $this->heading;
}
function getTranslateTag($subtag) {
return _H(sprintf('column.%s.%s.%s', $subtag, $this->queue_id, $this->id));
}
function getLocal($subtag) {
$tag = $this->getTranslateTag($subtag);
$T = CustomDataTranslation::translate($tag);
return $T != $tag ? $T : $this->get($subtag);
}
function getLocalHeading() {
return $this->getLocal('heading');
protected function setFlag($flag, $value=true, $field='flags') {
return $value
? $this->{$field} |= $flag
: $this->clearFlag($flag, $field);
}
protected function clearFlag($flag, $field='flags') {
return $this->{$field} &= ~$flag;
}
function isSortable() {
return $this->bits & self::FLAG_SORTABLE;
}
function setSortable($sortable) {
$this->setFlag(self::FLAG_SORTABLE, $sortable, 'bits');
}
function render($row) {
// Basic data
$text = $this->renderBasicValue($row);
if ($text && ($filter = $this->getFilter())) {
$text = $filter->filter($text, $row) ?: $text;
$styles = array();
if ($text instanceof LazyDisplayWrapper) {
$text = $text->display($styles);
}
// Truncate
$text = $this->applyTruncate($text, $row);
foreach ($this->getAnnotations() as $D) {
$text = $D->render($row, $text);
}
foreach ($this->getConditions() as $C) {
$text = $C->render($row, $text, $styles);
$style = Format::array_implode(':', ';', $styles);
return array($text, $style);
}
function renderBasicValue($row) {
$root = ($q = $this->getQueue()) ? $q->getRoot() : 'Ticket';
$fields = CustomQueue::getSearchableFields($root);
$primary = CustomQueue::getOrmPath($this->primary);
$secondary = CustomQueue::getOrmPath($this->secondary);
// Return a lazily ::display()ed value so that the value to be
// rendered by the field could be changed or display()ed when
// converted to a string.
if (($F = $fields[$primary])
&& (list(,$field) = $F)
&& ($T = $field->from_query($row, $primary))
) {
return new LazyDisplayWrapper($field, $T);
}
if (($F = $fields[$secondary])
&& (list(,$field) = $F)
&& ($T = $field->from_query($row, $secondary))
return new LazyDisplayWrapper($field, $T);
return new LazyDisplayWrapper($field, '');
function applyTruncate($text, $row) {
$offset = 0;
foreach ($this->getAnnotations() as $a)
$offset += $a->getWidth($row);
$width = $this->width - $offset;
$class = array();
case 'lclip':
$linfo = Internationalization::getCurrentLanguageInfo();
// Use `rtl` class to cut the beginning of LTR text. But, wrap
// the text with an appropriate direction so the ending
// punctuation is not rearranged.
$dir = $linfo['direction'] ?: 'ltr';
$text = sprintf('<span class="%s">%s</span>', $dir, $text);
$class[] = $dir == 'rtl' ? 'ltr' : 'rtl';
$class[] = 'bleed';
case 'ellipsis':
$class[] = 'truncate';
return sprintf('<span class="%s" style="max-width:%dpx">%s</span>',
implode(' ', $class), $width, $text);
function addToQuery($query, $field, $path) {
if (preg_match('/__answers!\d+__/', $path)) {
// Ensure that only one record is returned from the join through
// the entry and answers joins
return $query->annotate(array(
$path => SqlAggregate::MAX($path)
));
}
return $field->addToQuery($query, $path);
}
function mangleQuery($query, $root=null) {
$fields = CustomQueue::getSearchableFields($root ?: $this->getQueue()->getRoot());
if ($primary = $fields[$this->primary]) {
list(,$field) = $primary;
$query = $this->addToQuery($query, $field,
CustomQueue::getOrmPath($this->primary, $query));
}
if ($secondary = $fields[$this->secondary]) {
list(,$field) = $secondary;
$query = $this->addToQuery($query, $field,
CustomQueue::getOrmPath($this->secondary, $query));
if ($filter = $this->getFilter())
$query = $filter->mangleQuery($query, $this);
foreach ($this->getAnnotations() as $D) {
$query = $D->annotate($query);
}
// Conditions
foreach ($this->getConditions() as $C) {
$query = $C->annotate($query);
}
return $query;
}
function applySort($query, $reverse=false) {
$root = ($q = $this->getQueue()) ? $q->getRoot() : 'Ticket';
$fields = CustomQueue::getSearchableFields($root);
if ($primary = $fields[$this->primary]) {
list(,$field) = $primary;
$keys[] = array(CustomQueue::getOrmPath($this->primary, $query),
$field);
if ($secondary = $fields[$this->secondary]) {
list(,$field) = $secondary;
$keys[] = array(CustomQueue::getOrmPath($this->secondary,
$query), $field);
}
if (count($keys) > 1) {
$fields = array();
foreach ($keys as $key) {
list($path, $field) = $key;
foreach ($field->getSortKeys($path) as $field)
$fields[] = new SqlField($field);
// Force nulls to the buttom.
$fields[] = 'zzz';
$alias = sprintf('C%d', $this->getId());
$expr = call_user_func_array(array('SqlFunction', 'COALESCE'),
$fields);
$query->annotate(array($alias => $expr));
$reverse = $reverse ? '-' : '';
Loading
Loading full blame...