diff --git a/include/class.orm.php b/include/class.orm.php index 7c715f43a19002507cdf9ecbbb8ba8cb64ee8836..d8e70f6007b086fe77ab20ab0a79da96c97780eb 100644 --- a/include/class.orm.php +++ b/include/class.orm.php @@ -1105,7 +1105,7 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl // Load defaults from model $model = $this->model; $query = clone $this; - if (!$query->ordering && isset($model::$meta['ordering'])) + if (!$options['nosort'] && !$query->ordering && isset($model::$meta['ordering'])) $query->ordering = $model::$meta['ordering']; if (false !== $query->related && !$query->values && $model::$meta['select_related']) $query->related = $model::$meta['select_related']; @@ -1311,6 +1311,12 @@ class ModelInstanceManager extends ResultSet { if ($cache) $this->cache($m); } + elseif (get_class($m) != $modelClass) { + // Change the class of the object to be returned to match what + // was expected + // TODO: Emit a warning? + $m = new $modelClass($m->ht); + } // Wrap annotations in an AnnotatedModel if ($extras) { $m = new AnnotatedModel($m, $extras); @@ -1904,7 +1910,7 @@ class MySqlCompiler extends SqlCompiler { function __in($a, $b) { if (is_array($b)) { $vals = array_map(array($this, 'input'), $b); - $b = implode(', ', $vals); + $b = '('.implode(', ', $vals).')'; } // MySQL doesn't support LIMIT or OFFSET in subqueries. Instead, add // the query as a JOIN and add the join constraint into the WHERE @@ -1918,7 +1924,7 @@ class MySqlCompiler extends SqlCompiler { else { $b = $this->input($b); } - return sprintf('%s IN (%s)', $a, $b); + return sprintf('%s IN %s', $a, $b); } function __isnull($a, $b) { @@ -2009,7 +2015,7 @@ class MySqlCompiler extends SqlCompiler { if ($what instanceof QuerySet) { $q = $what->getQuery(array('nosort'=>true)); $this->params = array_merge($this->params, $q->params); - return $q->sql; + return '('.$q->sql.')'; } elseif ($what instanceof SqlFunction) { return $what->toSql($this); @@ -2079,7 +2085,7 @@ class MySqlCompiler extends SqlCompiler { // Compile the ORDER BY clause $sort = ''; - if (($columns = $queryset->getSortFields()) && !isset($this->options['nosort'])) { + if ($columns = $queryset->getSortFields()) { $orders = array(); foreach ($columns as $sort) { $dir = 'ASC'; @@ -2106,6 +2112,7 @@ class MySqlCompiler extends SqlCompiler { // Compile the field listing $fields = array(); $group_by = array(); + $model::_inspect(); $table = $this->quote($model::$meta['table']).' '.$rootAlias; // Handle related tables if ($queryset->related) { @@ -2113,7 +2120,6 @@ class MySqlCompiler extends SqlCompiler { $fieldMap = $theseFields = array(); $defer = $queryset->defer ?: array(); // Add local fields first - $model::_inspect(); foreach ($model::$meta['fields'] as $f) { // Handle deferreds if (isset($defer[$f])) diff --git a/include/class.search.php b/include/class.search.php index 137c1d375b041492ba8d285c6d2c27e792195867..bfc59f4c07101a850e91f92ba983457d4e44de88 100644 --- a/include/class.search.php +++ b/include/class.search.php @@ -450,7 +450,8 @@ class MysqlSearchBackend extends SearchBackend { return false; while ($row = db_fetch_row($res)) { - $ticket = Ticket::lookup($row[0]); + if (!($ticket = Ticket::lookup($row[0]))) + continue; $cdata = $ticket->loadDynamicData(); $content = array(); foreach ($cdata as $k=>$a) diff --git a/include/class.thread.php b/include/class.thread.php index e5a78cde2258b6980add5bd495b766ad5a4dbbd1..2b8be9fa597ec0ecbdcef95a24088f04a8cfd2e7 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -1553,6 +1553,10 @@ class ThreadEvent extends VerySimpleModel { $collabs = array(); if ($data['add']) { foreach ($data['add'] as $c) { + if (is_array($c)) + $c = sprintf(__("%s via %a" + /* e.g. "Me <me@company.me> via Email (to)" */), + $c[0], $c[1]); $collabs[] = Format::htmlchars($c); } } @@ -1666,34 +1670,31 @@ class ThreadEvent extends VerySimpleModel { include $inc . 'templates/thread-event.tmpl.php'; } - static function create($ht=false) { + static function create($ht=false, $user=false) { $inst = parent::create($ht); $inst->timestamp = SqlFunction::NOW(); global $thisstaff, $thisclient; - if ($thisstaff) { + $user = is_object($user) ? $user : $thisstaff ?: $thisclient; + if ($user instanceof Staff) { $inst->uid_type = 'S'; - $inst->uid = $thisstaff->getId(); + $inst->uid = $user->getId(); } - else if ($thisclient) { + elseif ($user instanceof User) { $inst->uid_type = 'U'; - $inst->uid = $thisclient->getId(); + $inst->uid = $user->getId(); } return $inst; } - static function forTicket($ticket, $state) { + static function forTicket($ticket, $state, $user=false) { $inst = static::create(array( 'staff_id' => $ticket->getStaffId(), 'team_id' => $ticket->getTeamId(), 'dept_id' => $ticket->getDeptId(), 'topic_id' => $ticket->getTopicId(), - )); - if (!isset($inst->uid_type) && $state == self::CREATED) { - $inst->uid_type = 'U'; - $inst->uid = $ticket->getOwnerId(); - } + ), $user); return $inst; } } @@ -1705,13 +1706,27 @@ class ThreadEvents extends InstrumentedList { ->update(array('annulled' => 1)); } - function log($object, $state, $data=null, $annul=null, $username=null) { + /** + * Add an event to the thread activity log. + * + * Parameters: + * $object - Object to log activity for + * $state - State name of the activity (one of 'created', 'edited', + * 'deleted', 'closed', 'reopened', 'error', 'collab', 'resent', + * 'assigned', 'transferred') + * $data - (array?) Details about the state change + * $user - (string|User|Staff) user triggering the state change + * $annul - (state) a corresponding state change that is annulled by + * this event + */ + function log($object, $state, $data=null, $user=null, $annul=null) { global $thisstaff, $thisclient; if ($object instanceof Ticket) - $event = ThreadEvent::forTicket($object, $state); + // TODO: Use $object->createEvent() + $event = ThreadEvent::forTicket($object, $state, $user); else - $event = ThreadEvent::create(); + $event = ThreadEvent::create(false, $user); # Annul previous entries if requested (for instance, reopening a # ticket will annul an 'closed' entry). This will be useful to @@ -1720,11 +1735,14 @@ class ThreadEvents extends InstrumentedList { $this->annul($annul); } - if ($username === null) { - if ($thisstaff) { - $username = $thisstaff->getUserName(); + $username = $user; + $user = is_object($user) ? $user : $thisclient ?: $thisstaff; + if (!is_string($username)) { + if ($user instanceof Staff) { + $username = $user->getUserName(); } - else if ($thisclient) { + // XXX: Use $user here + elseif ($thisclient) { if ($thisclient->hasAccount) $username = $thisclient->getAccount()->getUserName(); if (!$username) @@ -2097,6 +2115,7 @@ implements TemplateVariable { static $types = array( ObjectModel::OBJECT_TYPE_TASK => 'TaskThread', + ObjectModel::OBJECT_TYPE_TICKET => 'TicketThread', ); var $counts; diff --git a/include/class.ticket.php b/include/class.ticket.php index 3a48f7db8f3249d0cff914c4c15acbf2270fc726..8a771c3237eb81f0333b7af69855f358432bd54e 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -647,20 +647,20 @@ implements RestrictedAccess, Threadable { } function getLastRespondent() { - if (!isset($this->lastrespondent)) { $this->lastresponent = Staff::objects() ->filter(array( 'staff_id' => static::objects() ->filter(array( - 'thread__entry__type' => 'R', - 'thread__entry__staff_id__gt' => 0 + 'thread__entries__type' => 'R', + 'thread__entries__staff_id__gt' => 0 )) - ->values_flat('thread__entry__staff_id') - ->order_by('-thread__entry__id') - ->first() + ->values_flat('thread__entries__staff_id') + ->order_by('-thread__entries__id') + ->limit(1) )) - ->first(); + ->first() + ?: false; } return $this->lastrespondent; } @@ -799,7 +799,7 @@ implements RestrictedAccess, Threadable { return $fields[0]; } - function addCollaborator($user, $vars, &$errors) { + function addCollaborator($user, $vars, &$errors, $event=true) { if (!$user || $user->getId() == $this->getOwnerId()) return null; @@ -813,7 +813,8 @@ implements RestrictedAccess, Threadable { $this->collaborators = null; $this->recipients = null; - $this->logEvent('collab', array('add' => array($c->toString()))); + if ($event) + $this->logEvent('collab', array('add' => array($c->toString()))); return $c; } @@ -1020,9 +1021,9 @@ implements RestrictedAccess, Threadable { if ($this->getStatusId() == $status->getId()) return true; - //TODO: move this up. + // Perform checks on the *new* status, _before_ the status changes $ecb = null; - switch($status->getState()) { + switch ($status->getState()) { case 'closed': if ($this->getMissingRequiredFields()) { $errors['err'] = sprintf(__( @@ -1034,7 +1035,7 @@ implements RestrictedAccess, Threadable { $this->duedate = null; if ($thisstaff && $set_closing_agent) $this->staff = $thisstaff; - $this->clearOverdue(); + $this->clearOverdue(false); $ecb = function($t) { $t->logEvent('closed'); @@ -1046,7 +1047,7 @@ implements RestrictedAccess, Threadable { if ($this->isClosed()) { $this->closed = $this->lastupdate = $this->reopened = SqlFunction::NOW(); $ecb = function ($t) { - $t->logEvent('reopened', false, 'closed'); + $t->logEvent('reopened', false, null, 'closed'); }; } @@ -1066,7 +1067,7 @@ implements RestrictedAccess, Threadable { // Log status change b4 reload — if currently has a status. (On new // ticket, the ticket is opened and thereafter the status is set to // the requested status). - if ($current_status = $this->getStatus()) { + if ($hadStatus) { $alert = false; if ($comments) { // Send out alerts if comments are included @@ -1782,7 +1783,7 @@ implements RestrictedAccess, Threadable { return true; } - function clearOverdue() { + function clearOverdue($save=true) { if (!$this->isOverdue()) return true; @@ -1798,7 +1799,7 @@ implements RestrictedAccess, Threadable { if ($this->getSLADueDate() && Misc::db2gmtime($this->getSLADueDate()) <= Misc::gmtime()) $this->sla = null; - return $this->save(); + return $save ? $this->save() : true; } //Dept Tranfer...with alert.. done by staff @@ -2066,14 +2067,14 @@ implements RestrictedAccess, Threadable { continue; if (($user=User::fromVars($recipient))) - if ($c=$this->addCollaborator($user, $info, $errors)) + if ($c=$this->addCollaborator($user, $info, $errors, false)) // FIXME: This feels very unwise — should be a // string indexed array for future $collabs[] = array((string)$c, $recipient['source']); } // TODO: Can collaborators add others? if ($collabs) { - $this->logEvent('collab', array('add' => $collabs)); + $this->logEvent('collab', array('add' => $collabs), $message->user); } } @@ -2310,8 +2311,8 @@ implements RestrictedAccess, Threadable { } // History log -- used for statistics generation (pretty reports) - function logEvent($state, $data=null, $annul=null, $staff=null) { - $this->getThread()->getEvents()->log($this, $state, $data, $annul, $staff); + function logEvent($state, $data=null, $user=null, $annul=null) { + $this->getThread()->getEvents()->log($this, $state, $data, $user, $annul); } //Insert Internal Notes @@ -2566,11 +2567,11 @@ implements RestrictedAccess, Threadable { } Signal::send('model.updated', $this); - return true; + return $this->save(); } /*============== Static functions. Use Ticket::function(params); =============nolint*/ - function getIdByNumber($number, $email=null, $ticket=false) { + static function getIdByNumber($number, $email=null, $ticket=false) { if (!$number) return 0; @@ -2592,8 +2593,8 @@ implements RestrictedAccess, Threadable { } } - function lookupByNumber($number, $email=null) { - return self::getIdByNumber($number, $email, true); + static function lookupByNumber($number, $email=null) { + return static::getIdByNumber($number, $email, true); } static function isTicketNumberUnique($number) { @@ -3105,6 +3106,9 @@ implements RestrictedAccess, Threadable { $dept = $ticket->getDept(); + // Start tracking ticket lifecycle events (created should come first!) + $ticket->logEvent('created', null, $thisstaff ?: $user); + // Add organizational collaborators if ($org && $org->autoAddCollabs()) { $pris = $org->autoAddPrimaryContactsAsCollabs(); @@ -3148,11 +3152,10 @@ implements RestrictedAccess, Threadable { // Auto assign staff or team - auto assignment based on filter // rules. Both team and staff can be assigned if ($vars['staffId']) - $ticket->assignToStaff($vars['staffId'], _S('Auto Assignment')); + $ticket->assignToStaff($vars['staffId']); if ($vars['teamId']) // No team alert if also assigned to an individual agent - $ticket->assignToTeam($vars['teamId'], _S('Auto Assignment'), - !$vars['staffId']); + $ticket->assignToTeam($vars['teamId'], false, !$vars['staffId']); } // Update the estimated due date in the database @@ -3208,9 +3211,6 @@ implements RestrictedAccess, Threadable { $ticket->onOpenLimit($autorespond && strcasecmp($origin, 'staff')); } - /* Start tracking ticket lifecycle events */ - $ticket->logEvent('created'); - // Fire post-create signal (for extra email sending, searching) Signal::send('ticket.created', $ticket); diff --git a/include/class.user.php b/include/class.user.php index 655edfd00e4dcb9cd6f6023018a1192ff54c02c6..3ba2b54ae1d3cb0d7f194036d1a5412199b1afd4 100644 --- a/include/class.user.php +++ b/include/class.user.php @@ -51,7 +51,7 @@ class UserModel extends VerySimpleModel { 'account' => array( 'list' => false, 'null' => true, - 'reverse' => 'UserAccount.user', + 'reverse' => 'ClientAccount.user', ), 'org' => array( 'null' => true, diff --git a/include/client/templates/thread-entries.tmpl.php b/include/client/templates/thread-entries.tmpl.php index d031182cad2fed9947e5bf75a953b05fb81b0374..9b1fc704db6ed350c06b932abd3fcb93bcf00bf6 100644 --- a/include/client/templates/thread-entries.tmpl.php +++ b/include/client/templates/thread-entries.tmpl.php @@ -25,7 +25,7 @@ if (count($entries)) { // changes in dates between thread items. foreach ($entries as $entry) { // Emit all events prior to this entry - while ($event && $event->timestamp <= $entry->created) { + while ($event && $event->timestamp < $entry->created) { $event->render(ThreadEvent::MODE_CLIENT); $events->next(); $event = $events->current(); diff --git a/include/client/templates/thread-entry.tmpl.php b/include/client/templates/thread-entry.tmpl.php index 70e4bbb5c6fae24ba42a48a2c67520b044d03e58..6c16c0660d09c5e71527c90cffd302b0731df65a 100644 --- a/include/client/templates/thread-entry.tmpl.php +++ b/include/client/templates/thread-entry.tmpl.php @@ -57,6 +57,7 @@ if ($user && ($url = $user->get_gravatar(48))) </div> <div class="thread-body" id="thread-id-<?php echo $entry->getId(); ?>"> <div><?php echo $entry->getBody()->toHtml(); ?></div> + <div class="clear"></div> <?php if ($entry->has_attachments) { ?> <div class="attachments"><?php diff --git a/include/staff/templates/thread-entry.tmpl.php b/include/staff/templates/thread-entry.tmpl.php index cc5a704ab300058b15959a048dc0112521e63728..e7c31e2a512eb6eb614b9e88910dafc3a0a32107 100644 --- a/include/staff/templates/thread-entry.tmpl.php +++ b/include/staff/templates/thread-entry.tmpl.php @@ -45,7 +45,8 @@ if ($user && ($url = $user->get_gravatar(48))) </div> <?php echo sprintf(__('<b>%s</b> posted %s'), $name, - sprintf('<time class="relative" datetime="%s" title="%s">%s</time>', + sprintf('<a name="entry-%d" href="#entry-%1$s"><time class="relative" datetime="%s" title="%s">%s</time></a>', + $entry->id, date(DateTime::W3C, Misc::db2gmtime($entry->created)), Format::daydatetime($entry->created), Format::relativeTime(Misc::db2gmtime($entry->created)) @@ -57,6 +58,7 @@ if ($user && ($url = $user->get_gravatar(48))) </div> <div class="thread-body" id="thread-id-<?php echo $entry->getId(); ?>"> <div><?php echo $entry->getBody()->toHtml(); ?></div> + <div class="clear"></div> <?php if ($entry->has_attachments) { ?> <div class="attachments"><?php diff --git a/include/upgrader/streams/core/9143a511-0d6099a6.patch.sql b/include/upgrader/streams/core/9143a511-0d6099a6.patch.sql index 401b6780cfd4680deea77c8efa1a935f5184a5a3..c6bf19b42ba79cfde7f076d9b7b056856d0a0b55 100644 --- a/include/upgrader/streams/core/9143a511-0d6099a6.patch.sql +++ b/include/upgrader/streams/core/9143a511-0d6099a6.patch.sql @@ -22,7 +22,7 @@ CREATE TABLE `%TABLE_PREFIX%_ticket_thread_evt` WHERE `object_type` = 'T'; UPDATE `%TABLE_PREFIX%thread_event` A1 - JOIN `%TABLE_PREFIX%_ticket_thread_evt` A2 ON (A1.`thread_id` = A2.`object_id`) + LEFT JOIN `%TABLE_PREFIX%_ticket_thread_evt` A2 ON (A1.`thread_id` = A2.`object_id`) SET A1.`thread_id` = A2.`id`; DROP TABLE `%TABLE_PREFIX%_ticket_thread_evt`; diff --git a/scp/css/scp.css b/scp/css/scp.css index c965cb7a97cc4a025750ed740c6e0123b532ff97..0181a06d0fa67ae762e1e1c779de278fb5ab1350 100644 --- a/scp/css/scp.css +++ b/scp/css/scp.css @@ -70,6 +70,9 @@ div#header a { time[title]:hover { text-decoration: underline; } +a time.relative { + color: initial; +} .small[class^="icon-"], .small[class*=" icon-"] { diff --git a/scp/js/scp.js b/scp/js/scp.js index 2d93705cdbbe88dfc41d3e5e57faf06e5f1b40a4..ef9fd3071ba409b70e3d42f710a589ed0fc26a28 100644 --- a/scp/js/scp.js +++ b/scp/js/scp.js @@ -1068,3 +1068,30 @@ function addSearchParam(key, value) { //this will reload the page, it's likely better to store this until finished return kvp.join('&'); } + +// Periodically adjust relative times +window.relativeAdjust = setInterval(function() { + var prettyDate = function(time) { + var date = new Date((time || "").replace(/-/g, "/").replace(/[TZ]/g, " ")), + diff = (((new Date()).getTime() - date.getTime()) / 1000), + day_diff = Math.floor(diff / 86400); + + if (isNaN(day_diff) || day_diff < 0 || day_diff >= 31) return; + + return day_diff == 0 && ( + diff < 60 && __("just now") + || diff < 120 && __("about a minute ago") + || diff < 3600 && __("%d minutes ago").replace('%d', Math.floor(diff/60)) + || diff < 7200 && __("about an hour ago") + || diff < 86400 && __("%d hours ago").replace('%d', Math.floor(diff/86400)) + ) + || day_diff == 1 && __("yesterday") + || day_diff < 7 && __("%d days ago").replace('%d', day_diff); + // Longer dates don't need to change dynamically + }; + $('time.relative[datetime]').each(function() { + var rel = prettyDate($(this).attr('datetime')); + if (rel) $(this).text(rel); + }); +}, 20000); +