Feature #1189 » multi_values_for_r3739.diff
app/controllers/issues_controller.rb (working copy) | ||
---|---|---|
119 | 119 |
@edit_allowed = User.current.allowed_to?(:edit_issues, @project) |
120 | 120 |
@priorities = IssuePriority.all |
121 | 121 |
@time_entry = TimeEntry.new |
122 |
@custom_values = @issue.custom_field_values |
|
122 | 123 |
respond_to do |format| |
123 | 124 |
format.html { render :template => 'issues/show.rhtml' } |
124 | 125 |
format.xml { render :layout => false } |
... | ... | |
428 | 429 |
@edit_allowed = User.current.allowed_to?(:edit_issues, @project) |
429 | 430 |
@time_entry = TimeEntry.new |
430 | 431 |
|
432 |
@custom_values = @issue.custom_field_values |
|
431 | 433 |
@notes = params[:notes] |
432 | 434 |
@issue.init_journal(User.current, @notes) |
433 | 435 |
# User can change issue attributes only if he has :edit permission or if a workflow transition is allowed |
app/helpers/custom_fields_helper.rb (working copy) | ||
---|---|---|
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, 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 (working copy) | ||
---|---|---|
34 | 34 |
def before_validation |
35 | 35 |
# make sure these fields are not searchable |
36 | 36 |
self.searchable = false if %w(int float date bool).include?(field_format) |
37 |
# make sure only list field_format have allow_multi option |
|
38 |
self.allow_multi = false unless field_format == 'list' |
|
37 | 39 |
true |
38 | 40 |
end |
39 | 41 |
|
app/models/custom_value.rb (working copy) | ||
---|---|---|
65 | 65 |
end |
66 | 66 |
end |
67 | 67 |
end |
68 |
|
|
69 |
class CustomValuesCollection < Array |
|
70 |
attr_accessor :custom_field |
|
71 |
|
|
72 |
def initialize(custom_field, custom_values=[]) |
|
73 |
@custom_field = custom_field if custom_field.is_a?(CustomField) |
|
74 |
custom_values.map{ |x| self << x } |
|
75 |
self |
|
76 |
end |
|
77 |
|
|
78 |
def value |
|
79 |
self.uniq.map(&:value).delete_if {|x| x.blank?} |
|
80 |
end |
|
81 |
|
|
82 |
def value=(new_value) |
|
83 |
self.delete_if{ |x| true } |
|
84 |
new_value.map{ |x| self << x } |
|
85 |
end |
|
86 |
|
|
87 |
def save |
|
88 |
self.compact.each(&:save) |
|
89 |
end |
|
90 |
|
|
91 |
def valid? |
|
92 |
self.inject(true){ |bool,v| bool && v.valid? } |
|
93 |
end |
|
94 |
|
|
95 |
def validate |
|
96 |
self.uniq.map(&:validate) |
|
97 |
end |
|
98 |
|
|
99 |
def custom_field_id |
|
100 |
@custom_field.id |
|
101 |
end |
|
102 |
|
|
103 |
def method_missing(symbol, *args) |
|
104 |
if @custom_field.respond_to?(symbol) |
|
105 |
@custom_field.send(symbol, *args) |
|
106 |
elsif self.first && self.first.respond_to?(symbol) |
|
107 |
self.first.send(symbol, *args) |
|
108 |
else |
|
109 |
super |
|
110 |
end |
|
111 |
end |
|
112 |
end |
app/models/issue.rb (working copy) | ||
---|---|---|
211 | 211 |
due_date |
212 | 212 |
done_ratio |
213 | 213 |
estimated_hours |
214 |
custom_multi_values |
|
214 | 215 |
custom_field_values |
215 | 216 |
lock_version |
216 | 217 |
) unless const_defined?(:SAFE_ATTRIBUTES) |
... | ... | |
318 | 319 |
@issue_before_change = self.clone |
319 | 320 |
@issue_before_change.status = self.status |
320 | 321 |
@custom_values_before_change = {} |
321 |
self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
|
|
322 |
custom_field_values.each {|c| @custom_values_before_change.store c.custom_field.id, c.value }
|
|
322 | 323 |
# Make sure updated_on is updated when adding a note. |
323 | 324 |
updated_on_will_change! |
324 | 325 |
@current_journal |
... | ... | |
781 | 782 |
:value => send(c)) unless send(c)==@issue_before_change.send(c) |
782 | 783 |
} |
783 | 784 |
# custom fields changes |
784 |
custom_values.each {|c|
|
|
785 |
custom_field_values.each {|c|
|
|
785 | 786 |
next if (@custom_values_before_change[c.custom_field_id]==c.value || |
786 | 787 |
(@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?)) |
787 | 788 |
@current_journal.details << JournalDetail.new(:property => 'cf', |
app/models/journal_detail.rb (working copy) | ||
---|---|---|
19 | 19 |
belongs_to :journal |
20 | 20 |
|
21 | 21 |
def before_save |
22 |
self.value = value.join(", ") if value && value.is_a?(Array) |
|
23 |
self.old_value = old_value.join(", ") if old_value && old_value.is_a?(Array) |
|
22 | 24 |
self.value = value[0..254] if value && value.is_a?(String) |
23 | 25 |
self.old_value = old_value[0..254] if old_value && old_value.is_a?(String) |
24 | 26 |
end |
app/views/custom_fields/_form.rhtml (working copy) | ||
---|---|---|
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); |
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); |
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); |
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); |
46 |
Element.hide(p_multi.parentNode); |
|
42 | 47 |
break; |
43 | 48 |
default: |
44 | 49 |
Element.show(p_length.parentNode); |
45 | 50 |
Element.show(p_regexp.parentNode); |
46 | 51 |
if (p_searchable) Element.show(p_searchable.parentNode); |
47 | 52 |
Element.hide(p_values); |
53 |
Element.hide(p_multi.parentNode); |
|
48 | 54 |
break; |
49 | 55 |
} |
50 | 56 |
} |
... | ... | |
83 | 89 |
<p><%= f.check_box :is_for_all %></p> |
84 | 90 |
<p><%= f.check_box :is_filter %></p> |
85 | 91 |
<p><%= f.check_box :searchable %></p> |
92 |
<p><%= f.check_box :allow_multi %></p> |
|
86 | 93 |
|
87 | 94 |
<% when "UserCustomField" %> |
88 | 95 |
<p><%= f.check_box :is_required %></p> |
app/views/issues/_form_custom_fields.rhtml (working copy) | ||
---|---|---|
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 (working copy) | ||
---|---|---|
272 | 272 |
field_column_names: Columns |
273 | 273 |
field_time_zone: Time zone |
274 | 274 |
field_searchable: Searchable |
275 |
field_allow_multi: Allow multiple choices |
|
275 | 276 |
field_default_value: Default value |
276 | 277 |
field_comments_sorting: Display comments |
277 | 278 |
field_parent_title: Parent page |
config/locales/fr.yml (working copy) | ||
---|---|---|
291 | 291 |
field_column_names: Colonnes |
292 | 292 |
field_time_zone: Fuseau horaire |
293 | 293 |
field_searchable: Utilisé pour les recherches |
294 |
field_allow_multi: Permettre les choix multiples |
|
294 | 295 |
field_default_value: Valeur par défaut |
295 | 296 |
field_comments_sorting: Afficher les commentaires |
296 | 297 |
field_parent_title: Page parent |
config/locales/ja.yml (working copy) | ||
---|---|---|
303 | 303 |
field_column_names: 項目 |
304 | 304 |
field_time_zone: タイムゾーン |
305 | 305 |
field_searchable: 検索条件に設定可能とする |
306 |
field_allow_multi: 複数選択可能 |
|
306 | 307 |
field_default_value: デフォルト値 |
307 | 308 |
field_comments_sorting: コメントを表示 |
308 | 309 |
field_parent_title: 親ページ |
config/locales/zh-TW.yml (working copy) | ||
---|---|---|
363 | 363 |
field_column_names: 欄位 |
364 | 364 |
field_time_zone: 時區 |
365 | 365 |
field_searchable: 可用做搜尋條件 |
366 |
field_allow_multi: 允許多重選擇 |
|
366 | 367 |
field_default_value: 預設值 |
367 | 368 |
field_comments_sorting: 註解排序 |
368 | 369 |
field_parent_title: 父頁面 |
config/locales/zh.yml (working copy) | ||
---|---|---|
291 | 291 |
field_column_names: 列 |
292 | 292 |
field_time_zone: 时区 |
293 | 293 |
field_searchable: 可用作搜索条件 |
294 |
field_allow_multi: 允许多选 |
|
294 | 295 |
field_default_value: 默认值 |
295 | 296 |
field_comments_sorting: 显示注释 |
296 | 297 |
field_parent_title: 上级页面 |
db/migrate/20100512172200_add_custom_fields_multi.rb (revision 0) | ||
---|---|---|
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 (working copy) | ||
---|---|---|
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 (working copy) | ||
---|---|---|
54 | 54 |
@custom_field_values_changed = true |
55 | 55 |
values = values.stringify_keys |
56 | 56 |
custom_field_values.each do |custom_value| |
57 |
custom_value.value = values[custom_value.custom_field_id.to_s] if values.has_key?(custom_value.custom_field_id.to_s) |
|
57 |
if custom_value.is_a? CustomValuesCollection |
|
58 |
if custom_value.empty? |
|
59 |
values.each do |key, value| |
|
60 |
if (custom_value.custom_field_id == key.to_i && value.is_a?(Array)) |
|
61 |
if value.empty? |
|
62 |
custom_value << custom_values.build(:custom_field => custom_value.custom_field, :value => nil) |
|
63 |
else |
|
64 |
value.each do |v| |
|
65 |
custom_value << custom_values.build(:custom_field => custom_value.custom_field, :value => v) |
|
66 |
end |
|
67 |
end |
|
68 |
end |
|
69 |
end |
|
70 |
end |
|
71 |
CustomValue # otherwise Rails doesn't know the CustomValuesCollection class |
|
72 |
custom_value = CustomValuesCollection.new custom_value.custom_field, custom_value |
|
73 |
#end |
|
74 |
else |
|
75 |
custom_value.value = values[custom_value.custom_field_id.to_s] if values.has_key?(custom_value.custom_field_id.to_s) |
|
76 |
end |
|
58 | 77 |
end if values.is_a?(Hash) |
59 | 78 |
end |
60 | 79 |
|
61 | 80 |
def custom_field_values |
62 |
@custom_field_values ||= available_custom_fields.collect { |x| custom_values.detect { |v| v.custom_field == x } || custom_values.build(:custom_field => x, :value => nil) } |
|
81 |
@custom_field_values ||= available_custom_fields.collect do |x| |
|
82 |
if x.allow_multi |
|
83 |
CustomValue # otherwise Rails doesn't know the CustomValuesCollection class |
|
84 |
CustomValuesCollection.new x, custom_values.select{ |v| v.custom_field == x } |
|
85 |
else |
|
86 |
custom_values.detect { |v| v.custom_field == x } || custom_values.build(:custom_field => x, :value => nil) |
|
87 |
end |
|
88 |
end |
|
89 |
end |
|
90 |
|
|
91 |
def custom_multi_values=(values) |
|
92 |
values.each do |key, value| |
|
93 |
value.delete_if {|v| v.to_s == ""} if value.length > 1 |
|
94 |
end |
|
95 |
|
|
96 |
@old_custom_values ||= custom_values.select{ |x| x.custom_field.allow_multi } |
|
97 |
@custom_field_values_changed = true |
|
98 |
values = values.stringify_keys |
|
99 |
values.each do |key, value| |
|
100 |
custom_value = custom_field_values.detect{ |c| c.custom_field.id == key.to_i && c.allow_multi } |
|
101 |
value.each do |v| |
|
102 |
old = @old_custom_values.detect{ |u| u.custom_field == custom_value.custom_field && u.value == v } |
|
103 |
if old.blank? |
|
104 |
custom_value << custom_values.build(:custom_field => custom_value.custom_field, :value => v) |
|
105 |
else |
|
106 |
custom_value << old unless custom_value.include?(old) |
|
107 |
@old_custom_values.delete old |
|
108 |
end |
|
109 |
end if values.is_a?(Hash) && custom_value != nil && values.has_key?(custom_value.custom_field.id.to_s) |
|
110 |
end |
|
111 |
#delete old normal values |
|
112 |
@custom_field_values.each { |c| c.delete_if{ |x| @old_custom_values.include?(x) } if c.is_a?(CustomValuesCollection) } |
|
113 |
@custom_values.delete_if { |c| @old_custom_values.include?(c) } |
|
63 | 114 |
end |
64 | 115 |
|
65 | 116 |
def custom_field_values_changed? |
... | ... | |
73 | 124 |
|
74 | 125 |
def save_custom_field_values |
75 | 126 |
custom_field_values.each(&:save) |
127 |
@old_custom_values.each(&:destroy) unless @old_custom_values.blank? |
|
76 | 128 |
@custom_field_values_changed = false |
77 | 129 |
@custom_field_values = nil |
78 | 130 |
end |