Skip to content
Snippets Groups Projects
class.queue.php 92.1 KiB
Newer Older
<?php
/*********************************************************************
    class.queue.php

    Custom (ticket) queues for osTicket

    Jared Hancock <jared@osticket.com>
    Peter Rotich <peter@osticket.com>
    Copyright (c)  2006-2015 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:
**********************************************************************/

class CustomQueue extends VerySimpleModel {
    static $meta = array(
        'table' => QUEUE_TABLE,
        'pk' => array('id'),
        'ordering' => array('sort'),
        'select_related' => array('parent', 'default_sort'),
        'joins' => array(
            'children' => array(
                'reverse' => 'CustomQueue.parent',
                'constrain' => ['children__id__gt' => 0],
            ),
            'columns' => array(
                'reverse' => 'QueueColumnGlue.queue',
                'constrain' => array('staff_id' =>'QueueColumnGlue.staff_id'),
                'broker' => 'QueueColumnListBroker',
            'sorts' => array(
                'reverse' => 'QueueSortGlue.queue',
                'broker' => 'QueueSortListBroker',
            ),
            'default_sort' => array(
                'constraint' => array('sort_id' => 'QueueSort.id'),
                'null' => true,
            ),
Peter Rotich's avatar
Peter Rotich committed
            '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
Peter Rotich's avatar
Peter Rotich committed
    const FLAG_INHERIT_EXPORT  =  0x0080; // Inherit export fields from parent

Peter Rotich's avatar
Peter Rotich committed
    const FLAG_INHERIT_EVERYTHING = 0x158; // Maskf or all INHERIT flags
    var $_conditions;
    static function queues() {
        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);
Peter Rotich's avatar
Peter Rotich committed

       if ($this->hasFlag(self::FLAG_INHERIT_EXPORT) && !$this->parent_id)
            $this->clearFlag(self::FLAG_INHERIT_EXPORT);
    function getId() {
        return $this->id;
    }

    function getName() {
        return $this->title;
    }

    function getHref() {
        // TODO: Get base page from getRoot();
        $root = $this->getRoot();
        return 'tickets.php?queue='.$this->getId();
    }

    function getRoot() {
        switch ($this->root) {
        case 'T':
        default:
            return 'Ticket';
        }
    }

    function getPath() {
        return $this->path ?: $this->buildPath();
    }

    function criteriaRequired() {
        return true;
    }

    function getCriteria($include_parent=false) {
        if (!isset($this->criteria)) {
            $this->criteria = is_string($this->config)
                ? JsonDataParser::decode($this->config)
                : $this->config;
            // XXX: Drop this block in v1.12
            // Auto-upgrade v1.10 saved-search criteria to new format
            // But support new style with `conditions` support
            $old = @$this->config[0] === '{';
            if ($old && is_array($this->criteria)
                && !isset($this->criteria['conditions'])
            ) {
                // TODO: Upgrade old ORM path names
                // Parse criteria out of JSON if any.
                $this->criteria = self::isolateCriteria($this->criteria,
                        $this->getRoot());
            }
        }
        $criteria = $this->criteria ?: array();
        // Support new style with `conditions` support
        if (isset($criteria['criteria']))
            $criteria = $criteria['criteria'];
        if ($include_parent && $this->parent_id && $this->parent) {
            $criteria = array_merge($this->parent->getCriteria(true),
                $criteria);
        }
        return $criteria;
    }

    function describeCriteria($criteria=false){
        $all = $this->getSupportedMatches($this->getRoot());
        $items = array();
        $criteria = $criteria ?: $this->getCriteria(true);
        foreach ($criteria as $C) {
            list($path, $method, $value) = $C;
            if ($path === ':keywords') {
                $items[] = Format::htmlchars("\"{$value}\"");
                continue;
            }
            if (!isset($all[$path]))
                continue;
            list($label, $field) = $all[$path];
            $items[] = $field->describeSearch($method, $value, $label);
        }
        return implode("\nAND ", $items);
    }

    /**
     * Fetch an AdvancedSearchForm instance for use in displaying or
     * configuring this search in the user interface.
     *
     * Parameters:
     * $search - <array> Request parameters ($_POST) used to update the
     *      search beyond the current configuration of the search criteria
     * $searchables - search fields - default to current if not provided
    function getForm($source=null, $searchable=null) {
        $fields = array();
        $validator = false;
        if (!isset($searchable)) {
            $searchable = $this->getCurrentSearchFields($source);
            $validator = true;
            $fields = array(
                ':keywords' => new TextboxField(array(
                    'id' => 3001,
                    'configuration' => array(
                        'size' => 40,
                        'length' => 400,
                        'autofocus' => true,
                        'classes' => 'full-width headline',
                        'placeholder' => __('Keywords — Optional'),
                    ),
                )),
            );
        foreach ($searchable as $path=>$field)
            $fields = array_merge($fields, static::getSearchField($field, $path));

        $form = new AdvancedSearchForm($fields, $source);

        // Field selection validator
        if ($this->criteriaRequired()) {
            $form->addValidator(function($form) {
                    if (!$form->getNumFieldsSelected())
                        $form->addError(__('No fields selected for searching'));
                });
        }

        // Load state from current configuraiton
        if (!$source) {
            foreach ($this->getCriteria() as $I) {
                list($path, $method, $value) = $I;
                if ($path == ':keywords' && $method === null) {
                    if ($F = $form->getField($path))
                        $F->value = $value;
                    continue;
                }

                if (!($F = $form->getField("{$path}+search")))
                    continue;
                $F->value = true;

                if (!($F = $form->getField("{$path}+method")))
                    continue;
                $F->value = $method;

                if ($value && ($F = $form->getField("{$path}+{$method}")))
                    $F->value = $value;
            }
        }
        return $form;
    }

    /**
     * Fetch a bucket of fields for a custom search. The fields should be
     * added to a form before display. One searchable field may encompass 10
     * or more actual fields because fields are expanded to support multiple
     * search methods along with the fields for each search method. This
     * method returns all the FormField instances for all the searchable
     * model fields currently in use.
     *
     * Parameters:
     * $source - <array> data from a request. $source['fields'] is expected
     *      to contain a list extra fields by ORM path, of newly added
     *      fields not yet saved in this object's getCriteria().
     */
    function getCurrentSearchFields($source=array(), $criteria=array()) {
        static $basic = array(
            'Ticket' => array(
                'status__state',
                'dept_id',
                'assignee',
                'topic_id',
                'created',
                'est_duedate',
            )
        );

        $all = $this->getSupportedMatches();
        $core = array();

        // Include basic fields for new searches
        if (!isset($this->id))
            foreach ($basic[$this->getRoot()] as $path)
                if (isset($all[$path]))
                    $core[$path] = $all[$path];

        // Add others from current configuration
        foreach ($criteria ?: $this->getCriteria() as $C) {
            list($path) = $C;
            if (isset($all[$path]))
                $core[$path] = $all[$path];
        }

        if (isset($source['fields']))
            foreach ($source['fields'] as $path)
                if (isset($all[$path]))
                    $core[$path] = $all[$path];

        return $core;
    }

    /**
    * Fetch all supported ORM fields filterable by this search object.
    */
    function getSupportedFilters() {
        return static::getFilterableFields($this->getRoot());
    }


    /**
     * Get get supplemental matches for public queues.
     *
     */

    function getSupplementalMatches() {
        return array();
    }

    function getSupplementalCriteria() {
        return array();
    }

    /**
     * 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);

  /**
     * Fetch all searchable fileds, for the base object  which support quick filters.
     */
    function getFilterableFields($object) {
        $filters = array();
        foreach (static::getSearchableFields($object) as $p => $f) {
            list($label, $field) = $f;
            if ($field && $field->supportsQuickFilter())
                $filters[$p] = $f;
        }

        return $filters;
    }

    /**
     * Fetch the FormField instances used when for configuring a searchable
     * field in the user interface. This is the glue between a field
     * representing a searchable model field and the configuration of that
     * search in the user interface.
     *
     * Parameters:
     * $F - <array<string, FormField>> the label and the FormField instance
     *      representing the configurable search
     * $name - <string> ORM path for the search
     */
    static function getSearchField($F, $name) {
        list($label, $field) = $F;

        $pieces = array();
        $pieces["{$name}+search"] = new BooleanField(array(
            'id' => sprintf('%u', crc32($name)) >> 1,
            'configuration' => array(
                'desc' => $label ?: $field->getLocal('label'),
                'classes' => 'inline',
            ),
        ));
        $methods = $field->getSearchMethods();
aydreeihn's avatar
aydreeihn committed

        //remove future options for datetime fields that can't be in the future
        if (in_array($field->getLabel(), DateTimeField::getPastPresentLabels()))
          unset($methods['ndays'], $methods['future'], $methods['distfut']);

        $pieces["{$name}+method"] = new ChoiceField(array(
            'choices' => $methods,
            'default' => key($methods),
            'visibility' => new VisibilityConstraint(new Q(array(
                "{$name}+search__eq" => true,
            )), VisibilityConstraint::HIDDEN),
        ));
        $offs = 0;
        foreach ($field->getSearchMethodWidgets() as $m=>$w) {
            if (!$w)
                continue;
            list($class, $args) = $w;
            $args['required'] = true;
            $args['__searchval__'] = true;
            $args['visibility'] = new VisibilityConstraint(new Q(array(
                    "{$name}+method__eq" => $m,
                )), VisibilityConstraint::HIDDEN);
            $pieces["{$name}+{$m}"] = new $class($args);
        }
        return $pieces;
    }

    function getField($path) {
        $searchable = $this->getSupportedMatches();
        return $searchable[$path];
    }

    // Remove this and adjust advanced-search-criteria template to use the
    // getCriteria() list and getField()
    function getSearchFields($form=false) {
        $form = $form ?: $this->getForm();
        $searchable = $this->getCurrentSearchFields();
        $info = array();
        foreach ($form->getFields() as $f) {
            if (substr($f->get('name'), -7) == '+search') {
                $name = substr($f->get('name'), 0, -7);
                $value = null;
                // Determine the search method and fetch the original field
                if (($M = $form->getField("{$name}+method"))
                    && ($method = $M->getClean())
                    && (list(,$field) = $searchable[$name])
                ) {
                    // Request the field to generate a search Q for the
                    // search method and given value
                    if ($value = $form->getField("{$name}+{$method}"))
                        $value = $value->getClean();
                }
                $info[$name] = array(
                    'field' => $field,
                    'method' => $method,
                    'value' => $value,
                    'active' =>  $f->getClean(),
                );
            }
        }
        return $info;
    }

    /**
     * Take the criteria from the SavedSearch fields setup and isolate the
     * field name being search, the method used for searhing, and the method-
     * specific data entered in the UI.
     */
    static function isolateCriteria($criteria, $base='Ticket') {

        if (!is_array($criteria))

        $items = array();
        $searchable = static::getSearchableFields($base);
        foreach ($criteria as $k=>$v) {
            if (substr($k, -7) === '+method') {
                list($name,) = explode('+', $k, 2);
                if (!isset($searchable[$name]))
                    continue;

                // Require checkbox to be checked too
                if (!$criteria["{$name}+search"])
                    continue;

                // Lookup the field to search this condition
                list($label, $field) = $searchable[$name];
                // Get the search method
                $method = is_array($v) ? key($v) : $v;
                // Not all search methods require a value
                $value = $criteria["{$name}+{$method}"];

                $items[] = array($name, $method, $value);
            }
        }
        if (isset($criteria[':keywords'])
            && ($kw = $criteria[':keywords'])
        ) {
            $items[] = array(':keywords', null, $kw);
    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;
    }

Peter Rotich's avatar
Peter Rotich committed
    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'),
                'closed' =>         __('Closed Date'),
Peter Rotich's avatar
Peter Rotich committed
                'isoverdue' =>      __('Overdue'),
                'isanswered' =>     __('Answered'),
                'staff::getName' => __('Agent Assigned'),
                'team::getName' =>  __('Team Assigned'),
                'thread_count' =>   __('Thread Count'),
                'reopen_count' =>   __('Reopen Count'),
Peter Rotich's avatar
Peter Rotich committed
                'attachment_count' => __('Attachment Count'),
                ) + $cdata;

        return $fields;
    }

    function getExportFields($inherit=true) {
Peter Rotich's avatar
Peter Rotich committed

        $fields = array();
        if ($inherit
            && $this->parent_id
Peter Rotich's avatar
Peter Rotich committed
            && $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();
Peter Rotich's avatar
Peter Rotich committed
        }
        elseif ($this->isAQueue())
            $fields = $this->getExportableFields();
        if (!count($fields))
            $fields = $this->getExportableFields();
        return $fields;
    function getStandardColumns() {
        return $this->getColumns();
    }

    function getColumns($use_template=false) {
        if ($this->columns_id
            && ($q = CustomQueue::lookup($this->columns_id))
        ) {
            // Use columns from cited queue
            return $q->getColumns();
        }
        elseif ($this->parent_id
            && $this->hasFlag(self::FLAG_INHERIT_COLUMNS)
            && $this->parent
        ) {
            $columns = $this->parent->getColumns();
            foreach ($columns as $c)
                $c->setQueue($this);
            return $columns;
        elseif (count($this->columns)) {
            return $this->columns;
        // Use the columns of the "Open" queue as a default template
        if ($use_template && ($template = CustomQueue::lookup(1)))
            return $template->getColumns();

        // Last resort — use standard columns
        foreach (array(
            QueueColumn::placeholder(array(
Peter Rotich's avatar
Peter Rotich committed
                "id" => 1,
                "heading" => "Number",
                "primary" => 'number',
                "width" => 85,
Peter Rotich's avatar
Peter Rotich committed
                "bits" => QueueColumn::FLAG_SORTABLE,
                "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,
Peter Rotich's avatar
Peter Rotich committed
                "bits" => QueueColumn::FLAG_SORTABLE,
            QueueColumn::placeholder(array(
                "heading" => "Subject",
                "primary" => 'cdata__subject',
                "width" => 250,
Peter Rotich's avatar
Peter Rotich committed
                "bits" => QueueColumn::FLAG_SORTABLE,
                "filter" => "link:ticket",
                "annotations" => '[{"c":"TicketThreadCount","p":">"},{"c":"ThreadAttachmentCount","p":"a"},{"c":"OverdueFlagDecoration","p":"<"}]',
Peter Rotich's avatar
Peter Rotich committed
                "conditions" => '[{"crit":["isanswered","set",null],"prop":{"font-weight":"bold"}}]',
                "truncate" => 'ellipsis',
            )),
            QueueColumn::placeholder(array(
                "heading" => "From",
                "primary" => 'user__name',
                "width" => 150,
                "bits" => QueueColumn::FLAG_SORTABLE,
            QueueColumn::placeholder(array(
                "heading" => "Priority",
                "primary" => 'cdata__priority',
                "width" => 120,
                "bits" => QueueColumn::FLAG_SORTABLE,
            QueueColumn::placeholder(array(
                "heading" => "Assignee",
                "primary" => 'assignee',
                "width" => 100,
                "bits" => QueueColumn::FLAG_SORTABLE,
        ) as $col)
            $this->addColumn($col);

        return $this->getColumns();
    }

    function addColumn(QueueColumn $col) {
        $this->columns->add($col);
        $col->queue = $this;
    }

    function getSortOptions() {
        if ($this->inheritSorting() && $this->parent) {
            return $this->parent->getSortOptions();
        }
        return $this->sorts;
    }

    function getDefaultSortId() {
        if ($this->isDefaultSortInherited() && $this->parent
            && ($sort_id = $this->parent->getDefaultSortId())
        ) {
            return $sort_id;
        }
        return $this->sort_id;
    }

    function getDefaultSort() {
        if ($this->isDefaultSortInherited() && $this->parent
            && ($sort = $this->parent->getDefaultSort())
        ) {
            return $sort;
        }
        return $this->default_sort;
    }

    function getStatus() {
        return $this->hasFlag(self::FLAG_DISABLED)
            ? __('Disabled') : __('Active');
    function getChildren() {
        return $this->children;
    }

    function getPublicChildren() {
        return $this->children->findAll(array(
            'flags__hasbit' => self::FLAG_QUEUE
        ));
    }

    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
            ))
        ));
    }

    function export($options=array()) {
Peter Rotich's avatar
Peter Rotich committed

        if (!($query=$this->getBasicQuery()))
            return false;

        if (!($fields=$this->getExportFields()))
Peter Rotich's avatar
Peter Rotich committed
            return false;


        $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) {
        // Start with basic criteria
        $query = $this->getBasicQuery($form);

        // Apply quick filter
        if (isset($quick_filter)
            && ($qf = $this->getQuickFilterField($quick_filter))
        ) {
            $filter = @self::getOrmPath($this->getQuickFilter(), $query);
            $query = $qf->applyQuickFilter($query, $quick_filter,
        }

        // Apply column, annotations and conditions additions
        foreach ($this->getColumns() as $C) {
            $query = $C->mangleQuery($query, $this->getRoot());
    function getQuickFilter() {
        if ($this->filter == '::' && $this->parent) {
            return $this->parent->getQuickFilter();
        }
        return $this->filter;
    }

    function getQuickFilterField($value=null) {
        if ($this->filter == '::') {
            if ($this->parent) {
                return $this->parent->getQuickFilterField($value);
            }
        }
        elseif ($this->filter
            && ($fields = self::getSearchableFields($this->getRoot()))
            && (list(,$f) = @$fields[$this->filter])
            && $f->supportsQuickFilter()
        ) {
            $f->value = $value;
            return $f;
        }
    }

    /**
     * Get a description of a field in a search. Expects an entry from the
     * array retrieved in ::getSearchFields()
     */
    function describeField($info, $name=false) {
        $name = $name ?: $info['field']->get('label');
        return $info['field']->describeSearch($info['method'], $info['value'], $name);
    }

    function mangleQuerySet(QuerySet $qs, $form=false) {
        $qs = clone $qs;
        $searchable = $this->getSupportedMatches();

        // Figure out fields to search on
        foreach ($this->getCriteria() as $I) {
            list($name, $method, $value) = $I;

            // Consider keyword searching
            if ($name === ':keywords') {
                global $ost;
                $qs = $ost->searcher->find($value, $qs, false);
            }
            else {
                // XXX: Move getOrmPath to be more of a utility
                // Ensure the special join is created to support custom data joins
                $name = @static::getOrmPath($name, $qs);

                if (preg_match('/__answers!\d+__/', $name)) {
                    $qs->annotate(array($name => SqlAggregate::MAX($name)));
                }

                // Fetch a criteria Q for the query
                if (list(,$field) = $searchable[$name])
                    if ($q = $field->getSearchQ($method, $value, $name))
                        $qs = $qs->filter($q);
            }
        }

        return $qs;
    }

    function applyDefaultSort($qs) {
        // Apply default sort
        if ($sorter = $this->getDefaultSort()) {
            $qs = $sorter->applySort($qs, false, $this->getRoot());
        }
        return $qs;
    }

    function checkAccess(Staff $agent) {
        return $this->isPublic() || $this->checkOwnership($agent);
    }

    function checkOwnership(Staff $agent) {

        return ($agent->getId() == $this->staff_id &&
                !$this->isAQueue());
    }

    function isOwner(Staff $agent) {
        return $agent && $this->isPrivate() && $this->checkOwnership($agent);
aydreeihn's avatar
aydreeihn committed
    function isSaved() {
        return true;
    }

    function ignoreVisibilityConstraints(Staff $agent) {
aydreeihn's avatar
aydreeihn committed
        // For searches (not queues), some staff can have a permission to
aydreeihn's avatar
aydreeihn committed
        return ($this->isASearch()
                && $this->isOwner($agent)
                && $agent->canSearchEverything());
    }

    function inheritCriteria() {
        return $this->flags & self::FLAG_INHERIT_CRITERIA;
    }

    function inheritColumns() {
        return $this->hasFlag(self::FLAG_INHERIT_COLUMNS);
    }

    function useStandardColumns() {
        return !count($this->columns);
    }

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);
    }