diff --git a/trac/htdocs/css/report.css b/trac/htdocs/css/report.css
--- a/trac/htdocs/css/report.css
+++ b/trac/htdocs/css/report.css
@@ -38,7 +38,6 @@
 #filters td label { font-size: 11px }
 #filters td.mode { text-align: right }
 #filters td.filter { width: 100% }
-#filters td.filter label { padding-right: 1em }
 #filters td.actions { text-align: right; white-space: nowrap }
 
 #columns div label { 
diff --git a/trac/htdocs/js/query.js b/trac/htdocs/js/query.js
--- a/trac/htdocs/js/query.js
+++ b/trac/htdocs/js/query.js
@@ -225,13 +225,28 @@
         td.className = "filter";
         if (property.type == "select") {
           var element = createSelect(propertyName, property.options, true);
+          td.appendChild(element);
         } else if (property.type == "text") {
           var element = document.createElement("input");
           element.type = "text";
           element.name = propertyName;
           element.size = 42;
+          td.appendChild(element);
+        } else if (property.type == "time") {
+          var element = document.createElement("input");
+          element.type = "text";
+          element.name = propertyName;
+          element.size = 14;
+          td.appendChild(element);
+          td.appendChild(document.createTextNode(" "));
+          td.appendChild(createLabel("and"));
+          td.appendChild(document.createTextNode(" "));
+          var element2 = document.createElement("input");
+          element2.type = "text";
+          element2.name = propertyName + "_end";
+          element2.size = 14;
+          td.appendChild(element2);
         }
-        td.appendChild(element);
         element.focus();
         tr.appendChild(td);
       }
diff --git a/trac/ticket/api.py b/trac/ticket/api.py
--- a/trac/ticket/api.py
+++ b/trac/ticket/api.py
@@ -240,6 +240,12 @@
             field = {'name': name, 'type': 'text', 'label': name.title()}
             fields.append(field)
 
+        # Date/time fields
+        fields.append({'name': 'time', 'type': 'time',
+                       'label': _('Created')})
+        fields.append({'name': 'changetime', 'type': 'time',
+                       'label': _('Modified')})
+
         for field in self.get_custom_fields():
             if field['name'] in [f['name'] for f in fields]:
                 self.log.warning('Duplicate field name "%s" (ignoring)',
@@ -278,6 +284,11 @@
 
         fields.sort(lambda x, y: cmp(x['order'], y['order']))
         return fields
+
+    def get_field_synonyms(self):
+        """Return a mapping from field name synonyms to field names.
+        The synonyms are supposed to be more intuitive for custom queries."""
+        return {'created': 'time', 'modified': 'changetime'}
 
     # IPermissionRequestor methods
 
diff --git a/trac/ticket/model.py b/trac/ticket/model.py
--- a/trac/ticket/model.py
+++ b/trac/ticket/model.py
@@ -40,16 +40,22 @@
     def id_is_valid(num):
         return 0 < int(num) <= 1L << 31
 
+    # 0.11 compatibility
+    time_created = property(lambda self: self.values.get('time'))
+    time_changed = property(lambda self: self.values.get('changetime'))
+    
     def __init__(self, env, tkt_id=None, db=None, version=None):
         self.env = env
         self.resource = Resource('ticket', tkt_id, version)
         self.fields = TicketSystem(self.env).get_ticket_fields()
+        self.time_fields = [f['name'] for f in self.fields
+                            if f['type'] == 'time']
         self.values = {}
         if tkt_id is not None:
             self._fetch_ticket(tkt_id, db)
         else:
             self._init_defaults(db)
-            self.id = self.time_created = self.time_changed = None
+            self.id = None
         self._old = {}
 
     def _get_db(self, db):
@@ -66,7 +72,7 @@
     def _init_defaults(self, db=None):
         for field in self.fields:
             default = None
-            if field['name'] in ['resolution', 'status']:
+            if field['name'] in ['resolution', 'status', 'time', 'changetime']:
                 # Ignore for new - only change through workflow
                 pass
             elif not field.get('custom'):
@@ -93,7 +99,7 @@
             # Fetch the standard ticket fields
             std_fields = [f['name'] for f in self.fields if not f.get('custom')]
             cursor = db.cursor()
-            cursor.execute("SELECT %s,time,changetime FROM ticket WHERE id=%%s"
+            cursor.execute("SELECT %s FROM ticket WHERE id=%%s"
                            % ','.join(std_fields), (tkt_id,))
             row = cursor.fetchone()
         if not row:
@@ -102,9 +108,11 @@
 
         self.id = tkt_id
         for i in range(len(std_fields)):
-            self.values[std_fields[i]] = row[i] or ''
-        self.time_created = datetime.fromtimestamp(row[len(std_fields)], utc)
-        self.time_changed = datetime.fromtimestamp(row[len(std_fields) + 1], utc)
+            field = std_fields[i]
+            if field in self.time_fields:
+                self.values[field] = datetime.fromtimestamp(row[i], utc)
+            else:
+                self.values[field] = row[i] or ''
 
         # Fetch custom fields if available
         custom_fields = [f['name'] for f in self.fields if f.get('custom')]
@@ -164,7 +172,7 @@
         # Add a timestamp
         if when is None:
             when = datetime.now(utc)
-        self.time_created = self.time_changed = when
+        self.values['time'] = self.values['changetime'] = when
 
         cursor = db.cursor()
 
@@ -178,9 +186,13 @@
                 # No such component exists
                 pass
 
+        # Perform type conversions
+        values = dict(self.values)
+        for field in self.time_fields:
+            if field in values:
+                values[field] = to_timestamp(values[field])
+        
         # Insert ticket record
-        created = to_timestamp(self.time_created)
-        changed = to_timestamp(self.time_changed)
         std_fields = []
         custom_fields = []
         for f in self.fields:
@@ -190,10 +202,10 @@
                     custom_fields.append(fname)
                 else:
                     std_fields.append(fname)
-        cursor.execute("INSERT INTO ticket (%s,time,changetime) VALUES (%s)"
+        cursor.execute("INSERT INTO ticket (%s) VALUES (%s)"
                        % (','.join(std_fields),
-                          ','.join(['%s'] * (len(std_fields) + 2))),
-                       [self[name] for name in std_fields] + [created, changed])
+                          ','.join(['%s'] * len(std_fields))),
+                       [values[name] for name in std_fields])
         tkt_id = db.get_last_id(cursor, 'ticket')
 
         # Insert custom fields
@@ -289,7 +301,7 @@
             db.commit()
         old_values = self._old
         self._old = {}
-        self.time_changed = when
+        self.values['changetime'] = when
 
         for listener in TicketSystem(self.env).change_listeners:
             listener.ticket_changed(self, comment, author, old_values)
diff --git a/trac/ticket/query.py b/trac/ticket/query.py
--- a/trac/ticket/query.py
+++ b/trac/ticket/query.py
@@ -31,7 +31,7 @@
 from trac.resource import Resource
 from trac.ticket.api import TicketSystem
 from trac.util import Ranges
-from trac.util.datefmt import to_timestamp, utc
+from trac.util.datefmt import parse_date, to_timestamp, utc
 from trac.util.presentation import Paginator
 from trac.util.text import shorten_line
 from trac.util.translation import _, tag_
@@ -56,7 +56,8 @@
         self.env = env
         self.id = report # if not None, it's the corresponding saved query
         self.constraints = constraints or {}
-        self.order = order
+        synonyms = TicketSystem(self.env).get_field_synonyms()
+        self.order = synonyms.get(order, order)     # 0.11 compatibility
         self.desc = desc
         self.group = group
         self.groupdesc = groupdesc
@@ -100,20 +101,14 @@
         if verbose and 'description' not in rows: # 0.10 compatibility
             rows.append('description')
         self.fields = TicketSystem(self.env).get_ticket_fields()
+        self.time_fields = [f['name'] for f in self.fields
+                            if f['type'] == 'time']
         field_names = [f['name'] for f in self.fields]
         self.cols = [c for c in cols or [] if c in field_names or 
-                     c in ('id', 'time', 'changetime')]
+                     c == 'id']
         self.rows = [c for c in rows if c in field_names]
         if self.order != 'id' and self.order not in field_names:
-            # TODO: fix after adding time/changetime to the api.py
-            if order == 'created':
-                order = 'time'
-            elif order == 'modified':
-                order = 'changetime'
-            if order in ('time', 'changetime'):
-                self.order = order
-            else:
-                self.order = 'priority'
+            self.order = 'priority'
 
         if self.group not in field_names:
             self.group = None
@@ -124,6 +119,7 @@
         kw_strs = ['order', 'group', 'page', 'max']
         kw_arys = ['rows']
         kw_bools = ['desc', 'groupdesc', 'verbose']
+        synonyms = TicketSystem(env).get_field_synonyms()
         constraints = {}
         cols = []
         for filter_ in filters:
@@ -156,9 +152,11 @@
                 elif field in kw_bools:
                     kw[field] = True
                 elif field == 'col':
-                    cols.extend(processed_values)
+                    cols.extend(synonyms.get(value, value)
+                                for value in processed_values)
                 else:
-                    constraints[field] = processed_values
+                    constraints.setdefault(synonyms.get(field, field), 
+                                           []).extend(processed_values)
             except UnicodeError:
                 pass # field must be a str, see `get_href()`
         report = constraints.pop('report', None)
@@ -184,8 +182,6 @@
             if col in cols:
                 cols.remove(col)
                 cols.append(col)
-        # TODO: fix after adding time/changetime to the api.py
-        cols += ['time', 'changetime']
 
         # Semi-intelligently remove columns that are restricted to a single
         # value by a query constraint.
@@ -194,7 +190,7 @@
             constraint = self.constraints[col]
             if len(constraint) == 1 and constraint[0] \
                     and not constraint[0][0] in ('!', '~', '^', '$'):
-                if col in cols:
+                if col in cols and col not in self.time_fields:
                     cols.remove(col)
             if col == 'status' and not 'closed' in constraint \
                     and 'resolution' in cols:
@@ -298,7 +294,7 @@
                     result['href'] = req.href.ticket(val)
                 elif val is None:
                     val = '--'
-                elif name in ('changetime', 'time'):
+                elif name in self.time_fields:
                     val = datetime.fromtimestamp(int(val or 0), utc)
                 elif field and field['type'] == 'checkbox':
                     try:
@@ -424,17 +420,41 @@
 
         def get_constraint_sql(name, value, mode, neg):
             if name not in custom_fields:
-                name = 't.' + name
+                col = 't.' + name
             else:
-                name = name + '.value'
+                col = name + '.value'
             value = value[len(mode) + neg:]
 
+            if name in self.time_fields:
+                if '@' in value:
+                    (start, end) = [each.strip() for each in 
+                                    value.split('@', 1)]
+                else:
+                    (start, end) = (value.strip(), '')
+                col_cast = db.cast(col, 'int')
+                if start and end:
+                    start = to_timestamp(parse_date(start, req.tz))
+                    end = to_timestamp(parse_date(end, req.tz))
+                    return ("%s(%s>=%%s AND %s<%%s)" % (neg and 'NOT ' or '',
+                                                        col_cast, col_cast),
+                            (start, end))
+                elif start:
+                    start = to_timestamp(parse_date(start, req.tz))
+                    return ("%s%s>=%%s" % (neg and 'NOT ' or '', col_cast),
+                            (start, ))
+                elif end:
+                    end = to_timestamp(parse_date(end, req.tz))
+                    return ("%s%s<%%s" % (neg and 'NOT ' or '', col_cast),
+                            (end, ))
+                else:
+                    return None
+                
             if mode == '':
-                return ("COALESCE(%s,'')%s=%%s" % (name, neg and '!' or ''),
-                        value)
+                return ("COALESCE(%s,'')%s=%%s" % (col, neg and '!' or ''),
+                        (value, ))
+
             if not value:
                 return None
-            db = self.env.get_db_cnx()
             value = db.like_escape(value)
             if mode == '~':
                 value = '%' + value + '%'
@@ -442,10 +462,11 @@
                 value = value + '%'
             elif mode == '$':
                 value = '%' + value
-            return ("COALESCE(%s,'') %s%s" % (name, neg and 'NOT ' or '',
+            return ("COALESCE(%s,'') %s%s" % (col, neg and 'NOT ' or '',
                                               db.like()),
                     value)
 
+        db = self.env.get_db_cnx()
         clauses = []
         args = []
         for k, v in self.constraints.items():
@@ -479,7 +500,7 @@
                     clauses.append('%s(%s)' % (neg and 'NOT ' or '',
                                                ' OR '.join(id_clauses)))
             # Special case for exact matches on multiple values
-            elif not mode and len(v) > 1:
+            elif not mode and len(v) > 1 and k not in self.time_fields:
                 if k not in custom_fields:
                     col = 't.' + k
                 else:
@@ -500,12 +521,13 @@
                 else:
                     clauses.append("(" + " OR ".join(
                         [item[0] for item in constraint_sql]) + ")")
-                args += [item[1] for item in constraint_sql]
+                for item in constraint_sql:
+                    args.extend(item[1])
             elif len(v) == 1:
                 constraint_sql = get_constraint_sql(k, v[0], mode, neg)
                 if constraint_sql:
                     clauses.append(constraint_sql[0])
-                    args.append(constraint_sql[1])
+                    args.extend(constraint_sql[1])
 
         clauses = filter(None, clauses)
         if clauses or cached_ids:
@@ -531,7 +553,7 @@
             # FIXME: This is a somewhat ugly hack.  Can we also have the
             #        column type for this?  If it's an integer, we do first
             #        one, if text, we do 'else'
-            if name in ('id', 'time', 'changetime'):
+            if name == 'id' or name in self.time_fields:
                 sql.append("COALESCE(%s,0)=0%s," % (col, desc))
             else:
                 sql.append("COALESCE(%s,'')=''%s," % (col, desc))
@@ -576,10 +598,6 @@
         cols = self.get_columns()
         labels = dict([(f['name'], f['label']) for f in self.fields])
 
-        # TODO: remove after adding time/changetime to the api.py
-        labels['changetime'] = _('Modified')
-        labels['time'] = _('Created')
-
         headers = [{
             'name': col, 'label': labels.get(col, _('Ticket')),
             'href': self.get_href(context.href, order=col,
@@ -607,6 +625,10 @@
         modes['select'] = [
             {'name': _("is"), 'value': ""},
             {'name': _("is not"), 'value': "!"}
+        ]
+        modes['time'] = [
+            {'name': _("is between"), 'value': ""},
+            {'name': _("is not between"), 'value': "!"},
         ]
 
         groups = {}
@@ -807,9 +829,13 @@
 
     def _get_constraints(self, req):
         constraints = {}
-        ticket_fields = [f['name'] for f in
-                         TicketSystem(self.env).get_ticket_fields()]
+        fields = TicketSystem(self.env).get_ticket_fields()
+        synonyms = TicketSystem(self.env).get_field_synonyms()
+        ticket_fields = [f['name'] for f in fields]
         ticket_fields.append('id')
+        ticket_fields.extend(synonyms.iterkeys())
+        time_fields = [f['name'] for f in fields if f['type'] == 'time']
+        time_fields.extend(synonyms.iterkeys())
 
         # For clients without JavaScript, we remove constraints here if
         # requested
@@ -831,6 +857,11 @@
                 mode = req.args.get(field + '_mode')
                 if mode:
                     vals = [mode + x for x in vals]
+                if field in time_fields:
+                    ends = req.args.getlist(field + '_end')
+                    if ends:
+                        vals = [start + '@' + end 
+                                for (start, end) in zip(vals, ends)]
                 if field in remove_constraints:
                     idx = remove_constraints[field]
                     if idx >= 0:
@@ -839,7 +870,8 @@
                             continue
                     else:
                         continue
-                constraints[field] = vals
+                constraints.setdefault(synonyms.get(field, field), 
+                                       []).extend(vals)
 
         return constraints
 
diff --git a/trac/ticket/templates/query.html b/trac/ticket/templates/query.html
--- a/trac/ticket/templates/query.html
+++ b/trac/ticket/templates/query.html
@@ -42,7 +42,7 @@
             <py:for each="field_name, field in fields.items()">
               <py:for each="constraint_name, constraint in constraints.items()">
                 <tbody py:if="field_name == constraint_name"
-                  py:with="multiline = field.type in ('select', 'text')">
+                  py:with="multiline = field.type in ('select', 'text', 'time')">
                   <py:for each="constraint_idx, constraint_value in enumerate(constraint['values'])">
                     <tr class="${field_name}" py:if="multiline or constraint_idx == 0">
                       <py:choose test="constraint_idx">
@@ -94,6 +94,14 @@
 
                         <py:when test="'text'">
                           <input type="text" name="${field_name}" value="$constraint_value" size="42" />
+                        </py:when>
+                        
+                        <py:when test="'time'" py:with="(start, end) = '@' in constraint_value 
+                                                        and constraint_value.split('@', 1)
+                                                        or (constraint_value, constraint_value)">
+                          <input type="text" name="${field_name}" value="$start" size="14" />
+                          <label>and</label>
+                          <input type="text" name="${field_name}_end" value="$end" size="14" />
                         </py:when>
 
                       </td>
diff --git a/trac/ticket/templates/ticket.html b/trac/ticket/templates/ticket.html
--- a/trac/ticket/templates/ticket.html
+++ b/trac/ticket/templates/ticket.html
@@ -125,8 +125,8 @@
         <div id="ticket" py:if="ticket.exists or preview_mode"
           class="${preview_mode and 'ticketdraft' or None}">
           <div class="date">
-            <p py:if="ticket.exists">Opened ${dateinfo(ticket.time_created)} ago</p>
-            <p py:if="ticket.time_changed != ticket.time_created">Last modified ${dateinfo(ticket.time_changed)} ago</p>
+            <p py:if="ticket.exists">Opened ${dateinfo(ticket.time)} ago</p>
+            <p py:if="ticket.changetime != ticket.time">Last modified ${dateinfo(ticket.changetime)} ago</p>
             <p py:if="not ticket.exists"><i>(ticket not yet created)</i></p>
           </div>
           <!-- use a placeholder if it's a new ticket -->
diff --git a/trac/ticket/web_ui.py b/trac/ticket/web_ui.py
--- a/trac/ticket/web_ui.py
+++ b/trac/ticket/web_ui.py
@@ -520,7 +520,7 @@
                          'reassign_owner': req.authname,
                          'resolve_resolution': None,
                          # Store a timestamp for detecting "mid air collisions"
-                         'timestamp': str(ticket.time_changed)})
+                         'timestamp': str(ticket['changetime'])})
 
         self._insert_ticket_data(req, ticket, data,
                                  get_reporter_id(req, 'author'), field_changes)
@@ -653,7 +653,7 @@
         history = [c for c in history if any([f in text_fields
                                               for f in c['fields']])]
         history.append({'version': 0, 'comment': "''Initial version''",
-                        'date': ticket.time_created,
+                        'date': ticket['time'],
                         'author': ticket['reporter'] # not 100% accurate...
                         })
         data.update({'title': _('Ticket History'),
@@ -901,7 +901,7 @@
 
         # Mid air collision?
         if ticket.exists and (ticket._old or comment):
-            if req.args.get('ts') != str(ticket.time_changed):
+            if req.args.get('ts') != str(ticket['changetime']):
                 add_warning(req, _("Sorry, can not save your changes. "
                               "This ticket has been modified by someone else "
                               "since you started"))
@@ -1085,7 +1085,7 @@
 
             # per field settings
             if name in ('summary', 'reporter', 'description', 'status',
-                        'resolution'):
+                        'resolution', 'time', 'changetime'):
                 field['skip'] = True
             elif name == 'owner':
                 field['skip'] = True
diff --git a/trac/util/datefmt.py b/trac/util/datefmt.py
--- a/trac/util/datefmt.py
+++ b/trac/util/datefmt.py
@@ -172,9 +172,7 @@
 
 def parse_date(text, tzinfo=None):
     tzinfo = tzinfo or localtz
-    if text == 'now': # TODO: today, yesterday, etc.
-        return datetime.now(utc)
-    tm = None
+    dt = None
     text = text.strip()
     # normalize ISO time
     match = _ISO_8601_RE.match(text)
@@ -198,22 +196,25 @@
             tm = time.strptime('%s ' * 6 % (years, months, days,
                                             hours, minutes, seconds),
                                '%Y %m %d %H %M %S ')
+            dt = datetime(*(tm[0:6] + (0, tzinfo)))
         except ValueError:
             pass
-    else:
+    if dt is None:
         for format in ['%x %X', '%x, %X', '%X %x', '%X, %x', '%x', '%c',
                        '%b %d, %Y']:
             try:
                 tm = time.strptime(text, format)
+                dt = datetime(*(tm[0:6] + (0, tzinfo)))
                 break
             except ValueError:
                 continue
-    if tm == None:
+    if dt is None:
+        dt = _parse_relative_time(text, tzinfo)
+    if dt is None:
         hint = get_date_format_hint()        
         raise TracError('"%s" is an invalid date, or the date format '
                         'is not known. Try "%s" instead.' % (text, hint),
                         'Invalid Date')
-    dt = datetime(*(tm[0:6] + (0, tzinfo)))
     # Make sure we can convert it to a timestamp and back - fromtimestamp()
     # may raise ValueError if larger than platform C localtime() or gmtime()
     try:
@@ -223,6 +224,62 @@
                         'Try a date closer to present time.' % (text,),
                         'Invalid Date')
     return dt
+
+
+_REL_TIME_RE = re.compile(r'(\d+\.?\d*)\s*'
+                          r'(second|minute|hour|day|week|month|year)s?\s*'
+                          r'(?:ago)?$')
+_time_intervals = dict(
+    second=lambda v: timedelta(seconds=v),
+    minute=lambda v: timedelta(minutes=v),
+    hour=lambda v: timedelta(hours=v),
+    day=lambda v: timedelta(days=v),
+    week=lambda v: timedelta(weeks=v),
+    month=lambda v: timedelta(days=30 * v),
+    year=lambda v: timedelta(days=365 * v),
+)
+_TIME_START_RE = re.compile(r'(this|last)\s*'
+                            r'(second|minute|hour|day|week|month|year)$')
+_time_starts = dict(
+    second=lambda now: now.replace(microsecond=0),
+    minute=lambda now: now.replace(microsecond=0, second=0),
+    hour=lambda now: now.replace(microsecond=0, second=0, minute=0),
+    day=lambda now: now.replace(microsecond=0, second=0, minute=0, hour=0),
+    week=lambda now: now.replace(microsecond=0, second=0, minute=0, hour=0) \
+                     - timedelta(days=now.weekday()),
+    month=lambda now: now.replace(microsecond=0, second=0, minute=0, hour=0,
+                                  day=1),
+    year=lambda now: now.replace(microsecond=0, second=0, minute=0, hour=0,
+                                  day=1, month=1),
+)
+
+def _parse_relative_time(text, tzinfo):
+    now = datetime.now(tzinfo)
+    if text == 'now':
+        return now
+    if text == 'today':
+        return now.replace(microsecond=0, second=0, minute=0, hour=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 match:
+        (value, interval) = match.groups()
+        return now - _time_intervals[interval](float(value))
+    match = _TIME_START_RE.match(text)
+    if match:
+        (which, start) = match.groups()
+        dt = _time_starts[start](now)
+        if which == 'last':
+            if start == 'month':
+                if dt.month > 1:
+                    dt = dt.replace(month=dt.month - 1)
+                else:
+                    dt = dt.replace(year=dt.year - 1, month=12)
+            else:
+                dt -= _time_intervals[start](1)
+        return dt
+    return None
 
 
 # -- timezone utilities

