diff --git a/include/class.orm.php b/include/class.orm.php index 31084569e7e8f06356f370b4df49bd38a96c465e..f9e51f55332c15e6546cee9cec5688a29d19e10b 100644 --- a/include/class.orm.php +++ b/include/class.orm.php @@ -465,7 +465,15 @@ class SqlFunction { } function toSql($compiler, $model=false, $alias=false) { - return sprintf('%s(%s)%s', $this->func, implode(',', $this->args), + $args = array(); + foreach ($this->args as $A) { + if ($A instanceof SqlFunction) + $A = $A->toSql($compiler, $model); + else + $A = $compiler->input($A); + $args[] = $A; + } + return sprintf('%s(%s)%s', $this->func, implode(',', $args), $alias && $this->alias ? ' AS '.$compiler->quote($this->alias) : ''); } @@ -489,9 +497,14 @@ class SqlExpression extends SqlFunction { function toSql($compiler, $model=false, $alias=false) { $O = array(); - foreach ($this->args as $operand) - $O[] = $compiler->input($operand); - return implode(' '.$this->func.' ', $O); + foreach ($this->args as $operand) { + if ($operand instanceof SqlFunction) + $O[] = $operand->toSql($compiler, $model); + else + $O[] = $compiler->input($operand); + } + return implode(' '.$this->func.' ', $O) + . ($alias ? ' AS '.$compiler->quote($alias) : ''); } static function __callStatic($operator, $operands) { @@ -500,6 +513,8 @@ class SqlExpression extends SqlFunction { $operator = '-'; break; case 'plus': $operator = '+'; break; + case 'times': + $operator = '*'; break; default: throw new InvalidArgumentException('Invalid operator specified'); } @@ -511,9 +526,15 @@ class SqlInterval extends SqlFunction { var $type; function toSql($compiler, $model=false, $alias=false) { + $A = $this->args[0]; + if ($A instanceof SqlFunction) + $A = $A->toSql($compiler, $model); + else + $A = $compiler->input($A); return sprintf('INTERVAL %s %s', - $compiler->input($this->args[0]), - $this->func); + $A, + $this->func) + . ($alias ? ' AS '.$compiler->quote($alias) : ''); } static function __callStatic($interval, $args) { @@ -525,14 +546,13 @@ class SqlInterval extends SqlFunction { } class SqlField extends SqlFunction { - function __construct($table, $column=false) { - $this->column = $column ?: $table; - if ($column) - $this->table = $table; + function __construct($field) { + $this->field = $field; } function toSql($compiler, $model=false, $alias=false) { - return $compiler->quote($this->column); + list($field) = $compiler->getField($this->field, $model); + return $field; } } @@ -634,6 +654,12 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable { $this->ordering = array_merge($this->ordering, func_get_args()); return $this; } + function getSortFields() { + $ordering = $this->ordering; + if ($this->extra['order_by']) + $ordering = array_merge($ordering, $this->extra['order_by']); + return $ordering; + } function lock($how=false) { $this->lock = $how ?: self::LOCK_EXCLUSIVE; @@ -669,7 +695,8 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable { } function values() { - $this->values = func_get_args(); + foreach (func_get_args() as $A) + $this->values[$A] = $A; $this->iterator = 'HashArrayIterator'; // This disables related models $this->related = false; @@ -1087,6 +1114,23 @@ class FlatArrayIterator extends ResultSet { } } +class HashArrayIterator extends ResultSet { + function __construct($queryset) { + $this->resource = $queryset->getQuery(); + } + function fillTo($index) { + while ($this->resource && $index >= count($this->cache)) { + if ($row = $this->resource->getArray()) { + $this->cache[] = $row; + } else { + $this->resource->close(); + $this->resource = null; + break; + } + } + } +} + class InstrumentedList extends ModelInstanceManager { var $key; var $id; @@ -1715,15 +1759,20 @@ class MySqlCompiler extends SqlCompiler { // Compile the ORDER BY clause $sort = ''; - if ($queryset->ordering && !isset($this->options['nosort'])) { + if (($columns = $queryset->getSortFields()) && !isset($this->options['nosort'])) { $orders = array(); - foreach ($queryset->ordering as $sort) { + foreach ($columns as $sort) { $dir = 'ASC'; - if ($sort[0] == '-') { - $dir = 'DESC'; - $sort = substr($sort, 1); + if ($sort instanceof SqlFunction) { + $field = $sort->toSql($this, $model); + } + else { + if ($sort[0] == '-') { + $dir = 'DESC'; + $sort = substr($sort, 1); + } + list($field) = $this->getField($sort, $model); } - list($field) = $this->getField($sort, $model); // TODO: Throw exception if $field can be indentified as // invalid $orders[] = $field.' '.$dir; @@ -1782,12 +1831,15 @@ class MySqlCompiler extends SqlCompiler { } // Support retrieving only a list of values rather than a model elseif ($queryset->values) { - foreach ($queryset->values as $v) { + foreach ($queryset->values as $alias=>$v) { list($f) = $this->getField($v, $model); if ($f instanceof SqlFunction) - $fields[$f->toSql($this, $model)] = true; - else + $fields[$f->toSql($this, $model, $alias)] = true; + else { + if (!is_int($alias)) + $f .= ' AS '.$this->quote($alias); $fields[$f] = true; + } } } // Simple selection from one table @@ -1816,6 +1868,14 @@ class MySqlCompiler extends SqlCompiler { foreach ($model::$meta['pk'] as $pk) $group_by[] = $rootAlias .'.'. $pk; } + // Add in SELECT extras + if (isset($queryset->extra['select'])) { + foreach ($queryset->extra['select'] as $name=>$expr) { + if ($expr instanceof SqlFunction) + $expr = $expr->toSql($this, false, $name); + $fields[] = $expr; + } + } if (isset($queryset->distinct)) { foreach ($queryset->distinct as $d) list($group_by[]) = $this->getField($d, $model); diff --git a/include/class.sla.php b/include/class.sla.php index 93e39307175c053d345b31b0c597124268b1a029..490508660f54d2e1303bf391d48a8082eb758e05 100644 --- a/include/class.sla.php +++ b/include/class.sla.php @@ -14,6 +14,13 @@ vim: expandtab sw=4 ts=4 sts=4: **********************************************************************/ +class SlaModel extends VerySimpleModel { + static $meta = array( + 'table' => SLA_TABLE, + 'pk' => array('sla_id'), + ); +} + class SLA { var $id; diff --git a/include/class.ticket.php b/include/class.ticket.php index 25798eada6d869b02fb35770330bf90a618d325a..b113c1850798d04820c6034aecc426eda875aa42 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -53,6 +53,10 @@ class TicketModel extends VerySimpleModel { 'dept' => array( 'constraint' => array('dept_id' => 'Dept.dept_id'), ), + 'sla' => array( + 'constraint' => array('sla_id' => 'SlaModel.id'), + 'null' => true, + ), 'staff' => array( 'constraint' => array('staff_id' => 'Staff.staff_id'), 'null' => true, @@ -179,8 +183,6 @@ class Ticket { return false; $sql='SELECT ticket.*, lock_id, dept_name ' - .' ,IF(sla.id IS NULL, NULL, ' - .'DATE_ADD(ticket.created, INTERVAL sla.grace_period HOUR)) as sla_duedate ' .' ,count(distinct attach.attach_id) as attachments' .' FROM '.TICKET_TABLE.' ticket ' .' LEFT JOIN '.DEPT_TABLE.' dept ON (ticket.dept_id=dept.dept_id) ' @@ -410,7 +412,22 @@ class Ticket { } function getSLADueDate() { - return $this->ht['sla_duedate']; + if ($sla = $this->getSLA()) { + $dt = new DateTime($this->getCreateDate()); + + return $dt + ->add(new DateInterval('PT' . $sla->getGracePeriod() . 'H')) + ->format('Y-m-d H:i:s'); + } + } + + function updateEstDueDate() { + $estimatedDueDate = $this->getEstDueDate(); + if ($estimatedDueDate != $this->ht['est_duedate']) { + $sql = 'UPDATE '.TICKET_TABLE.' SET `est_duedate`='.db_input($estimatedDueDate) + .' WHERE `ticket_id`='.db_input($this->getId()); + db_query($sql); + } } function getEstDueDate() { @@ -878,10 +895,15 @@ class Ticket { function setSLAId($slaId) { if ($slaId == $this->getSLAId()) return true; - return db_query( + $rv = db_query( 'UPDATE '.TICKET_TABLE.' SET sla_id='.db_input($slaId) .' WHERE ticket_id='.db_input($this->getId())) && db_affected_rows(); + if ($rv) { + $this->ht['sla_id'] = $slaId; + $this->sla = null; + } + return $rv; } /** * Selects the appropriate service-level-agreement plan for this ticket. @@ -2251,10 +2273,14 @@ class Ticket { && (!$this->getSLA() || $this->getSLA()->isTransient())) $this->selectSLAId(); + // Update estimated due date in database + $estimatedDueDate = $this->getEstDueDate(); + $this->updateEstDueDate(); + // Clear overdue flag if duedate or SLA changes and the ticket is no longer overdue. if($this->isOverdue() - && (!$this->getEstDueDate() //Duedate + SLA cleared - || Misc::db2gmtime($this->getEstDueDate()) > Misc::gmtime() //New due date in the future. + && (!$estimatedDueDate //Duedate + SLA cleared + || Misc::db2gmtime($estimatedDueDate) > Misc::gmtime() //New due date in the future. )) { $this->clearOverdue(); } @@ -2801,6 +2827,9 @@ class Ticket { $ticket->assignToTeam($vars['teamId'], _S('Auto Assignment')); } + // Update the estimated due date in the database + $this->updateEstDueDate(); + /********** double check auto-response ************/ //Override auto responder if the FROM email is one of the internal emails...loop control. if($autorespond && (Email::getIdByEmail($ticket->getEmail()))) diff --git a/include/pear/Mail.php b/include/pear/Mail.php index 75132ac2a6c3e9d99bd1784feb41154f0cd71d3d..5d4d3b09dd61c14a501cc52cb8d2f9f1499bb98a 100644 --- a/include/pear/Mail.php +++ b/include/pear/Mail.php @@ -74,14 +74,16 @@ class Mail function &factory($driver, $params = array()) { $driver = strtolower($driver); - @include_once 'Mail/' . $driver . '.php'; $class = 'Mail_' . $driver; + if (!class_exists($class)) + include_once PEAR_DIR.'Mail/' . $driver . '.php'; + if (class_exists($class)) { $mailer = new $class($params); return $mailer; - } else { - return PEAR::raiseError('Unable to find class for driver ' . $driver); } + + return PEAR::raiseError('Unable to find class for driver ' . $driver); } /** diff --git a/include/staff/templates/status-options.tmpl.php b/include/staff/templates/status-options.tmpl.php index edfdf19564e0367a1ebaa97e3adc1e08d075cf04..cdcaa395bec9a424a61f6bf62f9b4a29f0ef58b2 100644 --- a/include/staff/templates/status-options.tmpl.php +++ b/include/staff/templates/status-options.tmpl.php @@ -15,7 +15,7 @@ $actions= array( ?> <span - class="action-button pull-right" + class="action-button" data-dropdown="#action-dropdown-statuses"> <i class="icon-caret-down pull-right"></i> <a class="tickets-action" diff --git a/include/staff/tickets.inc.php b/include/staff/tickets.inc.php index 478d99c9cb19f5c1bc4be4585ebcf3745bc612c2..3d72b883803150f312e5947a7a56ee95e5108ea5 100644 --- a/include/staff/tickets.inc.php +++ b/include/staff/tickets.inc.php @@ -2,6 +2,8 @@ $search = SavedSearch::create(); $tickets = TicketModel::objects(); $clear_button = false; +$date_header = $date_col = false; + // Add "other" fields (via $_POST['other'][]) switch(strtolower($_REQUEST['status'])){ //Status is overloaded @@ -36,6 +38,7 @@ default: $tickets = $search->mangleQuerySet($tickets, $form); $results_type=__('Advanced Search') . '<a class="action-button" href="?clear_filter"><i class="icon-ban-circle"></i> <em>' . __('clear') . '</em></a>'; + unset($_REQUEST['sort']); break; } // Fall-through and show open tickets @@ -71,11 +74,38 @@ $tickets->filter(Q::any($visibility)); // Select pertinent columns // ------------------------------------------------------------ -$tickets->select_related('lock', 'dept', 'staff', 'user', 'user__default_email', 'topic', 'status', 'cdata', 'cdata__:priority'); +#$tickets->select_related('lock', 'dept', 'staff', 'user', 'user__default_email', 'topic', 'status', 'cdata', 'cdata__:priority'); +$tickets->values('lock__lock_id', 'staff_id', 'isoverdue', 'team_id', 'ticket_id', 'number', 'cdata__subject', 'user__default_email__address', 'source', 'cdata__:priority__priority_color', 'cdata__:priority__priority_desc', 'status__name', 'status__state', 'dept__dept_name', 'user__name', 'updated'); // Apply requested quick filter // Apply requested sorting +switch ($_REQUEST['sort']) { +case 'number': + $tickets->extra(array( + 'order_by'=>array(SqlExpression::times(new SqlField('number'), 1)) + )); + break; +case 'created': + $tickets->order_by('-created'); + break; + +case 'priority,due': + $tickets->order_by('cdata__:priority__priority_urgency'); + // Fall through to add in due date filter +case 'due': + $date_header = __('Due Date'); + $date_col = 'est_duedate'; + $tickets->values('est_duedate'); + $tickets->filter(array('est_duedate__isnull'=>false)); + $tickets->order_by(new SqlField('est_duedate')); + break; + +default: +case 'updated': + $tickets->order_by('cdata__:priority__priority_urgency', '-updated'); + break; +} // Apply requested pagination $pagelimit=($_GET['limit'] && is_numeric($_GET['limit']))?$_GET['limit']:PAGE_LIMIT; @@ -117,19 +147,30 @@ $_SESSION[':Q:tickets'] = $tickets; $results_type.$showing; ?></a></h2> </div> <div class="pull-right flush-right"> - + <span style="display:inline-block"> + <span style="vertical-align: baseline">Sort:</span> + <select name="sort" onchange="javascript:addSearchParam('sort', $(this).val());"> +<?php foreach (array( + 'updated' => __('Most Recently Updated'), + 'created' => __('Most Recently Created'), + 'due' => __('Due Soon'), + 'priority,due' => __('Priority + Due Soon'), + 'number' => __('Ticket Number'), +) as $mode => $desc) { ?> + <option value="<?php echo $mode; ?>" <?php if ($mode == $_REQUEST['sort']) echo 'selected="selected"'; ?>><?php echo $desc; ?></option> +<?php } ?> + </select> + </span> <?php + if ($thisstaff->canManageTickets()) { + echo TicketStatus::status_options(); + } if ($thisstaff->canDeleteTickets()) { ?> - <a id="tickets-delete" class="action-button pull-right tickets-action" + <a id="tickets-delete" class="action-button tickets-action" href="#tickets/status/delete"><i class="icon-trash"></i> <?php echo __('Delete'); ?></a> <?php } ?> - <?php - if ($thisstaff->canManageTickets()) { - echo TicketStatus::status_options(); - } - ?> </div> </div> <div class="clear" style="margin-bottom:10px;"></div> @@ -145,27 +186,21 @@ $_SESSION[':Q:tickets'] = $tickets; <th width="8px"> </th> <?php } ?> <th width="70"> - <a <?php echo $id_sort; ?> href="tickets.php?sort=ID&order=<?php echo $negorder; ?><?php echo $qstr; ?>" - title="<?php echo sprintf(__('Sort by %s %s'), __('Ticket ID'), __($negorder)); ?>"><?php echo __('Ticket'); ?></a></th> + <?php echo __('Ticket'); ?></th> <th width="70"> - <a <?php echo $date_sort; ?> href="tickets.php?sort=date&order=<?php echo $negorder; ?><?php echo $qstr; ?>" - title="<?php echo sprintf(__('Sort by %s %s'), __('Date'), __($negorder)); ?>"><?php echo __('Date'); ?></a></th> + <?php echo $date_header ?: __('Date'); ?></th> <th width="280"> - <a <?php echo $subj_sort; ?> href="tickets.php?sort=subj&order=<?php echo $negorder; ?><?php echo $qstr; ?>" - title="<?php echo sprintf(__('Sort by %s %s'), __('Subject'), __($negorder)); ?>"><?php echo __('Subject'); ?></a></th> + <?php echo __('Subject'); ?></th> <th width="170"> - <a <?php echo $name_sort; ?> href="tickets.php?sort=name&order=<?php echo $negorder; ?><?php echo $qstr; ?>" - title="<?php echo sprintf(__('Sort by %s %s'), __('Name'), __($negorder)); ?>"><?php echo __('From');?></a></th> + <?php echo __('From');?></th> <?php if($search && !$status) { ?> <th width="60"> - <a <?php echo $status_sort; ?> href="tickets.php?sort=status&order=<?php echo $negorder; ?><?php echo $qstr; ?>" - title="<?php echo sprintf(__('Sort by %s %s'), __('Status'), __($negorder)); ?>"><?php echo __('Status');?></a></th> + <?php echo __('Status');?></th> <?php } else { ?> <th width="60" <?php echo $pri_sort;?>> - <a <?php echo $pri_sort; ?> href="tickets.php?sort=pri&order=<?php echo $negorder; ?><?php echo $qstr; ?>" - title="<?php echo sprintf(__('Sort by %s %s'), __('Priority'), __($negorder)); ?>"><?php echo __('Priority');?></a></th> + <?php echo __('Priority');?></th> <?php } @@ -173,19 +208,16 @@ $_SESSION[':Q:tickets'] = $tickets; //Closed by if(!strcasecmp($status,'closed')) { ?> <th width="150"> - <a <?php echo $staff_sort; ?> href="tickets.php?sort=staff&order=<?php echo $negorder; ?><?php echo $qstr; ?>" - title="<?php echo sprintf(__('Sort by %s %s'), __("Closing Agent's Name"), __($negorder)); ?>"><?php echo __('Closed By'); ?></a></th> + <?php echo __('Closed By'); ?></th> <?php } else { //assigned to ?> <th width="150"> - <a <?php echo $assignee_sort; ?> href="tickets.php?sort=assignee&order=<?php echo $negorder; ?><?php echo $qstr; ?>" - title="<?php echo sprintf(__('Sort by %s %s'), __('Assignee'), __($negorder)); ?>"><?php echo __('Assigned To'); ?></a></th> + <?php echo __('Assigned To'); ?></th> <?php } } else { ?> <th width="150"> - <a <?php echo $dept_sort; ?> href="tickets.php?sort=dept&order=<?php echo $negorder;?><?php echo $qstr; ?>" - title="<?php echo sprintf(__('Sort by %s %s'), __('Department'), __($negorder)); ?>"><?php echo __('Department');?></a></th> + <?php echo __('Department');?></th> <?php } ?> </tr> diff --git a/include/upgrader/streams/core/b26f29a6-d7480e1c.patch.sql b/include/upgrader/streams/core/b26f29a6-d7480e1c.patch.sql index d71ebd71f7038bc0b81ecabc28008cae21013db3..a44de85cd550a7e0d0ecbc58e3b1a4936790406c 100644 --- a/include/upgrader/streams/core/b26f29a6-d7480e1c.patch.sql +++ b/include/upgrader/streams/core/b26f29a6-d7480e1c.patch.sql @@ -149,6 +149,14 @@ UPDATE `%TABLE_PREFIX%user_account` A1 DROP TABLE %TABLE_PREFIX%_timezones; +ALTER TABLE `%TABLE_PREFIX%ticket` + ADD `est_duedate` datetime default NULL AFTER `duedate`; + +UPDATE `%TABLE_PREFIX%ticket` A1 + JOIN `%TABLE_PREFIX%sla` A2 ON (A1.sla_id = A2.id) + SET A1.`est_duedate` = + COALESCE(A1.`duedate`, A1.`created` + INTERVAL A2.`grace_period` HOUR); + -- Finished with patch UPDATE `%TABLE_PREFIX%config` SET `value` = 'd7480e1c31a1f20d6954ecbb342722d3' diff --git a/scp/css/dropdown.css b/scp/css/dropdown.css index 54c277ec96063ac30b7e33c0ec6824e7a0939db7..6105ea27fc4098cc3ba0fce3ae3467af0e8e7afb 100644 --- a/scp/css/dropdown.css +++ b/scp/css/dropdown.css @@ -105,6 +105,7 @@ text-decoration: none !important; line-height:18px; margin-left:5px; + vertical-align: bottom; } .action-button span, .action-button a { diff --git a/scp/css/scp.css b/scp/css/scp.css index 9d8183d8e123e4209f6f92676422259840135bac..6bbdcc8abcd61d9e67e4f5fbf7a19ec4aff505a0 100644 --- a/scp/css/scp.css +++ b/scp/css/scp.css @@ -459,7 +459,7 @@ table.list thead th { color:#000; text-align:left; vertical-align:top; - padding: 0 4px; + padding: 2px 4px; } table.list th a { diff --git a/scp/js/scp.js b/scp/js/scp.js index 5a93f7326fe8a19725388a39c0b0ed133ddea478..46d4d318a59409082c609a26e8d8c9f3544b81b7 100644 --- a/scp/js/scp.js +++ b/scp/js/scp.js @@ -840,3 +840,23 @@ function __(s) { return $.oststrings[s]; return s; } + +// Thanks, http://stackoverflow.com/a/487049 +function addSearchParam(key, value) { + key = encodeURI(key); value = encodeURI(value); + + var kvp = document.location.search.substr(1).split('&'); + var i=kvp.length; var x; + while (i--) { + x = kvp[i].split('='); + if (x[0]==key) { + x[1] = value; + kvp[i] = x.join('='); + break; + } + } + if(i<0) {kvp[kvp.length] = [key,value].join('=');} + + //this will reload the page, it's likely better to store this until finished + document.location.search = kvp.join('&'); +} diff --git a/setup/inc/streams/core/install-mysql.sql b/setup/inc/streams/core/install-mysql.sql index f1062b3715b919389119bcb264f7f26918edd860..b2c5b31009713b15c9fd2954409a5754064883d1 100644 --- a/setup/inc/streams/core/install-mysql.sql +++ b/setup/inc/streams/core/install-mysql.sql @@ -609,6 +609,7 @@ CREATE TABLE `%TABLE_PREFIX%ticket` ( `isoverdue` tinyint(1) unsigned NOT NULL default '0', `isanswered` tinyint(1) unsigned NOT NULL default '0', `duedate` datetime default NULL, + `est_duedate` datetime default NULL, `reopened` datetime default NULL, `closed` datetime default NULL, `lastmessage` datetime default NULL,