##// END OF EJS Templates
Adds support for multiselect custom fields (#1189)....
Jean-Philippe Lang -
r8601:cd6db6a3cbe4
parent child
Show More
@@ -0,0 +1,9
1 class AddCustomFieldsMultiple < ActiveRecord::Migration
2 def self.up
3 add_column :custom_fields, :multiple, :boolean, :default => false
4 end
5
6 def self.down
7 remove_column :custom_fields, :multiple
8 end
9 end
@@ -36,6 +36,7 module CustomFieldsHelper
36 def custom_field_tag(name, custom_value)
36 def custom_field_tag(name, custom_value)
37 custom_field = custom_value.custom_field
37 custom_field = custom_value.custom_field
38 field_name = "#{name}[custom_field_values][#{custom_field.id}]"
38 field_name = "#{name}[custom_field_values][#{custom_field.id}]"
39 field_name << "[]" if custom_field.multiple?
39 field_id = "#{name}_custom_field_values_#{custom_field.id}"
40 field_id = "#{name}_custom_field_values_#{custom_field.id}"
40
41
41 field_format = Redmine::CustomFieldFormat.find_by_name(custom_field.field_format)
42 field_format = Redmine::CustomFieldFormat.find_by_name(custom_field.field_format)
@@ -48,10 +49,22 module CustomFieldsHelper
48 when "bool"
49 when "bool"
49 hidden_field_tag(field_name, '0') + check_box_tag(field_name, '1', custom_value.true?, :id => field_id)
50 hidden_field_tag(field_name, '0') + check_box_tag(field_name, '1', custom_value.true?, :id => field_id)
50 when "list"
51 when "list"
51 blank_option = custom_field.is_required? ?
52 blank_option = ''
52 (custom_field.default_value.blank? ? "<option value=\"\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" : '') :
53 unless custom_field.multiple?
53 '<option></option>'
54 if custom_field.is_required?
54 select_tag(field_name, blank_option.html_safe + options_for_select(custom_field.possible_values_options(custom_value.customized), custom_value.value), :id => field_id)
55 unless custom_field.default_value.present?
56 blank_option = "<option value=\"\">--- #{l(:actionview_instancetag_blank_option)} ---</option>"
57 end
58 else
59 blank_option = '<option></option>'
60 end
61 end
62 s = select_tag(field_name, blank_option.html_safe + options_for_select(custom_field.possible_values_options(custom_value.customized), custom_value.value),
63 :id => field_id, :multiple => custom_field.multiple?)
64 if custom_field.multiple?
65 s << hidden_field_tag(field_name, '')
66 end
67 s
55 else
68 else
56 text_field_tag(field_name, custom_value.value, :id => field_id)
69 text_field_tag(field_name, custom_value.value, :id => field_id)
57 end
70 end
@@ -71,6 +84,7 module CustomFieldsHelper
71
84
72 def custom_field_tag_for_bulk_edit(name, custom_field, projects=nil)
85 def custom_field_tag_for_bulk_edit(name, custom_field, projects=nil)
73 field_name = "#{name}[custom_field_values][#{custom_field.id}]"
86 field_name = "#{name}[custom_field_values][#{custom_field.id}]"
87 field_name << "[]" if custom_field.multiple?
74 field_id = "#{name}_custom_field_values_#{custom_field.id}"
88 field_id = "#{name}_custom_field_values_#{custom_field.id}"
75 field_format = Redmine::CustomFieldFormat.find_by_name(custom_field.field_format)
89 field_format = Redmine::CustomFieldFormat.find_by_name(custom_field.field_format)
76 case field_format.try(:edit_as)
90 case field_format.try(:edit_as)
@@ -84,7 +98,11 module CustomFieldsHelper
84 [l(:general_text_yes), '1'],
98 [l(:general_text_yes), '1'],
85 [l(:general_text_no), '0']]), :id => field_id)
99 [l(:general_text_no), '0']]), :id => field_id)
86 when "list"
100 when "list"
87 select_tag(field_name, options_for_select([[l(:label_no_change_option), '']] + custom_field.possible_values_options(projects)), :id => field_id)
101 options = []
102 options << [l(:label_no_change_option), ''] unless custom_field.multiple?
103 options += custom_field.possible_values_options(projects)
104 select_tag(field_name, options_for_select(options),
105 :id => field_id, :multiple => custom_field.multiple?)
88 else
106 else
89 text_field_tag(field_name, '', :id => field_id)
107 text_field_tag(field_name, '', :id => field_id)
90 end
108 end
@@ -98,7 +116,11 module CustomFieldsHelper
98
116
99 # Return a string used to display a custom value
117 # Return a string used to display a custom value
100 def format_value(value, field_format)
118 def format_value(value, field_format)
101 Redmine::CustomFieldFormat.format_value(value, field_format) # Proxy
119 if value.is_a?(Array)
120 value.collect {|v| format_value(v, field_format)}.join(', ')
121 else
122 Redmine::CustomFieldFormat.format_value(value, field_format)
123 end
102 end
124 end
103
125
104 # Return an array of custom field formats which can be used in select_tag
126 # Return an array of custom field formats which can be used in select_tag
@@ -110,8 +132,18 module CustomFieldsHelper
110 def render_api_custom_values(custom_values, api)
132 def render_api_custom_values(custom_values, api)
111 api.array :custom_fields do
133 api.array :custom_fields do
112 custom_values.each do |custom_value|
134 custom_values.each do |custom_value|
113 api.custom_field :id => custom_value.custom_field_id, :name => custom_value.custom_field.name do
135 attrs = {:id => custom_value.custom_field_id, :name => custom_value.custom_field.name}
114 api.value custom_value.value
136 attrs.merge!(:multiple => true) if custom_value.custom_field.multiple?
137 api.custom_field attrs do
138 if custom_value.value.is_a?(Array)
139 api.array :value do
140 custom_value.value.each do |value|
141 api.value value unless value.blank?
142 end
143 end
144 else
145 api.value custom_value.value
146 end
115 end
147 end
116 end
148 end
117 end unless custom_values.empty?
149 end unless custom_values.empty?
@@ -161,7 +161,44 module IssuesHelper
161 out
161 out
162 end
162 end
163
163
164 # Returns the textual representation of a journal details
165 # as an array of strings
166 def details_to_strings(details, no_html=false)
167 strings = []
168 values_by_field = {}
169 details.each do |detail|
170 if detail.property == 'cf'
171 field_id = detail.prop_key
172 field = CustomField.find_by_id(field_id)
173 if field && field.multiple?
174 values_by_field[field_id] ||= {:added => [], :deleted => []}
175 if detail.old_value
176 values_by_field[field_id][:deleted] << detail.old_value
177 end
178 if detail.value
179 values_by_field[field_id][:added] << detail.value
180 end
181 next
182 end
183 end
184 strings << show_detail(detail, no_html)
185 end
186 values_by_field.each do |field_id, changes|
187 detail = JournalDetail.new(:property => 'cf', :prop_key => field_id)
188 if changes[:added].any?
189 detail.value = changes[:added]
190 strings << show_detail(detail, no_html)
191 elsif changes[:deleted].any?
192 detail.old_value = changes[:deleted]
193 strings << show_detail(detail, no_html)
194 end
195 end
196 strings
197 end
198
199 # Returns the textual representation of a single journal detail
164 def show_detail(detail, no_html=false)
200 def show_detail(detail, no_html=false)
201 multiple = false
165 case detail.property
202 case detail.property
166 when 'attr'
203 when 'attr'
167 field = detail.prop_key.to_s.gsub(/\_id$/, "")
204 field = detail.prop_key.to_s.gsub(/\_id$/, "")
@@ -192,6 +229,7 module IssuesHelper
192 when 'cf'
229 when 'cf'
193 custom_field = CustomField.find_by_id(detail.prop_key)
230 custom_field = CustomField.find_by_id(detail.prop_key)
194 if custom_field
231 if custom_field
232 multiple = custom_field.multiple?
195 label = custom_field.name
233 label = custom_field.name
196 value = format_value(detail.value, custom_field.field_format) if detail.value
234 value = format_value(detail.value, custom_field.field_format) if detail.value
197 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
235 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
@@ -232,6 +270,8 module IssuesHelper
232 when 'attr', 'cf'
270 when 'attr', 'cf'
233 if !detail.old_value.blank?
271 if !detail.old_value.blank?
234 l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe
272 l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe
273 elsif multiple
274 l(:text_journal_added, :label => label, :value => value).html_safe
235 else
275 else
236 l(:text_journal_set_to, :label => label, :value => value).html_safe
276 l(:text_journal_set_to, :label => label, :value => value).html_safe
237 end
277 end
@@ -31,7 +31,14 module QueriesHelper
31
31
32 def column_content(column, issue)
32 def column_content(column, issue)
33 value = column.value(issue)
33 value = column.value(issue)
34
34 if value.is_a?(Array)
35 value.collect {|v| column_value(column, issue, v)}.compact.sort.join(', ')
36 else
37 column_value(column, issue, value)
38 end
39 end
40
41 def column_value(column, issue, value)
35 case value.class.name
42 case value.class.name
36 when 'String'
43 when 'String'
37 if column.name == :subject
44 if column.name == :subject
@@ -38,6 +38,8 class CustomField < ActiveRecord::Base
38 def set_searchable
38 def set_searchable
39 # make sure these fields are not searchable
39 # make sure these fields are not searchable
40 self.searchable = false if %w(int float date bool).include?(field_format)
40 self.searchable = false if %w(int float date bool).include?(field_format)
41 # make sure only these fields can have multiple values
42 self.multiple = false unless %w(list user version).include?(field_format)
41 true
43 true
42 end
44 end
43
45
@@ -123,6 +125,7 class CustomField < ActiveRecord::Base
123 # objects by their value of the custom field.
125 # objects by their value of the custom field.
124 # Returns false, if the custom field can not be used for sorting.
126 # Returns false, if the custom field can not be used for sorting.
125 def order_statement
127 def order_statement
128 return nil if multiple?
126 case field_format
129 case field_format
127 when 'string', 'text', 'list', 'date', 'bool'
130 when 'string', 'text', 'list', 'date', 'bool'
128 # COALESCE is here to make sure that blank and NULL values are sorted equally
131 # COALESCE is here to make sure that blank and NULL values are sorted equally
@@ -161,14 +164,24 class CustomField < ActiveRecord::Base
161 nil
164 nil
162 end
165 end
163
166
164 # Returns the error message for the given value
167 # Returns the error messages for the given value
165 # or an empty array if value is a valid value for the custom field
168 # or an empty array if value is a valid value for the custom field
166 def validate_field_value(value)
169 def validate_field_value(value)
167 errs = []
170 errs = []
168 if is_required? && value.blank?
171 if value.is_a?(Array)
169 errs << ::I18n.t('activerecord.errors.messages.blank')
172 if !multiple?
173 errs << ::I18n.t('activerecord.errors.messages.invalid')
174 end
175 if is_required? && value.detect(&:present?).nil?
176 errs << ::I18n.t('activerecord.errors.messages.blank')
177 end
178 value.each {|v| errs += validate_field_value_format(v)}
179 else
180 if is_required? && value.blank?
181 errs << ::I18n.t('activerecord.errors.messages.blank')
182 end
183 errs += validate_field_value_format(value)
170 end
184 end
171 errs += validate_field_value_format(value)
172 errs
185 errs
173 end
186 end
174
187
@@ -429,7 +429,7 class Issue < ActiveRecord::Base
429 else
429 else
430 @attributes_before_change = attributes.dup
430 @attributes_before_change = attributes.dup
431 @custom_values_before_change = {}
431 @custom_values_before_change = {}
432 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
432 self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
433 end
433 end
434 # Make sure updated_on is updated when adding a note.
434 # Make sure updated_on is updated when adding a note.
435 updated_on_will_change!
435 updated_on_will_change!
@@ -1006,14 +1006,35 class Issue < ActiveRecord::Base
1006 end
1006 end
1007 if @custom_values_before_change
1007 if @custom_values_before_change
1008 # custom fields changes
1008 # custom fields changes
1009 custom_values.each {|c|
1009 custom_field_values.each {|c|
1010 before = @custom_values_before_change[c.custom_field_id]
1010 before = @custom_values_before_change[c.custom_field_id]
1011 after = c.value
1011 after = c.value
1012 next if before == after || (before.blank? && after.blank?)
1012 next if before == after || (before.blank? && after.blank?)
1013 @current_journal.details << JournalDetail.new(:property => 'cf',
1013
1014 :prop_key => c.custom_field_id,
1014 if before.is_a?(Array) || after.is_a?(Array)
1015 :old_value => before,
1015 before = [before] unless before.is_a?(Array)
1016 :value => after)
1016 after = [after] unless after.is_a?(Array)
1017
1018 # values removed
1019 (before - after).reject(&:blank?).each do |value|
1020 @current_journal.details << JournalDetail.new(:property => 'cf',
1021 :prop_key => c.custom_field_id,
1022 :old_value => value,
1023 :value => nil)
1024 end
1025 # values added
1026 (after - before).reject(&:blank?).each do |value|
1027 @current_journal.details << JournalDetail.new(:property => 'cf',
1028 :prop_key => c.custom_field_id,
1029 :old_value => nil,
1030 :value => value)
1031 end
1032 else
1033 @current_journal.details << JournalDetail.new(:property => 'cf',
1034 :prop_key => c.custom_field_id,
1035 :old_value => before,
1036 :value => after)
1037 end
1017 }
1038 }
1018 end
1039 end
1019 @current_journal.save
1040 @current_journal.save
@@ -57,7 +57,7 class QueryCustomFieldColumn < QueryColumn
57 def initialize(custom_field)
57 def initialize(custom_field)
58 self.name = "cf_#{custom_field.id}".to_sym
58 self.name = "cf_#{custom_field.id}".to_sym
59 self.sortable = custom_field.order_statement || false
59 self.sortable = custom_field.order_statement || false
60 if %w(list date bool int).include?(custom_field.field_format)
60 if %w(list date bool int).include?(custom_field.field_format) && !custom_field.multiple?
61 self.groupable = custom_field.order_statement
61 self.groupable = custom_field.order_statement
62 end
62 end
63 self.groupable ||= false
63 self.groupable ||= false
@@ -73,8 +73,8 class QueryCustomFieldColumn < QueryColumn
73 end
73 end
74
74
75 def value(issue)
75 def value(issue)
76 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
76 cv = issue.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
77 cv && @cf.cast_value(cv.value)
77 cv.size > 1 ? cv : cv.first
78 end
78 end
79
79
80 def css_classes
80 def css_classes
@@ -694,7 +694,13 class Query < ActiveRecord::Base
694 value.push User.current.id.to_s
694 value.push User.current.id.to_s
695 end
695 end
696 end
696 end
697 "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id} WHERE " +
697 not_in = nil
698 if operator == '!'
699 # Makes ! operator work for custom fields with multiple values
700 operator = '='
701 not_in = 'NOT'
702 end
703 "#{Issue.table_name}.id #{not_in} IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id} WHERE " +
698 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
704 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
699 end
705 end
700
706
@@ -9,6 +9,7 function toggle_custom_field_format() {
9 p_values = $("custom_field_possible_values");
9 p_values = $("custom_field_possible_values");
10 p_searchable = $("custom_field_searchable");
10 p_searchable = $("custom_field_searchable");
11 p_default = $("custom_field_default_value");
11 p_default = $("custom_field_default_value");
12 p_multiple = $("custom_field_multiple");
12
13
13 p_default.setAttribute('type','text');
14 p_default.setAttribute('type','text');
14 Element.show(p_default.parentNode);
15 Element.show(p_default.parentNode);
@@ -19,6 +20,7 function toggle_custom_field_format() {
19 Element.hide(p_regexp.parentNode);
20 Element.hide(p_regexp.parentNode);
20 if (p_searchable) Element.show(p_searchable.parentNode);
21 if (p_searchable) Element.show(p_searchable.parentNode);
21 Element.show(p_values.parentNode);
22 Element.show(p_values.parentNode);
23 Element.show(p_multiple.parentNode);
22 break;
24 break;
23 case "bool":
25 case "bool":
24 p_default.setAttribute('type','checkbox');
26 p_default.setAttribute('type','checkbox');
@@ -26,12 +28,14 function toggle_custom_field_format() {
26 Element.hide(p_regexp.parentNode);
28 Element.hide(p_regexp.parentNode);
27 if (p_searchable) Element.hide(p_searchable.parentNode);
29 if (p_searchable) Element.hide(p_searchable.parentNode);
28 Element.hide(p_values.parentNode);
30 Element.hide(p_values.parentNode);
31 Element.hide(p_multiple.parentNode);
29 break;
32 break;
30 case "date":
33 case "date":
31 Element.hide(p_length.parentNode);
34 Element.hide(p_length.parentNode);
32 Element.hide(p_regexp.parentNode);
35 Element.hide(p_regexp.parentNode);
33 if (p_searchable) Element.hide(p_searchable.parentNode);
36 if (p_searchable) Element.hide(p_searchable.parentNode);
34 Element.hide(p_values.parentNode);
37 Element.hide(p_values.parentNode);
38 Element.hide(p_multiple.parentNode);
35 break;
39 break;
36 case "float":
40 case "float":
37 case "int":
41 case "int":
@@ -39,6 +43,7 function toggle_custom_field_format() {
39 Element.show(p_regexp.parentNode);
43 Element.show(p_regexp.parentNode);
40 if (p_searchable) Element.hide(p_searchable.parentNode);
44 if (p_searchable) Element.hide(p_searchable.parentNode);
41 Element.hide(p_values.parentNode);
45 Element.hide(p_values.parentNode);
46 Element.hide(p_multiple.parentNode);
42 break;
47 break;
43 case "user":
48 case "user":
44 case "version":
49 case "version":
@@ -47,12 +52,14 function toggle_custom_field_format() {
47 if (p_searchable) Element.hide(p_searchable.parentNode);
52 if (p_searchable) Element.hide(p_searchable.parentNode);
48 Element.hide(p_values.parentNode);
53 Element.hide(p_values.parentNode);
49 Element.hide(p_default.parentNode);
54 Element.hide(p_default.parentNode);
55 Element.show(p_multiple.parentNode);
50 break;
56 break;
51 default:
57 default:
52 Element.show(p_length.parentNode);
58 Element.show(p_length.parentNode);
53 Element.show(p_regexp.parentNode);
59 Element.show(p_regexp.parentNode);
54 if (p_searchable) Element.show(p_searchable.parentNode);
60 if (p_searchable) Element.show(p_searchable.parentNode);
55 Element.hide(p_values.parentNode);
61 Element.hide(p_values.parentNode);
62 Element.hide(p_multiple.parentNode);
56 break;
63 break;
57 }
64 }
58 }
65 }
@@ -64,6 +71,7 function toggle_custom_field_format() {
64 <p><%= f.text_field :name, :required => true %></p>
71 <p><%= f.text_field :name, :required => true %></p>
65 <p><%= f.select :field_format, custom_field_formats_for_select(@custom_field), {}, :onchange => "toggle_custom_field_format();",
72 <p><%= f.select :field_format, custom_field_formats_for_select(@custom_field), {}, :onchange => "toggle_custom_field_format();",
66 :disabled => !@custom_field.new_record? %></p>
73 :disabled => !@custom_field.new_record? %></p>
74 <p><%= f.check_box :multiple, :disabled => !@custom_field.new_record? %></p>
67 <p><label for="custom_field_min_length"><%=l(:label_min_max_length)%></label>
75 <p><label for="custom_field_min_length"><%=l(:label_min_max_length)%></label>
68 <%= f.text_field :min_length, :size => 5, :no_label => true %> -
76 <%= f.text_field :min_length, :size => 5, :no_label => true %> -
69 <%= f.text_field :max_length, :size => 5, :no_label => true %><br />(<%=l(:text_min_max_length_info)%>)</p>
77 <%= f.text_field :max_length, :size => 5, :no_label => true %><br />(<%=l(:text_min_max_length_info)%>)</p>
@@ -8,8 +8,8
8
8
9 <% if journal.details.any? %>
9 <% if journal.details.any? %>
10 <ul class="details">
10 <ul class="details">
11 <% for detail in journal.details %>
11 <% details_to_strings(journal.details).each do |string| %>
12 <li><%= show_detail(detail) %></li>
12 <li><%= string %></li>
13 <% end %>
13 <% end %>
14 </ul>
14 </ul>
15 <% end %>
15 <% end %>
@@ -19,8 +19,8 xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do
19 end
19 end
20 xml.content "type" => "html" do
20 xml.content "type" => "html" do
21 xml.text! '<ul>'
21 xml.text! '<ul>'
22 change.details.each do |detail|
22 details_to_strings(change.details, false).each do |string|
23 xml.text! '<li>' + show_detail(detail, false) + '</li>'
23 xml.text! '<li>' + string + '</li>'
24 end
24 end
25 xml.text! '</ul>'
25 xml.text! '</ul>'
26 xml.text! textilizable(change, :notes, :only_path => false) unless change.notes.blank?
26 xml.text! textilizable(change, :notes, :only_path => false) unless change.notes.blank?
@@ -1,8 +1,8
1 <%= l(:text_issue_updated, :id => "##{@issue.id}", :author => h(@journal.user)) %>
1 <%= l(:text_issue_updated, :id => "##{@issue.id}", :author => h(@journal.user)) %>
2
2
3 <ul>
3 <ul>
4 <% for detail in @journal.details %>
4 <% details_to_strings(@journal.details).each do |string| %>
5 <li><%= show_detail(detail, true) %></li>
5 <li><%= string %></li>
6 <% end %>
6 <% end %>
7 </ul>
7 </ul>
8
8
@@ -1,7 +1,7
1 <%= l(:text_issue_updated, :id => "##{@issue.id}", :author => @journal.user) %>
1 <%= l(:text_issue_updated, :id => "##{@issue.id}", :author => @journal.user) %>
2
2
3 <% for detail in @journal.details -%>
3 <% details_to_strings(@journal.details, true).each do |string| -%>
4 <%= show_detail(detail, true) %>
4 <%= string %>
5 <% end -%>
5 <% end -%>
6
6
7 <%= @journal.notes if @journal.notes? %>
7 <%= @journal.notes if @journal.notes? %>
@@ -319,6 +319,7 en:
319 field_cvsroot: CVSROOT
319 field_cvsroot: CVSROOT
320 field_cvs_module: Module
320 field_cvs_module: Module
321 field_repository_is_default: Main repository
321 field_repository_is_default: Main repository
322 field_multiple: Multiple values
322
323
323 setting_app_title: Application title
324 setting_app_title: Application title
324 setting_app_subtitle: Application subtitle
325 setting_app_subtitle: Application subtitle
@@ -318,6 +318,7 fr:
318 field_is_private: PrivΓ©e
318 field_is_private: PrivΓ©e
319 field_commit_logs_encoding: Encodage des messages de commit
319 field_commit_logs_encoding: Encodage des messages de commit
320 field_repository_is_default: DΓ©pΓ΄t principal
320 field_repository_is_default: DΓ©pΓ΄t principal
321 field_multiple: Valeurs multiples
321
322
322 setting_app_title: Titre de l'application
323 setting_app_title: Titre de l'application
323 setting_app_subtitle: Sous-titre de l'application
324 setting_app_subtitle: Sous-titre de l'application
@@ -464,8 +464,8 module Redmine
464 " - " + journal.user.name)
464 " - " + journal.user.name)
465 pdf.Ln
465 pdf.Ln
466 pdf.SetFontStyle('I',8)
466 pdf.SetFontStyle('I',8)
467 for detail in journal.details
467 details_to_strings(journal.details, true).each do |string|
468 pdf.RDMMultiCell(190,5, "- " + show_detail(detail, true))
468 pdf.RDMMultiCell(190,5, "- " + string)
469 end
469 end
470 if journal.notes?
470 if journal.notes?
471 pdf.Ln unless journal.details.empty?
471 pdf.Ln unless journal.details.empty?
@@ -625,6 +625,36 class IssuesControllerTest < ActionController::TestCase
625 :ancestor => {:tag => 'table', :attributes => {:class => /issues/}}
625 :ancestor => {:tag => 'table', :attributes => {:class => /issues/}}
626 end
626 end
627
627
628 def test_index_with_multi_custom_field_column
629 field = CustomField.find(1)
630 field.update_attribute :multiple, true
631 issue = Issue.find(1)
632 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
633 issue.save!
634
635 get :index, :set_filter => 1, :c => %w(tracker subject cf_1)
636 assert_response :success
637
638 assert_tag :td,
639 :attributes => {:class => /cf_1/},
640 :content => 'MySQL, Oracle'
641 end
642
643 def test_index_with_multi_user_custom_field_column
644 field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
645 :tracker_ids => [1], :is_for_all => true)
646 issue = Issue.find(1)
647 issue.custom_field_values = {field.id => ['2', '3']}
648 issue.save!
649
650 get :index, :set_filter => 1, :c => ['tracker', 'subject', "cf_#{field.id}"]
651 assert_response :success
652
653 assert_tag :td,
654 :attributes => {:class => /cf_#{field.id}/},
655 :child => {:tag => 'a', :content => 'John Smith'}
656 end
657
628 def test_index_with_date_column
658 def test_index_with_date_column
629 Issue.find(1).update_attribute :start_date, '1987-08-24'
659 Issue.find(1).update_attribute :start_date, '1987-08-24'
630
660
@@ -1032,6 +1062,33 class IssuesControllerTest < ActionController::TestCase
1032 assert_no_tag 'a', :content => /Next/
1062 assert_no_tag 'a', :content => /Next/
1033 end
1063 end
1034
1064
1065 def test_show_with_multi_custom_field
1066 field = CustomField.find(1)
1067 field.update_attribute :multiple, true
1068 issue = Issue.find(1)
1069 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
1070 issue.save!
1071
1072 get :show, :id => 1
1073 assert_response :success
1074
1075 assert_tag :td, :content => 'MySQL, Oracle'
1076 end
1077
1078 def test_show_with_multi_user_custom_field
1079 field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
1080 :tracker_ids => [1], :is_for_all => true)
1081 issue = Issue.find(1)
1082 issue.custom_field_values = {field.id => ['2', '3']}
1083 issue.save!
1084
1085 get :show, :id => 1
1086 assert_response :success
1087
1088 # TODO: should display links
1089 assert_tag :td, :content => 'John Smith, Dave Lopper'
1090 end
1091
1035 def test_show_atom
1092 def test_show_atom
1036 get :show, :id => 2, :format => 'atom'
1093 get :show, :id => 2, :format => 'atom'
1037 assert_response :success
1094 assert_response :success
@@ -1104,6 +1161,40 class IssuesControllerTest < ActionController::TestCase
1104 assert_no_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'}
1161 assert_no_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'}
1105 end
1162 end
1106
1163
1164 def test_get_new_with_multi_custom_field
1165 field = IssueCustomField.find(1)
1166 field.update_attribute :multiple, true
1167
1168 @request.session[:user_id] = 2
1169 get :new, :project_id => 1, :tracker_id => 1
1170 assert_response :success
1171 assert_template 'new'
1172
1173 assert_tag 'select',
1174 :attributes => {:name => 'issue[custom_field_values][1][]', :multiple => 'multiple'},
1175 :children => {:count => 3},
1176 :child => {:tag => 'option', :attributes => {:value => 'MySQL'}, :content => 'MySQL'}
1177 assert_tag 'input',
1178 :attributes => {:name => 'issue[custom_field_values][1][]', :value => ''}
1179 end
1180
1181 def test_get_new_with_multi_user_custom_field
1182 field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
1183 :tracker_ids => [1], :is_for_all => true)
1184
1185 @request.session[:user_id] = 2
1186 get :new, :project_id => 1, :tracker_id => 1
1187 assert_response :success
1188 assert_template 'new'
1189
1190 assert_tag 'select',
1191 :attributes => {:name => "issue[custom_field_values][#{field.id}][]", :multiple => 'multiple'},
1192 :children => {:count => Project.find(1).users.count},
1193 :child => {:tag => 'option', :attributes => {:value => '2'}, :content => 'John Smith'}
1194 assert_tag 'input',
1195 :attributes => {:name => "issue[custom_field_values][#{field.id}][]", :value => ''}
1196 end
1197
1107 def test_get_new_without_default_start_date_is_creation_date
1198 def test_get_new_without_default_start_date_is_creation_date
1108 Setting.default_issue_start_date_to_creation_date = 0
1199 Setting.default_issue_start_date_to_creation_date = 0
1109
1200
@@ -1303,6 +1394,60 class IssuesControllerTest < ActionController::TestCase
1303 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1394 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1304 end
1395 end
1305
1396
1397 def test_post_create_with_multi_custom_field
1398 field = IssueCustomField.find_by_name('Database')
1399 field.update_attribute(:multiple, true)
1400
1401 @request.session[:user_id] = 2
1402 assert_difference 'Issue.count' do
1403 post :create, :project_id => 1,
1404 :issue => {:tracker_id => 1,
1405 :subject => 'This is the test_new issue',
1406 :description => 'This is the description',
1407 :priority_id => 5,
1408 :custom_field_values => {'1' => ['', 'MySQL', 'Oracle']}}
1409 end
1410 assert_response 302
1411 issue = Issue.first(:order => 'id DESC')
1412 assert_equal ['MySQL', 'Oracle'], issue.custom_field_value(1).sort
1413 end
1414
1415 def test_post_create_with_empty_multi_custom_field
1416 field = IssueCustomField.find_by_name('Database')
1417 field.update_attribute(:multiple, true)
1418
1419 @request.session[:user_id] = 2
1420 assert_difference 'Issue.count' do
1421 post :create, :project_id => 1,
1422 :issue => {:tracker_id => 1,
1423 :subject => 'This is the test_new issue',
1424 :description => 'This is the description',
1425 :priority_id => 5,
1426 :custom_field_values => {'1' => ['']}}
1427 end
1428 assert_response 302
1429 issue = Issue.first(:order => 'id DESC')
1430 assert_equal [''], issue.custom_field_value(1).sort
1431 end
1432
1433 def test_post_create_with_multi_user_custom_field
1434 field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
1435 :tracker_ids => [1], :is_for_all => true)
1436
1437 @request.session[:user_id] = 2
1438 assert_difference 'Issue.count' do
1439 post :create, :project_id => 1,
1440 :issue => {:tracker_id => 1,
1441 :subject => 'This is the test_new issue',
1442 :description => 'This is the description',
1443 :priority_id => 5,
1444 :custom_field_values => {field.id.to_s => ['', '2', '3']}}
1445 end
1446 assert_response 302
1447 issue = Issue.first(:order => 'id DESC')
1448 assert_equal ['2', '3'], issue.custom_field_value(field).sort
1449 end
1450
1306 def test_post_create_with_required_custom_field_and_without_custom_fields_param
1451 def test_post_create_with_required_custom_field_and_without_custom_fields_param
1307 field = IssueCustomField.find_by_name('Database')
1452 field = IssueCustomField.find_by_name('Database')
1308 field.update_attribute(:is_required, true)
1453 field.update_attribute(:is_required, true)
@@ -1822,6 +1967,27 class IssuesControllerTest < ActionController::TestCase
1822 assert_tag :input, :attributes => { :name => 'time_entry[comments]', :value => 'test_get_edit_with_params' }
1967 assert_tag :input, :attributes => { :name => 'time_entry[comments]', :value => 'test_get_edit_with_params' }
1823 end
1968 end
1824
1969
1970 def test_get_edit_with_multi_custom_field
1971 field = CustomField.find(1)
1972 field.update_attribute :multiple, true
1973 issue = Issue.find(1)
1974 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
1975 issue.save!
1976
1977 @request.session[:user_id] = 2
1978 get :edit, :id => 1
1979 assert_response :success
1980 assert_template 'edit'
1981
1982 assert_tag 'select', :attributes => {:name => 'issue[custom_field_values][1][]', :multiple => 'multiple'}
1983 assert_tag 'select', :attributes => {:name => 'issue[custom_field_values][1][]'},
1984 :child => {:tag => 'option', :attributes => {:value => 'MySQL', :selected => 'selected'}}
1985 assert_tag 'select', :attributes => {:name => 'issue[custom_field_values][1][]'},
1986 :child => {:tag => 'option', :attributes => {:value => 'PostgreSQL', :selected => nil}}
1987 assert_tag 'select', :attributes => {:name => 'issue[custom_field_values][1][]'},
1988 :child => {:tag => 'option', :attributes => {:value => 'Oracle', :selected => 'selected'}}
1989 end
1990
1825 def test_update_edit_form
1991 def test_update_edit_form
1826 @request.session[:user_id] = 2
1992 @request.session[:user_id] = 2
1827 xhr :put, :new, :project_id => 1,
1993 xhr :put, :new, :project_id => 1,
@@ -1979,6 +2145,27 class IssuesControllerTest < ActionController::TestCase
1979 assert mail.body.include?("Searchable field changed from 125 to New custom value")
2145 assert mail.body.include?("Searchable field changed from 125 to New custom value")
1980 end
2146 end
1981
2147
2148 def test_put_update_with_multi_custom_field_change
2149 field = CustomField.find(1)
2150 field.update_attribute :multiple, true
2151 issue = Issue.find(1)
2152 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
2153 issue.save!
2154
2155 @request.session[:user_id] = 2
2156 assert_difference('Journal.count') do
2157 assert_difference('JournalDetail.count', 3) do
2158 put :update, :id => 1,
2159 :issue => {
2160 :subject => 'Custom field change',
2161 :custom_field_values => { '1' => ['', 'Oracle', 'PostgreSQL'] }
2162 }
2163 end
2164 end
2165 assert_redirected_to :action => 'show', :id => '1'
2166 assert_equal ['Oracle', 'PostgreSQL'], Issue.find(1).custom_field_value(1).sort
2167 end
2168
1982 def test_put_update_with_status_and_assignee_change
2169 def test_put_update_with_status_and_assignee_change
1983 issue = Issue.find(1)
2170 issue = Issue.find(1)
1984 assert_equal 1, issue.status_id
2171 assert_equal 1, issue.status_id
@@ -2283,6 +2470,23 class IssuesControllerTest < ActionController::TestCase
2283 }
2470 }
2284 end
2471 end
2285
2472
2473 def test_get_bulk_edit_with_multi_custom_field
2474 field = CustomField.find(1)
2475 field.update_attribute :multiple, true
2476
2477 @request.session[:user_id] = 2
2478 get :bulk_edit, :ids => [1, 2]
2479 assert_response :success
2480 assert_template 'bulk_edit'
2481
2482 assert_tag :select,
2483 :attributes => {:name => "issue[custom_field_values][1][]"},
2484 :children => {
2485 :only => {:tag => 'option'},
2486 :count => 3
2487 }
2488 end
2489
2286 def test_bulk_update
2490 def test_bulk_update
2287 @request.session[:user_id] = 2
2491 @request.session[:user_id] = 2
2288 # update issues priority
2492 # update issues priority
@@ -2463,6 +2667,24 class IssuesControllerTest < ActionController::TestCase
2463 assert_equal '777', journal.details.first.value
2667 assert_equal '777', journal.details.first.value
2464 end
2668 end
2465
2669
2670 def test_bulk_update_multi_custom_field
2671 field = CustomField.find(1)
2672 field.update_attribute :multiple, true
2673
2674 @request.session[:user_id] = 2
2675 post :bulk_update, :ids => [1, 2, 3], :notes => 'Bulk editing multi custom field',
2676 :issue => {:priority_id => '',
2677 :assigned_to_id => '',
2678 :custom_field_values => {'1' => ['MySQL', 'Oracle']}}
2679
2680 assert_response 302
2681
2682 assert_equal ['MySQL', 'Oracle'], Issue.find(1).custom_field_value(1).sort
2683 assert_equal ['MySQL', 'Oracle'], Issue.find(3).custom_field_value(1).sort
2684 # the custom field is not associated with the issue tracker
2685 assert_nil Issue.find(2).custom_field_value(1)
2686 end
2687
2466 def test_bulk_update_unassign
2688 def test_bulk_update_unassign
2467 assert_not_nil Issue.find(2).assigned_to
2689 assert_not_nil Issue.find(2).assigned_to
2468 @request.session[:user_id] = 2
2690 @request.session[:user_id] = 2
@@ -258,6 +258,108 class ApiTest::IssuesTest < ActionController::IntegrationTest
258 end
258 end
259 end
259 end
260
260
261 context "with multi custom fields" do
262 setup do
263 field = CustomField.find(1)
264 field.update_attribute :multiple, true
265 issue = Issue.find(3)
266 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
267 issue.save!
268 end
269
270 context ".xml" do
271 should "display custom fields" do
272 get '/issues/3.xml'
273 assert_response :success
274 assert_tag :tag => 'issue',
275 :child => {
276 :tag => 'custom_fields',
277 :attributes => { :type => 'array' },
278 :child => {
279 :tag => 'custom_field',
280 :attributes => { :id => '1'},
281 :child => {
282 :tag => 'value',
283 :attributes => { :type => 'array' },
284 :children => { :count => 2 }
285 }
286 }
287 }
288
289 xml = Hash.from_xml(response.body)
290 custom_fields = xml['issue']['custom_fields']
291 assert_kind_of Array, custom_fields
292 field = custom_fields.detect {|f| f['id'] == '1'}
293 assert_kind_of Hash, field
294 assert_equal ['MySQL', 'Oracle'], field['value'].sort
295 end
296 end
297
298 context ".json" do
299 should "display custom fields" do
300 get '/issues/3.json'
301 assert_response :success
302 json = ActiveSupport::JSON.decode(response.body)
303 custom_fields = json['issue']['custom_fields']
304 assert_kind_of Array, custom_fields
305 field = custom_fields.detect {|f| f['id'] == 1}
306 assert_kind_of Hash, field
307 assert_equal ['MySQL', 'Oracle'], field['value'].sort
308 end
309 end
310 end
311
312 context "with empty value for multi custom field" do
313 setup do
314 field = CustomField.find(1)
315 field.update_attribute :multiple, true
316 issue = Issue.find(3)
317 issue.custom_field_values = {1 => ['']}
318 issue.save!
319 end
320
321 context ".xml" do
322 should "display custom fields" do
323 get '/issues/3.xml'
324 assert_response :success
325 assert_tag :tag => 'issue',
326 :child => {
327 :tag => 'custom_fields',
328 :attributes => { :type => 'array' },
329 :child => {
330 :tag => 'custom_field',
331 :attributes => { :id => '1'},
332 :child => {
333 :tag => 'value',
334 :attributes => { :type => 'array' },
335 :children => { :count => 0 }
336 }
337 }
338 }
339
340 xml = Hash.from_xml(response.body)
341 custom_fields = xml['issue']['custom_fields']
342 assert_kind_of Array, custom_fields
343 field = custom_fields.detect {|f| f['id'] == '1'}
344 assert_kind_of Hash, field
345 assert_equal [], field['value']
346 end
347 end
348
349 context ".json" do
350 should "display custom fields" do
351 get '/issues/3.json'
352 assert_response :success
353 json = ActiveSupport::JSON.decode(response.body)
354 custom_fields = json['issue']['custom_fields']
355 assert_kind_of Array, custom_fields
356 field = custom_fields.detect {|f| f['id'] == 1}
357 assert_kind_of Hash, field
358 assert_equal [], field['value'].sort
359 end
360 end
361 end
362
261 context "with attachments" do
363 context "with attachments" do
262 context ".xml" do
364 context ".xml" do
263 should "display attachments" do
365 should "display attachments" do
@@ -455,6 +557,24 class ApiTest::IssuesTest < ActionController::IntegrationTest
455 end
557 end
456 end
558 end
457
559
560 context "PUT /issues/3.xml with multi custom fields" do
561 setup do
562 field = CustomField.find(1)
563 field.update_attribute :multiple, true
564 @parameters = {:issue => {:custom_fields => [{'id' => '1', 'value' => ['MySQL', 'PostgreSQL'] }, {'id' => '2', 'value' => '150'}]}}
565 end
566
567 should "update custom fields" do
568 assert_no_difference('Issue.count') do
569 put '/issues/3.xml', @parameters, credentials('jsmith')
570 end
571
572 issue = Issue.find(3)
573 assert_equal '150', issue.custom_value_for(2).value
574 assert_equal ['MySQL', 'PostgreSQL'], issue.custom_field_value(1)
575 end
576 end
577
458 context "PUT /issues/3.xml with project change" do
578 context "PUT /issues/3.xml with project change" do
459 setup do
579 setup do
460 @parameters = {:issue => {:project_id => 2, :subject => 'Project changed'}}
580 @parameters = {:issue => {:project_id => 2, :subject => 'Project changed'}}
@@ -164,4 +164,26 class CustomFieldTest < ActiveSupport::TestCase
164 assert f.valid_field_value?('5')
164 assert f.valid_field_value?('5')
165 assert !f.valid_field_value?('6abc')
165 assert !f.valid_field_value?('6abc')
166 end
166 end
167
168 def test_multi_field_validation
169 f = CustomField.new(:field_format => 'list', :multiple => 'true', :possible_values => ['value1', 'value2'])
170
171 assert f.valid_field_value?(nil)
172 assert f.valid_field_value?('')
173 assert f.valid_field_value?([])
174 assert f.valid_field_value?([nil])
175 assert f.valid_field_value?([''])
176
177 assert f.valid_field_value?('value2')
178 assert !f.valid_field_value?('abc')
179
180 assert f.valid_field_value?(['value2'])
181 assert !f.valid_field_value?(['abc'])
182
183 assert f.valid_field_value?(['', 'value2'])
184 assert !f.valid_field_value?(['', 'abc'])
185
186 assert f.valid_field_value?(['value1', 'value2'])
187 assert !f.valid_field_value?(['value1', 'abc'])
188 end
167 end
189 end
@@ -921,6 +921,36 class IssueTest < ActiveSupport::TestCase
921 end
921 end
922 end
922 end
923
923
924 def test_journalized_multi_custom_field
925 field = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
926 :tracker_ids => [1], :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
927
928 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :subject => 'Test', :author_id => 1)
929
930 assert_difference 'Journal.count' do
931 assert_difference 'JournalDetail.count' do
932 issue.init_journal(User.first)
933 issue.custom_field_values = {field.id => ['value1']}
934 issue.save!
935 end
936 assert_difference 'JournalDetail.count' do
937 issue.init_journal(User.first)
938 issue.custom_field_values = {field.id => ['value1', 'value2']}
939 issue.save!
940 end
941 assert_difference 'JournalDetail.count', 2 do
942 issue.init_journal(User.first)
943 issue.custom_field_values = {field.id => ['value3', 'value2']}
944 issue.save!
945 end
946 assert_difference 'JournalDetail.count', 2 do
947 issue.init_journal(User.first)
948 issue.custom_field_values = {field.id => nil}
949 issue.save!
950 end
951 end
952 end
953
924 def test_description_eol_should_be_normalized
954 def test_description_eol_should_be_normalized
925 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
955 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
926 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
956 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
@@ -173,6 +173,44 class QueryTest < ActiveSupport::TestCase
173 assert_equal 2, issues.first.id
173 assert_equal 2, issues.first.id
174 end
174 end
175
175
176 def test_operator_is_on_multi_list_custom_field
177 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
178 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
179 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
180 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
181 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
182
183 query = Query.new(:name => '_')
184 query.add_filter("cf_#{f.id}", '=', ['value1'])
185 issues = find_issues_with_query(query)
186 assert_equal [1, 3], issues.map(&:id).sort
187
188 query = Query.new(:name => '_')
189 query.add_filter("cf_#{f.id}", '=', ['value2'])
190 issues = find_issues_with_query(query)
191 assert_equal [1], issues.map(&:id).sort
192 end
193
194 def test_operator_is_not_on_multi_list_custom_field
195 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
196 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
197 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
198 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
199 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
200
201 query = Query.new(:name => '_')
202 query.add_filter("cf_#{f.id}", '!', ['value1'])
203 issues = find_issues_with_query(query)
204 assert !issues.map(&:id).include?(1)
205 assert !issues.map(&:id).include?(3)
206
207 query = Query.new(:name => '_')
208 query.add_filter("cf_#{f.id}", '!', ['value2'])
209 issues = find_issues_with_query(query)
210 assert !issues.map(&:id).include?(1)
211 assert issues.map(&:id).include?(3)
212 end
213
176 def test_operator_greater_than
214 def test_operator_greater_than
177 query = Query.new(:project => Project.find(1), :name => '_')
215 query = Query.new(:project => Project.find(1), :name => '_')
178 query.add_filter('done_ratio', '>=', ['40'])
216 query.add_filter('done_ratio', '>=', ['40'])
@@ -492,7 +530,18 class QueryTest < ActiveSupport::TestCase
492
530
493 def test_groupable_columns_should_include_custom_fields
531 def test_groupable_columns_should_include_custom_fields
494 q = Query.new
532 q = Query.new
495 assert q.groupable_columns.detect {|c| c.is_a? QueryCustomFieldColumn}
533 column = q.groupable_columns.detect {|c| c.name == :cf_1}
534 assert_not_nil column
535 assert_kind_of QueryCustomFieldColumn, column
536 end
537
538 def test_groupable_columns_should_not_include_multi_custom_fields
539 field = CustomField.find(1)
540 field.update_attribute :multiple, true
541
542 q = Query.new
543 column = q.groupable_columns.detect {|c| c.name == :cf_1}
544 assert_nil column
496 end
545 end
497
546
498 def test_grouped_with_valid_column
547 def test_grouped_with_valid_column
@@ -527,6 +576,19 class QueryTest < ActiveSupport::TestCase
527 end
576 end
528 end
577 end
529
578
579 def test_sortable_columns_should_include_custom_field
580 q = Query.new
581 assert q.sortable_columns['cf_1']
582 end
583
584 def test_sortable_columns_should_not_include_multi_custom_field
585 field = CustomField.find(1)
586 field.update_attribute :multiple, true
587
588 q = Query.new
589 assert !q.sortable_columns['cf_1']
590 end
591
530 def test_default_sort
592 def test_default_sort
531 q = Query.new
593 q = Query.new
532 assert_equal [], q.sort_criteria
594 assert_equal [], q.sort_criteria
@@ -70,6 +70,12 module Redmine
70 key = custom_field_value.custom_field_id.to_s
70 key = custom_field_value.custom_field_id.to_s
71 if values.has_key?(key)
71 if values.has_key?(key)
72 value = values[key]
72 value = values[key]
73 if value.is_a?(Array)
74 value = value.reject(&:blank?).uniq
75 if value.empty?
76 value << ''
77 end
78 end
73 custom_field_value.value = value
79 custom_field_value.value = value
74 end
80 end
75 end
81 end
@@ -81,9 +87,17 module Redmine
81 x = CustomFieldValue.new
87 x = CustomFieldValue.new
82 x.custom_field = field
88 x.custom_field = field
83 x.customized = self
89 x.customized = self
84 cv = custom_values.detect { |v| v.custom_field == field }
90 if field.multiple?
85 cv ||= custom_values.build(:customized => self, :custom_field => field, :value => nil)
91 values = custom_values.select { |v| v.custom_field == field }
86 x.value = cv.value
92 if values.empty?
93 values << custom_values.build(:customized => self, :custom_field => field, :value => nil)
94 end
95 x.value = values.map(&:value)
96 else
97 cv = custom_values.detect { |v| v.custom_field == field }
98 cv ||= custom_values.build(:customized => self, :custom_field => field, :value => nil)
99 x.value = cv.value
100 end
87 x
101 x
88 end
102 end
89 end
103 end
@@ -115,10 +129,18 module Redmine
115 def save_custom_field_values
129 def save_custom_field_values
116 target_custom_values = []
130 target_custom_values = []
117 custom_field_values.each do |custom_field_value|
131 custom_field_values.each do |custom_field_value|
118 target = custom_values.detect {|cv| cv.custom_field == custom_field_value.custom_field}
132 if custom_field_value.value.is_a?(Array)
119 target ||= custom_values.build(:customized => self, :custom_field => custom_field_value.custom_field)
133 custom_field_value.value.each do |v|
120 target.value = custom_field_value.value
134 target = custom_values.detect {|cv| cv.custom_field == custom_field_value.custom_field && cv.value == v}
121 target_custom_values << target
135 target ||= custom_values.build(:customized => self, :custom_field => custom_field_value.custom_field, :value => v)
136 target_custom_values << target
137 end
138 else
139 target = custom_values.detect {|cv| cv.custom_field == custom_field_value.custom_field}
140 target ||= custom_values.build(:customized => self, :custom_field => custom_field_value.custom_field)
141 target.value = custom_field_value.value
142 target_custom_values << target
143 end
122 end
144 end
123 self.custom_values = target_custom_values
145 self.custom_values = target_custom_values
124 custom_values.each(&:save)
146 custom_values.each(&:save)
General Comments 0
You need to be logged in to leave comments. Login now