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 |