@@ -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 |
a |
|
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. |
|
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 |
<% |
|
11 | <% details_to_strings(journal.details).each do |string| %> | |
12 |
<li><%= s |
|
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 | |
|
22 | details_to_strings(change.details, false).each do |string| | |
23 |
xml.text! '<li>' + s |
|
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 |
<% |
|
4 | <% details_to_strings(@journal.details).each do |string| %> | |
5 |
|
|
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 |
<% |
|
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 |
|
|
467 | details_to_strings(journal.details, true).each do |string| | |
468 |
pdf.RDMMultiCell(190,5, "- " + s |
|
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 |
|
|
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 |
|
|
91 | values = custom_values.select { |v| v.custom_field == field } | |
86 |
|
|
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