diff --git a/trac/htdocs/css/report.css b/trac/htdocs/css/report.css
|
a
|
b
|
|
| 38 | 38 | #filters td label { font-size: 11px } |
| 39 | 39 | #filters td.mode { text-align: right } |
| 40 | 40 | #filters td.filter { width: 100% } |
| 41 | | #filters td.filter label { padding-right: 1em } |
| | 41 | #filters td.filter label.control { padding-right: 1em } |
| 42 | 42 | #filters td.actions { text-align: right; white-space: nowrap } |
| 43 | 43 | |
| 44 | 44 | #columns div label { |
diff --git a/trac/htdocs/js/query.js b/trac/htdocs/js/query.js
|
a
|
b
|
|
| 16 | 16 | // Removes an existing row from the filters table |
| 17 | 17 | function removeRow(button, propertyName) { |
| 18 | 18 | var tr = getAncestorByTagName(button, "tr"); |
| 19 | | |
| 20 | | var mode = null; |
| 21 | | var selects = tr.getElementsByTagName("select"); |
| 22 | | for (var i = 0; i < selects.length; i++) { |
| 23 | | if (selects[i].name == propertyName + "_mode") { |
| 24 | | mode = selects[i]; |
| 25 | | break; |
| 26 | | } |
| 27 | | } |
| 28 | | if (mode && (getAncestorByTagName(mode, "tr") == tr)) { |
| | 19 | var label = document.getElementById("label_" + propertyName); |
| | 20 | if (label && (getAncestorByTagName(label, "tr") == tr)) { |
| 29 | 21 | // Check whether there are more 'or' rows for this filter |
| 30 | 22 | var next = $(tr).next()[0]; |
| 31 | 23 | if (next && (next.className == propertyName)) { |
| … |
… |
|
| 42 | 34 | |
| 43 | 35 | var thisTh = getChildElementAt(tr, 0); |
| 44 | 36 | var nextTh = getChildElementAt(next, 0); |
| 45 | | next.insertBefore(thisTh, nextTh); |
| 46 | | nextTh.colSpan = 1; |
| 47 | | |
| 48 | | thisTd = getChildElementAt(tr, 0); |
| 49 | | nextTd = getChildElementAt(next, 1); |
| 50 | | next.replaceChild(thisTd, nextTd); |
| | 37 | if (nextTh.colSpan == 1) { |
| | 38 | next.replaceChild(thisTh, nextTh); |
| | 39 | } else { |
| | 40 | next.insertBefore(thisTh, nextTh); |
| | 41 | nextTh.colSpan = 1; |
| | 42 | thisTd = getChildElementAt(tr, 0); |
| | 43 | nextTd = getChildElementAt(next, 1); |
| | 44 | next.replaceChild(thisTd, nextTd); |
| | 45 | } |
| 51 | 46 | } |
| 52 | 47 | } |
| 53 | 48 | |
| … |
… |
|
| 117 | 112 | function createLabel(text, htmlFor) { |
| 118 | 113 | var label = document.createElement("label"); |
| 119 | 114 | if (text) label.appendChild(document.createTextNode(text)); |
| 120 | | if (htmlFor) label.htmlFor = htmlFor; |
| | 115 | if (htmlFor) { |
| | 116 | label.htmlFor = htmlFor; |
| | 117 | label.className = "control"; |
| | 118 | } |
| 121 | 119 | return label; |
| 122 | 120 | } |
| 123 | 121 | |
| | 122 | // Convenience function for creating an <input type="text"> |
| | 123 | function createText(name, size) { |
| | 124 | var input = document.createElement("input"); |
| | 125 | input.type = "text"; |
| | 126 | if (name) input.name = name; |
| | 127 | if (size) input.size = size; |
| | 128 | return input; |
| | 129 | } |
| | 130 | |
| 124 | 131 | // Convenience function for creating an <input type="checkbox"> |
| 125 | 132 | function createCheckbox(name, value, id) { |
| 126 | 133 | var input = document.createElement("input"); |
| … |
… |
|
| 182 | 189 | var th = document.createElement("th"); |
| 183 | 190 | th.scope = "row"; |
| 184 | 191 | if (!alreadyPresent) { |
| 185 | | th.appendChild(createLabel(property.label)); |
| | 192 | var label = createLabel(property.label); |
| | 193 | label.id = "label_" + propertyName; |
| | 194 | th.appendChild(label); |
| 186 | 195 | } else { |
| 187 | | th.colSpan = 2; |
| | 196 | th.colSpan = property.type == "time"? 1: 2; |
| 188 | 197 | th.appendChild(createLabel("or")); |
| 189 | 198 | } |
| 190 | 199 | tr.appendChild(th); |
| 191 | 200 | |
| 192 | 201 | var td = document.createElement("td"); |
| 193 | | if (property.type == "radio" || property.type == "checkbox") { |
| | 202 | var focusElement = null; |
| | 203 | if (property.type == "radio" || property.type == "checkbox" || property.type == "time") { |
| 194 | 204 | td.colSpan = 2; |
| 195 | 205 | td.className = "filter"; |
| 196 | 206 | if (property.type == "radio") { |
| … |
… |
|
| 201 | 211 | td.appendChild(createLabel(option ? option : "none", |
| 202 | 212 | propertyName + "_" + option)); |
| 203 | 213 | } |
| 204 | | } else { |
| | 214 | } else if (property.type == "checkbox") { |
| 205 | 215 | td.appendChild(createRadio(propertyName, "1", propertyName + "_on")); |
| 206 | 216 | td.appendChild(document.createTextNode(" ")); |
| 207 | 217 | td.appendChild(createLabel("yes", propertyName + "_on")); |
| 208 | 218 | td.appendChild(createRadio(propertyName, "0", propertyName + "_off")); |
| 209 | 219 | td.appendChild(document.createTextNode(" ")); |
| 210 | 220 | td.appendChild(createLabel("no", propertyName + "_off")); |
| | 221 | } else if (property.type == "time") { |
| | 222 | td.appendChild(createLabel("between")); |
| | 223 | td.appendChild(document.createTextNode(" ")); |
| | 224 | focusElement = createText(propertyName, 14); |
| | 225 | td.appendChild(focusElement); |
| | 226 | td.appendChild(document.createTextNode(" ")); |
| | 227 | td.appendChild(createLabel("and")); |
| | 228 | td.appendChild(document.createTextNode(" ")); |
| | 229 | td.appendChild(createText(propertyName + "_end", 14)); |
| 211 | 230 | } |
| 212 | 231 | tr.appendChild(td); |
| 213 | 232 | } else { |
| … |
… |
|
| 224 | 243 | td = document.createElement("td"); |
| 225 | 244 | td.className = "filter"; |
| 226 | 245 | if (property.type == "select") { |
| 227 | | var element = createSelect(propertyName, property.options, true); |
| | 246 | focusElement = createSelect(propertyName, property.options, true); |
| 228 | 247 | } else if ((property.type == "text") || (property.type == "textarea")) { |
| 229 | | var element = document.createElement("input"); |
| 230 | | element.type = "text"; |
| 231 | | element.name = propertyName; |
| 232 | | element.size = 42; |
| | 248 | focusElement = createText(propertyName, 42); |
| 233 | 249 | } |
| 234 | | td.appendChild(element); |
| 235 | | element.focus(); |
| | 250 | td.appendChild(focusElement); |
| 236 | 251 | tr.appendChild(td); |
| 237 | 252 | } |
| 238 | 253 | |
| … |
… |
|
| 266 | 281 | tbody.appendChild(tr); |
| 267 | 282 | insertionPoint.parentNode.insertBefore(tbody, insertionPoint); |
| 268 | 283 | } |
| | 284 | if(focusElement) |
| | 285 | focusElement.focus(); |
| 269 | 286 | |
| 270 | 287 | // Disable the add filter in the drop-down list |
| 271 | 288 | if (property.type == "radio" || property.type == "checkbox") { |
diff --git a/trac/ticket/api.py b/trac/ticket/api.py
|
a
|
b
|
|
| 240 | 240 | field = {'name': name, 'type': 'text', 'label': name.title()} |
| 241 | 241 | fields.append(field) |
| 242 | 242 | |
| | 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 | |
| 243 | 249 | for field in self.get_custom_fields(): |
| 244 | 250 | if field['name'] in [f['name'] for f in fields]: |
| 245 | 251 | self.log.warning('Duplicate field name "%s" (ignoring)', |
| … |
… |
|
| 281 | 287 | |
| 282 | 288 | fields.sort(lambda x, y: cmp(x['order'], y['order'])) |
| 283 | 289 | return fields |
| | 290 | |
| | 291 | def get_field_synonyms(self): |
| | 292 | """Return a mapping from field name synonyms to field names. |
| | 293 | The synonyms are supposed to be more intuitive for custom queries.""" |
| | 294 | return {'created': 'time', 'modified': 'changetime'} |
| 284 | 295 | |
| 285 | 296 | # IPermissionRequestor methods |
| 286 | 297 | |
diff --git a/trac/ticket/model.py b/trac/ticket/model.py
|
a
|
b
|
|
| 40 | 40 | def id_is_valid(num): |
| 41 | 41 | return 0 < int(num) <= 1L << 31 |
| 42 | 42 | |
| | 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 | |
| 43 | 47 | def __init__(self, env, tkt_id=None, db=None, version=None): |
| 44 | 48 | self.env = env |
| 45 | 49 | self.resource = Resource('ticket', tkt_id, version) |
| 46 | 50 | self.fields = TicketSystem(self.env).get_ticket_fields() |
| | 51 | self.time_fields = [f['name'] for f in self.fields |
| | 52 | if f['type'] == 'time'] |
| 47 | 53 | self.values = {} |
| 48 | 54 | if tkt_id is not None: |
| 49 | 55 | self._fetch_ticket(tkt_id, db) |
| 50 | 56 | else: |
| 51 | 57 | self._init_defaults(db) |
| 52 | | self.id = self.time_created = self.time_changed = None |
| | 58 | self.id = None |
| 53 | 59 | self._old = {} |
| 54 | 60 | |
| 55 | 61 | def _get_db(self, db): |
| … |
… |
|
| 66 | 72 | def _init_defaults(self, db=None): |
| 67 | 73 | for field in self.fields: |
| 68 | 74 | default = None |
| 69 | | if field['name'] in ['resolution', 'status']: |
| | 75 | if field['name'] in ['resolution', 'status', 'time', 'changetime']: |
| 70 | 76 | # Ignore for new - only change through workflow |
| 71 | 77 | pass |
| 72 | 78 | elif not field.get('custom'): |
| … |
… |
|
| 93 | 99 | # Fetch the standard ticket fields |
| 94 | 100 | std_fields = [f['name'] for f in self.fields if not f.get('custom')] |
| 95 | 101 | 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" |
| 97 | 103 | % ','.join(std_fields), (tkt_id,)) |
| 98 | 104 | row = cursor.fetchone() |
| 99 | 105 | if not row: |
| … |
… |
|
| 104 | 110 | for i in range(len(std_fields)): |
| 105 | 111 | value = row[i] |
| 106 | 112 | if value is not None: |
| 107 | | self.values[std_fields[i]] = row[i] |
| 108 | | self.time_created = datetime.fromtimestamp(row[len(std_fields)], utc) |
| 109 | | self.time_changed = datetime.fromtimestamp(row[len(std_fields) + 1], utc) |
| | 113 | field = std_fields[i] |
| | 114 | if field in self.time_fields: |
| | 115 | self.values[field] = datetime.fromtimestamp(value, utc) |
| | 116 | else: |
| | 117 | self.values[field] = value |
| 110 | 118 | |
| 111 | 119 | # Fetch custom fields if available |
| 112 | 120 | custom_fields = [f['name'] for f in self.fields if f.get('custom')] |
| … |
… |
|
| 166 | 174 | # Add a timestamp |
| 167 | 175 | if when is None: |
| 168 | 176 | when = datetime.now(utc) |
| 169 | | self.time_created = self.time_changed = when |
| | 177 | self.values['time'] = self.values['changetime'] = when |
| 170 | 178 | |
| 171 | 179 | cursor = db.cursor() |
| 172 | 180 | |
| … |
… |
|
| 180 | 188 | # No such component exists |
| 181 | 189 | pass |
| 182 | 190 | |
| | 191 | # Perform type conversions |
| | 192 | values = dict(self.values) |
| | 193 | for field in self.time_fields: |
| | 194 | if field in values: |
| | 195 | values[field] = to_timestamp(values[field]) |
| | 196 | |
| 183 | 197 | # Insert ticket record |
| 184 | | created = to_timestamp(self.time_created) |
| 185 | | changed = to_timestamp(self.time_changed) |
| 186 | 198 | std_fields = [] |
| 187 | 199 | custom_fields = [] |
| 188 | 200 | for f in self.fields: |
| … |
… |
|
| 192 | 204 | custom_fields.append(fname) |
| 193 | 205 | else: |
| 194 | 206 | std_fields.append(fname) |
| 195 | | cursor.execute("INSERT INTO ticket (%s,time,changetime) VALUES (%s)" |
| | 207 | cursor.execute("INSERT INTO ticket (%s) VALUES (%s)" |
| 196 | 208 | % (','.join(std_fields), |
| 197 | | ','.join(['%s'] * (len(std_fields) + 2))), |
| 198 | | [self[name] for name in std_fields] + [created, changed]) |
| | 209 | ','.join(['%s'] * len(std_fields))), |
| | 210 | [values[name] for name in std_fields]) |
| 199 | 211 | tkt_id = db.get_last_id(cursor, 'ticket') |
| 200 | 212 | |
| 201 | 213 | # Insert custom fields |
| … |
… |
|
| 291 | 303 | db.commit() |
| 292 | 304 | old_values = self._old |
| 293 | 305 | self._old = {} |
| 294 | | self.time_changed = when |
| | 306 | self.values['changetime'] = when |
| 295 | 307 | |
| 296 | 308 | for listener in TicketSystem(self.env).change_listeners: |
| 297 | 309 | listener.ticket_changed(self, comment, author, old_values) |
diff --git a/trac/ticket/notification.py b/trac/ticket/notification.py
|
a
|
b
|
|
| 149 | 149 | |
| 150 | 150 | def format_props(self): |
| 151 | 151 | 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')] |
| 153 | 154 | width = [0, 0, 0, 0] |
| 154 | 155 | i = 0 |
| 155 | 156 | for f in [f['name'] for f in fields if f['type'] != 'textarea']: |
diff --git a/trac/ticket/query.py b/trac/ticket/query.py
|
a
|
b
|
|
| 31 | 31 | from trac.resource import Resource |
| 32 | 32 | from trac.ticket.api import TicketSystem |
| 33 | 33 | from trac.util import Ranges |
| 34 | | from trac.util.datefmt import to_timestamp, utc |
| | 34 | from trac.util.datefmt import format_datetime, parse_date, to_timestamp, utc |
| 35 | 35 | from trac.util.presentation import Paginator |
| 36 | 36 | from trac.util.text import shorten_line |
| 37 | 37 | from trac.util.translation import _, tag_ |
| … |
… |
|
| 56 | 56 | self.env = env |
| 57 | 57 | self.id = report # if not None, it's the corresponding saved query |
| 58 | 58 | 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 |
| 60 | 61 | self.desc = desc |
| 61 | 62 | self.group = group |
| 62 | 63 | self.groupdesc = groupdesc |
| … |
… |
|
| 100 | 101 | if verbose and 'description' not in rows: # 0.10 compatibility |
| 101 | 102 | rows.append('description') |
| 102 | 103 | self.fields = TicketSystem(self.env).get_ticket_fields() |
| | 104 | self.time_fields = [f['name'] for f in self.fields |
| | 105 | if f['type'] == 'time'] |
| 103 | 106 | field_names = [f['name'] for f in self.fields] |
| 104 | 107 | self.cols = [c for c in cols or [] if c in field_names or |
| 105 | | c in ('id', 'time', 'changetime')] |
| | 108 | c == 'id'] |
| 106 | 109 | self.rows = [c for c in rows if c in field_names] |
| 107 | 110 | 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' |
| 117 | 112 | |
| 118 | 113 | if self.group not in field_names: |
| 119 | 114 | self.group = None |
| … |
… |
|
| 124 | 119 | kw_strs = ['order', 'group', 'page', 'max'] |
| 125 | 120 | kw_arys = ['rows'] |
| 126 | 121 | kw_bools = ['desc', 'groupdesc', 'verbose'] |
| | 122 | synonyms = TicketSystem(env).get_field_synonyms() |
| 127 | 123 | constraints = {} |
| 128 | 124 | cols = [] |
| 129 | 125 | for filter_ in filters: |
| … |
… |
|
| 131 | 127 | if len(filter_) != 2: |
| 132 | 128 | raise QuerySyntaxError(_('Query filter requires field and ' |
| 133 | 129 | 'constraints separated by a "="')) |
| 134 | | field,values = filter_ |
| | 130 | field, values = filter_ |
| 135 | 131 | if not field: |
| 136 | 132 | raise QuerySyntaxError(_('Query filter requires field name')) |
| 137 | 133 | # from last char of `field`, get the mode of comparison |
| … |
… |
|
| 157 | 153 | elif field in kw_bools: |
| 158 | 154 | kw[field] = True |
| 159 | 155 | elif field == 'col': |
| 160 | | cols.extend(processed_values) |
| | 156 | cols.extend(synonyms.get(value, value) |
| | 157 | for value in processed_values) |
| 161 | 158 | else: |
| 162 | | constraints[field] = processed_values |
| | 159 | constraints.setdefault(synonyms.get(field, field), |
| | 160 | []).extend(processed_values) |
| 163 | 161 | except UnicodeError: |
| 164 | 162 | pass # field must be a str, see `get_href()` |
| 165 | 163 | report = constraints.pop('report', None) |
| … |
… |
|
| 185 | 183 | if col in cols: |
| 186 | 184 | cols.remove(col) |
| 187 | 185 | cols.append(col) |
| 188 | | # TODO: fix after adding time/changetime to the api.py |
| 189 | | cols += ['time', 'changetime'] |
| 190 | 186 | |
| 191 | 187 | # Semi-intelligently remove columns that are restricted to a single |
| 192 | 188 | # value by a query constraint. |
| … |
… |
|
| 195 | 191 | constraint = self.constraints[col] |
| 196 | 192 | if len(constraint) == 1 and constraint[0] \ |
| 197 | 193 | and not constraint[0][0] in ('!', '~', '^', '$'): |
| 198 | | if col in cols: |
| | 194 | if col in cols and col not in self.time_fields: |
| 199 | 195 | cols.remove(col) |
| 200 | 196 | if col == 'status' and not 'closed' in constraint \ |
| 201 | 197 | and 'resolution' in cols: |
| … |
… |
|
| 299 | 295 | result['href'] = req.href.ticket(val) |
| 300 | 296 | elif val is None: |
| 301 | 297 | val = '--' |
| 302 | | elif name in ('changetime', 'time'): |
| | 298 | elif name in self.time_fields: |
| 303 | 299 | val = datetime.fromtimestamp(int(val or 0), utc) |
| 304 | 300 | elif field and field['type'] == 'checkbox': |
| 305 | 301 | try: |
| … |
… |
|
| 425 | 421 | |
| 426 | 422 | def get_constraint_sql(name, value, mode, neg): |
| 427 | 423 | if name not in custom_fields: |
| 428 | | name = 't.' + name |
| | 424 | col = 't.' + name |
| 429 | 425 | else: |
| 430 | | name = name + '.value' |
| | 426 | col = name + '.value' |
| 431 | 427 | value = value[len(mode) + neg:] |
| 432 | 428 | |
| | 429 | if name in self.time_fields: |
| | 430 | if ';' in value: |
| | 431 | (start, end) = [each.strip() for each in |
| | 432 | value.split(';', 1)] |
| | 433 | else: |
| | 434 | (start, end) = (value.strip(), '') |
| | 435 | col_cast = db.cast(col, 'int') |
| | 436 | if start and end: |
| | 437 | start = to_timestamp(parse_date(start, req.tz)) |
| | 438 | end = to_timestamp(parse_date(end, req.tz)) |
| | 439 | return ("%s(%s>=%%s AND %s<%%s)" % (neg and 'NOT ' or '', |
| | 440 | col_cast, col_cast), |
| | 441 | (start, end)) |
| | 442 | elif start: |
| | 443 | start = to_timestamp(parse_date(start, req.tz)) |
| | 444 | return ("%s%s>=%%s" % (neg and 'NOT ' or '', col_cast), |
| | 445 | (start, )) |
| | 446 | elif end: |
| | 447 | end = to_timestamp(parse_date(end, req.tz)) |
| | 448 | return ("%s%s<%%s" % (neg and 'NOT ' or '', col_cast), |
| | 449 | (end, )) |
| | 450 | else: |
| | 451 | return None |
| | 452 | |
| 433 | 453 | if mode == '': |
| 434 | | return ("COALESCE(%s,'')%s=%%s" % (name, neg and '!' or ''), |
| 435 | | value) |
| | 454 | return ("COALESCE(%s,'')%s=%%s" % (col, neg and '!' or ''), |
| | 455 | (value, )) |
| | 456 | |
| 436 | 457 | if not value: |
| 437 | 458 | return None |
| 438 | | db = self.env.get_db_cnx() |
| 439 | 459 | value = db.like_escape(value) |
| 440 | 460 | if mode == '~': |
| 441 | 461 | value = '%' + value + '%' |
| … |
… |
|
| 443 | 463 | value = value + '%' |
| 444 | 464 | elif mode == '$': |
| 445 | 465 | value = '%' + value |
| 446 | | return ("COALESCE(%s,'') %s%s" % (name, neg and 'NOT ' or '', |
| | 466 | return ("COALESCE(%s,'') %s%s" % (col, neg and 'NOT ' or '', |
| 447 | 467 | db.like()), |
| 448 | | value) |
| | 468 | (value, )) |
| 449 | 469 | |
| | 470 | db = self.env.get_db_cnx() |
| 450 | 471 | clauses = [] |
| 451 | 472 | args = [] |
| 452 | 473 | for k, v in self.constraints.items(): |
| … |
… |
|
| 480 | 501 | clauses.append('%s(%s)' % (neg and 'NOT ' or '', |
| 481 | 502 | ' OR '.join(id_clauses))) |
| 482 | 503 | # Special case for exact matches on multiple values |
| 483 | | elif not mode and len(v) > 1: |
| | 504 | elif not mode and len(v) > 1 and k not in self.time_fields: |
| 484 | 505 | if k not in custom_fields: |
| 485 | 506 | col = 't.' + k |
| 486 | 507 | else: |
| … |
… |
|
| 501 | 522 | else: |
| 502 | 523 | clauses.append("(" + " OR ".join( |
| 503 | 524 | [item[0] for item in constraint_sql]) + ")") |
| 504 | | args += [item[1] for item in constraint_sql] |
| | 525 | for item in constraint_sql: |
| | 526 | args.extend(item[1]) |
| 505 | 527 | elif len(v) == 1: |
| 506 | 528 | constraint_sql = get_constraint_sql(k, v[0], mode, neg) |
| 507 | 529 | if constraint_sql: |
| 508 | 530 | clauses.append(constraint_sql[0]) |
| 509 | | args.append(constraint_sql[1]) |
| | 531 | args.extend(constraint_sql[1]) |
| 510 | 532 | |
| 511 | 533 | clauses = filter(None, clauses) |
| 512 | 534 | if clauses: |
| … |
… |
|
| 530 | 552 | # FIXME: This is a somewhat ugly hack. Can we also have the |
| 531 | 553 | # column type for this? If it's an integer, we do first |
| 532 | 554 | # one, if text, we do 'else' |
| 533 | | if name in ('id', 'time', 'changetime'): |
| | 555 | if name == 'id' or name in self.time_fields: |
| 534 | 556 | sql.append("COALESCE(%s,0)=0%s," % (col, desc)) |
| 535 | 557 | else: |
| 536 | 558 | sql.append("COALESCE(%s,'')=''%s," % (col, desc)) |
| … |
… |
|
| 576 | 598 | labels = dict([(f['name'], f['label']) for f in self.fields]) |
| 577 | 599 | wikify = set(f['name'] for f in self.fields |
| 578 | 600 | if f['type'] == 'text' and f.get('format') == 'wiki') |
| 579 | | |
| 580 | | # TODO: remove after adding time/changetime to the api.py |
| 581 | | labels['changetime'] = _('Modified') |
| 582 | | labels['time'] = _('Created') |
| 583 | 601 | |
| 584 | 602 | headers = [{ |
| 585 | 603 | 'name': col, 'label': labels.get(col, _('Ticket')), |
| … |
… |
|
| 829 | 847 | |
| 830 | 848 | def _get_constraints(self, req): |
| 831 | 849 | constraints = {} |
| 832 | | ticket_fields = [f['name'] for f in |
| 833 | | TicketSystem(self.env).get_ticket_fields()] |
| | 850 | fields = TicketSystem(self.env).get_ticket_fields() |
| | 851 | synonyms = TicketSystem(self.env).get_field_synonyms() |
| | 852 | ticket_fields = [f['name'] for f in fields] |
| 834 | 853 | ticket_fields.append('id') |
| | 854 | ticket_fields.extend(synonyms.iterkeys()) |
| | 855 | time_fields = [f['name'] for f in fields if f['type'] == 'time'] |
| | 856 | time_fields.extend([k for (k, v) in synonyms.iteritems() |
| | 857 | if v in time_fields]) |
| 835 | 858 | |
| 836 | 859 | # For clients without JavaScript, we remove constraints here if |
| 837 | 860 | # requested |
| … |
… |
|
| 853 | 876 | mode = req.args.get(field + '_mode') |
| 854 | 877 | if mode: |
| 855 | 878 | vals = [mode + x for x in vals] |
| | 879 | if field in time_fields: |
| | 880 | ends = req.args.getlist(field + '_end') |
| | 881 | if ends: |
| | 882 | vals = [start + ';' + end |
| | 883 | for (start, end) in zip(vals, ends)] |
| 856 | 884 | if field in remove_constraints: |
| 857 | 885 | idx = remove_constraints[field] |
| 858 | 886 | if idx >= 0: |
| … |
… |
|
| 861 | 889 | continue |
| 862 | 890 | else: |
| 863 | 891 | continue |
| 864 | | constraints[field] = vals |
| | 892 | constraints.setdefault(synonyms.get(field, field), |
| | 893 | []).extend(vals) |
| 865 | 894 | |
| 866 | 895 | return constraints |
| 867 | 896 | |
| … |
… |
|
| 957 | 986 | if col in ('cc', 'reporter'): |
| 958 | 987 | value = Chrome(self.env).format_emails(context(ticket), |
| 959 | 988 | value) |
| | 989 | elif col in query.time_fields: |
| | 990 | value = format_datetime(value, tzinfo=req.tz) |
| 960 | 991 | values.append(unicode(value).encode('utf-8')) |
| 961 | 992 | writer.writerow(values) |
| 962 | 993 | return (content.getvalue(), '%s;charset=utf-8' % mimetype) |
diff --git a/trac/ticket/templates/query.html b/trac/ticket/templates/query.html
|
a
|
b
|
|
| 43 | 43 | <py:for each="field_name in field_names" py:with="field = fields[field_name]"> |
| 44 | 44 | <py:for each="constraint_name, constraint in constraints.items()"> |
| 45 | 45 | <tbody py:if="field_name == constraint_name" |
| 46 | | py:with="multiline = field.type in ('select', 'text', 'textarea')"> |
| | 46 | py:with="multiline = field.type in ('select', 'text', 'textarea', 'time')"> |
| 47 | 47 | <py:for each="constraint_idx, constraint_value in enumerate(constraint['values'])"> |
| 48 | 48 | <tr class="${field_name}" py:if="multiline or constraint_idx == 0"> |
| 49 | 49 | <py:choose test="constraint_idx"> |
| 50 | 50 | <py:when test="0"> |
| 51 | | <th scope="row"><label>$field.label</label></th> |
| 52 | | <td py:if="field.type not in ('radio', 'checkbox')" class="mode"> |
| | 51 | <th scope="row"><label id="label_${field_name}">$field.label</label></th> |
| | 52 | <td py:if="field.type not in ('radio', 'checkbox', 'time')" class="mode"> |
| 53 | 53 | <select name="${field_name}_mode"> |
| 54 | 54 | <option py:for="mode in modes[field.type]" value="$mode.value" |
| 55 | 55 | selected="${mode.value == constraint.mode and 'selected' or None}">$mode.name |
| … |
… |
|
| 58 | 58 | </td> |
| 59 | 59 | </py:when> |
| 60 | 60 | <py:otherwise><!--! not the first line of a multiline constraint --> |
| 61 | | <th colspan="2"><label>or</label></th> |
| | 61 | <th colspan="${field.type == 'time' and 1 or 2}"><label>or</label></th> |
| 62 | 62 | </py:otherwise> |
| 63 | 63 | </py:choose> |
| 64 | 64 | |
| 65 | | <td class="filter" colspan="${field.type in ('radio', 'checkbox') and 2 or None}" |