Feature #1189 » multi_values_for_r6402.diff
app/controllers/issues_controller.rb | ||
---|---|---|
118 | 118 |
@edit_allowed = User.current.allowed_to?(:edit_issues, @project) |
119 | 119 |
@priorities = IssuePriority.all |
120 | 120 |
@time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project) |
121 |
@custom_values = @issue.custom_field_values |
|
121 | 122 |
respond_to do |format| |
122 | 123 |
format.html { render :template => 'issues/show.rhtml' } |
123 | 124 |
format.api |
... | ... | |
285 | 286 |
@time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project) |
286 | 287 |
@time_entry.attributes = params[:time_entry] |
287 | 288 | |
289 |
@custom_values = @issue.custom_field_values |
|
288 | 290 |
@notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil) |
289 | 291 |
@issue.init_journal(User.current, @notes) |
290 | 292 |
@issue.safe_attributes = params[:issue] |
app/helpers/custom_fields_helper.rb | ||
---|---|---|
49 | 49 |
blank_option = custom_field.is_required? ? |
50 | 50 |
(custom_field.default_value.blank? ? "<option value=\"\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" : '') : |
51 | 51 |
'<option></option>' |
52 |
select_tag(field_name, blank_option + options_for_select(custom_field.possible_values_options(custom_value.customized), custom_value.value), :id => field_id) |
|
52 |
multi_image = custom_field.allow_multi ? |
|
53 |
link_to_function(image_tag('bullet_toggle_plus.png'), "toggle_multi_custom('#{custom_field.id}');", :style => "vertical-align: bottom;") : |
|
54 |
'' |
|
55 |
multiple = custom_field.allow_multi && custom_value.value.is_a?(Array) && custom_value.value.length > 1 |
|
56 |
select_name = custom_field.allow_multi ? "#{name}[custom_multi_values][#{custom_field.id}][]" : field_name |
|
57 |
select_tag(select_name, blank_option + options_for_select(custom_field.possible_values, custom_value.value), :id => field_id, :multiple => multiple) + multi_image |
|
53 | 58 |
else |
54 | 59 |
text_field_tag(field_name, custom_value.value, :id => field_id) |
55 | 60 |
end |
... | ... | |
92 | 97 |
# Return a string used to display a custom value |
93 | 98 |
def show_value(custom_value) |
94 | 99 |
return "" unless custom_value |
95 |
format_value(custom_value.value, custom_value.custom_field.field_format)
|
|
100 |
format_value((custom_value.value.is_a?(Array) ? custom_value.value.join("\n") : custom_value.value), custom_value.custom_field.field_format)
|
|
96 | 101 |
end |
97 | 102 |
|
98 | 103 |
# Return a string used to display a custom value |
app/models/custom_field.rb | ||
---|---|---|
33 | 33 |
def before_validation |
34 | 34 |
# make sure these fields are not searchable |
35 | 35 |
self.searchable = false if %w(int float date bool).include?(field_format) |
36 |
# make sure only list field_format have allow_multi option |
|
37 |
self.allow_multi = false unless field_format == 'list' |
|
36 | 38 |
true |
37 | 39 |
end |
38 | 40 |
|
app/models/custom_value.rb | ||
---|---|---|
69 | 69 |
end |
70 | 70 |
end |
71 | 71 |
end |
72 | ||
73 |
class CustomValuesCollection < Array |
|
74 |
attr_accessor :custom_field |
|
75 |
|
|
76 |
def initialize(custom_field, custom_values=[]) |
|
77 |
@custom_field = custom_field if custom_field.is_a?(CustomField) |
|
78 |
custom_values.map{ |x| self << x } |
|
79 |
self |
|
80 |
end |
|
81 | ||
82 |
def value |
|
83 |
self.uniq.map(&:value).delete_if {|x| x.blank?} |
|
84 |
end |
|
85 | ||
86 |
def value=(new_value) |
|
87 |
self.delete_if{ |x| true } |
|
88 |
new_value.map{ |x| self << x } |
|
89 |
end |
|
90 |
|
|
91 |
def save |
|
92 |
self.compact.each(&:save) |
|
93 |
end |
|
94 | ||
95 |
def valid? |
|
96 |
self.inject(true){ |bool,v| bool && v.valid? } |
|
97 |
end |
|
98 | ||
99 |
def validate |
|
100 |
self.uniq.map(&:validate) |
|
101 |
end |
|
102 | ||
103 |
def custom_field_id |
|
104 |
@custom_field.id |
|
105 |
end |
|
106 | ||
107 |
def method_missing(symbol, *args) |
|
108 |
if @custom_field.respond_to?(symbol) |
|
109 |
@custom_field.send(symbol, *args) |
|
110 |
elsif self.first && self.first.respond_to?(symbol) |
|
111 |
self.first.send(symbol, *args) |
|
112 |
else |
|
113 |
super |
|
114 |
end |
|
115 |
end |
|
116 |
end |
app/models/issue.rb | ||
---|---|---|
264 | 264 |
'due_date', |
265 | 265 |
'done_ratio', |
266 | 266 |
'estimated_hours', |
267 |
'custom_multi_values', |
|
267 | 268 |
'custom_field_values', |
268 | 269 |
'custom_fields', |
269 | 270 |
'lock_version', |
... | ... | |
393 | 394 |
@issue_before_change = self.clone |
394 | 395 |
@issue_before_change.status = self.status |
395 | 396 |
@custom_values_before_change = {} |
396 |
self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
|
|
397 |
custom_field_values.each {|c| @custom_values_before_change.store c.custom_field.id, c.value }
|
|
397 | 398 |
# Make sure updated_on is updated when adding a note. |
398 | 399 |
updated_on_will_change! |
399 | 400 |
@current_journal |
... | ... | |
886 | 887 |
:value => send(c)) |
887 | 888 |
} |
888 | 889 |
# custom fields changes |
889 |
custom_values.each {|c| |
|
890 |
custom_field_values.each {|c|
|
|
890 | 891 |
next if (@custom_values_before_change[c.custom_field_id]==c.value || |
891 | 892 |
(@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?)) |
892 | 893 |
@current_journal.details << JournalDetail.new(:property => 'cf', |
app/models/journal_detail.rb | ||
---|---|---|
22 | 22 |
private |
23 | 23 |
|
24 | 24 |
def normalize_values |
25 |
self.value = value.join(", ") if value && value.is_a?(Array) |
|
26 |
self.old_value = old_value.join(", ") if old_value && old_value.is_a?(Array) |
|
25 | 27 |
self.value = normalize(value) |
26 | 28 |
self.old_value = normalize(old_value) |
27 | 29 |
end |
app/views/custom_fields/_form.rhtml | ||
---|---|---|
5 | 5 |
function toggle_custom_field_format() { |
6 | 6 |
format = $("custom_field_field_format"); |
7 | 7 |
p_length = $("custom_field_min_length"); |
8 |
p_multi = $("custom_field_allow_multi"); |
|
8 | 9 |
p_regexp = $("custom_field_regexp"); |
9 | 10 |
p_values = $("custom_field_possible_values"); |
10 | 11 |
p_searchable = $("custom_field_searchable"); |
... | ... | |
19 | 20 |
Element.hide(p_regexp.parentNode); |
20 | 21 |
if (p_searchable) Element.show(p_searchable.parentNode); |
21 | 22 |
Element.show(p_values.parentNode); |
23 |
Element.show(p_multi.parentNode); |
|
22 | 24 |
break; |
23 | 25 |
case "bool": |
24 | 26 |
p_default.setAttribute('type','checkbox'); |
... | ... | |
26 | 28 |
Element.hide(p_regexp.parentNode); |
27 | 29 |
if (p_searchable) Element.hide(p_searchable.parentNode); |
28 | 30 |
Element.hide(p_values.parentNode); |
31 |
Element.hide(p_multi.parentNode); |
|
29 | 32 |
break; |
30 | 33 |
case "date": |
31 | 34 |
Element.hide(p_length.parentNode); |
32 | 35 |
Element.hide(p_regexp.parentNode); |
33 | 36 |
if (p_searchable) Element.hide(p_searchable.parentNode); |
34 | 37 |
Element.hide(p_values.parentNode); |
38 |
Element.hide(p_multi.parentNode); |
|
35 | 39 |
break; |
36 | 40 |
case "float": |
37 | 41 |
case "int": |
... | ... | |
39 | 43 |
Element.show(p_regexp.parentNode); |
40 | 44 |
if (p_searchable) Element.hide(p_searchable.parentNode); |
41 | 45 |
Element.hide(p_values.parentNode); |
46 |
Element.hide(p_multi.parentNode); |
|
42 | 47 |
break; |
43 | 48 |
case "user": |
44 | 49 |
case "version": |
... | ... | |
53 | 58 |
Element.show(p_regexp.parentNode); |
54 | 59 |
if (p_searchable) Element.show(p_searchable.parentNode); |
55 | 60 |
Element.hide(p_values.parentNode); |
61 |
Element.hide(p_multi.parentNode); |
|
56 | 62 |
break; |
57 | 63 |
} |
58 | 64 |
} |
... | ... | |
91 | 97 |
<p><%= f.check_box :is_for_all %></p> |
92 | 98 |
<p><%= f.check_box :is_filter %></p> |
93 | 99 |
<p><%= f.check_box :searchable %></p> |
100 |
<p><%= f.check_box :allow_multi %></p> |
|
94 | 101 |
|
95 | 102 |
<% when "UserCustomField" %> |
96 | 103 |
<p><%= f.check_box :is_required %></p> |
app/views/issues/_form_custom_fields.rhtml | ||
---|---|---|
1 |
<script type="text/javascript"> |
|
2 |
//<![CDATA[ |
|
3 |
function toggle_multi_custom(field) { |
|
4 |
select = $('issue_custom_field_values_' + field); |
|
5 |
if (select.multiple == true) { |
|
6 |
select.multiple = false; |
|
7 |
} else { |
|
8 |
select.multiple = true; |
|
9 |
} |
|
10 |
} |
|
11 |
//]]> |
|
12 |
</script> |
|
1 | 13 |
<div class="splitcontentleft"> |
2 | 14 |
<% i = 0 %> |
3 | 15 |
<% split_on = (@issue.custom_field_values.size / 2.0).ceil - 1 %> |
config/locales/en.yml | ||
---|---|---|
292 | 292 |
field_time_entries: Log time |
293 | 293 |
field_time_zone: Time zone |
294 | 294 |
field_searchable: Searchable |
295 |
field_allow_multi: Allow multiple choices |
|
295 | 296 |
field_default_value: Default value |
296 | 297 |
field_comments_sorting: Display comments |
297 | 298 |
field_parent_title: Parent page |
db/migrate/20100512172200_add_custom_fields_multi.rb | ||
---|---|---|
1 |
class AddCustomFieldsMulti < ActiveRecord::Migration |
|
2 |
def self.up |
|
3 |
add_column :custom_fields, :allow_multi, :boolean, :default => false |
|
4 |
end |
|
5 | ||
6 |
def self.down |
|
7 |
remove_column :custom_fields, :allow_multi |
|
8 |
end |
|
9 |
end |
test/unit/custom_value_test.rb | ||
---|---|---|
78 | 78 |
assert v.valid? |
79 | 79 |
end |
80 | 80 | |
81 |
def test_multi_list_field_validation |
|
82 |
f = CustomField.new(:field_format => 'list', :allow_multi => true, :possible_values => ['value1', 'value2']) |
|
83 |
v = CustomValuesCollection.new(:custom_field => f, :value => []) |
|
84 |
assert v.valid? |
|
85 |
v << CustomValue.new(:custom_field => f, :value => 'value1') |
|
86 |
assert v.valid? |
|
87 |
v << CustomValue.new(:custom_field => f, :value => 'value2') |
|
88 |
assert v.valid? |
|
89 |
v << CustomValue.new(:custom_field => f, :value => 'abc') |
|
90 |
assert !v.valid? |
|
91 |
end |
|
92 | ||
93 |
def test_multi_list_field_value |
|
94 |
f = CustomField.new(:field_format => 'list', :allow_multi => true, :possible_values => ['value1', 'value2']) |
|
95 |
v = CustomValuesCollection.new(:custom_field => f, :value => []) |
|
96 |
v << CustomValue.new(:custom_field => f, :value => 'value1') |
|
97 |
c = CustomValue.new(:custom_field => f, :value => 'value2') |
|
98 |
v << c |
|
99 |
assert_equal ['value1', 'value2'], v.value |
|
100 |
v << c |
|
101 |
assert ['value1', 'value2'], v.value |
|
102 |
end |
|
103 | ||
81 | 104 |
def test_int_field_validation |
82 | 105 |
f = CustomField.new(:field_format => 'int') |
83 | 106 |
v = CustomValue.new(:custom_field => f, :value => '') |
vendor/plugins/acts_as_customizable/lib/acts_as_customizable.rb | ||
---|---|---|
69 | 69 |
@custom_field_values_changed = true |
70 | 70 |
values = values.stringify_keys |
71 | 71 |
custom_field_values.each do |custom_value| |
72 |
custom_value.value = values[custom_value.custom_field_id.to_s] if values.has_key?(custom_value.custom_field_id.to_s) |
|
72 |
if custom_value.is_a? CustomValuesCollection |
|
73 |
if custom_value.empty? |
|
74 |
values.each do |key, value| |
|
75 |
if (custom_value.custom_field_id == key.to_i && value.is_a?(Array)) |
|
76 |
if value.empty? |
|
77 |
custom_value << custom_values.build(:custom_field => custom_value.custom_field, :value => nil) |
|
78 |
else |
|
79 |
value.each do |v| |
|
80 |
custom_value << custom_values.build(:custom_field => custom_value.custom_field, :value => v) |
|
81 |
end |
|
82 |
end |
|
83 |
end |
|
84 |
end |
|
85 |
end |
|
86 |
CustomValue # otherwise Rails doesn't know the CustomValuesCollection class |
|
87 |
custom_value = CustomValuesCollection.new custom_value.custom_field, custom_value |
|
88 |
#end |
|
89 |
else |
|
90 |
custom_value.value = values[custom_value.custom_field_id.to_s] if values.has_key?(custom_value.custom_field_id.to_s) |
|
91 |
end |
|
73 | 92 |
end if values.is_a?(Hash) |
74 | 93 |
self.custom_values = custom_field_values |
75 | 94 |
end |
76 | 95 |
|
77 | 96 |
def custom_field_values |
78 |
@custom_field_values ||= available_custom_fields.collect { |x| custom_values.detect { |v| v.custom_field == x } || custom_values.build(:customized => self, :custom_field => x, :value => nil) } |
|
97 |
@custom_field_values ||= available_custom_fields.collect do |x| |
|
98 |
if x.allow_multi |
|
99 |
CustomValue # otherwise Rails doesn't know the CustomValuesCollection class |
|
100 |
CustomValuesCollection.new x, custom_values.select{ |v| v.custom_field == x } |
|
101 |
else |
|
102 |
custom_values.detect { |v| v.custom_field == x } || custom_values.build(:custom_field => x, :value => nil) |
|
103 |
end |
|
104 |
end |
|
79 | 105 |
end |
80 | 106 |
|
81 |
def visible_custom_field_values |
|
82 |
custom_field_values.select(&:visible?) |
|
107 |
def custom_multi_values=(values) |
|
108 |
values.each do |key, value| |
|
109 |
value.delete_if {|v| v.to_s == ""} if value.length > 1 |
|
110 |
end |
|
111 |
|
|
112 |
@old_custom_values ||= custom_values.select{ |x| x.custom_field.allow_multi } |
|
113 |
@custom_field_values_changed = true |
|
114 |
values = values.stringify_keys |
|
115 |
values.each do |key, value| |
|
116 |
custom_value = custom_field_values.detect{ |c| c.custom_field.id == key.to_i && c.allow_multi } |
|
117 |
value.each do |v| |
|
118 |
old = @old_custom_values.detect{ |u| u.custom_field == custom_value.custom_field && u.value == v } |
|
119 |
if old.blank? |
|
120 |
custom_value << custom_values.build(:custom_field => custom_value.custom_field, :value => v) |
|
121 |
else |
|
122 |
custom_value << old unless custom_value.include?(old) |
|
123 |
@old_custom_values.delete old |
|
124 |
end |
|
125 |
end if values.is_a?(Hash) && custom_value != nil && values.has_key?(custom_value.custom_field.id.to_s) |
|
126 |
end |
|
127 |
#delete old normal values |
|
128 |
@custom_field_values.each { |c| c.delete_if{ |x| @old_custom_values.include?(x) } if c.is_a?(CustomValuesCollection) } |
|
129 |
@custom_values.delete_if { |c| @old_custom_values.include?(c) } |
|
83 | 130 |
end |
84 | 131 |
|
85 | 132 |
def custom_field_values_changed? |
... | ... | |
93 | 140 |
|
94 | 141 |
def save_custom_field_values |
95 | 142 |
custom_field_values.each(&:save) |
143 |
@old_custom_values.each(&:destroy) unless @old_custom_values.blank? |
|
96 | 144 |
@custom_field_values_changed = false |
97 | 145 |
@custom_field_values = nil |
98 | 146 |
end |