Source

custom-time / custom-time_parser.patch

Full commit
diff --git a/trac/ticket/model.py b/trac/ticket/model.py
--- a/trac/ticket/model.py
+++ b/trac/ticket/model.py
@@ -26,7 +26,8 @@
 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, to_utimestamp, utc, utcmax, \
+                              parse_custom_time
 from trac.util.translation import _
 
 __all__ = ['Ticket', 'Type', 'Status', 'Resolution', 'Priority', 'Severity',
@@ -139,9 +140,15 @@
                             # convert microsecond POSIX timestamp to datetime
                             dt = from_utimestamp(ts, default_now=False)
                         except ValueError: # invalid timestamp value
-                            # purge (string) as invalid value
-                            # DEVEL: might be optional to keep old value
-                            dt = None
+                            try:
+                                # convert parseable date/time string
+                                # DEVEL: poor mans migration helper
+                                dt = parse_custom_time(value)
+                            except ValueError:
+                                # purge non-parseable string as invalid value
+                                # DEVEL: might be optional to keep old value
+                                dt = None
+                                pass
                             pass
                     else:
                         # unify 'time unset' value
@@ -198,6 +205,22 @@
             if name[9:] not in values:
                 self[name[9:]] = '0'
 
+        # Convert custom time value to valid datetime
+        for name in [name for name in self.time_fields \
+                     if name in self.custom_fields]:
+            try:
+                # convert date/time string
+                t = self.values[name]
+                self.values[name] = parse_custom_time(t)
+            except KeyError:
+                # allow non-existing value (new ticket)
+                pass
+            except ValueError:
+                # allow string 
+                # don't purge invalid value before attempting to save
+                # DEVEL: Better emit helpful warning about invalid value.
+                pass
+
     def insert(self, when=None, db=None):
         """Add ticket to database.
 
@@ -250,6 +273,18 @@
 
             # Insert custom fields
             if custom_fields:
+                for name in self.time_fields:
+                    if name in custom_fields and values[name]:
+                        # Prepare custom time value for db record
+                        # try to parse time string, convert to POSIX timestamp
+                        try:
+                            dt = parse_custom_time(values[name])
+                            self[name] = to_utimestamp(dt, default_now=False)
+                        except TypeError:
+                            # purge invalid value
+                            self[name] = ''
+                            pass
+
                 cursor.executemany("""
                 INSERT INTO ticket_custom (ticket,name,value) VALUES (%s,%s,%s)
                 """, [(tkt_id[0], name, self[name]) for name in custom_fields])
@@ -333,6 +368,35 @@
                         num += 1
                 comment_num = str(num + 1)
 
+            for name in [name for name in self.time_fields if name in \
+                         self.custom_fields and name in self._old.keys()]:
+                # Prepare custom time value for db record
+                # try to parse time string, convert to POSIX timestamp
+                try:
+                    # DEVEL: don't parse twice, just convert
+                    dt = self.values[name]
+                    ts = to_utimestamp(dt, default_now=False)
+                    if ts is None:
+                        ts = ''
+                    self[name] = str(ts)
+                except TypeError: # invalid timestamp
+                    self[name] = '' # purge invalid value
+                    pass
+                except KeyError: # no newvalue
+                    pass
+                try:
+                    # DEVEL: don't parse twice, just convert
+                    dt = self._old[name]
+                    ts = to_utimestamp(dt, default_now=False)
+                    if ts is None:
+                        ts = ''
+                    self._old[name] = str(ts)
+                except TypeError: # invalid timestamp
+                    self._old[name] = '' # purge invalid value
+                    pass
+                except KeyError: # no oldvalue
+                    pass
+
             # store fields
             for name in self._old.keys():
                 if name in self.custom_fields:
diff --git a/trac/ticket/query.py b/trac/ticket/query.py
--- a/trac/ticket/query.py
+++ b/trac/ticket/query.py
@@ -32,7 +32,8 @@
 from trac.ticket.api import TicketSystem
 from trac.util import Ranges, as_bool
 from trac.util.datefmt import format_datetime, from_utimestamp, parse_date, \
-                              to_timestamp, to_utimestamp, utc
+                              to_timestamp, to_utimestamp, utc, \
+                              parse_custom_time
 from trac.util.presentation import Paginator
 from trac.util.text import empty, shorten_line, unicode_unquote
 from trac.util.translation import _, tag_
@@ -333,10 +334,25 @@
                     val = '--'
                 elif name in self.time_fields:
                     # handle custom time fields too
-                    # where timestamp number is stored as string
+                    # where time is stored as string
+                    ts = None
                     if not val == '':
-                        val = float(val)
-                    val = from_utimestamp(val, default_now=False)
+                        try:
+                            ts = float(val)
+                            # convert microsecond POSIX timestamp to datetime
+                            val = from_utimestamp(ts, default_now=False)
+                        except ValueError: # invalid POSIX time
+                            try:
+                                # convert parseable date/time string
+                                val = parse_custom_time(val)
+                            except ValueError:
+                                # keep old stings, but quote them
+                                # purge non-parseable string as invalid value
+                                # DEVEL: wouldn't show up eighter, since it
+                                # DEVEl: get's replaced with current time
+                                val = ''
+                                pass
+                            pass
                 elif field and field['type'] == 'checkbox':
                     try:
                         val = bool(int(val))
diff --git a/trac/util/datefmt.py b/trac/util/datefmt.py
--- a/trac/util/datefmt.py
+++ b/trac/util/datefmt.py
@@ -293,6 +293,27 @@
     return dt
 
 
+try:
+    # use eGenix.com time string parser mxDateTime.Parser
+    from mx.DateTime import Parser
+
+    def parse_custom_time(text):
+        """Parse input for custom time fields."""
+        if text == '' or text == None:
+            return None
+        ts = Parser.DateTimeFromString(text).ticks() * 1000000
+        return from_utimestamp(ts, default_now=False)
+
+except ImportError:
+    # use datetime parser, if mxDateTime is not installed
+    # This provides a minimal subset of parser capabilities.
+    def parse_custom_time(text):
+        """Parse input for custom time fields."""
+        if text == '' or text == None:
+            return None
+        return parse_date(text, default_now=False)
+
+
 _REL_TIME_RE = re.compile(
     r'(\d+\.?\d*)\s*'
     r'(second|minute|hour|day|week|month|year|[hdwmy])s?\s*'