diff --git a/include/ajax.search.php b/include/ajax.search.php index 93da251e7d164e9548550dce3b35cf16051bbcf3..dbc0a291b86d3c7bebb6f1c67333735b23739b9e 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) { 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 c9985089b344b1c8836f91ed596f13f47d1436d8..ea29885cbac6003708e8a68ab755651a04d6810c 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.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.queue.php b/include/class.queue.php index 8526de7005006f2310744bff29abf57ae133d4d8..48f82c81f44e47f7ee8dac4a8bac2eb75c84679b 100644 --- a/include/class.queue.php +++ b/include/class.queue.php @@ -574,7 +574,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 +582,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 +629,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 +791,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 +814,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()); - return Export::saveTickets($query, $fields, $filename, 'csv', - $options); + // 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; + }; + + $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(); } /** @@ -1427,7 +1473,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 +1508,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 +1527,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,9 +1558,10 @@ 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')) ->aggregate(array('count' => SqlAggregate::COUNT('events__id'))) @@ -1531,10 +1590,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 +1621,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 +1649,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 +1668,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 +1683,7 @@ extends QueueColumnAnnotation { static $icon = "lock"; static $desc = /* @trans */ 'Locked'; - function annotate($query) { + function annotate($query, $name=false) { global $thisstaff; return $query @@ -1649,7 +1710,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 +1749,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 +2045,7 @@ extends VerySimpleModel { var $_annotations; var $_conditions; var $_queue; // Apparent queue if being inherited + var $_fields; function getId() { return $this->id; @@ -2022,6 +2084,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 +2170,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 +2243,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)); } diff --git a/include/class.search.php b/include/class.search.php index e4c6111b5fe313438577394dcbae5cebf271ec3a..c32916504b3d4200ccdde403e00ebe4656c4a9cb 100644 --- a/include/class.search.php +++ b/include/class.search.php @@ -1004,21 +1004,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 +1250,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 +1297,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 +1312,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 +1343,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 +1436,7 @@ class OpenClosedTicketStatusList extends TicketStatusList { return $rv; } } + class TicketStatusChoiceField extends SelectionField { static $widget = 'ChoicesWidget'; @@ -1427,6 +1469,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.ticket.php b/include/class.ticket.php index 43446e89e67ed43a885f61045ec3266f3f1efbc2..0347004deffe2efb3a92ff50ea94d8434cf1a5d6 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'),