Skip to content
Snippets Groups Projects
class.queue.php 95.1 KiB
Newer Older
  • Learn to ignore specific revisions
  •         return $this->flags & self::FLAG_INHERIT_CRITERIA;
        }
    
        function inheritColumns() {
            return $this->hasFlag(self::FLAG_INHERIT_COLUMNS);
        }
    
    
        function useStandardColumns() {
            return !count($this->columns);
        }
    
    
    Peter Rotich's avatar
    Peter Rotich committed
        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);
        }
    
    
    aydreeihn's avatar
    aydreeihn committed
        function isASearch() {
            return !$this->isAQueue() || !$this->isSaved();
        }
    
    
        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);
        }
    
        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');
    
    Peter Rotich's avatar
    Peter Rotich committed
            elseif (($q=CustomQueue::lookup(array(
    
                            'title' => $vars['queue-name'],
    
    Peter Rotich's avatar
    Peter Rotich committed
                            'parent_id' => $vars['parent_id'] ?: 0,
    
                            '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');
            }
    
    
            // Configure quick filter options
    
            $this->filter = $vars['filter'];
    
            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']));
    
    Peter Rotich's avatar
    Peter Rotich committed
            $this->setFlag(self::FLAG_INHERIT_EXPORT,
    
                $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();
    
    Peter Rotich's avatar
    Peter Rotich committed
            // 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)) {
    
                $new = $order = $vars['sorts'];
    
                foreach ($this->sorts as $sort) {
                    $key = $sort->sort_id;
    
                    $idx = array_search($key, $vars['sorts']);
                    if (false === $idx) {
    
                        $this->sorts->remove($sort);
                    }
    
                    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,
    
                        'queue' => $this,
    
                        'sort' => array_search($id, $order),
                    ));
    
                    $this->sorts->add($sort, $glue);
    
                }
                // 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([
    
                    'criteria' => $this->criteria,
    
                    'conditions' => $conditions,
                ]);
    
                // Clear currently set criteria.and conditions.
                 $this->criteria = $this->_conditions = null;
    
            return 0 === count($errors);
        }
    
    
        function psave() {
            return parent::save();
        }
    
    
        function save($refetch=false) {
    
            $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);
            }
    
            return $this->columns->saveAll()
    
    Peter Rotich's avatar
    Peter Rotich committed
                && $this->exports->saveAll()
    
                && $this->sorts->saveAll();
    
        /**
         * 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);
        }
    
    
        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();
    
    JediKev's avatar
    JediKev committed
            if (!isset($vars['flags']))
                $queue->setFlag(self::FLAG_QUEUE);
    
    
        static function __create($vars) {
            $q = static::create($vars);
    
    Peter Rotich's avatar
    Peter Rotich committed
            foreach ($vars['columns'] ?: array() as $info) {
    
                $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 {
    
        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',
    
            $pos = $this->getPosition();
    
            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
    
    Peter Rotich's avatar
    Peter Rotich committed
        abstract function annotate($query, $name);
    
    
        // 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;
    
    
        static function addToQuery($query, $name=false) {
            $name = $name ?: static::$qname;
            $annotation = new Static(array());
            return $annotation->annotate($query, $name);
        }
    
        static function from_query($row, $name=false) {
            $name = $name ?: static::$qname;
            return $row[$name];
        }
    
    extends QueueColumnAnnotation {
    
        static $icon = 'comments-alt';
        static $qname = '_thread_count';
        static $desc = /* @trans */ 'Thread Count';
    
    
    Peter Rotich's avatar
    Peter Rotich committed
        function annotate($query, $name=false) {
            $name = $name ?: static::$qname;
    
            return $query->annotate(array(
    
    Peter Rotich's avatar
    Peter Rotich committed
                $name => 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>',
    
    
        function isVisible($row) {
            return $row[static::$qname] > 1;
        }
    
    Peter Rotich's avatar
    Peter Rotich committed
    class TicketReopenCount
    extends QueueColumnAnnotation {
        static $icon = 'folder-open-alt';
        static $qname = '_reopen_count';
        static $desc = /* @trans */ 'Reopen Count';
    
    
    Peter Rotich's avatar
    Peter Rotich committed
        function annotate($query, $name=false) {
            $name = $name ?: static::$qname;
    
    Peter Rotich's avatar
    Peter Rotich committed
            return $query->annotate(array(
    
    Peter Rotich's avatar
    Peter Rotich committed
                $name => TicketThread::objects()
    
    Peter Rotich's avatar
    Peter Rotich committed
                ->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(
                    '&nbsp;<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
    
    extends QueueColumnAnnotation {
    
        static $icon = 'paperclip';
        static $qname = '_att_count';
        static $desc = /* @trans */ 'Attachment Count';
    
    
    Peter Rotich's avatar
    Peter Rotich committed
        function annotate($query, $name=false) {
    
            // TODO: Convert to Thread attachments
    
    Peter Rotich's avatar
    Peter Rotich committed
            $name = $name ?: static::$qname;
    
            return $query->annotate(array(
    
    Peter Rotich's avatar
    Peter Rotich committed
                $name => 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;
        }
    
    class ThreadCollaboratorCount
    extends QueueColumnAnnotation {
        static $icon = 'group';
        static $qname = '_collabs';
        static $desc = /* @trans */ 'Collaborator Count';
    
    
    Peter Rotich's avatar
    Peter Rotich committed
        function annotate($query, $name=false) {
            $name = $name ?: static::$qname;
    
            return $query->annotate(array(
    
    Peter Rotich's avatar
    Peter Rotich committed
                $name => 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
    
    extends QueueColumnAnnotation {
    
        static $icon = 'exclamation';
        static $desc = /* @trans */ 'Overdue Icon';
    
    
    Peter Rotich's avatar
    Peter Rotich committed
        function annotate($query, $name=false) {
    
            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
    
    extends QueueColumnAnnotation {
    
        static $icon = 'phone';
        static $desc = /* @trans */ 'Ticket Source';
    
    
    Peter Rotich's avatar
    Peter Rotich committed
        function annotate($query, $name=false) {
    
            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';
    
    
    Peter Rotich's avatar
    Peter Rotich committed
        function annotate($query, $name=false) {
    
            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'];
        }
    
    class AssigneeAvatarDecoration
    extends QueueColumnAnnotation {
        static $icon = "user";
        static $desc = /* @trans */ 'Assignee Avatar';
    
    
    Peter Rotich's avatar
    Peter Rotich committed
        function annotate($query, $name=false) {
    
            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';
    
    
    Peter Rotich's avatar
    Peter Rotich committed
        function annotate($query, $name=false) {
    
            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 $queue;
    
        var $properties = array();
    
    
        function __construct($config, $queue=null) {
    
            $this->config = $config;
    
            $this->queue = $queue;
    
            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'];
        }
    
    
        function getSearchQ($query) {
    
            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)));
            }
    
    
            // Fetch a criteria Q for the query
    
            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);
    
                }
            }
            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) {
    
            if (is_string($config))
    
                $config = JsonDataParser::decode($config);
    
            if (!is_array($config))
                throw new BadMethodCallException('$config must be string or array');
    
    
            return new static($config, $queue);
    
        }
    }
    
    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(
    
                    'name' => $prop,
    
                    '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);
        }
    }
    
    
    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) {