Peter Suter avatar Peter Suter committed d556e23

1.1.1dev: Support custom ticket time fields:
* Convert between DB time strings and datetime objects in the model.
* Parse ticket time input to datetime objects.
* Add format hint to ticket time input controls.
* Add a `.format` attribute to custom time fields to allow rendering as relative, a date or a datetime.
* Render time fields in ticket box, ticket csv export and ticket changelog.
* Render time fields in ticket queries.
* Render time fields in notification emails.
* Support parsing and pretty printing relative time values in the future (e.g. ''tomorrow'', ''next month'' or ''in 3 hours'').
* Add some tests.

Implements #1942.

Based on patches by Steffen Hoffmann and Remy Blank.

Comments (0)

Files changed (13)

trac/ticket/api.py

 
         # Date/time fields
         fields.append({'name': 'time', 'type': 'time',
-                       'label': N_('Created')})
+                       'format': 'relative', 'label': N_('Created')})
         fields.append({'name': 'changetime', 'type': 'time',
-                       'label': N_('Modified')})
+                       'format': 'relative', 'label': N_('Modified')})
 
         for field in self.custom_fields:
             if field['name'] in [f['name'] for f in fields]:
                 field['format'] = config.get(name + '.format', 'plain')
                 field['width'] = config.getint(name + '.cols')
                 field['height'] = config.getint(name + '.rows')
+            elif field['type'] == 'time':
+                field['format'] = config.get(name + '.format', 'datetime')
             fields.append(field)
 
         fields.sort(lambda x, y: cmp((x['order'], x['name']),

trac/ticket/model.py

 from trac.ticket.api import TicketSystem
 from trac.util import embedded_numbers, partition
 from trac.util.text import empty
-from trac.util.datefmt import from_utimestamp, to_utimestamp, utc, utcmax
+from trac.util.datefmt import from_utimestamp, parse_date, to_utimestamp, \
+                              utc, utcmax
 from trac.util.translation import _
 
 __all__ = ['Ticket', 'Type', 'Status', 'Resolution', 'Priority', 'Severity',
     return ', '.join(cclist)
 
 
+def _db_str_to_datetime(value):
+    if value is None:
+        return None
+    try:
+        return from_utimestamp(long(value))
+    except ValueError:
+        pass
+    try:
+        return parse_date(value.strip(), utc, 'datetime')
+    except Exception:
+        return None
+
+
+def _datetime_to_db_str(dt, is_custom_field):
+    if not dt:
+        return ''        
+    ts = to_utimestamp(dt) 
+    if is_custom_field: 
+        # Padding with '0' would be easy to sort in report page for a user 
+        fmt = '%018d' if ts >= 0 else '%+017d' 
+        return fmt % ts 
+    else: 
+        return ts
+
+
 class Ticket(object):
 
     # Fields that must not be modified directly by the user
                 SELECT name, value FROM ticket_custom WHERE ticket=%s
                 """, (tkt_id,)):
             if name in self.custom_fields:
-                if value is None:
+                if name in self.time_fields:
+                    self.values[name] = _db_str_to_datetime(value)
+                elif value is None:
                     self.values[name] = empty
                 else:
                     self.values[name] = value
             self._old[name] = self.values.get(name)
         elif self._old[name] == value: # Change of field reverted
             del self._old[name]
-        if value:
+        if value and name not in self.time_fields:
             if isinstance(value, list):
                 raise TracError(_("Multi-values fields not supported yet"))
             if self.fields.by_name(name, {}).get('type') != 'textarea':
         """Populate the ticket with 'suitable' values from a dictionary"""
         field_names = [f['name'] for f in self.fields]
         for name in [name for name in values.keys() if name in field_names]:
-            self[name] = values.get(name, '')
+            self[name] = values[name]
 
         # We have to do an extra trick to catch unchecked checkboxes
         for name in [name for name in values.keys() if name[9:] in field_names
             self['owner'] = default_to_owner
 
         # Perform type conversions
-        values = dict(self.values)
-        for field in self.time_fields:
-            if field in values:
-                values[field] = to_utimestamp(values[field])
+        db_values = self._to_db_types(self.values)
 
         # Insert ticket record
         std_fields = []
             cursor.execute("INSERT INTO ticket (%s) VALUES (%s)"
                            % (','.join(std_fields),
                               ','.join(['%s'] * len(std_fields))),
-                           [values[name] for name in std_fields])
+                           [db_values[name] for name in std_fields])
             tkt_id = db.get_last_id(cursor, 'ticket')
 
             # Insert custom fields
             if custom_fields:
                 db.executemany(
                     """INSERT INTO ticket_custom (ticket, name, value)
-                      VALUES (%s, %s, %s)
-                      """,
-                    [(tkt_id, c, self[c]) for c in custom_fields])
+                       VALUES (%s, %s, %s)
+                       """, [(tkt_id, c, db_values.get(c, ''))
+                             for c in custom_fields])
 
         self.id = tkt_id
         self.resource = self.resource(id=tkt_id)
                     # we just leave the owner as is.
                     pass
 
+        # Perform type conversions
+        db_values = self._to_db_types(self.values)
+        old_db_values = self._to_db_types(self._old)
+
         with self.env.db_transaction as db:
             db("UPDATE ticket SET changetime=%s WHERE id=%s",
                (when_ts, self.id))
                                      """, (self.id, name)):
                         db("""UPDATE ticket_custom SET value=%s
                               WHERE ticket=%s AND name=%s
-                              """, (self[name], self.id, name))
+                              """, (db_values.get(name, ''), self.id, name))
                         break
                     else:
                         db("""INSERT INTO ticket_custom (ticket,name,value)
                               VALUES(%s,%s,%s)
-                              """, (self.id, name, self[name]))
+                              """, (self.id, name, db_values.get(name, '')))
                 else:
                     db("UPDATE ticket SET %s=%%s WHERE id=%%s" 
-                       % name, (self[name], self.id))
+                       % name, (db_values.get(name, ''), self.id))
                 db("""INSERT INTO ticket_change
                         (ticket,time,author,field,oldvalue,newvalue)
                       VALUES (%s, %s, %s, %s, %s, %s)
-                      """, (self.id, when_ts, author, name, self._old[name],
-                            self[name]))
+                      """, (self.id, when_ts, author, name, old_db_values[name],
+                            db_values.get(name, '')))
 
             # always save comment, even if empty 
             # (numbering support for timeline)
             listener.ticket_changed(self, comment, author, old_values)
         return int(cnum.rsplit('.', 1)[-1])
 
+    def _to_db_types(self, values):
+        values = values.copy()
+        for field, value in values.iteritems():
+            if field in self.time_fields:
+                is_custom_field = field in self.custom_fields
+                values[field] = _datetime_to_db_str(value, is_custom_field)
+        return values
+
     def get_changelog(self, when=None, db=None):
         """Return the changelog as a list of tuples of the form
         (time, author, field, oldvalue, newvalue, permanent).
                 ORDER BY time,permanent,author
                 """
             args = (self.id, sid, sid)
-        return [(from_utimestamp(t), author, field, oldvalue or '',
-                 newvalue or '', permanent)
-                for t, author, field, oldvalue, newvalue, permanent in
-                self.env.db_query(sql, args)]
+        log = []
+        for t, author, field, oldvalue, newvalue, permanent \
+                in self.env.db_query(sql, args):
+            if field in self.time_fields:
+                oldvalue = _db_str_to_datetime(oldvalue)
+                newvalue = _db_str_to_datetime(newvalue)
+            log.append((from_utimestamp(t), author, field,
+                        oldvalue or '', newvalue or '', permanent))
+        return log
 
     def delete(self, db=None):
         """Delete the ticket.

trac/ticket/notification.py

 from trac.config import *
 from trac.notification import NotifyEmail
 from trac.ticket.api import TicketSystem
-from trac.util.datefmt import to_utimestamp
+from trac.util.datefmt import format_date_or_datetime, get_timezone, \
+                              to_utimestamp
 from trac.util.text import obfuscate_email_address, text_width, wrap
 from trac.util.translation import deactivate, reactivate
 
                         if field in ['owner', 'reporter']:
                             old = self.obfuscate_email(old)
                             new = self.obfuscate_email(new)
+                        elif field in ticket.time_fields:
+                            format = ticket.fields.by_name(field).get('format')
+                            old = self.format_time_field(old, format)
+                            new = self.format_time_field(new, format)
                         newv = new
                         length = 7 + len(field)
                         spacer_old, spacer_new = ' ', ' '
             if not fname in tkt.values:
                 continue
             fval = tkt[fname] or ''
+            if fname in tkt.time_fields:
+                format = tkt.fields.by_name(fname).get('format')
+                fval = self.format_time_field(fval, format)
             if fval.find('\n') != -1:
                 continue
             if fname in ['owner', 'reporter']:
             if not tkt.values.has_key(fname):
                 continue
             fval = tkt[fname] or ''
+            if fname in tkt.time_fields:
+                format = tkt.fields.by_name(fname).get('format')
+                fval = self.format_time_field(fval, format)
             if fname in ['owner', 'reporter']:
                 fval = self.obfuscate_email(fval)
             if f['type'] == 'textarea' or '\n' in unicode(fval):
         
         return template.generate(**data).render('text', encoding=None).strip()
 
+    def format_time_field(self, value, format):
+        tzinfo = get_timezone(self.config.get('trac', 'default_timezone'))
+        return format_date_or_datetime(format, value, tzinfo=tzinfo) \
+               if value else ''
+
     def get_recipients(self, tktid):
         (torecipients, ccrecipients, reporter, owner) = \
             get_ticket_notification_recipients(self.env, self.config, 

trac/ticket/query.py

 from trac.ticket.api import TicketSystem
 from trac.ticket.model import Milestone, group_milestones, Ticket
 from trac.util import Ranges, as_bool
-from trac.util.datefmt import format_datetime, from_utimestamp, parse_date, \
-                              to_timestamp, to_utimestamp, utc, user_time
+from trac.util.datefmt import from_utimestamp, format_date_or_datetime, \
+                              parse_date, to_timestamp, to_utimestamp, utc, \
+                              user_time
 from trac.util.presentation import Paginator
 from trac.util.text import empty, shorten_line, quote_query_string
 from trac.util.translation import _, tag_, cleandoc_
                         if href is not None:
                             result['href'] = href.ticket(val)
                     elif name in self.time_fields:
-                        val = from_utimestamp(val)
+                        val = from_utimestamp(long(val)) if val else ''
                     elif field and field['type'] == 'checkbox':
                         try:
                             val = bool(int(val))
                         value = Chrome(self.env).format_emails(
                                     context.child(ticket), value)
                     elif col in query.time_fields:
-                        value = format_datetime(value, '%Y-%m-%d %H:%M:%S',
-                                                tzinfo=req.tz)
+                        format = query.fields.by_name(col).get('format')
+                        value = user_time(req, format_date_or_datetime,
+                                          format, value) if value else ''
                     values.append(unicode(value).encode('utf-8'))
                 writer.writerow(values)
         return (content.getvalue(), '%s;charset=utf-8' % mimetype)

trac/ticket/templates/query_results.html

                         class="${classes(closed=result.status == 'closed')}">#$result.id</a></td>
                     <td py:otherwise="" class="$name" py:choose="">
                       <a py:when="name == 'summary'" href="$result.href" title="View ticket">$value</a>
-                      <py:when test="isinstance(value, datetime)">${pretty_dateinfo(value, dateonly=True)}</py:when>
+                      <py:when test="header.field.type == 'time'">${pretty_dateinfo(value, header.field.format)}</py:when>
                       <py:when test="name == 'reporter'">${authorinfo(value)}</py:when>
                       <py:when test="name == 'cc'">${format_emails(ticket_context, value)}</py:when>
                       <py:when test="name == 'owner' and value">${authorinfo(value)}</py:when>

trac/ticket/templates/ticket.html

                                  checked="${value == option or None}" />
                           ${option}
                         </label>
+                        <input py:when="'time'" type="text" id="field-${field.name}" title="${field.format_hint}"
+                               name="field_${field.name}" value="${field.edit}" />
                         <py:otherwise><!--! Text input fields -->
                           <py:choose>
                             <span py:when="field.cc_entry"><!--! Special case for Cc: field -->

trac/ticket/templates/ticket_box.html

             colspan="${3 if fullrow else None}">
           <py:if test="field">
             <py:choose test="">
+              <py:when test="'dateinfo' in field">${pretty_dateinfo(field.dateinfo, field.format)}</py:when>
               <py:when test="'rendered' in field">${field.rendered}</py:when>
               <py:otherwise>${ticket[field.name]}</py:otherwise>
             </py:choose>

trac/ticket/tests/api.py

                           'order': 0, 'format': 'wiki', 'custom': True},
                          fields[0])
 
+    def test_custom_field_time(self):
+        self.env.config.set('ticket-custom', 'test', 'time')
+        self.env.config.set('ticket-custom', 'test.label', 'Test')
+        self.env.config.set('ticket-custom', 'test.value', '')
+        fields = TicketSystem(self.env).get_custom_fields()
+        self.assertEqual({'name': 'test', 'type': 'time', 'label': 'Test',
+                          'value': '', 'order': 0, 'format': 'datetime',
+                          'custom': True},
+                         fields[0])
+
     def test_custom_field_order(self):
         self.env.config.set('ticket-custom', 'test1', 'text')
         self.env.config.set('ticket-custom', 'test1.order', '2')

trac/ticket/tests/model.py

         self.assertEqual('on', ticket['cbon'])
         self.assertEqual('0', ticket['cboff'])
 
+    def test_custom_time(self):
+        # Add a custom field of type 'time'
+        self.env.config.set('ticket-custom', 'due', 'time')
+        ticket = Ticket(self.env)
+        self.assertFalse('due' in ticket.std_fields)
+        self.assertTrue('due' in ticket.time_fields)
+        ticket['reporter'] = 'john'
+        ticket['summary'] = 'Task1'
+        tktid = ticket.insert()
+        ticket = Ticket(self.env, tktid)
+        # Empty string is default value, but not a time stamp
+        self.assertEqual(None, ticket['due'])
+        ts = datetime(2011, 11, 11, 0, 0, 0, 0, utc)
+        ticket['due'] = ts
+        t1 = datetime(2001, 1, 1, 1, 1, 1, 0, utc)
+        ticket.save_changes('joe', when=t1)
+        self.assertEqual(ts, ticket['due'])
+        ticket['due'] = ''
+        t2 = datetime(2001, 1, 1, 1, 1, 2, 0, utc)
+        ticket.save_changes('joe', when=t2)
+        self.assertEqual('', ticket['due'])
+
     def test_changelog(self):
         tkt_id = self._insert_ticket('Test', reporter='joe', component='foo',
                                      milestone='bar')

trac/ticket/web_ui.py

 from trac.timeline.api import ITimelineEventProvider
 from trac.util import as_bool, as_int, get_reporter_id
 from trac.util.datefmt import (
-    format_datetime, from_utimestamp, to_utimestamp, utc
+    format_date_or_datetime, from_utimestamp, get_date_format_hint,
+    get_datetime_format_hint, parse_date, to_utimestamp, user_time, utc
 )
 from trac.util.text import (
     exception_to_unicode, empty, obfuscate_email_address, shorten_line,
         for each in Ticket.protected_fields:
             fields.pop(each, None)
             fields.pop('checkbox_' + each, None)    # See Ticket.populate()
+        for field, value in fields.iteritems():
+            if field in ticket.time_fields:
+                try:
+                    fields[field] = user_time(req, parse_date, value) \
+                                    if value else None
+                except TracError, e:
+                    # Handle bad user input for custom time fields gracefully.
+                    if field in ticket.custom_fields:
+                        # Leave it to _validate_ticket() to complain.
+                        fields[field] = value
+                    else:
+                        raise TracError(e)
         ticket.populate(fields)
         # special case for updating the Cc: field
         if 'cc_update' in req.args and 'revert_cc' not in req.args:
             if name in ('cc', 'reporter'):
                 value = Chrome(self.env).format_emails(context, value, ' ')
             elif name in ticket.time_fields:
-                value = format_datetime(value, '%Y-%m-%d %H:%M:%S',
-                                        tzinfo=req.tz)
+                format = ticket.fields.by_name(name).get('format')
+                value = user_time(req, format_date_or_datetime, format,
+                                  value) if value else ''
             cols.append(value.encode('utf-8'))
         writer.writerow(cols)
         return (content.getvalue(), '%s;charset=utf-8' % mimetype)
             # Shouldn't happen in "normal" circumstances, hence not a warning
             raise InvalidTicket(_("Invalid comment threading identifier"))
 
+        # Validate time field content
+        for field in ticket.time_fields:
+            value = ticket[field]
+            if not (field in ticket.std_fields or \
+                    isinstance(value, datetime)):
+                try:
+                    format = ticket.fields.by_name(field).get('format')
+                    ticket.values[field] = user_time(req, parse_date, value,
+                                                     hint=format) \
+                                          if value else None
+                except TracError, e:
+                    # Degrade TracError to warning.
+                    add_warning(req, e)
+                    ticket.values[field] = value
+                    valid = False
+
         # Custom validation rules
         for manipulator in self.ticket_manipulators:
             for field, message in manipulator.validate_ticket(req, ticket):
             type_ = field['type']
  
             # enable a link to custom query for all choice fields
-            if type_ not in ['text', 'textarea']:
+            if type_ not in ['text', 'textarea', 'time']:
                 field['rendered'] = self._query_link(req, name, ticket[name])
 
             # per field settings
                     field['rendered'] = \
                         format_to_html(self.env, context, ticket[name],
                                 escape_newlines=self.must_preserve_newlines)
+            elif type_ == 'time':
+                value = ticket[name]
+                field['timevalue'] = value
+                format = field.get('format', 'datetime')
+                field['rendered'] =  user_time(req, format_date_or_datetime, 
+                                               format, value) if value else ''
+                field['dateinfo'] = value
+                field['edit'] = user_time(req, format_date_or_datetime,
+                                          format, value) if value else ''
+                locale = getattr(req, 'lc_time', None)
+                if format == 'date':
+                    field['format_hint'] = get_date_format_hint(locale)
+                else:
+                    field['format_hint'] = get_datetime_format_hint(locale)
             
             # ensure sane defaults
             field.setdefault('optional', False)
                                                   resource_new)
             if rendered:
                 changes['rendered'] = rendered
+            elif ticket.fields.by_name(field, {}).get('type') == 'time':
+                format = ticket.fields.by_name(field).get('format')
+                changes['old'] = user_time(req, format_date_or_datetime,
+                                           format, old) if old else ''
+                changes['new'] = user_time(req, format_date_or_datetime,
+                                           format, new) if new else ''
 
     def _render_property_diff(self, req, ticket, field, old, new, 
                               resource_new=None):

trac/timeline/web_ui.py

 from trac.util import as_int
 from trac.util.datefmt import format_date, format_datetime, format_time, \
                               parse_date, to_utimestamp, utc, \
-                              pretty_timedelta,  user_time
+                              pretty_timedelta,  user_time, localtz
 from trac.util.text import exception_to_unicode, to_unicode
 from trac.util.translation import _, tag_
 from trac.web import IRequestHandler, IRequestFilter
     def post_process_request(self, req, template, data, content_type):
         if data:
             def pretty_dateinfo(date, format=None, dateonly=False):
-                absolute = user_time(req, format_datetime, date)
-                relative = pretty_timedelta(date)
+                if not date:
+                    return ''
+                if format == 'date':
+                    absolute = user_time(req, format_date, date)
+                else:
+                    absolute = user_time(req, format_datetime, date)
+                now = datetime.now(localtz)
+                relative = pretty_timedelta(date, now)
                 if not format:
                     format = req.session.get('dateinfo',
                                  Chrome(self.env).default_dateinfo_format)
-                if format == 'absolute':
+                if format == 'relative':
+                    if date > now:
+                        label = _("in %(relative)s", relative=relative) \
+                                if not dateonly else relative
+                        title = _("on %(date)s at %(time)s",
+                                  date=user_time(req, format_date, date),
+                                  time=user_time(req, format_time, date))
+                        return tag.span(label, title=title)
+                    else:
+                        label = _("%(relative)s ago", relative=relative) \
+                                if not dateonly else relative
+                        title = _("See timeline at %(absolutetime)s",
+                                  absolutetime=absolute)
+                else:
                     if dateonly:
                         label = absolute
                     elif req.lc_time == 'iso8601':
                         label = _("at %(iso8601)s", iso8601=absolute)
+                    elif format == 'date':
+                        label = _("on %(date)s", date=absolute)
                     else:
                         label = _("on %(date)s at %(time)s",
                                   date=user_time(req, format_date, date),
                                   time=user_time(req, format_time, date))
+                    if date > now:
+                        title = _("in %(relative)s", relative=relative)
+                        return tag.span(label, title=title)
                     title = _("See timeline %(relativetime)s ago",
                               relativetime=relative)
-                else:
-                    label = _("%(relativetime)s ago", relativetime=relative) \
-                            if not dateonly else relative
-                    title = _("See timeline at %(absolutetime)s",
-                              absolutetime=absolute)
                 return self.get_timeline_link(req, date, label,
                                               precision='second', title=title)
             def dateinfo(date):

trac/util/datefmt.py

         dt = _parse_relative_time(text, tzinfo)
     if dt is None:
         hint = {'datetime': get_datetime_format_hint,
-                'date': get_date_format_hint
+                'date': get_date_format_hint,
+                'relative': get_datetime_format_hint
                }.get(hint, lambda(l): hint)(locale)
         raise TracError(_('"%(date)s" is an invalid date, or the date format '
                           'is not known. Try "%(hint)s" instead.', 
     t = tzinfo.localize(datetime(*(values[k] for k in 'yMdhms')))
     return tzinfo.normalize(t)
 
-_REL_TIME_RE = re.compile(
-    r'(\d+\.?\d*)\s*'
-    r'(second|minute|hour|day|week|month|year|[hdwmy])s?\s*'
-    r'(?:ago)?$')
+_REL_FUTURE_RE = re.compile(
+    r'(?:in|\+)\s*(\d+\.?\d*)\s*'
+    r'(second|minute|hour|day|week|month|year|[hdwmy])s?$')
+_REL_PAST_RE = re.compile(
+    r'(?:-\s*)?(\d+\.?\d*)\s*'
+    r'(second|minute|hour|day|week|month|year|[hdwmy])s?\s*(?:ago)?$')
 _time_intervals = dict(
     second=lambda v: timedelta(seconds=v),
     minute=lambda v: timedelta(minutes=v),
     m=lambda v: timedelta(days=30 * v),
     y=lambda v: timedelta(days=365 * v),
 )
-_TIME_START_RE = re.compile(r'(this|last)\s*'
+_TIME_START_RE = re.compile(r'(this|last|next)\s*'
                             r'(second|minute|hour|day|week|month|year)$')
 _time_starts = dict(
     second=lambda now: now.replace(microsecond=0),
     if text == 'yesterday':
         return now.replace(microsecond=0, second=0, minute=0, hour=0) \
                - timedelta(days=1)
-    match = _REL_TIME_RE.match(text)
+    if text == 'tomorrow':
+        return now.replace(microsecond=0, second=0, minute=0, hour=0) \
+               + timedelta(days=1)
+    match = _REL_FUTURE_RE.match(text)
+    if match:
+        (value, interval) = match.groups()
+        return now + _time_intervals[interval](float(value))
+    match = _REL_PAST_RE.match(text)
     if match:
         (value, interval) = match.groups()
         return now - _time_intervals[interval](float(value))
                     dt = dt.replace(year=dt.year - 1, month=12)
             else:
                 dt -= _time_intervals[start](1)
+        elif which == 'next':
+            if start == 'month':
+                if dt.month < 12:
+                    dt = dt.replace(month=dt.month + 1)
+                else:
+                    dt = dt.replace(year=dt.year + 1, month=1)
+            else:
+                dt += _time_intervals[start](1)
         return dt
     return None
 
         kwargs['locale'] = getattr(req, 'lc_time', None)
     return func(*args, **kwargs)
 
+def format_date_or_datetime(format, *args, **kwargs):
+    if format == 'date':
+        return format_date(*args, **kwargs)
+    else:
+        return format_datetime(*args, **kwargs)
+    
 # -- timezone utilities
 
 class FixedOffset(tzinfo):

trac/web/chrome.py

     from_utimestamp, http_date, utc, get_date_format_jquery_ui, is_24_hours,
     get_time_format_jquery_ui, user_time, get_month_names_jquery_ui,
     get_day_names_jquery_ui, get_timezone_list_jquery_ui,
-    get_first_week_day_jquery_ui)
+    get_first_week_day_jquery_ui, localtz)
 from trac.util.translation import _, get_available_locales
 from trac.web.api import IRequestHandler, ITemplateStreamFilter, HTTPNotFound
 from trac.web.href import Href
             show_email_addresses = False
 
         def pretty_dateinfo(date, format=None, dateonly=False):
-            absolute = user_time(req, format_datetime, date)
-            relative = pretty_timedelta(date)
+            if not date:
+                return ''
+            if format == 'date':
+                absolute = user_time(req, format_date, date)
+            else:
+                absolute = user_time(req, format_datetime, date)
+            now = datetime.datetime.now(localtz)
+            relative = pretty_timedelta(date, now)
             if not format:
                 format = req.session.get('dateinfo',
                                          self.default_dateinfo_format)
-            if format == 'absolute':
+            in_or_ago = _("in %(relative)s", relative=relative) \
+                        if date > now else \
+                        _("%(relative)s ago", relative=relative)
+            if format == 'relative':
+                label = in_or_ago if not dateonly else relative
+                title = absolute
+            else:
                 label = absolute
-                title = _("%(relativetime)s ago", relativetime=relative)
-            else:
-                label = _("%(relativetime)s ago", relativetime=relative) \
-                        if not dateonly else relative
-                title = absolute
-            return tag.span(label, title=title)
+                title = in_or_ago
+            return tag.span(label, title=title)  
 
         def dateinfo(date):
             return pretty_dateinfo(date, format='relative', dateonly=True)
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.