Index: trac/ticket/api.py
===================================================================
--- trac/ticket/api.py (revision 150)
+++ trac/ticket/api.py (working copy)
@@ -380,7 +380,7 @@
'label': config.get(name + '.label') or name.capitalize(),
'value': config.get(name + '.value', '')
}
- if field['type'] == 'select' or field['type'] == 'radio':
+ if field['type'] in ('select', 'radio', 'multi'):
field['options'] = config.getlist(name + '.options', sep='|')
if '' in field['options']:
field['optional'] = True
Index: trac/ticket/web_ui.py
===================================================================
--- trac/ticket/web_ui.py (revision 150)
+++ trac/ticket/web_ui.py (working copy)
@@ -1139,7 +1139,14 @@
if name in ticket.values and name in ticket._old:
value = ticket[name]
if value:
- if value not in field['options']:
+ if field['type'] == 'multi':
+ values = value.split('|')
+ if len(values) > 1:
+ values.remove('') # get rid of trailing ''
+ else:
+ values = (value,)
+ for val in values:
+ if val not in field['options']:
add_warning(req, '"%s" is not a valid value for '
'the %s field.' % (value, name))
valid = False
@@ -1388,19 +1395,32 @@
field['cc_update'] = 'cc_update' in req.args or None
# per type settings
- if type_ in ('radio', 'select'):
+ if type_ in ('radio', 'select', 'multi'):
if ticket.exists:
value = ticket.values.get(name)
options = field['options']
optgroups = []
for x in field.get('optgroups', []):
optgroups.extend(x['options'])
- if value and \
- (not value in options and \
- not value in optgroups):
+ if value:
+ if type_ == 'multi':
+ values = value.split('|')
+ if len(values) > 1:
+ values.remove('')
+ else:
+ values = (value,)
+ for val in values:
+ if not val in options and not val in optgroups:
# Current ticket value must be visible,
# even if it's not among the possible values
- options.append(value)
+ options.append(val)
+ # Rendered output should be pretty for multi-values
+ if len(values) > 1:
+ from genshi.builder import Element
+ choices = Element('ul', class_='multi-value')
+ for val in values:
+ choices.append(Element('li')(val))
+ field['rendered'] = choices
elif type_ == 'checkbox':
value = ticket.values.get(name)
if value in ('1', '0'):
@@ -1615,6 +1635,13 @@
elif field == 'keywords':
old_list, new_list = old.split(), new.split()
sep = ' '
+ elif type_ == 'multi':
+ old_list = (old or '').split('|')
+ if old_list.count(''):
+ old_list.remove('')
+ new_list = new.split('|')
+ if new_list.count(''):
+ new_list.remove('')
if (old_list, new_list) != (None, None):
added = [tag.em(render_elt(x)) for x in new_list
if x not in old_list]
Index: trac/ticket/model.py
===================================================================
--- trac/ticket/model.py (revision 150)
+++ trac/ticket/model.py (working copy)
@@ -132,6 +132,8 @@
def __setitem__(self, name, value):
"""Log ticket modifications so the table ticket_change can be updated
"""
+ if isinstance(value, list): # account for multi-selects
+ value = '|'.join(value) + '|'
if name in self.values and self.values[name] == value:
return
if name not in self._old: # Changed field
@@ -140,7 +142,7 @@
del self._old[name]
if value:
if isinstance(value, list):
- raise TracError(_("Multi-values fields not supported yet"))
+ value = '|'.join(value) + '|'
field = [field for field in self.fields if field['name'] == name]
if field and field[0].get('type') != 'textarea':
value = value.strip()
@@ -171,6 +173,11 @@
if name[9:] not in values:
self[name[9:]] = '0'
+ # We do something similar for empty multi-selects
+ for f in self.fields:
+ if f['type'] == 'multi' and not f['name'] in values:
+ self[f['name']] = ''
+
def insert(self, when=None, db=None):
"""Add ticket to database.
Index: trac/ticket/query.py
===================================================================
--- trac/ticket/query.py (revision 150)
+++ trac/ticket/query.py (working copy)
@@ -162,7 +162,7 @@
field, values = filter_
# from last chars of `field`, get the mode of comparison
mode = ''
- if field and field[-1] in ('~', '^', '$') \
+ if field and field[-1] in ('~', '^', '$', '|') \
and not field in cls.substitutions:
mode = field[-1]
field = field[:-1]
@@ -339,6 +339,14 @@
val = bool(int(val))
except (TypeError, ValueError):
val = False
+ elif field and field['type'] == 'multi':
+ val = (val or '').split('|')
+ if val.count(''):
+ val.remove('')
+ if len(val):
+ val = ', '.join(val)
+ else:
+ val = 'None'
result[name] = val
results.append(result)
cursor.close()
@@ -535,6 +543,8 @@
value = value + '%'
elif mode == '$':
value = '%' + value
+ elif mode == '|':
+ value = '%' + value + '|%'
return ("COALESCE(%s,'') %s%s" % (col, neg and 'NOT ' or '',
db.like()),
(value, ))
@@ -549,7 +559,7 @@
# starts-with, negation, etc.)
neg = v[0].startswith('!')
mode = ''
- if len(v[0]) > neg and v[0][neg] in ('~', '^', '$'):
+ if len(v[0]) > neg and v[0][neg] in ('~', '^', '$', '|'):
mode = v[0][neg]
# Special case id ranges
@@ -675,6 +685,10 @@
{'name': _("is"), 'value': ""},
{'name': _("is not"), 'value': "!"},
]
+ modes['multi'] = [
+ {'name': _("contains"), 'value': "|"},
+ {'name': _("does not contain"), 'value': "!"},
+ ]
modes['id'] = [
{'name': _("is"), 'value': ""},
{'name': _("is not"), 'value': "!"},
@@ -693,7 +707,7 @@
if neg:
val = val[1:]
mode = ''
- if val[:1] in ('~', '^', '$') \
+ if val[:1] in ('~', '^', '$', '|') \
and not val in self.substitutions:
mode, val = val[:1], val[1:]
constraint['mode'] = (neg and '!' or '') + mode
Index: trac/ticket/templates/ticket.html
===================================================================
--- trac/ticket/templates/ticket.html (revision 150)
+++ trac/ticket/templates/ticket.html (working copy)
@@ -291,6 +291,19 @@
value="$option" py:content="option">
+