Newer
Older
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
!$this->hasFlag(self::FLAG_INHERIT_EXPORT)) {
$new = $vars['exports'];
$order = array_keys($new);
foreach ($this->exports as $f) {
$key = $f->getPath();
if (!isset($vars['exports'][$key])) {
$this->exports->remove($f);
continue;
}
$info = $vars['exports'][$key];
$f->set('heading', $info['heading']);
$f->set('sort',array_search($key, $order));
unset($new[$key]);
}
foreach($new as $k => $field) {
$f = QueueExport::create(array(
//'queue_id' => $this->getId(),
'path' => $k,
'heading' => $field['heading'],
'sort' => array_search($k, $order)));
$this->exports->add($f);
}
$this->exports->sort(function($f) { return $f->sort; });
}
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,
]);
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);
}
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
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 {
1196
1197
1198
1199
1200
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 $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
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
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;
}
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
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'];
}
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
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');
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
}
}
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);
}
}
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
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);
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