diff --git a/include/class.client.php b/include/class.client.php index 5b819e2125857909a86ae27992b67b040cf23bbf..704b046c6b8384bef8eba09f7a2e275e2726fd69 100644 --- a/include/class.client.php +++ b/include/class.client.php @@ -55,6 +55,11 @@ implements EmailContact, ITicketUser, TemplateVariable { } + // Required for Internationalization::getCurrentLanguage() in templates + function getLanguage() { + return $this->user->getLanguage(); + } + static function getVarScope() { return array( 'name' => array('class' => 'PersonsName', 'desc' => __('Full name')), diff --git a/include/class.format.php b/include/class.format.php index 83eb012168a119a405549be8e9872240f4808f17..59becb5d231ee103831dc1f6421b80ed8ef7a63d 100644 --- a/include/class.format.php +++ b/include/class.format.php @@ -438,7 +438,7 @@ class Format { } function __formatDate($timestamp, $format, $fromDb, $dayType, $timeType, - $strftimeFallback, $timezone) { + $strftimeFallback, $timezone, $user=false) { global $cfg; if ($timestamp && $fromDb) { @@ -446,7 +446,7 @@ class Format { } if (class_exists('IntlDateFormatter')) { $formatter = new IntlDateFormatter( - Internationalization::getCurrentLocale(), + Internationalization::getCurrentLocale($user), $dayType, $timeType, $timezone, @@ -492,40 +492,40 @@ class Format { return strtotime($date); } - function time($timestamp, $fromDb=true, $format=false, $timezone=false) { + function time($timestamp, $fromDb=true, $format=false, $timezone=false, $user=false) { global $cfg; return self::__formatDate($timestamp, $format ?: $cfg->getTimeFormat(), $fromDb, IDF_NONE, IDF_SHORT, - '%x', $timezone ?: $cfg->getTimezone()); + '%x', $timezone ?: $cfg->getTimezone(), $user); } - function date($timestamp, $fromDb=true, $format=false, $timezone=false) { + function date($timestamp, $fromDb=true, $format=false, $timezone=false, $user=false) { global $cfg; return self::__formatDate($timestamp, $format ?: $cfg->getDateFormat(), $fromDb, IDF_SHORT, IDF_NONE, - '%X', $timezone ?: $cfg->getTimezone()); + '%X', $timezone ?: $cfg->getTimezone(), $user); } - function datetime($timestamp, $fromDb=true, $timezone=false) { + function datetime($timestamp, $fromDb=true, $timezone=false, $user=false) { global $cfg; return self::__formatDate($timestamp, $cfg->getDateTimeFormat(), $fromDb, IDF_SHORT, IDF_SHORT, - '%X %x', $timezone ?: $cfg->getTimezone()); + '%X %x', $timezone ?: $cfg->getTimezone(), $user); } - function daydatetime($timestamp, $fromDb=true, $timezone=false) { + function daydatetime($timestamp, $fromDb=true, $timezone=false, $user=false) { global $cfg; return self::__formatDate($timestamp, $cfg->getDayDateTimeFormat(), $fromDb, IDF_FULL, IDF_SHORT, - '%X %x', $timezone ?: $cfg->getTimezone()); + '%X %x', $timezone ?: $cfg->getTimezone(), $user); } function getStrftimeFormat($format) { @@ -710,6 +710,84 @@ class Format { } return $text; } + + function relativeTime($to, $from=false) { + $timestamp = $to ?: Misc::gmtime(); + if (gettype($timestamp) === 'string') + $timestamp = strtotime($timestamp); + $from = $from ?: Misc::gmtime(); + if (gettype($timestamp) === 'string') + $from = strtotime($from); + $timeDiff = $from - $timestamp; + $absTimeDiff = abs($timeDiff); + + // within 2 seconds + if ($absTimeDiff <= 2) { + return $timeDiff >= 0 ? __('just now') : __('now'); + } + + // within a minute + if ($absTimeDiff < 60) { + return sprintf($timeDiff >= 0 ? __('%d seconds ago') : __('in %d seconds'), $absTimeDiff); + } + + // within 2 minutes + if ($absTimeDiff < 120) { + return sprintf($timeDiff >= 0 ? __('about a minute ago') : __('in about a minute')); + } + + // within an hour + if ($absTimeDiff < 3600) { + return sprintf($timeDiff >= 0 ? __('%d minutes ago') : __('in %d minutes'), $absTimeDiff / 60); + } + + // within 2 hours + if ($absTimeDiff < 7200) { + return ($timeDiff >= 0 ? __('about an hour ago') : __('in about an hour')); + } + + // within 24 hours + if ($absTimeDiff < 86400) { + return sprintf($timeDiff >= 0 ? __('%d hours ago') : __('in %d hours'), $absTimeDiff / 3600); + } + + // within 2 days + $days2 = 2 * 86400; + if ($absTimeDiff < $days2) { + // XXX: yesterday / tomorrow? + return $absTimeDiff >= 0 ? __('1 day ago') : __('in 1 day'); + } + + // within 29 days + $days29 = 29 * 86400; + if ($absTimeDiff < $days29) { + return sprintf($timeDiff >= 0 ? __('%d days ago') : __('in %d days'), $absTimeDiff / 86400); + } + + // within 60 days + $days60 = 60 * 86400; + if ($absTimeDiff < $days60) { + return ($timeDiff >= 0 ? __('about a month ago') : __('in about a month')); + } + + $currTimeYears = date('Y', $from); + $timestampYears = date('Y', $timestamp); + $currTimeMonths = $currTimeYears * 12 + date('n', $from); + $timestampMonths = $timestampYears * 12 + date('n', $timestamp); + + // within a year + $monthDiff = $currTimeMonths - $timestampMonths; + if ($monthDiff < 12 && $monthDiff > -12) { + return sprintf($monthDiff >= 0 ? __('%d months ago') : __('in %d months'), abs($monthDiff)); + } + + $yearDiff = $currTimeYears - $timestampYears; + if ($yearDiff < 2 && $yearDiff > -2) { + return $yearDiff >= 0 ? __('a year ago') : __('in a year'); + } + + return sprintf($yearDiff >= 0 ? __('%d years ago') : __('in %d years'), abs($yearDiff)); + } } if (!class_exists('IntlDateFormatter')) { @@ -722,4 +800,101 @@ else { define('IDF_SHORT', IntlDateFormatter::SHORT); define('IDF_FULL', IntlDateFormatter::FULL); } + +class FormattedLocalDate +implements TemplateVariable { + var $date; + var $timezone; + var $fromdb; + + function __construct($date, $timezone=false, $user=false, $fromdb=true) { + $this->date = $date; + $this->timezone = $timezone; + $this->user = $user; + $this->fromdb = $fromdb; + } + + function asVar() { + return $this->getVar('long'); + } + + function __toString() { + return $this->asVar(); + } + + function getVar($what) { + global $cfg; + + if (method_exists($this, 'get' . ucfirst($what))) + return call_user_func(array($this, 'get'.ucfirst($what))); + + switch ($what) { + case 'short': + return Format::date($this->date, $this->fromdb, false, $this->timezone, $this->user); + case 'long': + return Format::datetime($this->date, $this->fromdb, $this->timezone, $this->user); + case 'time': + return Format::time($this->date, $this->fromdb, false, $this->timezone, $this->user); + case 'full': + return Format::daydatetime($this->date, $this->fromdb, $this->timezone, $this->user); + } + } + + static function getVarScope() { + return array( + 'full' => 'Expanded date, e.g. day, month dd, yyyy', + 'long' => 'Date and time, e.g. d/m/yyyy hh:mm', + 'short' => 'Date only, e.g. d/m/yyyy', + 'time' => 'Time only, e.g. hh:mm', + ); + } +} + +class FormattedDate +extends FormattedLocalDate { + function asVar() { + return $this->getVar('system')->asVar(); + } + + function __toString() { + global $cfg; + return (string) new FormattedLocalDate($this->date, $cfg->getTimezone(), false, $this->fromdb); + } + + function getVar($what) { + global $cfg; + + if ($rv = parent::getVar($what)) + return $rv; + + switch ($what) { + case 'system': + return new FormattedLocalDate($this->date, $cfg->getDefaultTimezone()); + } + } + + function getHumanize() { + return Format::relativeTime(Misc::db2gmtime($this->date)); + } + + function getUser($context) { + global $cfg; + + // Fetch $recipient from the context and find that user's time zone + if ($recipient = $context->getObj('recipient')) { + $tz = $recipient->getTimezone() ?: $cfg->getDefaultTimezone(); + return new FormattedLocalDate($this->date, $tz, $recipient); + } + } + + static function getVarScope() { + return parent::getVarScope() + array( + 'humanize' => 'Humanized time, e.g. about an hour ago', + 'user' => array( + 'class' => 'FormattedLocalDate', 'desc' => "Localize to recipient's time zone and locale"), + 'system' => array( + 'class' => 'FormattedLocalDate', 'desc' => 'Localize to system default time zone'), + ); + } +} ?> diff --git a/include/class.forms.php b/include/class.forms.php index d63b17129e439a20c114195f3fbc0e253ad6eb66..66eb26b5e38cfa9dcd61b3040adaaf3bfc621f98 100644 --- a/include/class.forms.php +++ b/include/class.forms.php @@ -518,6 +518,24 @@ class FormField { return $this->toString($value); } + /** + * Fetch a value suitable for embedding the value of this field in an + * email template. Reference implementation uses ::to_php(); + */ + function asVar($value, $id=false) { + return $this->to_php($value, $id); + } + + /** + * Fetch the var type used with the email templating system's typeahead + * feature. This helps with variable expansion if supported by this + * field's ::asVar() method. This method should return a valid classname + * which implements the `TemplateVariable` interface. + */ + function asVarType() { + return false; + } + /** * Convert the field data to something matchable by filtering. The * primary use of this is for ticket filtering. @@ -1293,6 +1311,14 @@ class DatetimeField extends FormField { return (int) $value; } + function asVar($value, $id=false) { + if (!$value) return null; + return new FormattedDate((int) $value, 'UTC', false, false); + } + function asVarType() { + return 'FormattedDate'; + } + function toString($value) { global $cfg; $config = $this->getConfiguration(); diff --git a/include/class.i18n.php b/include/class.i18n.php index e0fcd9e11b9d864f66326bd9735abb244dfcd77a..c6e4bfc61b50973d19a3a2ad38d140178c2ef49f 100644 --- a/include/class.i18n.php +++ b/include/class.i18n.php @@ -379,14 +379,17 @@ class Internationalization { return self::getDefaultLanguage(); } - static function getCurrentLocale() { + static function getCurrentLocale($user=false) { global $thisstaff, $cfg; + if ($user) { + return self::getCurrentLanguage($user); + } // FIXME: Move this majic elsewhere - see upgrade bug note in // class.staff.php if ($thisstaff) { return $thisstaff->getLocale() - ?: self::getCurrentLanguage(); + ?: self::getCurrentLanguage($thisstaff); } if (!($locale = $cfg->getDefaultLocale())) diff --git a/include/class.ticket.php b/include/class.ticket.php index c13e85fbb3d6fd2d0193f7bd0f6deaedd1cd3882..98f29976495429535feb60b6d4f138ac925d34ef 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -1824,21 +1824,15 @@ implements RestrictedAccess, Threadable, TemplateVariable { return sprintf('%s/scp/tickets.php?id=%d', $cfg->getBaseUrl(), $this->getId()); break; case 'create_date': - return Format::datetime($this->getCreateDate(), true, 'UTC'); + return new FormattedDate($this->getCreateDate()); break; case 'due_date': - $duedate =''; - if($this->getEstDueDate()) - $duedate = Format::datetime($this->getEstDueDate(), true, 'UTC'); - - return $duedate; + if ($due = $this->getEstDueDate()) + return new FormattedDate($due); break; case 'close_date': - $closedate =''; - if($this->isClosed()) - $closedate = Format::datetime($this->getCloseDate(), true, 'UTC'); - - return $closedate; + if ($this->isClosed()) + return new FormattedDate($this->getCloseDate()); break; case 'user': return $this->getOwner(); @@ -1857,12 +1851,18 @@ implements RestrictedAccess, Threadable, TemplateVariable { static function getVarScope() { $base = array( 'assigned' => 'Assigned agent and/or team', - 'close_date' => 'Date of ticket closure', - 'create_date' => 'Ticket create date', + 'close_date' => array( + 'class' => 'FormattedDate', 'desc' => 'Date of ticket closure', + ), + 'create_date' => array( + 'class' => 'FormattedDate', 'desc' => 'Ticket create date', + ), 'dept' => array( 'class' => 'Dept', 'desc' => 'Department', ), - 'due_date' => 'Ticket due date', + 'due_date' => array( + 'class' => 'FormattedDate', 'desc' => 'Ticket due date', + ), 'email' => 'Default email address of ticket owner', 'name' => array( 'class' => 'PersonsName', 'desc' => __('Name of ticket owner'), diff --git a/include/class.user.php b/include/class.user.php index 5c9eee9f952dd3823dbd15de24d1c11ed9f50766..028832c7d743c767c6fd7e27fba0c0fa31822539 100644 --- a/include/class.user.php +++ b/include/class.user.php @@ -291,6 +291,15 @@ implements TemplateVariable { return $this->created; } + function getTimezone() { + global $cfg; + + if (($acct = $this->getAccount()) && ($tz = $acct->getTimezone())) { + return $tz; + } + return $cfg->getDefaultTimezone(); + } + function addForm($form, $sort=1, $data=null) { $entry = $form->instanciate($sort, $data); $entry->set('object_type', 'U'); @@ -1001,6 +1010,10 @@ class UserAccountModel extends VerySimpleModel { return $lang; } + function getTimezone() { + return $this->timezone; + } + function save($refetch=false) { // Serialize the extra column on demand if (isset($this->_extra)) { diff --git a/include/class.variable.php b/include/class.variable.php index 3186739ca4bc42dfc13a9712407d87d70c5a52bc..051f9f6a5290eb60135fe1f28d525ffe4c0bf697 100644 --- a/include/class.variable.php +++ b/include/class.variable.php @@ -65,14 +65,14 @@ class VariableReplacer { if (!$var) { if (method_exists($obj, 'asVar')) - return call_user_func(array($obj, 'asVar')); + return call_user_func(array($obj, 'asVar'), $this); elseif (method_exists($obj, '__toString')) return (string) $obj; } list($v, $part) = explode('.', $var, 2); if ($v && is_callable(array($obj, 'get'.ucfirst($v)))) { - $rv = call_user_func(array($obj, 'get'.ucfirst($v))); + $rv = call_user_func(array($obj, 'get'.ucfirst($v)), $this); if(!$rv || !is_object($rv)) return $rv; @@ -86,7 +86,7 @@ class VariableReplacer { return ""; list($tag, $remainder) = explode('.', $var, 2); - if(($rv = call_user_func(array($obj, 'getVar'), $tag))===false) + if(($rv = call_user_func(array($obj, 'getVar'), $tag, $this))===false) return ""; if(!is_object($rv)) @@ -176,6 +176,9 @@ class VariableReplacer { continue; $desc = $f->getLocal('label'); + if (($class = $f->asVarType()) && class_exists($class)) { + $desc = array('desc' => $desc, 'class' => $class); + } $items[$name] = $desc; foreach (VariableReplacer::compileFieldScope($f) as $name2=>$desc) { $items["$name.$name2"] = $desc; @@ -198,6 +201,9 @@ class VariableReplacer { if ($recurse) { foreach (static::compileFieldScope($f, $recurse-1, $name) as $name2=>$desc) { + if (($class = $f->asVarType()) && class_exists($class)) { + $desc = array('desc' => $desc, 'class' => $class); + } $items["$name.$name2"] = $desc; } } diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php index e2cc27c5f3441b18d42b9c6b227b6ae0583158dc..f8b5d21a1fcb38ac9291e57495f233f328debe94 100644 --- a/include/staff/ticket-view.inc.php +++ b/include/staff/ticket-view.inc.php @@ -187,7 +187,7 @@ if($ticket->isOverdue()) </tr> <tr> <th><?php echo __('Create Date');?>:</th> - <td><?php echo Format::datetime($ticket->getCreateDate()); ?></td> + <td><?php echo Format::relativeTime($ticket->getCreateDate()); ?></td> </tr> </table> </td>