##// END OF EJS Templates
Patch ActiveRecord::Errors#full_messages so that it contains custom values error messages....
Jean-Philippe Lang -
r2454:a64b8695c8c2
parent child
Show More
@@ -1,50 +1,80
1
1
2 require 'activerecord'
2 require 'activerecord'
3
3
4 module ActiveRecord
4 module ActiveRecord
5 class Base
5 class Base
6 include Redmine::I18n
6 include Redmine::I18n
7
7
8 # Translate attribute names for validation errors display
8 # Translate attribute names for validation errors display
9 def self.human_attribute_name(attr)
9 def self.human_attribute_name(attr)
10 l("field_#{attr.to_s.gsub(/_id$/, '')}")
10 l("field_#{attr.to_s.gsub(/_id$/, '')}")
11 end
11 end
12 end
12 end
13 end
13 end
14
14
15 module ActiveRecord
16 class Errors
17 def full_messages(options = {})
18 full_messages = []
19
20 @errors.each_key do |attr|
21 @errors[attr].each do |message|
22 next unless message
23
24 if attr == "base"
25 full_messages << message
26 elsif attr == "custom_values"
27 # Replace the generic "custom values is invalid"
28 # with the errors on custom values
29 @base.custom_values.each do |value|
30 value.errors.each do |attr, msg|
31 full_messages << value.custom_field.name + ' ' + msg
32 end
33 end
34 else
35 attr_name = @base.class.human_attribute_name(attr)
36 full_messages << attr_name + ' ' + message
37 end
38 end
39 end
40 full_messages
41 end
42 end
43 end
44
15 module ActionView
45 module ActionView
16 module Helpers
46 module Helpers
17 module DateHelper
47 module DateHelper
18 # distance_of_time_in_words breaks when difference is greater than 30 years
48 # distance_of_time_in_words breaks when difference is greater than 30 years
19 def distance_of_date_in_words(from_date, to_date = 0, options = {})
49 def distance_of_date_in_words(from_date, to_date = 0, options = {})
20 from_date = from_date.to_date if from_date.respond_to?(:to_date)
50 from_date = from_date.to_date if from_date.respond_to?(:to_date)
21 to_date = to_date.to_date if to_date.respond_to?(:to_date)
51 to_date = to_date.to_date if to_date.respond_to?(:to_date)
22 distance_in_days = (to_date - from_date).abs
52 distance_in_days = (to_date - from_date).abs
23
53
24 I18n.with_options :locale => options[:locale], :scope => :'datetime.distance_in_words' do |locale|
54 I18n.with_options :locale => options[:locale], :scope => :'datetime.distance_in_words' do |locale|
25 case distance_in_days
55 case distance_in_days
26 when 0..60 then locale.t :x_days, :count => distance_in_days
56 when 0..60 then locale.t :x_days, :count => distance_in_days
27 when 61..720 then locale.t :about_x_months, :count => (distance_in_days / 30).round
57 when 61..720 then locale.t :about_x_months, :count => (distance_in_days / 30).round
28 else locale.t :over_x_years, :count => (distance_in_days / 365).round
58 else locale.t :over_x_years, :count => (distance_in_days / 365).round
29 end
59 end
30 end
60 end
31 end
61 end
32 end
62 end
33 end
63 end
34 end
64 end
35
65
36 ActionView::Base.field_error_proc = Proc.new{ |html_tag, instance| "#{html_tag}" }
66 ActionView::Base.field_error_proc = Proc.new{ |html_tag, instance| "#{html_tag}" }
37
67
38 # Adds :async_smtp and :async_sendmail delivery methods
68 # Adds :async_smtp and :async_sendmail delivery methods
39 # to perform email deliveries asynchronously
69 # to perform email deliveries asynchronously
40 module AsynchronousMailer
70 module AsynchronousMailer
41 %w(smtp sendmail).each do |type|
71 %w(smtp sendmail).each do |type|
42 define_method("perform_delivery_async_#{type}") do |mail|
72 define_method("perform_delivery_async_#{type}") do |mail|
43 Thread.start do
73 Thread.start do
44 send "perform_delivery_#{type}", mail
74 send "perform_delivery_#{type}", mail
45 end
75 end
46 end
76 end
47 end
77 end
48 end
78 end
49
79
50 ActionMailer::Base.send :include, AsynchronousMailer
80 ActionMailer::Base.send :include, AsynchronousMailer
@@ -1,231 +1,244
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class IssueTest < Test::Unit::TestCase
20 class IssueTest < Test::Unit::TestCase
21 fixtures :projects, :users, :members,
21 fixtures :projects, :users, :members,
22 :trackers, :projects_trackers,
22 :trackers, :projects_trackers,
23 :issue_statuses, :issue_categories,
23 :issue_statuses, :issue_categories,
24 :enumerations,
24 :enumerations,
25 :issues,
25 :issues,
26 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
26 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
27 :time_entries
27 :time_entries
28
28
29 def test_create
29 def test_create
30 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.priorities.first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30')
30 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.priorities.first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30')
31 assert issue.save
31 assert issue.save
32 issue.reload
32 issue.reload
33 assert_equal 1.5, issue.estimated_hours
33 assert_equal 1.5, issue.estimated_hours
34 end
34 end
35
35
36 def test_create_minimal
36 def test_create_minimal
37 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.priorities.first, :subject => 'test_create')
37 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.priorities.first, :subject => 'test_create')
38 assert issue.save
38 assert issue.save
39 assert issue.description.nil?
39 assert issue.description.nil?
40 end
40 end
41
41
42 def test_create_with_required_custom_field
42 def test_create_with_required_custom_field
43 field = IssueCustomField.find_by_name('Database')
43 field = IssueCustomField.find_by_name('Database')
44 field.update_attribute(:is_required, true)
44 field.update_attribute(:is_required, true)
45
45
46 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
46 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
47 assert issue.available_custom_fields.include?(field)
47 assert issue.available_custom_fields.include?(field)
48 # No value for the custom field
48 # No value for the custom field
49 assert !issue.save
49 assert !issue.save
50 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
50 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
51 # Blank value
51 # Blank value
52 issue.custom_field_values = { field.id => '' }
52 issue.custom_field_values = { field.id => '' }
53 assert !issue.save
53 assert !issue.save
54 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
54 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
55 # Invalid value
55 # Invalid value
56 issue.custom_field_values = { field.id => 'SQLServer' }
56 issue.custom_field_values = { field.id => 'SQLServer' }
57 assert !issue.save
57 assert !issue.save
58 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
58 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
59 # Valid value
59 # Valid value
60 issue.custom_field_values = { field.id => 'PostgreSQL' }
60 issue.custom_field_values = { field.id => 'PostgreSQL' }
61 assert issue.save
61 assert issue.save
62 issue.reload
62 issue.reload
63 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
63 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
64 end
64 end
65
65
66 def test_errors_full_messages_should_include_custom_fields_errors
67 field = IssueCustomField.find_by_name('Database')
68
69 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
70 assert issue.available_custom_fields.include?(field)
71 # Invalid value
72 issue.custom_field_values = { field.id => 'SQLServer' }
73
74 assert !issue.valid?
75 assert_equal 1, issue.errors.full_messages.size
76 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}", issue.errors.full_messages.first
77 end
78
66 def test_update_issue_with_required_custom_field
79 def test_update_issue_with_required_custom_field
67 field = IssueCustomField.find_by_name('Database')
80 field = IssueCustomField.find_by_name('Database')
68 field.update_attribute(:is_required, true)
81 field.update_attribute(:is_required, true)
69
82
70 issue = Issue.find(1)
83 issue = Issue.find(1)
71 assert_nil issue.custom_value_for(field)
84 assert_nil issue.custom_value_for(field)
72 assert issue.available_custom_fields.include?(field)
85 assert issue.available_custom_fields.include?(field)
73 # No change to custom values, issue can be saved
86 # No change to custom values, issue can be saved
74 assert issue.save
87 assert issue.save
75 # Blank value
88 # Blank value
76 issue.custom_field_values = { field.id => '' }
89 issue.custom_field_values = { field.id => '' }
77 assert !issue.save
90 assert !issue.save
78 # Valid value
91 # Valid value
79 issue.custom_field_values = { field.id => 'PostgreSQL' }
92 issue.custom_field_values = { field.id => 'PostgreSQL' }
80 assert issue.save
93 assert issue.save
81 issue.reload
94 issue.reload
82 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
95 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
83 end
96 end
84
97
85 def test_should_not_update_attributes_if_custom_fields_validation_fails
98 def test_should_not_update_attributes_if_custom_fields_validation_fails
86 issue = Issue.find(1)
99 issue = Issue.find(1)
87 field = IssueCustomField.find_by_name('Database')
100 field = IssueCustomField.find_by_name('Database')
88 assert issue.available_custom_fields.include?(field)
101 assert issue.available_custom_fields.include?(field)
89
102
90 issue.custom_field_values = { field.id => 'Invalid' }
103 issue.custom_field_values = { field.id => 'Invalid' }
91 issue.subject = 'Should be not be saved'
104 issue.subject = 'Should be not be saved'
92 assert !issue.save
105 assert !issue.save
93
106
94 issue.reload
107 issue.reload
95 assert_equal "Can't print recipes", issue.subject
108 assert_equal "Can't print recipes", issue.subject
96 end
109 end
97
110
98 def test_should_not_recreate_custom_values_objects_on_update
111 def test_should_not_recreate_custom_values_objects_on_update
99 field = IssueCustomField.find_by_name('Database')
112 field = IssueCustomField.find_by_name('Database')
100
113
101 issue = Issue.find(1)
114 issue = Issue.find(1)
102 issue.custom_field_values = { field.id => 'PostgreSQL' }
115 issue.custom_field_values = { field.id => 'PostgreSQL' }
103 assert issue.save
116 assert issue.save
104 custom_value = issue.custom_value_for(field)
117 custom_value = issue.custom_value_for(field)
105 issue.reload
118 issue.reload
106 issue.custom_field_values = { field.id => 'MySQL' }
119 issue.custom_field_values = { field.id => 'MySQL' }
107 assert issue.save
120 assert issue.save
108 issue.reload
121 issue.reload
109 assert_equal custom_value.id, issue.custom_value_for(field).id
122 assert_equal custom_value.id, issue.custom_value_for(field).id
110 end
123 end
111
124
112 def test_category_based_assignment
125 def test_category_based_assignment
113 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.priorities.first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
126 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.priorities.first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
114 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
127 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
115 end
128 end
116
129
117 def test_copy
130 def test_copy
118 issue = Issue.new.copy_from(1)
131 issue = Issue.new.copy_from(1)
119 assert issue.save
132 assert issue.save
120 issue.reload
133 issue.reload
121 orig = Issue.find(1)
134 orig = Issue.find(1)
122 assert_equal orig.subject, issue.subject
135 assert_equal orig.subject, issue.subject
123 assert_equal orig.tracker, issue.tracker
136 assert_equal orig.tracker, issue.tracker
124 assert_equal orig.custom_values.first.value, issue.custom_values.first.value
137 assert_equal orig.custom_values.first.value, issue.custom_values.first.value
125 end
138 end
126
139
127 def test_should_close_duplicates
140 def test_should_close_duplicates
128 # Create 3 issues
141 # Create 3 issues
129 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.priorities.first, :subject => 'Duplicates test', :description => 'Duplicates test')
142 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.priorities.first, :subject => 'Duplicates test', :description => 'Duplicates test')
130 assert issue1.save
143 assert issue1.save
131 issue2 = issue1.clone
144 issue2 = issue1.clone
132 assert issue2.save
145 assert issue2.save
133 issue3 = issue1.clone
146 issue3 = issue1.clone
134 assert issue3.save
147 assert issue3.save
135
148
136 # 2 is a dupe of 1
149 # 2 is a dupe of 1
137 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
150 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
138 # And 3 is a dupe of 2
151 # And 3 is a dupe of 2
139 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
152 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
140 # And 3 is a dupe of 1 (circular duplicates)
153 # And 3 is a dupe of 1 (circular duplicates)
141 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
154 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
142
155
143 assert issue1.reload.duplicates.include?(issue2)
156 assert issue1.reload.duplicates.include?(issue2)
144
157
145 # Closing issue 1
158 # Closing issue 1
146 issue1.init_journal(User.find(:first), "Closing issue1")
159 issue1.init_journal(User.find(:first), "Closing issue1")
147 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
160 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
148 assert issue1.save
161 assert issue1.save
149 # 2 and 3 should be also closed
162 # 2 and 3 should be also closed
150 assert issue2.reload.closed?
163 assert issue2.reload.closed?
151 assert issue3.reload.closed?
164 assert issue3.reload.closed?
152 end
165 end
153
166
154 def test_should_not_close_duplicated_issue
167 def test_should_not_close_duplicated_issue
155 # Create 3 issues
168 # Create 3 issues
156 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.priorities.first, :subject => 'Duplicates test', :description => 'Duplicates test')
169 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.priorities.first, :subject => 'Duplicates test', :description => 'Duplicates test')
157 assert issue1.save
170 assert issue1.save
158 issue2 = issue1.clone
171 issue2 = issue1.clone
159 assert issue2.save
172 assert issue2.save
160
173
161 # 2 is a dupe of 1
174 # 2 is a dupe of 1
162 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
175 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
163 # 2 is a dup of 1 but 1 is not a duplicate of 2
176 # 2 is a dup of 1 but 1 is not a duplicate of 2
164 assert !issue2.reload.duplicates.include?(issue1)
177 assert !issue2.reload.duplicates.include?(issue1)
165
178
166 # Closing issue 2
179 # Closing issue 2
167 issue2.init_journal(User.find(:first), "Closing issue2")
180 issue2.init_journal(User.find(:first), "Closing issue2")
168 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
181 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
169 assert issue2.save
182 assert issue2.save
170 # 1 should not be also closed
183 # 1 should not be also closed
171 assert !issue1.reload.closed?
184 assert !issue1.reload.closed?
172 end
185 end
173
186
174 def test_move_to_another_project_with_same_category
187 def test_move_to_another_project_with_same_category
175 issue = Issue.find(1)
188 issue = Issue.find(1)
176 assert issue.move_to(Project.find(2))
189 assert issue.move_to(Project.find(2))
177 issue.reload
190 issue.reload
178 assert_equal 2, issue.project_id
191 assert_equal 2, issue.project_id
179 # Category changes
192 # Category changes
180 assert_equal 4, issue.category_id
193 assert_equal 4, issue.category_id
181 # Make sure time entries were move to the target project
194 # Make sure time entries were move to the target project
182 assert_equal 2, issue.time_entries.first.project_id
195 assert_equal 2, issue.time_entries.first.project_id
183 end
196 end
184
197
185 def test_move_to_another_project_without_same_category
198 def test_move_to_another_project_without_same_category
186 issue = Issue.find(2)
199 issue = Issue.find(2)
187 assert issue.move_to(Project.find(2))
200 assert issue.move_to(Project.find(2))
188 issue.reload
201 issue.reload
189 assert_equal 2, issue.project_id
202 assert_equal 2, issue.project_id
190 # Category cleared
203 # Category cleared
191 assert_nil issue.category_id
204 assert_nil issue.category_id
192 end
205 end
193
206
194 def test_copy_to_the_same_project
207 def test_copy_to_the_same_project
195 issue = Issue.find(1)
208 issue = Issue.find(1)
196 copy = nil
209 copy = nil
197 assert_difference 'Issue.count' do
210 assert_difference 'Issue.count' do
198 copy = issue.move_to(issue.project, nil, :copy => true)
211 copy = issue.move_to(issue.project, nil, :copy => true)
199 end
212 end
200 assert_kind_of Issue, copy
213 assert_kind_of Issue, copy
201 assert_equal issue.project, copy.project
214 assert_equal issue.project, copy.project
202 assert_equal "125", copy.custom_value_for(2).value
215 assert_equal "125", copy.custom_value_for(2).value
203 end
216 end
204
217
205 def test_copy_to_another_project_and_tracker
218 def test_copy_to_another_project_and_tracker
206 issue = Issue.find(1)
219 issue = Issue.find(1)
207 copy = nil
220 copy = nil
208 assert_difference 'Issue.count' do
221 assert_difference 'Issue.count' do
209 copy = issue.move_to(Project.find(3), Tracker.find(2), :copy => true)
222 copy = issue.move_to(Project.find(3), Tracker.find(2), :copy => true)
210 end
223 end
211 assert_kind_of Issue, copy
224 assert_kind_of Issue, copy
212 assert_equal Project.find(3), copy.project
225 assert_equal Project.find(3), copy.project
213 assert_equal Tracker.find(2), copy.tracker
226 assert_equal Tracker.find(2), copy.tracker
214 # Custom field #2 is not associated with target tracker
227 # Custom field #2 is not associated with target tracker
215 assert_nil copy.custom_value_for(2)
228 assert_nil copy.custom_value_for(2)
216 end
229 end
217
230
218 def test_issue_destroy
231 def test_issue_destroy
219 Issue.find(1).destroy
232 Issue.find(1).destroy
220 assert_nil Issue.find_by_id(1)
233 assert_nil Issue.find_by_id(1)
221 assert_nil TimeEntry.find_by_issue_id(1)
234 assert_nil TimeEntry.find_by_issue_id(1)
222 end
235 end
223
236
224 def test_overdue
237 def test_overdue
225 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
238 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
226 assert !Issue.new(:due_date => Date.today).overdue?
239 assert !Issue.new(:due_date => Date.today).overdue?
227 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
240 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
228 assert !Issue.new(:due_date => nil).overdue?
241 assert !Issue.new(:due_date => nil).overdue?
229 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
242 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
230 end
243 end
231 end
244 end
General Comments 0
You need to be logged in to leave comments. Login now