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],
),
'reverse' => 'QueueColumnGlue.queue',
'broker' => 'QueueColumnListBroker',
'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);
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
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 getCriteria($include_parent=false) {
if (!isset($this->criteria)) {
$old = @$this->config[0] === '{';
$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
$this->criteria = $this->isolateCriteria($this->criteria);
}
}
$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);
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
}
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
*/
function getForm($source=null) {
$searchable = $this->getCurrentSearchFields($source);
$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);
$form->addValidator(function($form) {
$selected = 0;
foreach ($form->getFields() as $F) {
if (substr($F->get('name'), -7) == '+search' && $F->getClean())
$selected += 1;
// Consider keyword searches
elseif ($F->get('name') == ':keywords' && $F->getClean())
$selected += 1;
}
if (!$selected)
$form->addError(__('No fields selected for searching'));
});
// Load state from current configuraiton
if (!$source) {
foreach ($this->getCriteria() as $I) {
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
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
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
300
301
302
303
304
305
306
307
308
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()) {
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 ($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 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);
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
return $fields;
}
/**
* 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();
$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.
*/
function isolateCriteria($criteria, $root=null) {
$searchable = static::getSearchableFields($root ?: $this->getRoot());
$items = array();
return null;
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 and value
$method = $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;
}
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
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'),
'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 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
) {
return $this->parent->getColumns();
}
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":"<"}]',
"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;
}
}
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
/**
* Get a description of a field in a search. Expects an entry from the
* array retrieved in ::getSearchFields()
*/
function describeField($info, $name=false) {
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 $agent->getId() == $this->staff_id
|| $this->hasFlag(self::FLAG_PUBLIC);
}
function ignoreVisibilityConstraints() {
global $thisstaff;
// For saved searches (not queues), staff can have a permission to
// see all records
return !$this->isAQueue()
&& $thisstaff->hasPerm(SearchBackend::PERM_EVERYTHING);
}
function inheritCriteria() {
return $this->flags & self::FLAG_INHERIT_CRITERIA;
}
function inheritColumns() {
return $this->hasFlag(self::FLAG_INHERIT_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}/";
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
}
function getFullName() {
$base = $this->getName();
if ($this->parent)
$base = sprintf("%s / %s", $this->parent->getFullName(), $base);
return $base;
}
function isAQueue() {
return $this->hasFlag(self::FLAG_QUEUE);
}
function isPrivate() {
return !$this->isAQueue() && !$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);
}
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
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['name'])
$errors['name'] = __('A title is required');
elseif (($q=CustomQueue::lookup(array(
'title' => $vars['name'],
'parent_id' => $vars['parent_id'] ?: 0,
'staff_id' => $this->staff_id)))
&& $q->getId() != $this->id
)
$errors['name'] = __('Saved queue with same name exists');
$this->title = $vars['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,
$this->parent_id > 0 && 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 (isset($vars['columns']) && !$this->hasFlag(self::FLAG_INHERIT_COLUMNS)) {
$key = $col->column_id;
if (!isset($vars['columns'][$key])) {
$this->columns->remove($col);
continue;
$info = $vars['columns'][$key];
$col->set('sort', array_search($key, $order));
$col->set('heading', $info['heading']);
$col->set('width', $info['width']);
$col->setSortable($info['sortable']);
$glue = new QueueColumnGlue(array(
'sort' => array_search($info['column_id'], $order),
'heading' => $info['heading'],
'width' => $info['width'] ?: 100,
'bits' => $info['sortable'] ? QueueColumn::FLAG_SORTABLE : 0,
));
$glue->queue = $this;
$this->columns->add(
QueueColumn::lookup($info['column_id']), $glue);
}
// Re-sort the in-memory columns array
$this->columns->sort(function($c) { return $c->sort; });
}
// 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->config = JsonDataEncoder::encode([
'criteria' => $this->isolateCriteria($form->getClean()),
'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);
}
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
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) {
global $thisstaff;
$queue = new static($vars);
$queue->created = SqlFunction::NOW();
$queue->setFlag(self::FLAG_QUEUE);
if ($thisstaff)
$queue->staff_id = $thisstaff->getId();
return $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 {
1262
1263
1264
1265
1266
1267
1268
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
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;
}
}
class ThreadAttachmentCount
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
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;
}
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
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'];
}
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
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) {
// 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, $root='Ticket') {
$searchable = CustomQueue::getSearchableFields($root);
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');
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
}
}
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);
}
}
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
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;
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
function getQueue() {
return $this->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);
// Filter
if ($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;
$query = $field->applyOrderBy($query, $reverse,
CustomQueue::getOrmPath($this->primary, $query));
}
if ($secondary = $fields[$this->secondary]) {
list(,$field) = $secondary;
$query = $field->applyOrderBy($query, $reverse,
CustomQueue::getOrmPath($this->secondary, $query));
}
return $query;
}
function getDataConfigForm($source=false) {
return new QueueColDataConfigForm($source ?: $this->getDbFields(),
array('id' => $this->id));
}
if (!isset($this->_annotations)) {
$this->_annotations = array();
if ($this->annotations
&& ($anns = JsonDataParser::decode($this->annotations))
) {
foreach ($anns as $D)
if ($T = QueueColumnAnnotation::fromJson($D))
$this->_annotations[] = $T;
}
}
function getConditions($include_queue=true) {
if (!isset($this->_conditions)) {
$this->_conditions = array();
if ($this->conditions
&& ($conds = JsonDataParser::decode($this->conditions))
) {
foreach ($conds as $C)
if ($T = QueueColumnCondition::fromJson($C))
$this->_conditions[] = $T;
}
// Support row-spanning conditions
if ($include_queue && ($q = $this->getQueue())
&& ($q_conds = $q->getConditions())
) {
$this->_conditions = array_merge($this->_conditions, $q_conds);
}
return $this->_conditions;
}
$c = new static($vars);
$c->save();
return $c;
}
static function placeholder($vars) {
return static::__hydrate($vars);
}
function update($vars, $root='Ticket') {
$form = $this->getDataConfigForm($vars);
foreach ($form->getClean() as $k=>$v)
$this->set($k, $v);
// Do the annotations
$this->_annotations = $annotations = array();
if (isset($vars['annotations'])) {
foreach (@$vars['annotations'] as $i=>$class) {
if ($vars['deco_column'][$i] != $this->id)
continue;
if (!class_exists($class) || !is_subclass_of($class, 'QueueColumnAnnotation'))
continue;
$json = array('c' => $class, 'p' => $vars['deco_pos'][$i]);
$annotations[] = $json;
$this->_annotations[] = QueueColumnAnnotation::fromJson($json);
}
$this->_conditions = $conditions = array();
if (isset($vars['conditions'])) {
list($this->_conditions, $conditions)
= self::getConditionsFromPost($vars, $this->id, $root);
$this->annotations = JsonDataEncoder::encode($annotations);
$this->conditions = JsonDataEncoder::encode($conditions);
}
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
static function getConditionsFromPost(array $vars, $myid, $root='Ticket') {
$condition_objects = $conditions = array();
if (!isset($vars['conditions']))
return array($condition_objects, $conditions);
foreach (@$vars['conditions'] as $i=>$id) {
if ($vars['condition_column'][$i] != $myid)
// Not a condition for this column
continue;
// Determine the criteria
$name = $vars['condition_field'][$i];
$fields = CustomQueue::getSearchableFields($root);
if (!isset($fields[$name]))
// No such field exists for this queue root type
continue;
$parts = CustomQueue::getSearchField($fields[$name], $name);
$search_form = new SimpleForm($parts, $vars, array('id' => $id));
$search_form->getField("{$name}+search")->value = true;
$crit = $search_form->getClean();
// Check the box to enable searching on the field
$crit["{$name}+search"] = true;
// Isolate only the critical parts of the criteria
$crit = QueueColumnCondition::isolateCriteria($crit);
// Determine the properties
$props = array();
foreach ($vars['properties'] as $i=>$cid) {
if ($cid != $id)
// Not a property for this condition
continue;
// Determine the property configuration
$prop = $vars['property_name'][$i];
if (!($F = QueueColumnConditionProperty::getField($prop))) {
// Not a valid property
continue;
}
$prop_form = new SimpleForm(array($F), $vars, array('id' => $cid));
$props[$prop] = $prop_form->getField($prop)->getClean();
}
$json = array('crit' => $crit, 'prop' => $props);
$condition_objects[] = QueueColumnCondition::fromJson($json);
$conditions[] = $json;
}
return array($condition_objects, $conditions);
}
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
class QueueExport
extends VerySimpleModel {
static $meta = array(
'table' => QUEUE_EXPORT_TABLE,
'pk' => array('id'),
'joins' => array(
'queue' => array(
'constraint' => array('queue_id' => 'CustomQueue.id'),
),
),
'select_related' => array('queue'),
'ordering' => array('sort'),
);
function getPath() {
return $this->path;
}
function getField() {
return $this->getPath();
}
function getHeading() {
return $this->heading;
}
static function create($vars=false) {
$inst = new static($vars);
return $inst;
}
}
Loading
Loading full blame...