diff --git a/bootstrap.php b/bootstrap.php index 2703395faa6a5548847cfa4590cbbf0f4067bc19..8b7ea7eb9e20e2e4e74ee554242ac110afc861ee 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -102,6 +102,7 @@ class Bootstrap { define('THREAD_COLLABORATOR_TABLE', $prefix.'thread_collaborator'); define('TICKET_STATUS_TABLE', $prefix.'ticket_status'); define('TICKET_PRIORITY_TABLE',$prefix.'ticket_priority'); + define('EVENT_TABLE',$prefix.'event'); define('TASK_TABLE', $prefix.'task'); define('TASK_CDATA_TABLE', $prefix.'task__cdata'); @@ -293,6 +294,10 @@ class Bootstrap { } if (extension_loaded('iconv')) iconv_set_encoding('internal_encoding', 'UTF-8'); + + function mb_str_wc($str) { + return count(preg_split('~[^\p{L}\p{N}\'].+~u', trim($str))); + } } function croak($message) { diff --git a/include/ajax.search.php b/include/ajax.search.php index 93da251e7d164e9548550dce3b35cf16051bbcf3..fabe2d810fdd8e694d566b418b656c028e94a916 100644 --- a/include/ajax.search.php +++ b/include/ajax.search.php @@ -135,7 +135,7 @@ class SearchAjaxAPI extends AjaxController { $_SESSION[$key] = $keep; } } - + function _hashCriteria($criteria, $size=10) { $parts = array(); foreach ($criteria as $C) { @@ -395,7 +395,7 @@ class SearchAjaxAPI extends AjaxController { if ($ids && is_array($ids)) $criteria = array('id__in' => $ids); - $counts = SavedQueue::ticketsCount($thisstaff, $criteria, 'q'); + $counts = SavedQueue::counts($thisstaff, $criteria); Http::response(200, false, 'application/json'); return $this->encode($counts); } diff --git a/include/api.tickets.php b/include/api.tickets.php index c209086a9d57a682462f2d2966c5fd41db69b176..8d20e391cfa1aebb66ad5d154a16f2fd1510d1ef 100644 --- a/include/api.tickets.php +++ b/include/api.tickets.php @@ -14,7 +14,8 @@ class TicketApiController extends ApiController { "attachments" => array("*" => array("name", "type", "data", "encoding", "size") ), - "message", "ip", "priorityId" + "message", "ip", "priorityId", + "system_emails", "thread_entry_recipients" ); # Fetch dynamic form field names for the given help topic and add # the names to the supported request structure diff --git a/include/class.config.php b/include/class.config.php index 004a1e8e8a44d2be9fdcc1cde54acd0a03300d73..d7518a91f947786ba15ba69a345f5ee2018a7281 100644 --- a/include/class.config.php +++ b/include/class.config.php @@ -30,10 +30,6 @@ class Config { # new settings and the corresponding default values. var $defaults = array(); # List of default values - - # Items - var $items = null; - function __construct($section=null, $defaults=array()) { if ($section) $this->section = $section; @@ -133,18 +129,11 @@ class Config { function destroy() { unset($this->session); - if ($this->items) - $this->items->delete(); - - return true; + return $this->items()->delete() > 0; } function items() { - - if (!isset($this->items)) - $this->items = ConfigItem::items($this->section, $this->section_column); - - return $this->items; + return ConfigItem::items($this->section, $this->section_column); } } diff --git a/include/class.export.php b/include/class.export.php index 4fc1bbf7bd1c2bcf00412ca74f1c45223fae4f2f..2ea048ae92996d72523d50f28849eb6a2cc85490 100644 --- a/include/class.export.php +++ b/include/class.export.php @@ -76,7 +76,7 @@ class Export { ->aggregate(array('count' => SqlAggregate::COUNT('entries__attachments__id'))), 'reopen_count' => TicketThread::objects() ->filter(array('ticket__ticket_id' => new SqlField('ticket_id', 1))) - ->filter(array('events__annulled' => 0, 'events__state' => 'reopened')) + ->filter(array('events__annulled' => 0, 'events__event_id' => Event::getIdByName('reopened'))) ->aggregate(array('count' => SqlAggregate::COUNT('events__id'))), 'thread_count' => TicketThread::objects() ->filter(array('ticket__ticket_id' => new SqlField('ticket_id', 1))) diff --git a/include/class.file.php b/include/class.file.php index e61b5afe76516fcecfd880359bb6193c13164b4e..5bbd3d8859b1303648bc6464a9075040b8a1e3c9 100644 --- a/include/class.file.php +++ b/include/class.file.php @@ -258,7 +258,9 @@ class AttachmentFile extends VerySimpleModel { $type = $this->getType() ?: 'application/octet-stream'; if (isset($_REQUEST['overridetype'])) $type = $_REQUEST['overridetype']; - Http::download($name ?: $this->getName(), $type, null, 'inline'); + elseif (!strcasecmp($disposition, 'attachment')) + $type = 'application/octet-stream'; + Http::download($name ?: $this->getName(), $type, null, $disposition); header('Content-Length: '.$this->getSize()); $this->sendData(false); exit(); diff --git a/include/class.format.php b/include/class.format.php index a2d72340dcf02e3cba45990e4e455ab8146604e3..d3d9d7ae9ae68e7994e302fc70be0d079f6cae35 100644 --- a/include/class.format.php +++ b/include/class.format.php @@ -667,20 +667,20 @@ class Format { '%x', $timezone ?: $cfg->getTimezone(), $user); } - function datetime($timestamp, $fromDb=true, $timezone=false, $user=false) { + function datetime($timestamp, $fromDb=true, $format=false, $timezone=false, $user=false) { global $cfg; return self::__formatDate($timestamp, - $cfg->getDateTimeFormat(), $fromDb, + $format ?: $cfg->getDateTimeFormat(), $fromDb, IDF_SHORT, IDF_SHORT, '%x %X', $timezone ?: $cfg->getTimezone(), $user); } - function daydatetime($timestamp, $fromDb=true, $timezone=false, $user=false) { + function daydatetime($timestamp, $fromDb=true, $format=false, $timezone=false, $user=false) { global $cfg; return self::__formatDate($timestamp, - $cfg->getDayDateTimeFormat(), $fromDb, + $format ?: $cfg->getDayDateTimeFormat(), $fromDb, IDF_FULL, IDF_SHORT, '%x %X', $timezone ?: $cfg->getTimezone(), $user); } diff --git a/include/class.forms.php b/include/class.forms.php index 4fb4de4609802681c71b57d2c9b8009d66d0aa77..1b13c175f637adc8614e746aa10f522d4ade7d1c 100644 --- a/include/class.forms.php +++ b/include/class.forms.php @@ -1960,6 +1960,40 @@ class ChoiceField extends FormField { } } +class NumericField extends FormField { + + function getSearchMethods() { + return array( + 'equal' => __('Equal'), + 'greater' => __('Greater Than'), + 'less' => __('Less Than'), + ); + } + + function getSearchMethodWidgets() { + return array( + 'equal' => array('TextboxField', array( + 'configuration' => array( + 'validator' => 'number', + 'size' => 6 + ), + )), + 'greater' => array('TextboxField', array( + 'configuration' => array( + 'validator' => 'number', + 'size' => 6 + ), + )), + 'less' => array('TextboxField', array( + 'configuration' => array( + 'validator' => 'number', + 'size' => 6 + ), + )), + ); + } +} + class DatetimeField extends FormField { static $widget = 'DatetimePickerWidget'; @@ -2043,7 +2077,7 @@ class DatetimeField extends FormField { function to_php($value) { - if (strtotime($value) <= 0) + if (!is_numeric($value) && strtotime($value) <= 0) return 0; return $value; @@ -2056,8 +2090,9 @@ class DatetimeField extends FormField { return ''; $config = $this->getConfiguration(); + $format = $config['format'] ?: false; if ($config['gmt']) - return $this->format((int) $datetime->format('U')); + return $this->format((int) $datetime->format('U'), $format); // Force timezone if field has one. if ($config['timezone']) { @@ -2066,10 +2101,10 @@ class DatetimeField extends FormField { } $value = $this->format($datetime->format('U'), - $datetime->getTimezone()->getName()); - + $datetime->getTimezone()->getName(), + $format); // No need to show timezone - if (!$config['time']) + if (!$config['time'] || $format) return $value; // Display is NOT timezone aware show entry's timezone. @@ -2083,16 +2118,16 @@ class DatetimeField extends FormField { return ($timestamp > 0) ? $timestamp : ''; } - function format($timestamp, $timezone=false) { + function format($timestamp, $timezone=false, $format=false) { if (!$timestamp || $timestamp <= 0) return ''; $config = $this->getConfiguration(); if ($config['time']) - $formatted = Format::datetime($timestamp, false, $timezone); + $formatted = Format::datetime($timestamp, false, $format, $timezone); else - $formatted = Format::date($timestamp, false, false, $timezone); + $formatted = Format::date($timestamp, false, $format, $timezone); return $formatted; } diff --git a/include/class.i18n.php b/include/class.i18n.php index 17709bee89b112527e2158910641d18608c64555..ab692eb6292cd2a4181249920eadc04006054eeb 100644 --- a/include/class.i18n.php +++ b/include/class.i18n.php @@ -64,6 +64,7 @@ class Internationalization { 'ticket_status.yaml' => 'TicketStatus::__create', // Role 'role.yaml' => 'Role::__create', + 'event.yaml' => 'Event::__create', 'file.yaml' => 'AttachmentFile::__create', 'sequence.yaml' => 'Sequence::__create', 'queue_column.yaml' => 'QueueColumn::__create', diff --git a/include/class.list.php b/include/class.list.php index e5c84fbd91b58cc2f8dc742bbec745bba1151882..f8055805c2e692eec4fb446db79c8585241260f8 100644 --- a/include/class.list.php +++ b/include/class.list.php @@ -1421,6 +1421,9 @@ implements CustomListItem, TemplateVariable, Searchable { } function display() { + + return $this->getLocalName(); + return sprintf('<a class="preview" href="#" data-preview="#list/%d/items/%d/preview">%s</a>', $this->getListId(), diff --git a/include/class.orm.php b/include/class.orm.php index 106e11465db81e2093041086435f67bcbdb3a627..cd30ffa974c1ed1cb95137b0d357a25b1d1b7610 100644 --- a/include/class.orm.php +++ b/include/class.orm.php @@ -333,6 +333,11 @@ class VerySimpleModel { return static::getMeta()->newInstance($row); } + function __wakeup() { + // If a model is stashed in a session, refresh the model from the database + $this->refetch(); + } + function get($field, $default=false) { if (array_key_exists($field, $this->ht)) return $this->ht[$field]; @@ -1142,6 +1147,7 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl const OPT_NOSORT = 'nosort'; const OPT_NOCACHE = 'nocache'; const OPT_MYSQL_FOUND_ROWS = 'found_rows'; + const OPT_INDEX_HINT = 'indexhint'; const ITER_MODELS = 1; const ITER_HASH = 2; @@ -1281,6 +1287,10 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl return $this; } + function addExtraJoin(array $join) { + return $this->extra(array('joins' => array($join))); + } + function distinct() { foreach (func_get_args() as $D) $this->distinct[] = $D; @@ -1477,6 +1487,18 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl return isset($this->options[$option]); } + function getOption($option) { + return @$this->options[$option] ?: false; + } + + function setOption($option, $value) { + $this->options[$option] = $value; + } + + function clearOption($option) { + unset($this->options[$option]); + } + function countSelectFields() { $count = count($this->values) + count($this->annotations); if (isset($this->extra['select'])) @@ -2611,13 +2633,22 @@ class SqlCompiler { foreach ($queryset->extra['tables'] as $S) { $join = ' JOIN '; // Left joins require an ON () clause - if ($lastparen = strrpos($S, '(')) { - if (preg_match('/\bon\b/i', substr($S, $lastparen - 4, 4))) - $join = ' LEFT' . $join; - } + // TODO: Have a way to indicate a LEFT JOIN $sql .= $join.$S; } } + + // Add extra joins from QuerySet + if (isset($queryset->extra['joins'])) { + foreach ($queryset->extra['joins'] as $J) { + list($base, $constraints, $alias) = $J; + $join = $constraints ? ' LEFT JOIN ' : ' JOIN '; + $sql .= "{$join}{$base} $alias"; + if ($constraints instanceof Q) + $sql .= ' ON ('.$this->compileQ($constraints, $queryset->model).')'; + } + } + return $sql; } @@ -2965,6 +2996,7 @@ class MySqlCompiler extends SqlCompiler { $meta = $model::getMeta(); $table = $this->quote($meta['table']).' '.$rootAlias; // Handle related tables + $need_group_by = false; if ($queryset->related) { $count = 0; $fieldMap = $theseFields = array(); @@ -3010,13 +3042,16 @@ class MySqlCompiler extends SqlCompiler { } // Support retrieving only a list of values rather than a model elseif ($queryset->values) { + $additional_group_by = array(); foreach ($queryset->values as $alias=>$v) { list($f) = $this->getField($v, $model); $unaliased = $f; if ($f instanceof SqlFunction) { $fields[$f->toSql($this, $model, $alias)] = true; if ($f instanceof SqlAggregate) { - // Don't group_by aggregate expressions + // Don't group_by aggregate expressions, but if there is an + // aggergate expression, then we need a GROUP BY clause. + $need_group_by = true; continue; } } @@ -3028,8 +3063,10 @@ class MySqlCompiler extends SqlCompiler { // If there are annotations, add in these fields to the // GROUP BY clause if ($queryset->annotations && !$queryset->distinct) - $group_by[] = $unaliased; + $additional_group_by[] = $unaliased; } + if ($need_group_by && $additional_group_by) + $group_by = array_merge($group_by, $additional_group_by); } // Simple selection from one table elseif (!$queryset->aggregated) { @@ -3050,6 +3087,8 @@ class MySqlCompiler extends SqlCompiler { foreach ($queryset->annotations as $alias=>$A) { // The root model will receive the annotations, add in the // annotation after the root model's fields + if ($A instanceof SqlAggregate) + $need_group_by = true; $T = $A->toSql($this, $model, $alias); if ($fieldMap) { array_splice($fields, count($fieldMap[0][0]), 0, array($T)); @@ -3061,7 +3100,7 @@ class MySqlCompiler extends SqlCompiler { } } // If no group by has been set yet, use the root model pk - if (!$group_by && !$queryset->aggregated && !$queryset->distinct) { + if (!$group_by && !$queryset->aggregated && !$queryset->distinct && $need_group_by) { foreach ($meta['pk'] as $pk) $group_by[] = $rootAlias .'.'. $pk; } @@ -3083,12 +3122,15 @@ class MySqlCompiler extends SqlCompiler { $group_by = $group_by ? ' GROUP BY '.implode(', ', $group_by) : ''; $joins = $this->getJoins($queryset); + if ($hint = $queryset->getOption(QuerySet::OPT_INDEX_HINT)) { + $hint = " USE INDEX ({$hint})"; + } $sql = 'SELECT '; if ($queryset->hasOption(QuerySet::OPT_MYSQL_FOUND_ROWS)) $sql .= 'SQL_CALC_FOUND_ROWS '; $sql .= implode(', ', $fields).' FROM ' - .$table.$joins.$where.$group_by.$having.$sort; + .$table.$hint.$joins.$where.$group_by.$having.$sort; // UNIONS if ($queryset->chain) { // If the main query is sorted, it will need parentheses diff --git a/include/class.pagenate.php b/include/class.pagenate.php index b20ad52a5da542faa088d21d31045a338761c313..70d1ca1262d3111ed962bc54a5233d335fdd4f2a 100644 --- a/include/class.pagenate.php +++ b/include/class.pagenate.php @@ -22,6 +22,7 @@ class PageNate { var $total; var $page; var $pages; + var $approx=false; function __construct($total,$page,$limit=20,$url='') { @@ -32,7 +33,7 @@ class PageNate { $this->setTotal($total); } - function setTotal($total) { + function setTotal($total, $approx=false) { $this->total = intval($total); $this->pages = ceil( $this->total / $this->limit ); @@ -42,6 +43,7 @@ class PageNate { if (($this->limit-1)*$this->start > $this->total) { $this->start -= $this->start % $this->limit; } + $this->approx = $approx; } function setURL($url='',$vars='') { @@ -97,8 +99,12 @@ class PageNate { } $html=__('Showing')." "; if ($this->total > 0) { - $html .= sprintf(__('%1$d - %2$d of %3$d' /* Used in pagination output */), - $start, $end, $this->total); + if ($this->approx) + $html .= sprintf(__('%1$d - %2$d of about %3$d' /* Used in pagination output */), + $start, $end, $this->total); + else + $html .= sprintf(__('%1$d - %2$d of %3$d' /* Used in pagination output */), + $start, $end, $this->total); }else{ $html .= " 0 "; } diff --git a/include/class.queue.php b/include/class.queue.php index 5d23711784dec4364be188ba6f1308c22108596a..819327074d1ed64219afa96794c47897ddad0d61 100644 --- a/include/class.queue.php +++ b/include/class.queue.php @@ -174,10 +174,7 @@ class CustomQueue extends VerySimpleModel { */ 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, @@ -188,11 +185,17 @@ class CustomQueue extends VerySimpleModel { 'classes' => 'full-width headline', 'placeholder' => __('Keywords — Optional'), ), + 'validators' => function($self, $v) { + if (mb_str_wc($v) > 3) + $self->addError(__('Search term cannot have more than 3 keywords')); + }, )), ); + + $searchable = $this->getCurrentSearchFields($source); } - foreach ($searchable as $path=>$field) + foreach ($searchable ?: array() as $path => $field) $fields = array_merge($fields, static::getSearchField($field, $path)); $form = new AdvancedSearchForm($fields, $source); @@ -574,7 +577,7 @@ class CustomQueue extends VerySimpleModel { continue; $name = $f->get('name') ?: 'field_'.$f->get('id'); - $key = 'cdata.'.$name; + $key = 'cdata__'.$name; $cdata[$key] = $f->getLocal('label'); } @@ -582,22 +585,22 @@ class CustomQueue extends VerySimpleModel { $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'), + 'cdata__subject' => __('Subject'), + 'user__name' => __('From'), + 'user__emails__address' => __('From Email'), + 'cdata__priority' => __('Priority'), + 'dept_id' => __('Department'), + 'topic_id' => __('Help Topic'), 'source' => __('Source'), - 'status::getName' =>__('Current Status'), + 'status__id' =>__('Current Status'), 'lastupdate' => __('Last Updated'), 'est_duedate' => __('SLA Due Date'), 'duedate' => __('Due Date'), 'closed' => __('Closed Date'), 'isoverdue' => __('Overdue'), 'isanswered' => __('Answered'), - 'staff::getName' => __('Agent Assigned'), - 'team::getName' => __('Team Assigned'), + 'staff_id' => __('Agent Assigned'), + 'team_id' => __('Team Assigned'), 'thread_count' => __('Thread Count'), 'reopen_count' => __('Reopen Count'), 'attachment_count' => __('Attachment Count'), @@ -629,6 +632,22 @@ class CustomQueue extends VerySimpleModel { return $fields; } + function getExportColumns($fields=array()) { + $columns = array(); + $fields = $fields ?: $this->getExportFields(); + $i = 0; + foreach ($fields as $path => $label) { + $c = QueueColumn::placeholder(array( + 'id' => $i++, + 'heading' => $label, + 'primary' => $path, + )); + $c->setQueue($this); + $columns[$path] = $c; + } + return $columns; + } + function getStandardColumns() { return $this->getColumns(); } @@ -775,14 +794,13 @@ class CustomQueue extends VerySimpleModel { } function export($options=array()) { + global $thisstaff; - if (!($query=$this->getBasicQuery())) - return false; - - if (!($fields=$this->getExportFields())) + if (!$thisstaff + || !($query=$this->getBasicQuery()) + || !($fields=$this->getExportFields())) return false; - $filename = sprintf('%s Tickets-%s.csv', $this->getName(), strftime('%Y%m%d')); @@ -799,14 +817,45 @@ class CustomQueue extends VerySimpleModel { $filename ="$filename.csv"; } - if (isset($opts['delimiter'])) + if (isset($opts['delimiter']) && !$options['delimiter']) $options['delimiter'] = $opts['delimiter']; } + // Apply columns + $columns = $this->getExportColumns($fields); + $headers = array(); // Reset fields based on validity of columns + foreach ($columns as $column) { + $query = $column->mangleQuery($query, $this->getRoot()); + $headers[] = $column->getHeading(); + } + + // Apply visibility + if (!$this->ignoreVisibilityConstraints($thisstaff)) + $query->filter($thisstaff->getTicketsVisibility()); + + // Render Util + $render = function ($row) use($columns) { + if (!$row) return false; + + $record = array(); + foreach ($columns as $path => $column) { + $record[] = (string) $column->from_query($row) ?: + $row[$path] ?: ''; + } + return $record; + }; - return Export::saveTickets($query, $fields, $filename, 'csv', - $options); + $delimiter = $options['delimiter'] ?: + Internationalization::getCSVDelimiter(); + $output = fopen('php://output', 'w'); + Http::download($filename, "text/csv"); + fputs($output, chr(0xEF) . chr(0xBB) . chr(0xBF)); + fputcsv($output, $headers, $delimiter); + foreach ($query as $row) + fputcsv($output, $render($row), $delimiter); + fclose($output); + exit(); } /** @@ -952,7 +1001,8 @@ class CustomQueue extends VerySimpleModel { } function inheritCriteria() { - return $this->flags & self::FLAG_INHERIT_CRITERIA; + return $this->flags & self::FLAG_INHERIT_CRITERIA && + $this->parent_id; } function inheritColumns() { @@ -1005,8 +1055,7 @@ class CustomQueue extends VerySimpleModel { } function isPrivate() { - return !$this->isAQueue() && !$this->isPublic() && - $this->staff_id; + return !$this->isAQueue() && $this->staff_id; } function isPublic() { @@ -1035,6 +1084,57 @@ class CustomQueue extends VerySimpleModel { $this->clearFlag(self::FLAG_DISABLED); } + function getRoughCount() { + if (($count = $this->getRoughCountAPC()) !== false) + return $count; + + $query = Ticket::objects(); + $Q = $this->getBasicQuery(); + $expr = SqlCase::N()->when(new SqlExpr(new Q($Q->constraints)), + new SqlField('ticket_id')); + $query = $query->aggregate(array( + "ticket_count" => SqlAggregate::COUNT($expr) + )); + + $row = $query->values()->one(); + return $row['ticket_count']; + } + + function getRoughCountAPC() { + if (!function_exists('apcu_store')) + return false; + + $key = "rough.counts.".SECRET_SALT; + $cached = false; + $counts = apcu_fetch($key, $cached); + if ($cached === true && isset($counts["q{$this->id}"])) + return $counts["q{$this->id}"]; + + // Fetch rough counts of all queues. That is, fetch a total of the + // counts based on the queue criteria alone. Do no consider agent + // access. This should be fast and "rought" + $queues = static::objects() + ->filter(['flags__hasbit' => CustomQueue::FLAG_PUBLIC]) + ->exclude(['flags__hasbit' => CustomQueue::FLAG_DISABLED]); + + $query = Ticket::objects(); + $prefix = ""; + + foreach ($queues as $queue) { + $Q = $queue->getBasicQuery(); + $expr = SqlCase::N()->when(new SqlExpr(new Q($Q->constraints)), + new SqlField('ticket_id')); + $query = $query->aggregate(array( + "q{$queue->id}" => SqlAggregate::COUNT($expr) + )); + } + + $counts = $query->values()->one(); + + apcu_store($key, $counts, 900); + return @$counts["q{$this->id}"]; + } + function updateExports($fields, $save=true) { if (!$fields) @@ -1130,8 +1230,7 @@ class CustomQueue extends VerySimpleModel { // 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_CRITERIA, $this->parent_id); $this->setFlag(self::FLAG_INHERIT_COLUMNS, isset($vars['inherit-columns'])); $this->setFlag(self::FLAG_INHERIT_EXPORT, @@ -1252,6 +1351,8 @@ class CustomQueue extends VerySimpleModel { if ($this->dirty) $this->updated = SqlFunction::NOW(); + + $clearCounts = ($this->dirty || $this->__new__); if (!($rv = parent::save($refetch || $this->dirty))) return $rv; @@ -1270,6 +1371,11 @@ class CustomQueue extends VerySimpleModel { }; $move_children($this); } + + // Refetch the queue counts + if ($clearCounts) + SavedQueue::clearCounts(); + return $this->columns->saveAll() && $this->exports->saveAll() && $this->sorts->saveAll(); @@ -1290,23 +1396,35 @@ class CustomQueue extends VerySimpleModel { * visible queues. * $pid - <int> parent_id of root queue. Default is zero (top-level) */ - static function getHierarchicalQueues(Staff $staff, $pid=0) { - $all = static::objects() + static function getHierarchicalQueues(Staff $staff, $pid=0, + $primary=true) { + $query = static::objects() + ->annotate(array('_sort' => SqlCase::N() + ->when(array('sort' => 0), 999) + ->otherwise(new SqlField('sort')))) ->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(); - + ->order_by('parent_id', '_sort', 'title'); + $all = $query->asArray(); // Find all the queues with a given parent - $for_parent = function($pid) use ($all, &$for_parent) { + $for_parent = function($pid) use ($primary, $all, &$for_parent) { $results = []; foreach (new \ArrayIterator($all) as $q) { - if ($q->parent_id == $pid) - $results[] = [ $q, $for_parent($q->getId()) ]; + if ($q->parent_id != $pid) + continue; + + if ($pid == 0 && ( + ($primary && !$q->isAQueue()) + || (!$primary && $q->isAQueue()))) + continue; + + $results[] = [ $q, $for_parent($q->getId()) ]; } + return $results; }; @@ -1346,8 +1464,10 @@ class CustomQueue extends VerySimpleModel { $queue = new static($vars); $queue->created = SqlFunction::NOW(); - if (!isset($vars['flags'])) + if (!isset($vars['flags'])) { + $queue->setFlag(self::FLAG_PUBLIC); $queue->setFlag(self::FLAG_QUEUE); + } return $queue; } @@ -1427,7 +1547,7 @@ abstract class QueueColumnAnnotation { } // Add the annotation to a QuerySet - abstract function annotate($query); + 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 @@ -1462,6 +1582,17 @@ abstract class QueueColumnAnnotation { 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]; + } } class TicketThreadCount @@ -1470,9 +1601,10 @@ extends QueueColumnAnnotation { static $qname = '_thread_count'; static $desc = /* @trans */ 'Thread Count'; - function annotate($query) { + function annotate($query, $name=false) { + $name = $name ?: static::$qname; return $query->annotate(array( - static::$qname => TicketThread::objects() + $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'))) @@ -1500,11 +1632,12 @@ extends QueueColumnAnnotation { static $qname = '_reopen_count'; static $desc = /* @trans */ 'Reopen Count'; - function annotate($query) { + function annotate($query, $name=false) { + $name = $name ?: static::$qname; return $query->annotate(array( - static::$qname => TicketThread::objects() + $name => TicketThread::objects() ->filter(array('ticket__ticket_id' => new SqlField('ticket_id', 1))) - ->filter(array('events__annulled' => 0, 'events__state' => 'reopened')) + ->filter(array('events__annulled' => 0, 'events__event_id' => Event::getIdByName('reopened'))) ->aggregate(array('count' => SqlAggregate::COUNT('events__id'))) )); } @@ -1531,10 +1664,11 @@ extends QueueColumnAnnotation { static $qname = '_att_count'; static $desc = /* @trans */ 'Attachment Count'; - function annotate($query) { + function annotate($query, $name=false) { // TODO: Convert to Thread attachments + $name = $name ?: static::$qname; return $query->annotate(array( - static::$qname => TicketThread::objects() + $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'))) @@ -1561,9 +1695,10 @@ extends QueueColumnAnnotation { static $qname = '_collabs'; static $desc = /* @trans */ 'Collaborator Count'; - function annotate($query) { + function annotate($query, $name=false) { + $name = $name ?: static::$qname; return $query->annotate(array( - static::$qname => TicketThread::objects() + $name => TicketThread::objects() ->filter(array('ticket__ticket_id' => new SqlField('ticket_id', 1))) ->aggregate(array('count' => SqlAggregate::COUNT('collaborators__id'))) )); @@ -1588,7 +1723,7 @@ extends QueueColumnAnnotation { static $icon = 'exclamation'; static $desc = /* @trans */ 'Overdue Icon'; - function annotate($query) { + function annotate($query, $name=false) { return $query->values('isoverdue'); } @@ -1607,7 +1742,7 @@ extends QueueColumnAnnotation { static $icon = 'phone'; static $desc = /* @trans */ 'Ticket Source'; - function annotate($query) { + function annotate($query, $name=false) { return $query->values('source'); } @@ -1622,7 +1757,7 @@ extends QueueColumnAnnotation { static $icon = "lock"; static $desc = /* @trans */ 'Locked'; - function annotate($query) { + function annotate($query, $name=false) { global $thisstaff; return $query @@ -1649,7 +1784,7 @@ extends QueueColumnAnnotation { static $icon = "user"; static $desc = /* @trans */ 'Assignee Avatar'; - function annotate($query) { + function annotate($query, $name=false) { return $query->values('staff_id', 'team_id'); } @@ -1688,7 +1823,7 @@ extends QueueColumnAnnotation { static $icon = "user"; static $desc = /* @trans */ 'User Avatar'; - function annotate($query) { + function annotate($query, $name=false) { return $query->values('user_id'); } @@ -1984,6 +2119,7 @@ extends VerySimpleModel { var $_annotations; var $_conditions; var $_queue; // Apparent queue if being inherited + var $_fields; function getId() { return $this->id; @@ -2022,6 +2158,25 @@ extends VerySimpleModel { $this->_queue = $queue; } + function getFields() { + if (!isset($this->_fields)) { + $root = ($q = $this->getQueue()) ? $q->getRoot() : 'Ticket'; + $fields = CustomQueue::getSearchableFields($root); + $primary = CustomQueue::getOrmPath($this->primary); + $secondary = CustomQueue::getOrmPath($this->secondary); + if (($F = $fields[$primary]) && (list(,$field) = $F)) + $this->_fields[$primary] = $field; + if (($F = $fields[$secondary]) && (list(,$field) = $F)) + $this->_fields[$secondary] = $field; + } + return $this->_fields; + } + + function getField($path=null) { + $fields = $this->getFields(); + return @$fields[$path ?: $this->primary]; + } + function getWidth() { return $this->width ?: 100; } @@ -2089,29 +2244,36 @@ extends VerySimpleModel { } function renderBasicValue($row) { - $root = ($q = $this->getQueue()) ? $q->getRoot() : 'Ticket'; - $fields = CustomQueue::getSearchableFields($root); + $fields = $this->getFields(); $primary = CustomQueue::getOrmPath($this->primary); $secondary = CustomQueue::getOrmPath($this->secondary); // Return a lazily ::display()ed value so that the value to be // rendered by the field could be changed or display()ed when // converted to a string. - if (($F = $fields[$primary]) - && (list(,$field) = $F) - && ($T = $field->from_query($row, $primary)) + && ($T = $F->from_query($row, $primary)) ) { - return new LazyDisplayWrapper($field, $T); + return new LazyDisplayWrapper($F, $T); } if (($F = $fields[$secondary]) - && (list(,$field) = $F) - && ($T = $field->from_query($row, $secondary)) + && ($T = $F->from_query($row, $secondary)) ) { - return new LazyDisplayWrapper($field, $T); + return new LazyDisplayWrapper($F, $T); } - return new LazyDisplayWrapper($field, ''); + return new LazyDisplayWrapper($F, ''); + } + + function from_query($row) { + if (!($f = $this->getField($this->primary))) + return ''; + + $val = $f->to_php($f->from_query($row, $this->primary)); + if (!is_string($val)) + $val = $f->display($val); + + return $val; } function applyTruncate($text, $row) { @@ -2155,14 +2317,12 @@ extends VerySimpleModel { function mangleQuery($query, $root=null) { // Basic data - $fields = CustomQueue::getSearchableFields($root ?: $this->getQueue()->getRoot()); - if ($primary = $fields[$this->primary]) { - list(,$field) = $primary; + $fields = $this->getFields(); + if ($field = $fields[$this->primary]) { $query = $this->addToQuery($query, $field, CustomQueue::getOrmPath($this->primary, $query)); } - if ($secondary = $fields[$this->secondary]) { - list(,$field) = $secondary; + if ($field = $fields[$this->secondary]) { $query = $this->addToQuery($query, $field, CustomQueue::getOrmPath($this->secondary, $query)); } @@ -2557,6 +2717,7 @@ extends VerySimpleModel { ); var $_columns; + var $_extra; function getRoot($hint=false) { switch ($hint ?: $this->root) { @@ -2574,6 +2735,12 @@ extends VerySimpleModel { return $this->id; } + function getExtra() { + if (isset($this->extra) && !isset($this->_extra)) + $this->_extra = JsonDataParser::decode($this->extra); + return $this->_extra; + } + function applySort(QuerySet $query, $reverse=false, $root=false) { $fields = CustomQueue::getSearchableFields($this->getRoot($root)); foreach ($this->getColumnPaths() as $path=>$descending) { @@ -2584,6 +2751,10 @@ extends VerySimpleModel { CustomQueue::getOrmPath($path, $query)); } } + // Add index hint if defined + if (($extra = $this->getExtra()) && isset($extra['index'])) { + $query->setOption(QuerySet::OPT_INDEX_HINT, $extra['index']); + } return $query; } @@ -2617,6 +2788,11 @@ extends VerySimpleModel { array('id' => $this->id)); } + function getAdvancedConfigForm($source=false) { + return new QueueSortAdvancedConfigForm($source ?: $this->getExtra(), + array('id' => $this->id)); + } + static function forQueue(CustomQueue $queue) { return static::objects()->filter([ 'root' => $queue->root ?: 'T', @@ -2652,6 +2828,11 @@ extends VerySimpleModel { $this->columns = JsonDataEncoder::encode($columns); } + if ($this->getExtra() !== null) { + $extra = $this->getAdvancedConfigForm($vars)->getClean(); + $this->extra = JsonDataEncoder::encode($extra); + } + if (count($errors)) return false; @@ -2911,3 +3092,24 @@ extends AbstractForm { ); } } + +class QueueSortAdvancedConfigForm +extends AbstractForm { + function getInstructions() { + return __('If unsure, leave these options blank and unset'); + } + + function buildFields() { + return array( + 'index' => new TextboxField(array( + 'label' => __('Database Index'), + 'hint' => __('Use this index when sorting on this column'), + 'required' => false, + 'layout' => new GridFluidCell(12), + 'configuration' => array( + 'placeholder' => __('Automatic'), + ), + )), + ); + } +} diff --git a/include/class.report.php b/include/class.report.php index 12d7322d3dbc6665fed18b86d8b738d49eb091ba..dbff0ba38f68714692c091d9214b079223351360 100644 --- a/include/class.report.php +++ b/include/class.report.php @@ -77,26 +77,31 @@ class OverviewReport { function getPlotData() { list($start, $stop) = $this->getDateRange(); + $states = array("created", "closed", "reopened", "assigned", "overdue", "transferred"); + $event_ids = Event::getIds(); # Fetch all types of events over the timeframe - $res = db_query('SELECT DISTINCT(state) FROM '.THREAD_EVENT_TABLE + $res = db_query('SELECT DISTINCT(E.name) FROM '.THREAD_EVENT_TABLE + .' T JOIN '.EVENT_TABLE . ' E ON E.id = T.event_id' .' WHERE timestamp BETWEEN '.$start.' AND '.$stop - .' AND state IN ("created", "closed", "reopened", "assigned", "overdue", "transferred")' + .' AND T.event_id IN ('.implode(",",$event_ids).')' .' ORDER BY 1'); $events = array(); while ($row = db_fetch_row($res)) $events[] = $row[0]; # TODO: Handle user => db timezone offset # XXX: Implement annulled column from the %ticket_event table - $res = db_query('SELECT state, DATE_FORMAT(timestamp, \'%Y-%m-%d\'), ' + $res = db_query('SELECT H.name, DATE_FORMAT(timestamp, \'%Y-%m-%d\'), ' .'COUNT(DISTINCT T.id)' .' FROM '.THREAD_EVENT_TABLE. ' E ' + . ' LEFT JOIN '.EVENT_TABLE. ' H + ON (E.event_id = H.id)' .' JOIN '.THREAD_TABLE. ' T ON (T.id = E.thread_id AND T.object_type = "T") ' .' WHERE E.timestamp BETWEEN '.$start.' AND '.$stop .' AND NOT annulled' - .' AND E.state IN ("created", "closed", "reopened", "assigned", "overdue", "transferred")' - .' GROUP BY E.state, DATE_FORMAT(E.timestamp, \'%Y-%m-%d\')' + .' AND E.event_id IN ('.implode(",",$event_ids).')' + .' GROUP BY E.event_id, DATE_FORMAT(E.timestamp, \'%Y-%m-%d\')' .' ORDER BY 2, 1'); # Initialize array of plot values $plots = array(); @@ -139,6 +144,11 @@ class OverviewReport { function getTabularData($group='dept') { global $thisstaff; + $event_ids = Event::getIds(); + $event = function ($name) use ($event_ids) { + return $event_ids[$name]; + }; + list($start, $stop) = $this->getDateRange(); $times = ThreadEvent::objects() ->constrain(array( @@ -148,8 +158,8 @@ class OverviewReport { )) ->constrain(array( 'thread__events' => array( - 'thread__events__state' => 'created', - 'state' => 'closed', + 'thread__events__event_id' => $event('created'), + 'event_id' => $event('closed'), 'annulled' => 0, ), )) @@ -174,27 +184,27 @@ class OverviewReport { ->aggregate(array( 'Opened' => SqlAggregate::COUNT( SqlCase::N() - ->when(new Q(array('state' => 'created')), 1) + ->when(new Q(array('event_id' => $event('created'))), 1) ), 'Assigned' => SqlAggregate::COUNT( SqlCase::N() - ->when(new Q(array('state' => 'assigned')), 1) + ->when(new Q(array('event_id' => $event('assigned'))), 1) ), 'Overdue' => SqlAggregate::COUNT( SqlCase::N() - ->when(new Q(array('state' => 'overdue')), 1) + ->when(new Q(array('event_id' => $event('overdue'))), 1) ), 'Closed' => SqlAggregate::COUNT( SqlCase::N() - ->when(new Q(array('state' => 'closed')), 1) + ->when(new Q(array('event_id' => $event('closed'))), 1) ), 'Reopened' => SqlAggregate::COUNT( SqlCase::N() - ->when(new Q(array('state' => 'reopened')), 1) + ->when(new Q(array('event_id' => $event('reopened'))), 1) ), 'Deleted' => SqlAggregate::COUNT( SqlCase::N() - ->when(new Q(array('state' => 'deleted')), 1) + ->when(new Q(array('event_id' => $event('deleted'))), 1) ), )); diff --git a/include/class.search.php b/include/class.search.php index 8729bffa8937ca9eef4dcd3c82fdef6679781fc7..6fa8e18c93e51e8e1e9e119f92fdc5b458c6c20a 100755 --- a/include/class.search.php +++ b/include/class.search.php @@ -384,9 +384,11 @@ class MysqlSearchBackend extends SearchBackend { $criteria->extra(array( 'tables' => array( str_replace(array(':', '{}'), array(TABLE_PREFIX, $search), - "(SELECT COALESCE(Z3.`object_id`, Z5.`ticket_id`, Z8.`ticket_id`) as `ticket_id`, SUM({}) AS `relevance` FROM `:_search` Z1 LEFT JOIN `:thread_entry` Z2 ON (Z1.`object_type` = 'H' AND Z1.`object_id` = Z2.`id`) LEFT JOIN `:thread` Z3 ON (Z2.`thread_id` = Z3.`id` AND Z3.`object_type` = 'T') LEFT JOIN `:ticket` Z5 ON (Z1.`object_type` = 'T' AND Z1.`object_id` = Z5.`ticket_id`) LEFT JOIN `:user` Z6 ON (Z6.`id` = Z1.`object_id` and Z1.`object_type` = 'U') LEFT JOIN `:organization` Z7 ON (Z7.`id` = Z1.`object_id` AND Z7.`id` = Z6.`org_id` AND Z1.`object_type` = 'O') LEFT JOIN :ticket Z8 ON (Z8.`user_id` = Z6.`id`) WHERE {} GROUP BY `ticket_id`) Z1"), - ) + "(SELECT COALESCE(Z3.`object_id`, Z5.`ticket_id`, Z8.`ticket_id`) as `ticket_id`, Z1.relevance FROM (SELECT Z1.`object_id`, Z1.`object_type`, {} AS `relevance` FROM `:_search` Z1 WHERE {} ORDER BY relevance DESC) Z1 LEFT JOIN `:thread_entry` Z2 ON (Z1.`object_type` = 'H' AND Z1.`object_id` = Z2.`id`) LEFT JOIN `:thread` Z3 ON (Z2.`thread_id` = Z3.`id` AND Z3.`object_type` = 'T') LEFT JOIN `:ticket` Z5 ON (Z1.`object_type` = 'T' AND Z1.`object_id` = Z5.`ticket_id`) LEFT JOIN `:user` Z6 ON (Z6.`id` = Z1.`object_id` and Z1.`object_type` = 'U') LEFT JOIN `:organization` Z7 ON (Z7.`id` = Z1.`object_id` AND Z7.`id` = Z6.`org_id` AND Z1.`object_type` = 'O') LEFT JOIN `:ticket` Z8 ON (Z8.`user_id` = Z6.`id`)) Z1"), + ), )); + $criteria->extra(array('order_by' => array(array(new SqlCode('Z1.relevance', 'DESC'))))); + $criteria->filter(array('ticket_id'=>new SqlCode('Z1.`ticket_id`'))); break; @@ -481,7 +483,7 @@ class MysqlSearchBackend extends SearchBackend { LEFT JOIN `".TABLE_PREFIX."_search` A2 ON (A1.`id` = A2.`object_id` AND A2.`object_type`='H') WHERE A2.`object_id` IS NULL AND (A1.poster <> 'SYSTEM') AND (LENGTH(A1.`title`) + LENGTH(A1.`body`) > 0) - ORDER BY A1.`id` DESC LIMIT 500"; + LIMIT 500"; if (!($res = db_query_unbuffered($sql, $auto_create))) return false; @@ -501,7 +503,7 @@ class MysqlSearchBackend extends SearchBackend { $sql = "SELECT A1.`ticket_id` FROM `".TICKET_TABLE."` A1 LEFT JOIN `".TABLE_PREFIX."_search` A2 ON (A1.`ticket_id` = A2.`object_id` AND A2.`object_type`='T') WHERE A2.`object_id` IS NULL - ORDER BY A1.`ticket_id` DESC LIMIT 300"; + LIMIT 300"; if (!($res = db_query_unbuffered($sql, $auto_create))) return false; @@ -524,8 +526,7 @@ class MysqlSearchBackend extends SearchBackend { $sql = "SELECT A1.`id` FROM `".USER_TABLE."` A1 LEFT JOIN `".TABLE_PREFIX."_search` A2 ON (A1.`id` = A2.`object_id` AND A2.`object_type`='U') - WHERE A2.`object_id` IS NULL - ORDER BY A1.`id` DESC"; + WHERE A2.`object_id` IS NULL"; if (!($res = db_query_unbuffered($sql, $auto_create))) return false; @@ -550,8 +551,7 @@ class MysqlSearchBackend extends SearchBackend { $sql = "SELECT A1.`id` FROM `".ORGANIZATION_TABLE."` A1 LEFT JOIN `".TABLE_PREFIX."_search` A2 ON (A1.`id` = A2.`object_id` AND A2.`object_type`='O') - WHERE A2.`object_id` IS NULL - ORDER BY A1.`id` DESC"; + WHERE A2.`object_id` IS NULL"; if (!($res = db_query_unbuffered($sql, $auto_create))) return false; @@ -575,8 +575,7 @@ class MysqlSearchBackend extends SearchBackend { require_once INCLUDE_DIR . 'class.faq.php'; $sql = "SELECT A1.`faq_id` FROM `".FAQ_TABLE."` A1 LEFT JOIN `".TABLE_PREFIX."_search` A2 ON (A1.`faq_id` = A2.`object_id` AND A2.`object_type`='K') - WHERE A2.`object_id` IS NULL - ORDER BY A1.`faq_id` DESC"; + WHERE A2.`object_id` IS NULL"; if (!($res = db_query_unbuffered($sql, $auto_create))) return false; @@ -701,18 +700,19 @@ class SavedQueue extends CustomQueue { return $this->_columns; } + static function getHierarchicalQueues(Staff $staff) { + return CustomQueue::getHierarchicalQueues($staff, 0, false); + } + /** * Fetch an AdvancedSearchForm instance for use in displaying or * configuring this search in the user interface. * */ function getForm($source=null, $searchable=array()) { - global $thisstaff; - - if (!$this->isAQueue()) - $searchable = $this->getCurrentSearchFields($source, - parent::getCriteria()); - else // Only allow supplemental matches. + $searchable = null; + if ($this->isAQueue()) + // Only allow supplemental matches. $searchable = array_intersect_key($this->getCurrentSearchFields($source), $this->getSupplementalMatches()); @@ -847,19 +847,49 @@ class SavedQueue extends CustomQueue { return (!$errors); } - static function ticketsCount($agent, $criteria=array(), - $prefix='') { + function getCount($agent, $cached=true) { + $criteria = $cached ? array() : array('id' => $this->getId()); + $counts = self::counts($agent, $criteria, $cached); + return $counts["q{$this->getId()}"] ?: 0; + } + + // Get ticket counts for queues the agent has acces to. + static function counts($agent, $criteria=array(), $cached=true) { if (!$agent instanceof Staff) return array(); - $queues = SavedQueue::objects() + // Cache TLS in seconds + $ttl = 3600; + // Cache key based on agent and salt of the installation + $key = "counts.queues.{$agent->getId()}.".SECRET_SALT; + if ($criteria && is_array($criteria)) // Consider additional criteria. + $key .= '.'.md5(serialize($criteria)); + + // only consider cache if requesed + if ($cached) { + if (function_exists('apcu_store')) { + $found = false; + $counts = apcu_fetch($key, $found); + if ($found === true) + return $counts; + } elseif (isset($_SESSION[$key]) + && isset($_SESSION[$key]['qcount']) + && (time() - $_SESSION[$key]['time']) < $ttl) { + return $_SESSION[$key]['qcount']; + } else { + // Auto clear missed session cache (if any) + unset($_SESSION[$key]); + } + } + + $queues = static::objects() ->filter(Q::any(array( - 'flags__hasbit' => CustomQueue::FLAG_PUBLIC, + 'flags__hasbit' => CustomQueue::FLAG_QUEUE, 'staff_id' => $agent->getId(), ))); - if ($criteria) + if ($criteria && is_array($criteria)) $queues->filter($criteria); $query = Ticket::objects(); @@ -870,11 +900,43 @@ class SavedQueue extends CustomQueue { $Q = $queue->getBasicQuery(); $expr = SqlCase::N()->when(new SqlExpr(new Q($Q->constraints)), new SqlField('ticket_id')); $query->aggregate(array( - "$prefix{$queue->id}" => SqlAggregate::COUNT($expr, true) + "q{$queue->id}" => SqlAggregate::COUNT($expr, true) )); + + // Add extra tables joins (if any) + if ($Q->extra && isset($Q->extra['tables'])) { + $contraints = array(); + if ($Q->constraints) + $constraints = new Q($Q->constraints); + foreach ($Q->extra['tables'] as $T) + $query->addExtraJoin(array($T, $constraints, '')); + } } - return $query->values()->one(); + $counts = $query->values()->one(); + // Always cache the results + if (function_exists('apcu_store')) { + apcu_store($key, $counts, $ttl); + } else { + // Poor man's cache + $_SESSION[$key]['qcount'] = $counts; + $_SESSION[$key]['time'] = time(); + } + + return $counts; + } + + static function clearCounts() { + if (function_exists('apcu_store')) { + if (class_exists('APCUIterator')) { + $regex = '/^counts.queues.\d+.' . preg_quote(SECRET_SALT, '/') . '$/'; + foreach (new APCUIterator($regex, APC_ITER_KEY) as $key) { + apcu_delete($key); + } + } + // Also clear rough counts + apcu_delete("rough.counts.".SECRET_SALT); + } } static function lookup($criteria) { @@ -903,6 +965,10 @@ class SavedSearch extends SavedQueue { function isSaved() { return (!$this->__new__); } + + function getCount($agent, $cached=true) { + return 500; + } } class AdhocSearch @@ -1004,21 +1070,30 @@ class AdvancedSearchSelectionField extends ChoiceField { } class HelpTopicChoiceField extends AdvancedSearchSelectionField { + static $_topics; + function hasIdValue() { return true; } function getChoices($verbose=false) { - return Topic::getHelpTopics(false, Topic::DISPLAY_DISABLED); + if (!isset($this->_topics)) + $this->_topics = Topic::getHelpTopics(false, Topic::DISPLAY_DISABLED); + + return $this->_topics; } } require_once INCLUDE_DIR . 'class.dept.php'; class DepartmentChoiceField extends AdvancedSearchSelectionField { - var $_choices = null; + static $_depts; + var $_choices; function getChoices($verbose=false) { - return Dept::getDepartments(); + if (!isset($this->_depts)) + $this->_depts = Dept::getDepartments(); + + return $this->_depts; } function getQuickFilterChoices() { @@ -1241,13 +1316,23 @@ trait ZeroMeansUnset { class AgentSelectionField extends AdvancedSearchSelectionField { use ZeroMeansUnset; + static $_agents; + function getChoices($verbose=false) { - return array('M' => __('Me')) + Staff::getStaffMembers(); + if (!isset($this->_agents)) { + $this->_agents = array('M' => __('Me')) + + Staff::getStaffMembers(); + } + return $this->_agents; } function toString($value) { + $choices = $this->getChoices(); $selection = array(); + if (!is_array($value)) + $value = array($value => $value); + foreach ($value as $k => $v) if (isset($choices[$k])) $selection[] = $choices[$k]; @@ -1278,9 +1363,13 @@ class AgentSelectionField extends AdvancedSearchSelectionField { } class DepartmentManagerSelectionField extends AgentSelectionField { + static $_members; function getChoices($verbose=false) { - return Staff::getStaffMembers(); + if (isset($this->_members)) + $this->_members = Staff::getStaffMembers(); + + return $this->_members; } function getSearchQ($method, $value, $name=false) { @@ -1289,9 +1378,14 @@ class DepartmentManagerSelectionField extends AgentSelectionField { } class TeamSelectionField extends AdvancedSearchSelectionField { + static $_teams; function getChoices($verbose=false) { - return array('T' => __('One of my teams')) + Team::getTeams(); + if (!isset($this->_teams)) + $this->_teams = array('T' => __('One of my teams')) + + Team::getTeams(); + + return $this->_teams; } function getSearchQ($method, $value, $name=false) { @@ -1315,6 +1409,19 @@ class TeamSelectionField extends AdvancedSearchSelectionField { $reverse = $reverse ? '-' : ''; return $query->order_by("{$reverse}team__name"); } + + function toString($value) { + $choices = $this->getChoices(); + $selection = array(); + if (!is_array($value)) + $value = array($value => $value); + foreach ($value as $k => $v) + if (isset($choices[$k])) + $selection[] = $choices[$k]; + return $selection ? implode(',', $selection) : + parent::toString($value); + } + } class TicketStateChoiceField extends AdvancedSearchSelectionField { @@ -1395,6 +1502,7 @@ class OpenClosedTicketStatusList extends TicketStatusList { return $rv; } } + class TicketStatusChoiceField extends SelectionField { static $widget = 'ChoicesWidget'; @@ -1427,6 +1535,50 @@ class TicketStatusChoiceField extends SelectionField { } } +class TicketThreadCountField extends NumericField { + + function addToQuery($query, $name=false) { + return TicketThreadCount::addToQuery($query, $name); + } + + function from_query($row, $name=false) { + return TicketThreadCount::from_query($row, $name); + } +} + +class TicketReopenCountField extends NumericField { + + function addToQuery($query, $name=false) { + return TicketReopenCount::addToQuery($query, $name); + } + + function from_query($row, $name=false) { + return TicketReopenCount::from_query($row, $name); + } +} + +class ThreadAttachmentCountField extends NumericField { + + function addToQuery($query, $name=false) { + return ThreadAttachmentCount::addToQuery($query, $name); + } + + function from_query($row, $name=false) { + return ThreadAttachmentCount::from_query($row, $name); + } +} + +class ThreadCollaboratorCountField extends NumericField { + + function addToQuery($query, $name=false) { + return ThreadCollaboratorCount::addToQuery($query, $name); + } + + function from_query($row, $name=false) { + return ThreadCollaboratorCount::from_query($row, $name); + } +} + interface Searchable { // Fetch an array of [ orm__path => Field() ] pairs. The field label is // used when this list is rendered in a dropdown, and the field search diff --git a/include/class.thread.php b/include/class.thread.php index 481a34835aed3ce179a8c83bf8e07b3ed3b84596..9488835a3d80c68f1a8a949f2d21094f4b414368 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -2054,16 +2054,80 @@ class ThreadEvent extends VerySimpleModel { $subclasses[$class::$state] = $class; } } + $this->state = Event::getNameById($this->event_id); if (!($class = $subclasses[$this->state])) return $this; return new $class($this->ht); } } +class Event extends VerySimpleModel { + static $meta = array( + 'table' => EVENT_TABLE, + 'pk' => array('id'), + ); + + function getInfo() { + return $this->ht; + } + + function getId() { + return $this->id; + } + + function getName() { + return $this->name; + } + + function getDescription() { + return $this->description; + } + + static function getNameById($id) { + return array_search($id, self::getIds()); + } + + static function getIdByName($name) { + $ids = self::getIds(); + return $ids[$name] ?: 0; + } + + static function getIds() { + static $ids; + + if (!isset($ids)) { + $ids = array(); + $events = self::objects()->values_flat('id', 'name'); + foreach ($events as $row) { + list($id, $name) = $row; + $ids[$name] = $id; + } + } + + return $ids; + } + + static function create($vars=false, &$errors=array()) { + $event = new static($vars); + return $event; + } + + static function __create($vars, &$errors=array()) { + $event = self::create($vars); + $event->save(); + return $event; + } + + function save($refetch=false) { + return parent::save($refetch); + } +} + class ThreadEvents extends InstrumentedList { function annul($event) { + $event_id = Event::getIdByName($event); $this->queryset - ->filter(array('state' => $event)) + ->filter(array('event_id' => $event_id)) ->update(array('annulled' => 1)); } @@ -2118,7 +2182,7 @@ class ThreadEvents extends InstrumentedList { } } $event->username = $username; - $event->state = $state; + $event->event_id = Event::getIdByName($state); if ($data) { if (is_array($data)) diff --git a/include/class.ticket.php b/include/class.ticket.php index 43446e89e67ed43a885f61045ec3266f3f1efbc2..37c80c73c669fa1b9f59dbbebfed6eb563b3fc3d 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -2114,27 +2114,39 @@ implements RestrictedAccess, Threadable, Searchable { )), 'created' => new DatetimeField(array( 'label' => __('Create Date'), - 'configuration' => array('fromdb' => true), + 'configuration' => array( + 'fromdb' => true, 'time' => true, + 'format' => 'y-MM-dd HH:mm:ss'), )), 'duedate' => new DatetimeField(array( 'label' => __('Due Date'), - 'configuration' => array('fromdb' => true), + 'configuration' => array( + 'fromdb' => true, 'time' => true, + 'format' => 'y-MM-dd HH:mm:ss'), )), 'est_duedate' => new DatetimeField(array( 'label' => __('SLA Due Date'), - 'configuration' => array('fromdb' => true), + 'configuration' => array( + 'fromdb' => true, 'time' => true, + 'format' => 'y-MM-dd HH:mm:ss'), )), 'reopened' => new DatetimeField(array( 'label' => __('Reopen Date'), - 'configuration' => array('fromdb' => true), + 'configuration' => array( + 'fromdb' => true, 'time' => true, + 'format' => 'y-MM-dd HH:mm:ss'), )), 'closed' => new DatetimeField(array( 'label' => __('Close Date'), - 'configuration' => array('fromdb' => true), + 'configuration' => array( + 'fromdb' => true, 'time' => true, + 'format' => 'y-MM-dd HH:mm:ss'), )), 'lastupdate' => new DatetimeField(array( 'label' => __('Last Update'), - 'configuration' => array('fromdb' => true), + 'configuration' => array( + 'fromdb' => true, 'time' => true, + 'format' => 'y-MM-dd HH:mm:ss'), )), 'assignee' => new AssigneeChoiceField(array( 'label' => __('Assignee'), @@ -2171,6 +2183,18 @@ implements RestrictedAccess, Threadable, Searchable { 'isassigned' => new AssignedField(array( 'label' => __('Assigned'), )), + 'thread_count' => new TicketThreadCountField(array( + 'label' => __('Thread Count'), + )), + 'attachment_count' => new ThreadAttachmentCountField(array( + 'label' => __('Attachment Count'), + )), + 'collaborator_count' => new ThreadCollaboratorCountField(array( + 'label' => __('Collaborator Count'), + )), + 'reopen_count' => new TicketReopenCountField(array( + 'label' => __('Reopen Count'), + )), 'ip_address' => new TextboxField(array( 'label' => __('IP Address'), 'configuration' => array('validator' => 'ip'), @@ -3176,6 +3200,9 @@ implements RestrictedAccess, Threadable, Searchable { function save($refetch=false) { if ($this->dirty) { $this->updated = SqlFunction::NOW(); + if (isset($this->dirty['status_id'])) + // Refetch the queue counts + SavedQueue::clearCounts(); } return parent::save($this->dirty || $refetch); } @@ -4186,7 +4213,8 @@ implements RestrictedAccess, Threadable, Searchable { Punt for now */ - $sql='SELECT ticket_id FROM '.TICKET_TABLE.' T1 ' + $sql='SELECT ticket_id FROM '.TICKET_TABLE.' T1' + .' USE INDEX (status_id)' .' INNER JOIN '.TICKET_STATUS_TABLE.' status ON (status.id=T1.status_id AND status.state="open") ' .' LEFT JOIN '.SLA_TABLE.' T2 ON (T1.sla_id=T2.id AND T2.flags & 1 = 1) ' diff --git a/include/class.user.php b/include/class.user.php index 0fa35e4a14d62514632bcf163f80fc25e216b49c..8d6bd1a7bb76f9197d1b6b1844271aec337121d2 100644 --- a/include/class.user.php +++ b/include/class.user.php @@ -635,11 +635,11 @@ implements TemplateVariable, Searchable { } function deleteAllTickets() { - $deleted = TicketStatus::lookup(array('state' => 'deleted')); + $status_id = TicketStatus::lookup(array('state' => 'deleted')); foreach($this->tickets as $ticket) { if (!$T = Ticket::lookup($ticket->getId())) continue; - if (!$T->setStatus($deleted)) + if (!$T->setStatus($status_id)) return false; } $this->tickets->reset(); diff --git a/include/cli/modules/upgrade.php b/include/cli/modules/upgrade.php index 297d6d56f84d36dcdc1ad23c42410a09d483b165..383c47f5c66226ac32f6ae102f502e27fcbe1645 100644 --- a/include/cli/modules/upgrade.php +++ b/include/cli/modules/upgrade.php @@ -51,12 +51,12 @@ class CliUpgrader extends Module { $cfg = $ost->getConfig(); while (true) { - if ($upgrader->getTask()) { - // If there's anythin in the model cache (like a Staff - // object or something), ensure that changes to the database - // model won't cause crashes - ModelInstanceManager::flushCache(); + // If there's anythin in the model cache (like a Staff + // object or something), ensure that changes to the database + // model won't cause crashes + ModelInstanceManager::flushCache(); + if ($upgrader->getTask()) { // More pending tasks - doTasks returns the number of pending tasks $this->stdout->write("... {$upgrader->getNextAction()}\n"); $upgrader->doTask(); diff --git a/include/client/templates/thread-entries.tmpl.php b/include/client/templates/thread-entries.tmpl.php index f54fa4f6b228243fb3a1c20588fd127752929082..b3df17773728dbb2154d6c1707360e82f02fd2d1 100644 --- a/include/client/templates/thread-entries.tmpl.php +++ b/include/client/templates/thread-entries.tmpl.php @@ -1,6 +1,12 @@ <?php +$states = array('created', 'closed', 'reopened', 'edited', 'collab'); +$event_ids = array(); +foreach ($states as $state) { + $eid = Event::getIdByName($state); + $event_ids[] = $eid; +} $events = $events - ->filter(array('state__in' => array('created', 'closed', 'reopened', 'edited', 'collab'))) + ->filter(array('event_id__in' => $event_ids)) ->order_by('id'); $eventCount = count($events); $events = new IteratorIterator($events->getIterator()); diff --git a/include/i18n/en_US/event.yaml b/include/i18n/en_US/event.yaml new file mode 100644 index 0000000000000000000000000000000000000000..7c9c8ed5bb93d531a50a21f6b84f6a4caf0f07ee --- /dev/null +++ b/include/i18n/en_US/event.yaml @@ -0,0 +1,61 @@ +# +# event.yaml +# +# Events initially inserted into the system. +# +--- +- id: 1 + name: created + description: + +- id: 2 + name: closed + description: + +- id: 3 + name: reopened + description: + +- id: 4 + name: assigned + description: + +- id: 5 + name: released + description: + +- id: 6 + name: transferred + description: + +- id: 7 + name: referred + description: + +- id: 8 + name: overdue + description: + +- id: 9 + name: edited + description: + +- id: 10 + name: viewed + description: + +- id: 11 + name: error + description: + +- id: 12 + name: collab + description: + +- id: 13 + name: resent + description: + +- id: 14 + name: deleted + description: diff --git a/include/i18n/en_US/queue.yaml b/include/i18n/en_US/queue.yaml index ab2b1a4bbb3453e14c7843b81a75d65751b94d9e..34a35c28707db92ce80c80af9ed8728b1cd1ce7d 100644 --- a/include/i18n/en_US/queue.yaml +++ b/include/i18n/en_US/queue.yaml @@ -29,6 +29,7 @@ --- - id: 1 title: Open + parent_id: 0 flags: 0x03 sort: 1 root: T @@ -69,14 +70,57 @@ - sort_id: 2 - sort_id: 3 - sort_id: 4 + - sort_id: 6 + - sort_id: 7 - id: 2 + title: Open + parent_id: 1 + flags: 0x2b + root: T + sort: 1 + sort_id: 4 + config: '{"criteria":[["isanswered","nset",null]],"conditions":[]}' + columns: + - column_id: 1 + bits: 1 + sort: 1 + width: 100 + heading: Ticket + - column_id: 10 + bits: 1 + sort: 2 + width: 150 + heading: Last Updated + - column_id: 3 + bits: 1 + sort: 3 + width: 300 + heading: Subject + - column_id: 4 + bits: 1 + sort: 4 + width: 185 + heading: From + - column_id: 5 + bits: 1 + sort: 5 + width: 85 + heading: Priority + - column_id: 8 + bits: 1 + sort: 6 + width: 160 + heading: Assigned To + +- id: 3 title: Answered parent_id: 1 - flags: 0x03 + flags: 0x2b root: T sort: 2 - config: '[["isanswered","set",null]]' + sort_id: 4 + config: '{"criteria":[["isanswered","set",null]],"conditions":[]}' columns: - column_id: 1 bits: 1 @@ -87,7 +131,7 @@ bits: 1 sort: 2 width: 150 - heading: Last Update + heading: Last Updated - column_id: 3 bits: 1 sort: 3 @@ -108,13 +152,53 @@ sort: 6 width: 160 heading: Assigned To - sorts: - - sort_id: 1 - - sort_id: 2 - - sort_id: 3 - - sort_id: 4 -- id: 3 +- id: 4 + title: Overdue + parent_id: 1 + flags: 0x2b + root: T + sort: 3 + sort_id: 4 + config: '{"criteria":[["isoverdue","set",null]],"conditions":[]}' + columns: + - column_id: 1 + bits: 1 + sort: 1 + width: 100 + heading: Ticket + - column_id: 9 + bits: 1 + sort: 1 + sort: 9 + width: 150 + heading: Due Date + - column_id: 3 + bits: 1 + sort: 1 + sort: 3 + width: 300 + heading: Subject + - column_id: 4 + bits: 1 + sort: 1 + sort: 4 + width: 185 + heading: From + - column_id: 5 + bits: 1 + sort: 1 + sort: 5 + width: 85 + heading: Priority + - column_id: 8 + bits: 1 + sort: 1 + sort: 6 + width: 160 + heading: Assigned To + +- id: 5 title: My Tickets parent_id: 0 flags: 0x03 @@ -157,25 +241,27 @@ - sort_id: 2 - sort_id: 3 - sort_id: 4 + - sort_id: 6 + - sort_id: 7 -- id: 4 - title: Closed - flags: 0x03 - sort: 4 +- id: 6 + title: Assigned to Me + parent_id: 5 + flags: 0x2b root: T - sort_id: 5 - config: '[["status__state","includes",{"closed":"Closed"}]]' + sort: 1 + config: '{"criteria":[["assignee","includes",{"M":"Me"}]],"conditions":[]}' columns: - column_id: 1 bits: 1 sort: 1 width: 100 heading: Ticket - - column_id: 7 + - column_id: 10 bits: 1 sort: 2 width: 150 - heading: Date Closed + heading: Last Update - column_id: 3 bits: 1 sort: 3 @@ -188,28 +274,29 @@ heading: From - column_id: 5 bits: 1 - sort: 1 sort: 5 width: 85 heading: Priority - - column_id: 8 + - column_id: 11 bits: 1 - sort: 1 sort: 6 width: 160 - heading: Closed By + heading: Department sorts: - - sort_id: 5 - sort_id: 1 - sort_id: 2 + - sort_id: 3 + - sort_id: 4 + - sort_id: 6 + - sort_id: 7 -- id: 5 - title: Assigned - parent_id: 1 - flags: 0x03 +- id: 7 + title: Assigned to Teams + parent_id: 5 + flags: 0x2b root: T - sort: 3 - config: '[["assignee","assigned",null]]' + sort: 2 + config: '{"criteria":[["assignee","!includes",{"M":"Me"}]],"conditions":[]}' columns: - column_id: 1 bits: 1 @@ -236,53 +323,323 @@ sort: 5 width: 85 heading: Priority + - column_id: 14 + bits: 1 + sort: 6 + width: 160 + heading: Team + sorts: + - sort_id: 1 + - sort_id: 2 + - sort_id: 3 + - sort_id: 4 + - sort_id: 6 + - sort_id: 7 + +- id: 8 + parent_id: 0 + title: Closed + flags: 0x03 + sort: 4 + root: T + sort_id: 5 + config: '{"criteria":[["status__state","includes",{"closed":"Closed"}]],"conditions":[]}' + columns: + - column_id: 1 + bits: 1 + sort: 1 + width: 100 + heading: Ticket + - column_id: 7 + bits: 1 + sort: 2 + width: 150 + heading: Date Closed + - column_id: 3 + bits: 1 + sort: 3 + width: 300 + heading: Subject + - column_id: 4 + bits: 1 + sort: 4 + width: 185 + heading: From - column_id: 8 bits: 1 + sort: 1 sort: 6 width: 160 - heading: Assigned To + heading: Closed By + sorts: + - sort_id: 5 + - sort_id: 1 + - sort_id: 2 + - sort_id: 3 + - sort_id: 4 + - sort_id: 6 + - sort_id: 7 -- id: 6 - title: Overdue - parent_id: 1 +- id: 9 + parent_id: 8 + title: Today flags: 0x2b + sort: 1 root: T - sort: 4 - sort_id: 4 - config: '[["isoverdue","set",null]]' + sort_id: 5 + config: '{"criteria":[["closed","period","td"]],"conditions":[]}' columns: - column_id: 1 bits: 1 sort: 1 width: 100 heading: Ticket - - column_id: 9 + - column_id: 7 + bits: 1 + sort: 2 + width: 150 + heading: Date Closed + - column_id: 3 + bits: 1 + sort: 3 + width: 300 + heading: Subject + - column_id: 4 + bits: 1 + sort: 4 + width: 185 + heading: From + - column_id: 8 bits: 1 sort: 1 - sort: 9 + sort: 6 + width: 160 + heading: Closed By + sorts: + - sort_id: 5 + - sort_id: 1 + - sort_id: 2 + - sort_id: 3 + - sort_id: 4 + - sort_id: 6 + - sort_id: 7 + +- id: 10 + parent_id: 8 + title: Yesterday + flags: 0x2b + sort: 2 + root: T + sort_id: 5 + config: '{"criteria":[["closed","period","yd"]],"conditions":[]}' + columns: + - column_id: 1 + bits: 1 + sort: 1 + width: 100 + heading: Ticket + - column_id: 7 + bits: 1 + sort: 2 width: 150 - heading: Due Date + heading: Date Closed - column_id: 3 + bits: 1 + sort: 3 + width: 300 + heading: Subject + - column_id: 4 + bits: 1 + sort: 4 + width: 185 + heading: From + - column_id: 8 + bits: 1 + sort: 1 + sort: 6 + width: 160 + heading: Closed By + sorts: + - sort_id: 5 + - sort_id: 1 + - sort_id: 2 + - sort_id: 3 + - sort_id: 4 + - sort_id: 6 + - sort_id: 7 + +- id: 11 + parent_id: 8 + title: This Week + flags: 0x2b + sort: 3 + root: T + sort_id: 5 + config: '{"criteria":[["closed","period","tw"]],"conditions":[]}' + columns: + - column_id: 1 bits: 1 sort: 1 + width: 100 + heading: Ticket + - column_id: 7 + bits: 1 + sort: 2 + width: 150 + heading: Date Closed + - column_id: 3 + bits: 1 sort: 3 width: 300 heading: Subject - column_id: 4 + bits: 1 + sort: 4 + width: 185 + heading: From + - column_id: 8 bits: 1 sort: 1 + sort: 6 + width: 160 + heading: Closed By + sorts: + - sort_id: 5 + - sort_id: 1 + - sort_id: 2 + - sort_id: 3 + - sort_id: 4 + - sort_id: 6 + - sort_id: 7 + +- id: 12 + parent_id: 8 + title: This Month + flags: 0x2b + sort: 4 + root: T + sort_id: 5 + config: '{"criteria":[["closed","period","tm"]],"conditions":[]}' + columns: + - column_id: 1 + bits: 1 + sort: 1 + width: 100 + heading: Ticket + - column_id: 7 + bits: 1 + sort: 2 + width: 150 + heading: Date Closed + - column_id: 3 + bits: 1 + sort: 3 + width: 300 + heading: Subject + - column_id: 4 + bits: 1 sort: 4 width: 185 heading: From - - column_id: 5 + - column_id: 8 bits: 1 sort: 1 - sort: 5 - width: 85 - heading: Priority + sort: 6 + width: 160 + heading: Closed By + sorts: + - sort_id: 5 + - sort_id: 1 + - sort_id: 2 + - sort_id: 3 + - sort_id: 4 + - sort_id: 6 + - sort_id: 7 + +- id: 13 + parent_id: 8 + title: This Quarter + flags: 0x2b + sort: 5 + root: T + sort_id: 6 + config: '{"criteria":[["closed","period","tq"]],"conditions":[]}' + columns: + - column_id: 1 + bits: 1 + sort: 1 + width: 100 + heading: Ticket + - column_id: 7 + bits: 1 + sort: 2 + width: 150 + heading: Date Closed + - column_id: 3 + bits: 1 + sort: 3 + width: 300 + heading: Subject + - column_id: 4 + bits: 1 + sort: 4 + width: 185 + heading: From - column_id: 8 bits: 1 sort: 1 sort: 6 width: 160 - heading: Assigned To + heading: Closed By + sorts: + - sort_id: 5 + - sort_id: 1 + - sort_id: 2 + - sort_id: 3 + - sort_id: 4 + - sort_id: 6 + - sort_id: 7 + +- id: 14 + parent_id: 8 + title: This Year + flags: 0x2b + sort: 6 + root: T + sort_id: 7 + config: '{"criteria":[["closed","period","ty"]],"conditions":[]}' + columns: + - column_id: 1 + bits: 1 + sort: 1 + width: 100 + heading: Ticket + - column_id: 7 + bits: 1 + sort: 2 + width: 150 + heading: Date Closed + - column_id: 3 + bits: 1 + sort: 3 + width: 300 + heading: Subject + - column_id: 4 + bits: 1 + sort: 4 + width: 185 + heading: From + - column_id: 8 + bits: 1 + sort: 1 + sort: 6 + width: 160 + heading: Closed By + sorts: + - sort_id: 5 + - sort_id: 1 + - sort_id: 2 + - sort_id: 3 + - sort_id: 4 + - sort_id: 6 + - sort_id: 7 diff --git a/include/i18n/en_US/queue_column.yaml b/include/i18n/en_US/queue_column.yaml index 6f7419a4af0e0074293b957b07f813668d1889e4..03250da19a83b3118bbd04eb4792218c232928dd 100644 --- a/include/i18n/en_US/queue_column.yaml +++ b/include/i18n/en_US/queue_column.yaml @@ -129,3 +129,10 @@ truncate: "wrap" annotations: "[]" conditions: "[]" + +- id: 14 + name: "Team" + primary: "team_id" + truncate: "wrap" + annotations: "[]" + conditions: "[]" diff --git a/include/i18n/en_US/queue_sort.yaml b/include/i18n/en_US/queue_sort.yaml index 09b0fb87f6530db783274f2716be91ff4f1cc8d3..35cd47d4a2dd1b12b3ff6af4931a733ba9172eea 100644 --- a/include/i18n/en_US/queue_sort.yaml +++ b/include/i18n/en_US/queue_sort.yaml @@ -27,3 +27,7 @@ - id: 6 name: Create Date columns: '["-created"]' + +- id: 7 + name: Update Date + columns: '["-lastupdate"]' diff --git a/include/staff/queue.inc.php b/include/staff/queue.inc.php index b936699df859013ba8706f9c27daba2c310a840e..fa9862bb830d4997296aad36fb3fbbbe2bc154b2 100644 --- a/include/staff/queue.inc.php +++ b/include/staff/queue.inc.php @@ -4,10 +4,11 @@ if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin()) die('Access Denied'); $info = $qs = array(); - +$parent = null; if (!$queue) { $queue = CustomQueue::create(array( 'flags' => CustomQueue::FLAG_QUEUE, + 'parent_id' => 0, )); } if ($queue->__new__) { @@ -16,6 +17,7 @@ if ($queue->__new__) { $submit_text=__('Create'); } else { + $parent = $queue->parent; $title=__('Manage Custom Queue'); $action='update'; $submit_text=__('Save Changes'); @@ -29,8 +31,6 @@ else { <input type="hidden" name="do" value="<?php echo $action; ?>"> <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>"> <input type="hidden" name="id" value="<?php echo $info['id']; ?>"> - <input type="hidden" name="root" value="<?php echo Format::htmlchars($_REQUEST['t']); ?>"> - <h2><a href="settings.php?t=tickets#queues"><?php echo __('Ticket Queues'); ?></a> <i class="icon-caret-right" style="color:rgba(0,0,0,.3);"></i> <?php echo $title; ?> <?php if (isset($queue->id)) { ?><small> @@ -63,20 +63,33 @@ else { <br/> <div class="error"><?php echo $errors['queue-name']; ?></div> <br/> + <div> + <div><strong><?php echo __("Parent Queue"); ?>:</strong></div> + <select name="parent_id" id="parent-id"> + <option value="0">— <?php echo __('Top-Level Queue'); ?> —</option> + <?php foreach (CustomQueue::queues() as $cq) { + // Queue cannot be a descendent of itself + if ($cq->id == $queue->id) + continue; + if (strpos($cq->path, "/{$queue->id}/") !== false) + continue; + ?> + <option value="<?php echo $cq->id; ?>" + <?php if ($cq->getId() == $queue->parent_id) echo 'selected="selected"'; ?> + ><?php echo $cq->getFullName(); ?></option> + <?php } ?> + </select> + <span class="error"><?php echo Format::htmlchars($errors['parent_id']); ?></span> + </div> + <div class="faded <?php echo $parent ? ' ': 'hidden'; ?>" + id="inherited-parent" style="margin-top: 1em;"> + <div><strong><i class="icon-caret-down"></i> <?php echo __('Inherited Criteria'); ?></strong></div> + <div id="parent-criteria"> + <?php echo $parent ? nl2br(Format::htmlchars($parent->describeCriteria())) : ''; ?> + </div> + </div> + <hr/> <div><strong><?php echo __("Queue Search Criteria"); ?></strong></div> - <label class="checkbox" style="line-height:1.3em"> - <input type="checkbox" class="checkbox" name="inherit" <?php - if ($queue->inheritCriteria()) echo 'checked="checked"'; - ?>/> - <?php echo __('Include parent search criteria'); - if ($queue->parent) { ?> - <span id="parent_q_crit" class="faded"> - <i class="icon-caret-right"></i> - <br/><?php - echo nl2br(Format::htmlchars($queue->parent->describeCriteria())); - ?></span> -<?php } ?> - </label> <hr/> <div class="error"><?php echo $errors['criteria']; ?></div> <div class="advanced-search"> @@ -89,27 +102,6 @@ else { </div> </td> <td style="width:35%; padding-left:40px; vertical-align:top"> - <div><strong><?php echo __("Parent Queue"); ?>:</strong></div> - <select name="parent_id" onchange="javascript: - $('#parent_q_crit').toggle($(this).find(':selected').val() - == <?php echo $queue->parent_id ?: 0; ?>);"> - <option value="0">— <?php echo __('Top-Level Queue'); ?> —</option> -<?php foreach (CustomQueue::queues() as $cq) { - // Queue cannot be a descendent of itself - if ($cq->id == $queue->id) - continue; - if (strpos($cq->path, "/{$queue->id}/") !== false) - continue; -?> - <option value="<?php echo $cq->id; ?>" - <?php if ($cq->getId() == $queue->parent_id) echo 'selected="selected"'; ?> - ><?php echo $cq->getFullName(); ?></option> -<?php } ?> - </select> - <div class="error"><?php echo Format::htmlchars($errors['parent_id']); ?></div> - - <br/> - <br/> <div><strong><?php echo __("Quick Filter"); ?></strong></div> <hr/> <select name="filter"> @@ -309,6 +301,27 @@ var Q = setInterval(function() { ); } ?> }, 25); +$('select#parent-id').change(function() { + var form = $(this).closest('form'); + var qid = parseInt($(this).val(), 10) || 0; + + if (qid > 0) { + $.ajax({ + type: "GET", + url: 'ajax.php/queue/'+qid, + dataType: 'json', + success: function(queue) { + $('#parent-name', form).html(queue.name); + $('#parent-criteria', form).html(queue.criteria); + $('#inherited-parent', form).fadeIn(); + } + }) + .done(function() { }) + .fail(function() { }); + } else { + $('#inherited-parent', form).fadeOut(); + } +}); }(); </script> </table> diff --git a/include/staff/templates/advanced-search-criteria.tmpl.php b/include/staff/templates/advanced-search-criteria.tmpl.php index 9348cf79be9a81e3cb762f1bcaf96604acedf437..309ce63cd117687c0a530cfe6935609c38e05729 100644 --- a/include/staff/templates/advanced-search-criteria.tmpl.php +++ b/include/staff/templates/advanced-search-criteria.tmpl.php @@ -11,7 +11,7 @@ if (($search instanceof SavedQueue) && !$search->checkOwnership($thisstaff)) { echo '<div class="faded">'. nl2br(Format::htmlchars($search->describeCriteria())). '</div><br>'; // Show any supplemental filters - if ($matches && count($info)) { + if ($matches) { ?> <div id="ticket-flags" style="padding:5px; border-top: 1px dotted #777;"> diff --git a/include/staff/templates/queue-navigation.tmpl.php b/include/staff/templates/queue-navigation.tmpl.php index d1061c8d43da152d5686b428fe6ee0b50943f8f1..380e03af961d5f8f6901d9bf63eadb55bb7679a4 100644 --- a/include/staff/templates/queue-navigation.tmpl.php +++ b/include/staff/templates/queue-navigation.tmpl.php @@ -15,6 +15,9 @@ $selected = (!isset($_REQUEST['a']) && $_REQUEST['queue'] == $this_queue->getId <div class="customQ-dropdown"> <ul class="scroll-height"> <!-- Add top-level queue (with count) --> + + <?php + if (!$children) { ?> <li class="top-level"> <span class="pull-right newItemQ queue-count" data-queue-id="<?php echo $q->id; ?>"><span class="faded-more">-</span> @@ -25,9 +28,9 @@ $selected = (!isset($_REQUEST['a']) && $_REQUEST['queue'] == $this_queue->getId <?php echo Format::htmlchars($q->getName()); ?> </a> - </h4> </li> - + <?php + } ?> <!-- Start Dropdown and child queues --> <?php foreach ($childs as $_) { list($q, $children) = $_; diff --git a/include/staff/templates/queue-savedsearches-nav.tmpl.php b/include/staff/templates/queue-savedsearches-nav.tmpl.php index ef06cf06c91db2b6e5a32ea0395885cc8d05cd25..34a8aff168cb10326f1e8a927f506565d3de13fb 100644 --- a/include/staff/templates/queue-savedsearches-nav.tmpl.php +++ b/include/staff/templates/queue-savedsearches-nav.tmpl.php @@ -4,6 +4,10 @@ // $searches = All visibile saved searches // $child_selected - <bool> true if the selected queue is a descendent // $adhoc - not FALSE if an adhoc advanced search exists + +$searches = SavedQueue::getHierarchicalQueues($thisstaff); +if ($queue && !$queue->parent_id && $queue->staff_id) + $child_selected = true; ?> <li class="primary-only item <?php if ($child_selected) echo 'active'; ?>"> <?php @@ -16,14 +20,9 @@ <div class="customQ-dropdown"> <ul class="scroll-height"> <!-- Start Dropdown and child queues --> - <?php foreach ($searches->findAll(array( - 'staff_id' => $thisstaff->getId(), - 'parent_id' => 0, - Q::not(array( - 'flags__hasbit' => CustomQueue::FLAG_PUBLIC - )) - )) as $q) { - if ($q->checkAccess($thisstaff)) + <?php foreach ($searches as $search) { + list($q, $children) = $search; + if ($q->checkAccess($thisstaff)) include 'queue-subnavigation.tmpl.php'; } ?> <?php diff --git a/include/staff/templates/queue-sorting-edit.tmpl.php b/include/staff/templates/queue-sorting-edit.tmpl.php index 0a2d98b0246433b59ea0f9cc1d575deb3a4ed41b..a001201a2beb94e6c3391f2965e6353882dfc8c3 100644 --- a/include/staff/templates/queue-sorting-edit.tmpl.php +++ b/include/staff/templates/queue-sorting-edit.tmpl.php @@ -5,6 +5,7 @@ * $column - <QueueColumn> instance for this column */ $sortid = $sort->getId(); +$advanced = in_array('extra', $sort::getMeta()->getFieldNames()); ?> <h3 class="drag-handle"><?php echo __('Manage Sort Options'); ?> — <?php echo $sort->get('name') ?></h3> @@ -14,10 +15,30 @@ $sortid = $sort->getId(); <form method="post" action="#tickets/search/sort/edit/<?php echo $sortid; ?>"> +<?php if ($advanced) { ?> + <ul class="clean tabs"> + <li class="active"><a href="#fields"><i class="icon-columns"></i> + <?php echo __('Fields'); ?></a></li> + <li><a href="#advanced"><i class="icon-cog"></i> + <?php echo __('Advanced'); ?></a></li> + </ul> + + <div class="tab_content" id="fields"> +<?php } ?> + <?php include 'queue-sorting.tmpl.php'; ?> +<?php if ($advanced) { ?> + </div> + + <div class="hidden tab_content" id="advanced"> + <?php echo $sort->getAdvancedConfigForm()->asTable(); ?> + </div> + +<?php } ?> + <hr> <p class="full-width"> <span class="buttons pull-left"> diff --git a/include/staff/templates/queue-tickets.tmpl.php b/include/staff/templates/queue-tickets.tmpl.php index 37d7cced74789b85fd6aa1f6cf6b0dc720eacc30..fe82ff9a21460eda9090a000f4fda147a99bc323 100644 --- a/include/staff/templates/queue-tickets.tmpl.php +++ b/include/staff/templates/queue-tickets.tmpl.php @@ -76,8 +76,20 @@ if (!$sorted && isset($sort['queuesort'])) { $page = ($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1; $pageNav = new Pagenate(PHP_INT_MAX, $page, PAGE_LIMIT); $tickets = $pageNav->paginateSimple($tickets); -$count = $tickets->total(); -$pageNav->setTotal($count); + +// Creative twist here. Create a new query copying the query criteria, sort, limit, +// and offset. Then join this new query to the $tickets query and clear the +// criteria, sort, limit, and offset from the outer query. +$criteria = clone $tickets; +$criteria->annotations = $criteria->related = $criteria->aggregated = []; +$tickets->constraints = $tickets->extra = []; +$tickets = $tickets->filter(['ticket_id__in' => $criteria->values_flat('ticket_id')]) + ->limit(false)->offset(false)->order_by(false); +# Index hint should be used on the $criteria query only +$tickets->clearOption(QuerySet::OPT_INDEX_HINT); + +$count = $queue->getCount($thisstaff); +$pageNav->setTotal($count, true); $pageNav->setURL('tickets.php', $args); ?> diff --git a/include/upgrader/streams/core.sig b/include/upgrader/streams/core.sig index fbebb45e63ee1f85f4388d1ead81fa6b7403dd4f..00b20916567f6418d4c640ae0895c26b3499d242 100644 --- a/include/upgrader/streams/core.sig +++ b/include/upgrader/streams/core.sig @@ -1 +1 @@ -26fd79dc5443f37779f9d2c4108058f4 +00c949a623b82848baaf3480b51307e3 diff --git a/include/upgrader/streams/core/0ca85857-86707325.patch.sql b/include/upgrader/streams/core/0ca85857-86707325.patch.sql index 2962d23e40f284614bd877ee0317e1183f7a21bf..2daaf3b7cdab96ac80265205de51dbe6fc0926ef 100644 --- a/include/upgrader/streams/core/0ca85857-86707325.patch.sql +++ b/include/upgrader/streams/core/0ca85857-86707325.patch.sql @@ -17,10 +17,6 @@ CREATE TABLE `%TABLE_PREFIX%thread_referral` ( KEY `thread_id` (`thread_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -ALTER TABLE `%TABLE_PREFIX%thread_event` - CHANGE `state` `state` enum('created','closed','reopened','assigned','transferred', 'referred', 'overdue','edited','viewed','error','collab','resent', 'deleted') NOT NULL; - - -- Finished with patch UPDATE `%TABLE_PREFIX%config` SET `value` = '86707325fc571e56242fccc46fd24466' diff --git a/include/upgrader/streams/core/26fd79dc-00c949a6.cleanup.sql b/include/upgrader/streams/core/26fd79dc-00c949a6.cleanup.sql new file mode 100644 index 0000000000000000000000000000000000000000..50cad9d04f5cb4a2ee7b773c2841637a6659a2de --- /dev/null +++ b/include/upgrader/streams/core/26fd79dc-00c949a6.cleanup.sql @@ -0,0 +1,3 @@ +-- Drop the state field from thread_events +ALTER TABLE `%TABLE_PREFIX%thread_event` + DROP COLUMN `state`; diff --git a/include/upgrader/streams/core/26fd79dc-00c949a6.patch.sql b/include/upgrader/streams/core/26fd79dc-00c949a6.patch.sql new file mode 100644 index 0000000000000000000000000000000000000000..774e248b961eee5cf30ee8459e3ca76496e27059 --- /dev/null +++ b/include/upgrader/streams/core/26fd79dc-00c949a6.patch.sql @@ -0,0 +1,45 @@ +/** +* @signature 00c949a623b82848baaf3480b51307e3 +* @version v1.11.0 +* @title Database Optimization +* +* This patch is for optimizing our database to handle large amounts of data +* more smoothly. +* +* 1. remove states in thread_event table and add them to their own event table +*/ + +-- Create a new table to store events +CREATE TABLE `%TABLE_PREFIX%event` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(60) NOT NULL, + `description` varchar(60) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `name` (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%event` (`id`, `name`, `description`) +VALUES + (1,'created',''), + (2,'closed',''), + (3,'reopened',''), + (4,'assigned',''), + (5,'released',''), + (6,'transferred',''), + (7,'referred',''), + (8,'overdue',''), + (9,'edited',''), + (10,'viewed',''), + (11,'error',''), + (12,'collab',''), + (13,'resent',''), + (14,'deleted',''); + +-- Add event_id column to thread_events +ALTER TABLE `%TABLE_PREFIX%thread_event` + ADD `event_id` int(11) unsigned AFTER `thread_id`; + +-- Finished with patch +UPDATE `%TABLE_PREFIX%config` + SET `value` = '00c949a623b82848baaf3480b51307e3', `updated` = NOW() + WHERE `key` = 'schema_signature' AND `namespace` = 'core'; diff --git a/include/upgrader/streams/core/26fd79dc-00c949a6.task.php b/include/upgrader/streams/core/26fd79dc-00c949a6.task.php new file mode 100644 index 0000000000000000000000000000000000000000..01895a67dc5f85e7cbb0df73d05a0f2599dba64a --- /dev/null +++ b/include/upgrader/streams/core/26fd79dc-00c949a6.task.php @@ -0,0 +1,134 @@ +<?php + +class EventEnumRemoval extends MigrationTask { + var $description = "Remove the Enum 'state' field from ThreadEvents"; + var $queue; + var $skipList; + var $errorList = array(); + var $limit = 20000; + + function sleep() { + return array('queue'=>$this->queue, 'skipList'=>$this->skipList); + } + function wakeup($stuff) { + $this->queue = $stuff['queue']; + $this->skipList = $stuff['skipList']; + while (!$this->isFinished()) + $this->do_batch(30, $this->limit); + } + + function run($max_time) { + $this->do_batch($max_time * 0.9, $this->limit); + } + + function isFinished() { + return $this->getQueueLength() == 0; + } + + function do_batch($time=30, $max=0) { + if(!$this->queueEvents($max) || !$this->getQueueLength()) + return 0; + + $this->setStatus("{$this->getQueueLength()} events remaining"); + + $count = 0; + $start = Misc::micro_time(); + while ($this->getQueueLength() && (Misc::micro_time()-$start) < $time) { + if($this->next() && $max && ++$count>=$max) { + break; + } + } + + return $this->queueEvents($max); + } + + function queueEvents($limit=0){ + global $cfg, $ost; + + # Since the queue is persistent - we want to make sure we get to empty + # before we find more events. + if(($qc=$this->getQueueLength())) + return $qc; + + $sql = "SELECT COUNT(t.id) FROM ".THREAD_EVENT_TABLE. " t + INNER JOIN ".EVENT_TABLE. " e ON (e.name=t.state) + WHERE t.event_id IS NULL"; + + //XXX: Do a hard fail or error querying the database? + if(!($res=db_query($sql))) + return $this->error('Unable to query DB for Thread Event migration!'); + + $count = db_result($res); + + // Force the log message to the database + $ost->logDebug("Thread Event Migration", 'Found '.$count + .' events to migrate', true); + + if($count == 0) + return 0; //Nothing else to do!! + + $start = db_result(db_query("SELECT id FROM ".THREAD_EVENT_TABLE. " + WHERE event_id IS NULL + ORDER BY id ASC LIMIT 1")); + + $this->queue = array(); + $info=array( + 'count' => $count, + 'start' => $start, + 'end' => $start + $limit + ); + $this->enqueue($info); + + return $this->getQueueLength(); + } + + function skip($eventId, $error) { + $this->skipList[] = $eventId; + + return $this->error($error." (ID #$eventId)"); + } + + function error($what) { + global $ost; + + $this->errors++; + $this->errorList[] = $what; + // Log the error but don't send the alert email + $ost->logError('Upgrader: Thread Event Migrater', $what, false); + # Assist in returning FALSE for inline returns with this method + return false; + } + + function getErrors() { + return $this->errorList; + } + + function getSkipList() { + return $this->skipList; + } + + function enqueue($info) { + $this->queue[] = $info; + } + + function getQueueLength() { + return count($this->queue); + } + + function next() { + # Fetch next item -- use the last item so the array indices don't + # need to be recalculated for every shift() operation. + $info = array_pop($this->queue); + + $sql = "UPDATE ".THREAD_EVENT_TABLE. " t + INNER JOIN ".EVENT_TABLE. " e ON (e.name=t.state) + SET t.event_id = e.id + WHERE t.event_id IS NULL AND t.id <= ". $info['end']; + + db_query($sql); + + return true; + } +} +return 'EventEnumRemoval'; +?> diff --git a/include/upgrader/streams/core/70921d5c-26fd79dc.patch.sql b/include/upgrader/streams/core/70921d5c-26fd79dc.patch.sql index f3bc7e0b201ea76bb59c6ff4de8f1ed272740d82..fd5953f5b6870f0cd53ffd5e42f8e36ea0019670 100644 --- a/include/upgrader/streams/core/70921d5c-26fd79dc.patch.sql +++ b/include/upgrader/streams/core/70921d5c-26fd79dc.patch.sql @@ -5,10 +5,6 @@ * * This patch is for final revisions needed for v1.11 */ - -ALTER TABLE `%TABLE_PREFIX%thread_event` - CHANGE `state` `state` enum('created','closed','reopened','assigned', 'released', 'transferred', 'referred', 'overdue','edited','viewed','error','collab','resent', 'deleted') NOT NULL; - ALTER TABLE `%TABLE_PREFIX%attachment` ADD INDEX `file_object` (`file_id`,`object_id`); diff --git a/scp/autocron.php b/scp/autocron.php index 170ab3a8b421286bdfe1db6a648a13e7b971946d..cc124455f3a246ef97f37ce3fe7482d8d9797e93 100644 --- a/scp/autocron.php +++ b/scp/autocron.php @@ -45,6 +45,11 @@ if ($sec < 180 || !$ost || $ost->isUpgradePending()) require_once(INCLUDE_DIR.'class.cron.php'); +// Run tickets count every 3rd run or so... force new count by skipping cached +// results +if ((mt_rand(1, 12) % 3) == 0) + SavedQueue::counts($thisstaff, array(), false); + // Clear staff obj to avoid false credit internal notes & auto-assignment $thisstaff = null; diff --git a/scp/css/scp.css b/scp/css/scp.css index 878333317fb1d3e17f90f6d87dab46254a3b00f9..f255ae506c045beacf0499e528f31b4b5adee4b1 100644 --- a/scp/css/scp.css +++ b/scp/css/scp.css @@ -3525,6 +3525,12 @@ table.grid.form caption { margin-bottom: 5px; } +.grid.form .field > .field-hint-text { + font-style: italic; + margin: 0 10px 5px 10px; + opacity: 0.8; +} + #basic_search { background-color: #f4f4f4; margin: -10px 0; diff --git a/scp/js/scp.js b/scp/js/scp.js index c83673cd3ce944df8139dcef8f3561e7f6707b0c..2b346edcb469f9df5789ad3b591e6935b79bf473 100644 --- a/scp/js/scp.js +++ b/scp/js/scp.js @@ -1120,7 +1120,7 @@ if ($.support.pjax) { if (!$this.hasClass('no-pjax') && !$this.closest('.no-pjax').length && $this.attr('href').charAt(0) != '#') - $.pjax.click(event, {container: $this.data('pjaxContainer') || $('#pjax-container'), timeout: 2000}); + $.pjax.click(event, {container: $this.data('pjaxContainer') || $('#pjax-container'), timeout: 30000}); }) } diff --git a/scp/queues.php b/scp/queues.php index f02f4efc8a5c3faa6d047d57d9962fa5fd0e86db..997fefa2678cae361a9654c6efecfaccf0199fe9 100644 --- a/scp/queues.php +++ b/scp/queues.php @@ -44,7 +44,7 @@ if ($_POST) { $queue = CustomQueue::create(array( 'staff_id' => 0, 'title' => $_POST['queue-name'], - 'root' => $_POST['root'] ?: 'T' + 'root' => 'T' )); if ($queue->update($_POST, $errors) && $queue->save(true)) { diff --git a/scp/tickets.php b/scp/tickets.php index d100f38d7b1af8e1505301922754e5925bc264dc..86fec81f28395a49905bcd3a342bfaf159be5d15 100644 --- a/scp/tickets.php +++ b/scp/tickets.php @@ -79,16 +79,20 @@ if (!$ticket) { elseif (isset($_GET['a']) && $_GET['a'] === 'search' && ($_GET['query']) ) { - $key = substr(md5($_GET['query']), -10); - if ($_GET['search-type'] == 'typeahead') { - // Use a faster index - $criteria = ['user__emails__address', 'equal', $_GET['query']]; - } - else { - $criteria = [':keywords', null, $_GET['query']]; + $wc = mb_str_wc($_GET['query']); + if ($wc < 4) { + $key = substr(md5($_GET['query']), -10); + if ($_GET['search-type'] == 'typeahead') { + // Use a faster index + $criteria = ['user__emails__address', 'equal', $_GET['query']]; + } else { + $criteria = [':keywords', null, $_GET['query']]; + } + $_SESSION['advsearch'][$key] = [$criteria]; + $queue_id = "adhoc,{$key}"; + } else { + $errors['err'] = __('Search term cannot have more than 3 keywords'); } - $_SESSION['advsearch'][$key] = [$criteria]; - $queue_id = "adhoc,{$key}"; } $queue_key = sprintf('::Q:%s', ObjectModel::OBJECT_TYPE_TICKET); @@ -462,7 +466,7 @@ foreach ($queues as $_) { || false !== strpos($queue->getPath(), "/{$q->getId()}/")); include STAFFINC_DIR . 'templates/queue-navigation.tmpl.php'; - return ($child_selected || $selected); + return $child_selected; }); } @@ -473,10 +477,7 @@ $nav->addSubMenu(function() use ($queue) { // A queue is selected if it is the one being displayed. It is // "child" selected if its ID is in the path of the one selected $child_selected = $queue instanceof SavedSearch; - $searches = SavedSearch::forStaff($thisstaff)->getIterator(); - include STAFFINC_DIR . 'templates/queue-savedsearches-nav.tmpl.php'; - return ($child_selected || $selected); }); diff --git a/setup/inc/streams/core/install-mysql.sql b/setup/inc/streams/core/install-mysql.sql index fbbb6e45050facca4ad2c6390657073d14b23c96..d023e83ca506ac49e41578256a0a7ead0cb41766 100644 --- a/setup/inc/streams/core/install-mysql.sql +++ b/setup/inc/streams/core/install-mysql.sql @@ -711,15 +711,24 @@ CREATE TABLE `%TABLE_PREFIX%lock` ( KEY `staff_id` (`staff_id`) ) DEFAULT CHARSET=utf8; +DROP TABLE IF EXISTS `%TABLE_PREFIX%event`; +CREATE TABLE `%TABLE_PREFIX%event` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(60) NOT NULL, + `description` varchar(60) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `name` (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + DROP TABLE IF EXISTS `%TABLE_PREFIX%thread_event`; CREATE TABLE `%TABLE_PREFIX%thread_event` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `thread_id` int(11) unsigned NOT NULL default '0', + `event_id` int(11) unsigned DEFAULT NULL, `staff_id` int(11) unsigned NOT NULL, `team_id` int(11) unsigned NOT NULL, `dept_id` int(11) unsigned NOT NULL, `topic_id` int(11) unsigned NOT NULL, - `state` enum('created','closed','reopened','assigned','released','transferred', 'referred', 'overdue','edited','viewed','error','collab','resent', 'deleted') NOT NULL, `data` varchar(1024) DEFAULT NULL COMMENT 'Encoded differences', `username` varchar(128) NOT NULL default 'SYSTEM', `uid` int(11) unsigned DEFAULT NULL, @@ -727,8 +736,8 @@ CREATE TABLE `%TABLE_PREFIX%thread_event` ( `annulled` tinyint(1) unsigned NOT NULL default '0', `timestamp` datetime NOT NULL, PRIMARY KEY (`id`), - KEY `ticket_state` (`thread_id`, `state`, `timestamp`), - KEY `ticket_stats` (`timestamp`, `state`) + KEY `ticket_state` (`thread_id`, `event_id`, `timestamp`), + KEY `ticket_stats` (`timestamp`, `event_id`) ) DEFAULT CHARSET=utf8; DROP TABLE IF EXISTS `%TABLE_PREFIX%thread_referral`;