Edgewall Software

Ticket #2288: 2288-time-queries-r7499.2.patch

File 2288-time-queries-r7499.2.patch, 34.4 KB (added by rblank, 3 months ago)

Updated patch fixing unit tests and a few issues

  • trac/htdocs/css/report.css

    diff --git a/trac/htdocs/css/report.css b/trac/htdocs/css/report.css
    a b  
    3838#filters td label { font-size: 11px } 
    3939#filters td.mode { text-align: right } 
    4040#filters td.filter { width: 100% } 
    41 #filters td.filter label { padding-right: 1em } 
    4241#filters td.actions { text-align: right; white-space: nowrap } 
    4342 
    4443#columns div label {  
  • trac/htdocs/js/query.js

    diff --git a/trac/htdocs/js/query.js b/trac/htdocs/js/query.js
    a b  
    225225        td.className = "filter"; 
    226226        if (property.type == "select") { 
    227227          var element = createSelect(propertyName, property.options, true); 
     228          td.appendChild(element); 
    228229        } else if (property.type == "text") { 
    229230          var element = document.createElement("input"); 
    230231          element.type = "text"; 
    231232          element.name = propertyName; 
    232233          element.size = 42; 
     234          td.appendChild(element); 
     235        } else if (property.type == "time") { 
     236          var element = document.createElement("input"); 
     237          element.type = "text"; 
     238          element.name = propertyName; 
     239          element.size = 14; 
     240          td.appendChild(element); 
     241          td.appendChild(document.createTextNode(" ")); 
     242          td.appendChild(createLabel("and")); 
     243          td.appendChild(document.createTextNode(" ")); 
     244          var element2 = document.createElement("input"); 
     245          element2.type = "text"; 
     246          element2.name = propertyName + "_end"; 
     247          element2.size = 14; 
     248          td.appendChild(element2); 
    233249        } 
    234         td.appendChild(element); 
    235250        element.focus(); 
    236251        tr.appendChild(td); 
    237252      } 
  • trac/ticket/api.py

    diff --git a/trac/ticket/api.py b/trac/ticket/api.py
    a b  
    240240            field = {'name': name, 'type': 'text', 'label': name.title()} 
    241241            fields.append(field) 
    242242 
     243        # Date/time fields 
     244        fields.append({'name': 'time', 'type': 'time', 
     245                       'label': _('Created')}) 
     246        fields.append({'name': 'changetime', 'type': 'time', 
     247                       'label': _('Modified')}) 
     248 
    243249        for field in self.get_custom_fields(): 
    244250            if field['name'] in [f['name'] for f in fields]: 
    245251                self.log.warning('Duplicate field name "%s" (ignoring)', 
     
    278284 
    279285        fields.sort(lambda x, y: cmp(x['order'], y['order'])) 
    280286        return fields 
     287 
     288    def get_field_synonyms(self): 
     289        """Return a mapping from field name synonyms to field names. 
     290        The synonyms are supposed to be more intuitive for custom queries.""" 
     291        return {'created': 'time', 'modified': 'changetime'} 
    281292 
    282293    # IPermissionRequestor methods 
    283294 
  • trac/ticket/model.py

    diff --git a/trac/ticket/model.py b/trac/ticket/model.py
    a b  
    4040    def id_is_valid(num): 
    4141        return 0 < int(num) <= 1L << 31 
    4242 
     43    # 0.11 compatibility 
     44    time_created = property(lambda self: self.values.get('time')) 
     45    time_changed = property(lambda self: self.values.get('changetime')) 
     46     
    4347    def __init__(self, env, tkt_id=None, db=None, version=None): 
    4448        self.env = env 
    4549        self.resource = Resource('ticket', tkt_id, version) 
    4650        self.fields = TicketSystem(self.env).get_ticket_fields() 
     51        self.time_fields = [f['name'] for f in self.fields 
     52                            if f['type'] == 'time'] 
    4753        self.values = {} 
    4854        if tkt_id is not None: 
    4955            self._fetch_ticket(tkt_id, db) 
    5056        else: 
    5157            self._init_defaults(db) 
    52             self.id = self.time_created = self.time_changed = None 
     58            self.id = None 
    5359        self._old = {} 
    5460 
    5561    def _get_db(self, db): 
     
    6672    def _init_defaults(self, db=None): 
    6773        for field in self.fields: 
    6874            default = None 
    69             if field['name'] in ['resolution', 'status']: 
     75            if field['name'] in ['resolution', 'status', 'time', 'changetime']: 
    7076                # Ignore for new - only change through workflow 
    7177                pass 
    7278            elif not field.get('custom'): 
     
    9399            # Fetch the standard ticket fields 
    94100            std_fields = [f['name'] for f in self.fields if not f.get('custom')] 
    95101            cursor = db.cursor() 
    96             cursor.execute("SELECT %s,time,changetime FROM ticket WHERE id=%%s" 
     102            cursor.execute("SELECT %s FROM ticket WHERE id=%%s" 
    97103                           % ','.join(std_fields), (tkt_id,)) 
    98104            row = cursor.fetchone() 
    99105        if not row: 
     
    102108 
    103109        self.id = tkt_id 
    104110        for i in range(len(std_fields)): 
    105             self.values[std_fields[i]] = row[i] or '' 
    106         self.time_created = datetime.fromtimestamp(row[len(std_fields)], utc) 
    107         self.time_changed = datetime.fromtimestamp(row[len(std_fields) + 1], utc) 
     111            field = std_fields[i] 
     112            if field in self.time_fields: 
     113                self.values[field] = datetime.fromtimestamp(row[i], utc) 
     114            else: 
     115                self.values[field] = row[i] or '' 
    108116 
    109117        # Fetch custom fields if available 
    110118        custom_fields = [f['name'] for f in self.fields if f.get('custom')] 
     
    164172        # Add a timestamp 
    165173        if when is None: 
    166174            when = datetime.now(utc) 
    167         self.time_created = self.time_changed = when 
     175        self.values['time'] = self.values['changetime'] = when 
    168176 
    169177        cursor = db.cursor() 
    170178 
     
    178186                # No such component exists 
    179187                pass 
    180188 
     189        # Perform type conversions 
     190        values = dict(self.values) 
     191        for field in self.time_fields: 
     192            if field in values: 
     193                values[field] = to_timestamp(values[field]) 
     194         
    181195        # Insert ticket record 
    182         created = to_timestamp(self.time_created) 
    183         changed = to_timestamp(self.time_changed) 
    184196        std_fields = [] 
    185197        custom_fields = [] 
    186198        for f in self.fields: 
     
    190202                    custom_fields.append(fname) 
    191203                else: 
    192204                    std_fields.append(fname) 
    193         cursor.execute("INSERT INTO ticket (%s,time,changetime) VALUES (%s)" 
     205        cursor.execute("INSERT INTO ticket (%s) VALUES (%s)" 
    194206                       % (','.join(std_fields), 
    195                           ','.join(['%s'] * (len(std_fields) + 2))), 
    196                        [self[name] for name in std_fields] + [created, changed]) 
     207                          ','.join(['%s'] * len(std_fields))), 
     208                       [values[name] for name in std_fields]) 
    197209        tkt_id = db.get_last_id(cursor, 'ticket') 
    198210 
    199211        # Insert custom fields 
     
    289301            db.commit() 
    290302        old_values = self._old 
    291303        self._old = {} 
    292         self.time_changed = when 
     304        self.values['changetime'] = when 
    293305 
    294306        for listener in TicketSystem(self.env).change_listeners: 
    295307            listener.ticket_changed(self, comment, author, old_values) 
  • trac/ticket/notification.py

    diff --git a/trac/ticket/notification.py b/trac/ticket/notification.py
    a b  
    149149 
    150150    def format_props(self): 
    151151        tkt = self.ticket 
    152         fields = [f for f in tkt.fields if f['name'] not in ('summary', 'cc')] 
     152        fields = [f for f in tkt.fields  
     153                  if f['name'] not in ('summary', 'cc', 'time', 'changetime')] 
    153154        width = [0, 0, 0, 0] 
    154155        i = 0 
    155156        for f in [f['name'] for f in fields if f['type'] != 'textarea']: 
  • trac/ticket/query.py

    diff --git a/trac/ticket/query.py b/trac/ticket/query.py
    a b  
    3131from trac.resource import Resource 
    3232from trac.ticket.api import TicketSystem 
    3333from trac.util import Ranges 
    34 from trac.util.datefmt import to_timestamp, utc 
     34from trac.util.datefmt import format_datetime, parse_date, to_timestamp, utc 
    3535from trac.util.presentation import Paginator 
    3636from trac.util.text import shorten_line 
    3737from trac.util.translation import _, tag_ 
     
    5656        self.env = env 
    5757        self.id = report # if not None, it's the corresponding saved query 
    5858        self.constraints = constraints or {} 
    59         self.order = order 
     59        synonyms = TicketSystem(self.env).get_field_synonyms() 
     60        self.order = synonyms.get(order, order)     # 0.11 compatibility 
    6061        self.desc = desc 
    6162        self.group = group 
    6263        self.groupdesc = groupdesc 
     
    100101        if verbose and 'description' not in rows: # 0.10 compatibility 
    101102            rows.append('description') 
    102103        self.fields = TicketSystem(self.env).get_ticket_fields() 
     104        self.time_fields = [f['name'] for f in self.fields 
     105                            if f['type'] == 'time'] 
    103106        field_names = [f['name'] for f in self.fields] 
    104107        self.cols = [c for c in cols or [] if c in field_names or  
    105                      c in ('id', 'time', 'changetime')] 
     108                     c == 'id'] 
    106109        self.rows = [c for c in rows if c in field_names] 
    107110        if self.order != 'id' and self.order not in field_names: 
    108             # TODO: fix after adding time/changetime to the api.py 
    109             if order == 'created': 
    110                 order = 'time' 
    111             elif order == 'modified': 
    112                 order = 'changetime' 
    113             if order in ('time', 'changetime'): 
    114                 self.order = order 
    115             else: 
    116                 self.order = 'priority' 
     111            self.order = 'priority' 
    117112 
    118113        if self.group not in field_names: 
    119114            self.group = None 
     
    124119        kw_strs = ['order', 'group', 'page', 'max'] 
    125120        kw_arys = ['rows'] 
    126121        kw_bools = ['desc', 'groupdesc', 'verbose'] 
     122        synonyms = TicketSystem(env).get_field_synonyms() 
    127123        constraints = {} 
    128124        cols = [] 
    129125        for filter_ in filters: 
     
    131127            if len(filter_) != 2: 
    132128                raise QuerySyntaxError(_('Query filter requires field and '  
    133129                                         'constraints separated by a "="')) 
    134             field,values = filter_ 
     130            field, values = filter_ 
    135131            if not field: 
    136132                raise QuerySyntaxError(_('Query filter requires field name')) 
    137133            # from last char of `field`, get the mode of comparison 
     
    156152                elif field in kw_bools: 
    157153                    kw[field] = True 
    158154                elif field == 'col': 
    159                     cols.extend(processed_values) 
     155                    cols.extend(synonyms.get(value, value) 
     156                                for value in processed_values) 
    160157                else: 
    161                     constraints[field] = processed_values 
     158                    constraints.setdefault(synonyms.get(field, field),  
     159                                           []).extend(processed_values) 
    162160            except UnicodeError: 
    163161                pass # field must be a str, see `get_href()` 
    164162        report = constraints.pop('report', None) 
     
    184182            if col in cols: 
    185183                cols.remove(col) 
    186184                cols.append(col) 
    187         # TODO: fix after adding time/changetime to the api.py 
    188         cols += ['time', 'changetime'] 
    189185 
    190186        # Semi-intelligently remove columns that are restricted to a single 
    191187        # value by a query constraint. 
     
    194190            constraint = self.constraints[col] 
    195191            if len(constraint) == 1 and constraint[0] \ 
    196192                    and not constraint[0][0] in ('!', '~', '^', '$'): 
    197                 if col in cols: 
     193                if col in cols and col not in self.time_fields: 
    198194                    cols.remove(col) 
    199195            if col == 'status' and not 'closed' in constraint \ 
    200196                    and 'resolution' in cols: 
     
    298294                    result['href'] = req.href.ticket(val) 
    299295                elif val is None: 
    300296                    val = '--' 
    301                 elif name in ('changetime', 'time'): 
     297                elif name in self.time_fields: 
    302298                    val = datetime.fromtimestamp(int(val or 0), utc) 
    303299                elif field and field['type'] == 'checkbox': 
    304300                    try: 
     
    424420 
    425421        def get_constraint_sql(name, value, mode, neg): 
    426422            if name not in custom_fields: 
    427                 name = 't.' + name 
     423                col = 't.' + name 
    428424            else: 
    429                 name = name + '.value' 
     425                col = name + '.value' 
    430426            value = value[len(mode) + neg:] 
    431427 
     428            if name in self.time_fields: 
     429                if ';' in value: 
     430                    (start, end) = [each.strip() for each in  
     431                                    value.split(';', 1)] 
     432                else: 
     433                    (start, end) = (value.strip(), '') 
     434                col_cast = db.cast(col, 'int') 
     435                if start and end: 
     436                    start = to_timestamp(parse_date(start, req.tz)) 
     437                    end = to_timestamp(parse_date(end, req.tz)) 
     438                    return ("%s(%s>=%%s AND %s<%%s)" % (neg and 'NOT ' or '', 
     439                                                        col_cast, col_cast), 
     440                            (start, end)) 
     441                elif start: 
     442                    start = to_timestamp(parse_date(start, req.tz)) 
     443                    return ("%s%s>=%%s" % (neg and 'NOT ' or '', col_cast), 
     444                            (start, )) 
     445                elif end: 
     446                    end = to_timestamp(parse_date(end, req.tz)) 
     447                    return ("%s%s<%%s" % (neg and 'NOT ' or '', col_cast), 
     448                            (end, )) 
     449                else: 
     450                    return None 
     451                 
    432452            if mode == '': 
    433                 return ("COALESCE(%s,'')%s=%%s" % (name, neg and '!' or ''), 
    434                         value) 
     453                return ("COALESCE(%s,'')%s=%%s" % (col, neg and '!' or ''), 
     454                        (value, )) 
     455 
    435456            if not value: 
    436457                return None 
    437             db = self.env.get_db_cnx() 
    438458            value = db.like_escape(value) 
    439459            if mode == '~': 
    440460                value = '%' + value + '%' 
     
    442462                value = value + '%' 
    443463            elif mode == '$': 
    444464                value = '%' + value 
    445             return ("COALESCE(%s,'') %s%s" % (name, neg and 'NOT ' or '', 
     465            return ("COALESCE(%s,'') %s%s" % (col, neg and 'NOT ' or '', 
    446466                                              db.like()), 
    447                     value) 
     467                    (value, )) 
    448468 
     469        db = self.env.get_db_cnx() 
    449470        clauses = [] 
    450471        args = [] 
    451472        for k, v in self.constraints.items(): 
     
    479500                    clauses.append('%s(%s)' % (neg and 'NOT ' or '', 
    480501                                               ' OR '.join(id_clauses))) 
    481502            # Special case for exact matches on multiple values 
    482             elif not mode and len(v) > 1: 
     503            elif not mode and len(v) > 1 and k not in self.time_fields: 
    483504                if k not in custom_fields: 
    484505                    col = 't.' + k 
    485506                else: 
     
    500521                else: 
    501522                    clauses.append("(" + " OR ".join( 
    502523                        [item[0] for item in constraint_sql]) + ")") 
    503                 args += [item[1] for item in constraint_sql] 
     524                for item in constraint_sql: 
     525                    args.extend(item[1]) 
    504526            elif len(v) == 1: 
    505527                constraint_sql = get_constraint_sql(k, v[0], mode, neg) 
    506528                if constraint_sql: 
    507529                    clauses.append(constraint_sql[0]) 
    508                     args.append(constraint_sql[1]) 
     530                    args.extend(constraint_sql[1]) 
    509531 
    510532        clauses = filter(None, clauses) 
    511533        if clauses or cached_ids: 
     
    531553            # FIXME: This is a somewhat ugly hack.  Can we also have the 
    532554            #        column type for this?  If it's an integer, we do first 
    533555            #        one, if text, we do 'else' 
    534             if name in ('id', 'time', 'changetime'): 
     556            if name == 'id' or name in self.time_fields: 
    535557                sql.append("COALESCE(%s,0)=0%s," % (col, desc)) 
    536558            else: 
    537559                sql.append("COALESCE(%s,'')=''%s," % (col, desc)) 
     
    576598        cols = self.get_columns() 
    577599        labels = dict([(f['name'], f['label']) for f in self.fields]) 
    578600 
    579         # TODO: remove after adding time/changetime to the api.py 
    580         labels['changetime'] = _('Modified') 
    581         labels['time'] = _('Created') 
    582  
    583601        headers = [{ 
    584602            'name': col, 'label': labels.get(col, _('Ticket')), 
    585603            'href': self.get_href(context.href, order=col, 
     
    607625        modes['select'] = [ 
    608626            {'name': _("is"), 'value': ""}, 
    609627            {'name': _("is not"), 'value': "!"} 
     628        ] 
     629        modes['time'] = [ 
     630            {'name': _("is between"), 'value': ""}, 
     631            {'name': _("is not between"), 'value': "!"}, 
    610632        ] 
    611633 
    612634        groups = {} 
     
    807829 
    808830    def _get_constraints(self, req): 
    809831        constraints = {} 
    810         ticket_fields = [f['name'] for f in 
    811                          TicketSystem(self.env).get_ticket_fields()] 
     832        fields = TicketSystem(self.env).get_ticket_fields() 
     833        synonyms = TicketSystem(self.env).get_field_synonyms() 
     834        ticket_fields = [f['name'] for f in fields] 
    812835        ticket_fields.append('id') 
     836        ticket_fields.extend(synonyms.iterkeys()) 
     837        time_fields = [f['name'] for f in fields if f['type'] == 'time'] 
     838        time_fields.extend([k for (k, v) in synonyms.iteritems()  
     839                            if v in time_fields]) 
    813840 
    814841        # For clients without JavaScript, we remove constraints here if 
    815842        # requested 
     
    831858                mode = req.args.get(field + '_mode') 
    832859                if mode: 
    833860                    vals = [mode + x for x in vals] 
     861                if field in time_fields: 
     862                    ends = req.args.getlist(field + '_end') 
     863                    if ends: 
     864                        vals = [start + ';' + end  
     865                                for (start, end) in zip(vals, ends)] 
    834866                if field in remove_constraints: 
    835867                    idx = remove_constraints[field] 
    836868                    if idx >= 0: 
     
    839871                            continue 
    840872                    else: 
    841873                        continue 
    842                 constraints[field] = vals 
     874                constraints.setdefault(synonyms.get(field, field),  
     875                                       []).extend(vals) 
    843876 
    844877        return constraints 
    845878 
     
    935968                    if col in ('cc', 'reporter'): 
    936969                        value = Chrome(self.env).format_emails(context(ticket), 
    937970                                                               value) 
     971                    elif col in query.time_fields: 
     972                        value = format_datetime(value, tzinfo=req.tz) 
    938973                    values.append(unicode(value).encode('utf-8')) 
    939974                writer.writerow(values) 
    940975        return (content.getvalue(), '%s;charset=utf-8' % mimetype) 
  • trac/ticket/templates/query.html

    diff --git a/trac/ticket/templates/query.html b/trac/ticket/templates/query.html
    a b  
    4242            <py:for each="field_name, field in fields.items()"> 
    4343              <py:for each="constraint_name, constraint in constraints.items()"> 
    4444                <tbody py:if="field_name == constraint_name" 
    45                   py:with="multiline = field.type in ('select', 'text')"> 
     45                  py:with="multiline = field.type in ('select', 'text', 'time')"> 
    4646                  <py:for each="constraint_idx, constraint_value in enumerate(constraint['values'])"> 
    4747                    <tr class="${field_name}" py:if="multiline or constraint_idx == 0"> 
    4848                      <py:choose test="constraint_idx"> 
     
    9494 
    9595                        <py:when test="'text'"> 
    9696                          <input type="text" name="${field_name}" value="$constraint_value" size="42" /> 
     97                        </py:when> 
     98                         
     99                        <py:when test="'time'" py:with="(start, end) = ';' in constraint_value  
     100                                                        and constraint_value.split(';', 1) 
     101                                                        or (constraint_value, '')"> 
     102                          <input type="text" name="${field_name}" value="$start" size="14" /> 
     103                          <label>and</label> 
     104                          <input type="text" name="${field_name}_end" value="$end" size="14" /> 
    97105                        </py:when> 
    98106 
    99107                      </td> 
  • trac/ticket/templates/ticket.html

    diff --git a/trac/ticket/templates/ticket.html b/trac/ticket/templates/ticket.html
    a b  
    125125        <div id="ticket" py:if="ticket.exists or preview_mode" 
    126126          class="${preview_mode and 'ticketdraft' or None}"> 
    127127          <div class="date"> 
    128             <p py:if="ticket.exists">Opened ${dateinfo(ticket.time_created)} ago</p> 
    129             <p py:if="ticket.time_changed != ticket.time_created">Last modified ${dateinfo(ticket.time_changed)} ago</p> 
     128            <p py:if="ticket.exists">Opened ${dateinfo(ticket.time)} ago</p> 
     129            <p py:if="ticket.changetime != ticket.time">Last modified ${dateinfo(ticket.changetime)} ago</p> 
    130130            <p py:if="not ticket.exists"><i>(ticket not yet created)</i></p> 
    131131          </div> 
    132132          <!-- use a placeholder if it's a new ticket --> 
  • trac/ticket/tests/notification.py

    diff --git a/trac/ticket/tests/notification.py b/trac/ticket/tests/notification.py
    a b  
    605605                # note project title / URL are not validated yet 
    606606 
    607607        # ticket properties which are not expected in the banner 
    608         xlist = ['summary', 'description', 'link', 'comment', 'new'] 
     608        xlist = ['summary', 'description', 'link', 'comment', 'new', 
     609                 'time', 'changetime'] 
    609610        # check banner content (field exists, msg value matches ticket value) 
    610611        for p in [prop for prop in ticket.values.keys() if prop not in xlist]: 
    611612            self.failIf(not props.has_key(p)) 
  • trac/ticket/tests/query.py

    diff --git a/trac/ticket/tests/query.py b/trac/ticket/tests/query.py
    a b  
    22from trac.mimeview import Context 
    33from trac.test import Mock, EnvironmentStub, MockPerm 
    44from trac.ticket.query import Query, QueryModule 
     5from trac.util.datefmt import utc 
    56from trac.web.href import Href 
    67from trac.wiki.formatter import LinkFormatter 
    78from trac.db.sqlite_backend import sqlite_version 
     
    3536 
    3637    def setUp(self): 
    3738        self.env = EnvironmentStub(default_data=True) 
    38         self.req = Mock(href=self.env.href, authname='anonymous') 
     39        self.req = Mock(href=self.env.href, authname='anonymous', tz=utc) 
    3940         
    4041 
    4142    def test_all_ordered_by_id(self): 
     
    336337        self.assertEqual([], args) 
    337338        tickets = query.execute(self.req) 
    338339 
     340    def test_constrained_by_time_range(self): 
     341        query = Query.from_string(self.env, 'created=2008-08-01;2008-09-01', order='id') 
     342        sql, args = query.get_sql(self.req) 
     343        self.assertEqualSQL(sql, 
     344"""SELECT t.id AS id,t.summary AS summary,t.time AS time,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.changetime AS changetime,priority.value AS priority_value 
     345FROM ticket AS t 
     346  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority) 
     347WHERE (CAST(t.time AS int)>=%s AND CAST(t.time AS int)<%s) 
     348ORDER BY COALESCE(t.id,0)=0,t.id""") 
     349        self.assertEqual([1217548800, 1220227200], args) 
     350        tickets = query.execute(self.req) 
     351 
     352    def test_constrained_by_time_range_exclusion(self): 
     353        query = Query.from_string(self.env, 'created!=2008-08-01;2008-09-01', order='id') 
     354        sql, args = query.get_sql(self.req) 
     355        self.assertEqualSQL(sql, 
     356"""SELECT t.id AS id,t.summary AS summary,t.time AS time,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.changetime AS changetime,priority.value AS priority_value 
     357FROM ticket AS t 
     358  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority) 
     359WHERE NOT (CAST(t.time AS int)>=%s AND CAST(t.time AS int)<%s) 
     360ORDER BY COALESCE(t.id,0)=0,t.id""") 
     361        self.assertEqual([1217548800, 1220227200], args) 
     362        tickets = query.execute(self.req) 
     363 
     364    def test_constrained_by_time_range_open_right(self): 
     365        query = Query.from_string(self.env, 'created=2008-08-01;', order='id') 
     366        sql, args = query.get_sql(self.req) 
     367        self.assertEqualSQL(sql, 
     368"""SELECT t.id AS id,t.summary AS summary,t.time AS time,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.changetime AS changetime,priority.value AS priority_value 
     369FROM ticket AS t 
     370  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority) 
     371WHERE CAST(t.time AS int)>=%s 
     372ORDER BY COALESCE(t.id,0)=0,t.id""") 
     373        self.assertEqual([1217548800], args) 
     374        tickets = query.execute(self.req) 
     375 
     376    def test_constrained_by_time_range_open_left(self): 
     377        query = Query.from_string(self.env, 'created=;2008-09-01', order='id') 
     378        sql, args = query.get_sql(self.req) 
     379        self.assertEqualSQL(sql, 
     380"""SELECT t.id AS id,t.summary AS summary,t.time AS time,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.changetime AS changetime,priority.value AS priority_value 
     381FROM ticket AS t 
     382  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority) 
     383WHERE CAST(t.time AS int)<%s 
     384ORDER BY COALESCE(t.id,0)=0,t.id""") 
     385        self.assertEqual([1220227200], args) 
     386        tickets = query.execute(self.req) 
     387 
     388    def test_constrained_by_time_range_modified(self): 
     389        query = Query.from_string(self.env, 'modified=2008-08-01;2008-09-01', order='id') 
     390        sql, args = query.get_sql(self.req) 
     391        self.assertEqualSQL(sql, 
     392"""SELECT t.id AS id,t.summary AS summary,t.changetime AS changetime,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.time AS time,priority.value AS priority_value 
     393FROM ticket AS t 
     394  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority) 
     395WHERE (CAST(t.changetime AS int)>=%s AND CAST(t.changetime AS int)<%s) 
     396ORDER BY COALESCE(t.id,0)=0,t.id""") 
     397        self.assertEqual([1217548800, 1220227200], args)