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 |