Skip to content
Snippets Groups Projects
class.queue.php 19.8 KiB
Newer Older
  • Learn to ignore specific revisions
  • <?php
    /*********************************************************************
        class.queue.php
    
        Custom (ticket) queues for osTicket
    
        Jared Hancock <jared@osticket.com>
        Peter Rotich <peter@osticket.com>
        Copyright (c)  2006-2013 osTicket
        http://www.osticket.com
    
        Released under the GNU General Public License WITHOUT ANY WARRANTY.
        See LICENSE.TXT for details.
    
        vim: expandtab sw=4 ts=4 sts=4:
    **********************************************************************/
    require_once INCLUDE_DIR . 'class.search.php';
    
    class CustomQueue extends SavedSearch {
        static $meta = array(
            'joins' => array(
                'columns' => array(
                    'reverse' => 'QueueColumn.queue',
                ),
            ),
        );
    
        static function objects() {
            return parent::objects()->filter(array(
                'flags__hasbit' => static::FLAG_QUEUE
            ));
        }
    
        static function getDecorations($root) {
            // Ticket decorations
            return array(
                'TicketThreadCount',
                'ThreadAttachmentCount',
                'OverdueFlagDecoration',
                'TicketSourceDecoration'
            );
        }
    
        function getColumns() {
            if (!count($this->columns)) {
                foreach (array(
                    new QueueColumn(array(
                        "id" => 1,
                        "heading" => "Number",
                        "primary" => 'number',
                        "width" => 100,
                    )),
                    new QueueColumn(array(
                        "id" => 2,
                        "heading" => "Created",
                        "primary" => 'created',
                        "width" => 100,
                    )),
                    new QueueColumn(array(
                        "id" => 3,
                        "heading" => "Subject",
                        "primary" => 'cdata__subject',
                        "width" => 250,
                    )),
                    new QueueColumn(array(
                        "id" => 4,
                        "heading" => "From",
                        "primary" => 'user__name',
                        "width" => 150,
                    )),
                    new QueueColumn(array(
                        "id" => 5,
                        "heading" => "Priority",
                        "primary" => 'cdata__priority',
                        "width" => 120,
                    )),
                    new QueueColumn(array(
                        "id" => 6,
                        "heading" => "Assignee",
                        "primary" => 'assignee',
                        "secondary" => 'team__name',
                        "width" => 100,
                    )),
                ) as $c) {
                    $this->addColumn($c);
                }
            }
            return $this->columns;
        }
    
        function addColumn(QueueColumn $col) {
            $this->columns->add($col);
            $col->queue = $this;
        }
    
        function getRoot() {
            return 'Ticket';
        }
    
        function getBasicQuery($form=false) {
            $root = $this->getRoot();
            $query = $root::objects();
            return $this->mangleQuerySet($query, $form);
        }
    
        /**
         * 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) {
            $query = $this->getBasicQuery($form);
            foreach ($this->getColumns() as $C) {
                $query = $C->mangleQuery($query);
            }
            return $query;
        }
    }
    
    abstract class QueueDecoration {
        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 decorations 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';
        }
    }
    
    class TicketThreadCount
    extends QueueDecoration {
        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
                );
            }
        }
    }
    
    class ThreadAttachmentCount
    extends QueueDecoration {
        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);
            }
        }
    }
    
    class OverdueFlagDecoration
    extends QueueDecoration {
        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>';
    
        }
    }
    
    class TicketSourceDecoration
    extends QueueDecoration {
        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 DataSourceField
    extends ChoiceField {
        function getChoices() {
            $config = $this->getConfiguration();
            $root = $config['root'];
            $fields = array();
            foreach (SavedSearch::getSearchableFields($root) as $path=>$f) {
                $fields[$path] = $f->get('label');
            }
            return $fields;
        }
    }
    
    class QueueColumnCondition {
        var $config;
        var $properties = array();
    
        function __construct($config) {
            $this->config = $config;
            if (is_array($config['prop']))
                $this->properties = $config['prop'];
        }
    
        function getProperties() {
            return $this->properties;
        }
    
        function getField() {
        }
    
        // Add the annotation to a QuerySet
        function annotate($query) {
            $criteria = $this->config['crit'];
            $searchable = SavedSearch::getSearchableFields('Ticket');
    
            // Setup a dummy form with a source for field setup
            $form = new Form($criteria);
            $fields = array();
            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
                    $field = $searchable[$name];
    
                    // Get the search method and value
                    $breakout = SavedSearch::getSearchField($field, $name);
                    $method = $breakout["{$name}+method"];
                    $method->setForm($form);
                    if (!($method = $method->getClean()))
                        continue;
    
                    if (!($value = $breakout["{$name}+{$method}"]))
                        continue;
    
                    // Fetch a criteria Q for the query
                    $value = $value->getClean();
                    $Q = $field->getSearchQ($method, $value, $name);
    
                    // Add an annotation to the query
                    $query = $query->annotate(array(
                        $this->getAnnotationName() => new SqlExpr($Q)
                    ));
    
                    // Only one field can be considered in the condition
                    break;
                }
            }
            return $query;
        }
    
        function render($row, $text) {
            $field = $this->getAnnotationName();
            if ($V = $row[$field]) {
                $style = array();
                foreach ($this->getProperties() as $css=>$value) {
                    $style[] = "{$css}:{$value}";
                }
                $text = sprintf('<span style="%s">%s</span>',
                    implode(' ', $style), $text);
            }
            return $text;
        }
    
        function getAnnotationName() {
            return 'howdy';
        }
    
        static function fromJson($config) {
            if (is_string($config))
                $config = JsonDataParser::decode($cnofig);
            if (!is_array($config))
                throw new BadMethodCallException('$config must be string or array');
    
            return new static($config);
        }
    }
    
    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[$this->property]);
        }
    
        static function getField($prop) {
            $choices = static::$properties[$prop];
            if (is_array($choices))
                return new ChoiceField(array(
                    'choices' => array_combine($choices, $choices),
                ));
            elseif (class_exists($choices))
                return new $choices();
        }
    
        function getChoices() {
            if (isset($this->property))
                return static::$properties[$this->property];
    
            $keys = array_keys(static::$properties);
            return array_combine($keys, $keys);
        }
    }
    
    
    /**
     * Object version of JSON-serialized column array which has several
     * properties:
     *
     * {
     *   "heading": "Header Text",
     *   "primary": "user__name",
     *   "secondary": null,
     *   "width": 100,
     *   "link": 'ticket',
     *   "truncate": "wrap",
     *   "filter": "UsersName"
     *   "annotations": [
     *     {
     *       "c": "ThreadCollabCount",
     *       "p": ">"
     *     }
     *   ],
     *   "conditions": [
     *     {
     *       "crit": {
     *         "created+method": {"ndaysago": "in the last n days"}, "created+ndaysago": {"until":"7"}
     *       },
     *       "prop": {
     *         "font-weight": "bold"
     *       }
     *     }
     *   ]
     * }
     */
    class QueueColumn
    extends VerySimpleModel {
        static $meta = array(
            'table' => QUEUE_COLUMN_TABLE,
            'pk' => array('id'),
            'joins' => array(
                'queue' => array(
                    'constraint' => array('queue_id' => 'CustomQueue.id'),
                ),
            ),
        );
    
        var $_decorations = array();
        var $_conditions = array();
    
        function __onload() {
            if ($this->annotations) {
                foreach ($this->annotations as $D)
                    $this->_decorations[] = QueueDecoration::fromJson($D) ?: array();
            }
            if ($this->conditions) {
                foreach ($this->conditions as $C)
                    $this->_conditions[] = QueueColumnCondition::fromJson($C) ?: array();
            }
        }
    
        function getId() {
            return $this->id;
        }
    
        function getQueue() {
            return $this->queue;
        }
    
        function getHeading() {
            return $this->heading;
        }
    
        function getWidth() {
            return $this->width ?: 100;
        }
    
        function getLink($row) {
            $link = $this->link;
            switch (strtolower($link)) {
            case 'root':
            case 'ticket':
                return Ticket::getLink($row['ticket_id']);
            case 'user':
                return User::getLink($row['user_id']);
            case 'org':
                return Organization::getLink($row['user__org_id']);
            }
        }
    
        function render($row) {
            // Basic data
            $text = $this->renderBasicValue($row);
    
            // Truncate
            if ($class = $this->getTruncateClass()) {
                $text = sprintf('<span class="%s">%s</span>', $class, $text);
            }
    
            // Link
            if ($link = $this->getLink($row)) {
                $text = sprintf('<a href="%s">%s</a>', $link, $text);
            }
    
            // Decorations and conditions
            foreach ($this->_decorations as $D) {
                $text = $D->render($row, $text);
            }
            foreach ($this->_conditions as $C) {
                $text = $C->render($row, $text);
            }
            return $text;
        }
    
        function renderBasicValue($row) {
            $root = $this->getQueue()->getRoot();
            $fields = SavedSearch::getSearchableFields($root);
            $primary = $this->getOrmPath($this->primary);
            $secondary = $this->getOrmPath($this->secondary);
    
            // TODO: Consider data filter if configured
    
            if (($F = $fields[$primary]) && ($T = $F->from_query($row, $primary)))
                return $F->display($F->to_php($T));
    
            if (($F = $fields[$secondary]) && ($T = $F->from_query($row, $secondary)))
                return $F->display($F->to_php($T));
        }
    
        function getTruncateClass() {
            switch ($this->truncate) {
            case 'ellipsis':
                return 'trucate';
            case 'clip':
                return 'truncate clip';
            default:
            case 'wrap':
                return false;
            }
        }
    
        function mangleQuery($query) {
            // Basic data
            $fields = SavedSearch::getSearchableFields($this->getQueue()->getRoot());
            if ($primary = $fields[$this->primary])
                $query = $primary->addToQuery($query,
                    $this->getOrmPath($this->primary));
    
            if ($secondary = $fields[$this->secondary])
                $query = $secondary->addToQuery($query,
                    $this->getOrmPath($this->secondary));
    
            switch ($this->link) {
            // XXX: Consider the ROOT of the related queue
            case 'ticket':
                $query = $query->values('ticket_id');
                break;
            case 'user':
                $query = $query->values('user_id');
                break;
            case 'org':
                $query = $query->values('user__org_id');
                break;
            }
    
            // Decorations
            foreach ($this->_decorations as $D) {
                $query = $D->annotate($query);
            }
    
            // Conditions
            foreach ($this->_conditions as $C) {
                $query = $C->annotate($query);
            }
    
            return $query;
        }
    
        function getDataConfigForm($source=false) {
            return new QueueColDataConfigForm($source ?: $this->ht,
                array('id' => $this->id));
        }
    
        function getOrmPath($name) {
            return $name;
        }
    
        function getDecorations() {
            return $this->_decorations;
        }
    
        function getConditions() {
            return $this->_conditions;
        }
    
        /**
         * Create a CustomQueueColumn from vars (_POST) received from an
         * update request.
         */
        static function create($vars=array()) {
            $inst = parent::create($vars);
            // TODO: Convert decorations and conditions
            return $inst;
        }
    
        function update($vars) {
            $form = $this->getDataConfigForm($vars);
            foreach ($form->getClean() as $k=>$v)
                $this->set($k, $v);
    
    
            // Do the decorations
            $this->_decorations = $this->decorations = array();
            foreach ($vars['decorations'] as $i=>$class) {
                if (!class_exists($class) || !is_subclass_of($class, 'QueueDecoration'))
                    continue;
                if ($vars['deco_column'][$i] != $this->id)
                    continue;
                $json = array('c' => $class, 'p' => $vars['deco_pos'][$i]);
                $this->_decorations[] = QueueDecoration::fromJson($json);
                $this->decorations[] = $json;
            }
            // Store as JSON array
            $this->decorations = JsonDataEncoder::encode($this->decorations);
    
        }
    }
    
    class QueueColDataConfigForm
    extends AbstractForm {
        function buildFields() {
            return array(
                'primary' => new DataSourceField(array(
                    'label' => __('Primary Data Source'),
                    'required' => true,
                    'configuration' => array(
                        'root' => 'Ticket',
                    ),
                    'layout' => new GridFluidCell(6),
                )),
                'secondary' => new DataSourceField(array(
                    'label' => __('Secondary Data Source'),
                    'configuration' => array(
                        'root' => 'Ticket',
                    ),
                    'layout' => new GridFluidCell(6),
                )),
                'heading' => new TextboxField(array(
                    'label' => __('Heading'),
                    'required' => true,
                    'layout' => new GridFluidCell(3),
                )),
                'link' => new ChoiceField(array(
                    'label' => __('Link'),
                    'required' => false,
                    'choices' => array(
                        'ticket' => __('Ticket'),
                        'user' => __('User'),
                        'org' => __('Organization'),
                    ),
                    'layout' => new GridFluidCell(3),
                )),
                'width' => new TextboxField(array(
                    'label' => __('Width'),
                    'default' => 75,
                    'configuration' => array(
                        'validator' => 'number',
                    ),
                    'layout' => new GridFluidCell(3),
                )),
                'truncate' => new ChoiceField(array(
                    'label' => __('Text Overflow'),
                    'choices' => array(
                        'wrap' => __("Wrap Lines"),
                        'ellipsis' => __("Add Ellipsis"),
                        'clip' => __("Clip Text"),
                    ),
                    'default' => 'wrap',
                    'layout' => new GridFluidCell(3),
                )),
            );
        }
    }