##// END OF EJS Templates
Makes issue description a non-required field (#2456)....
Jean-Philippe Lang -
r2244:bc270b31c366
parent child
Show More
@@ -1,280 +1,280
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 class Issue < ActiveRecord::Base
18 class Issue < ActiveRecord::Base
19 belongs_to :project
19 belongs_to :project
20 belongs_to :tracker
20 belongs_to :tracker
21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
25 belongs_to :priority, :class_name => 'Enumeration', :foreign_key => 'priority_id'
25 belongs_to :priority, :class_name => 'Enumeration', :foreign_key => 'priority_id'
26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
27
27
28 has_many :journals, :as => :journalized, :dependent => :destroy
28 has_many :journals, :as => :journalized, :dependent => :destroy
29 has_many :time_entries, :dependent => :delete_all
29 has_many :time_entries, :dependent => :delete_all
30 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
30 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
31
31
32 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
32 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
33 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
33 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
34
34
35 acts_as_attachable :after_remove => :attachment_removed
35 acts_as_attachable :after_remove => :attachment_removed
36 acts_as_customizable
36 acts_as_customizable
37 acts_as_watchable
37 acts_as_watchable
38 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
38 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
39 :include => [:project, :journals],
39 :include => [:project, :journals],
40 # sort by id so that limited eager loading doesn't break with postgresql
40 # sort by id so that limited eager loading doesn't break with postgresql
41 :order_column => "#{table_name}.id"
41 :order_column => "#{table_name}.id"
42 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"},
42 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"},
43 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}
43 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}
44
44
45 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
45 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
46 :author_key => :author_id
46 :author_key => :author_id
47
47
48 validates_presence_of :subject, :description, :priority, :project, :tracker, :author, :status
48 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
49 validates_length_of :subject, :maximum => 255
49 validates_length_of :subject, :maximum => 255
50 validates_inclusion_of :done_ratio, :in => 0..100
50 validates_inclusion_of :done_ratio, :in => 0..100
51 validates_numericality_of :estimated_hours, :allow_nil => true
51 validates_numericality_of :estimated_hours, :allow_nil => true
52
52
53 def after_initialize
53 def after_initialize
54 if new_record?
54 if new_record?
55 # set default values for new records only
55 # set default values for new records only
56 self.status ||= IssueStatus.default
56 self.status ||= IssueStatus.default
57 self.priority ||= Enumeration.default('IPRI')
57 self.priority ||= Enumeration.default('IPRI')
58 end
58 end
59 end
59 end
60
60
61 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
61 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
62 def available_custom_fields
62 def available_custom_fields
63 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
63 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
64 end
64 end
65
65
66 def copy_from(arg)
66 def copy_from(arg)
67 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
67 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
68 self.attributes = issue.attributes.dup
68 self.attributes = issue.attributes.dup
69 self.custom_values = issue.custom_values.collect {|v| v.clone}
69 self.custom_values = issue.custom_values.collect {|v| v.clone}
70 self
70 self
71 end
71 end
72
72
73 # Move an issue to a new project and tracker
73 # Move an issue to a new project and tracker
74 def move_to(new_project, new_tracker = nil)
74 def move_to(new_project, new_tracker = nil)
75 transaction do
75 transaction do
76 if new_project && project_id != new_project.id
76 if new_project && project_id != new_project.id
77 # delete issue relations
77 # delete issue relations
78 unless Setting.cross_project_issue_relations?
78 unless Setting.cross_project_issue_relations?
79 self.relations_from.clear
79 self.relations_from.clear
80 self.relations_to.clear
80 self.relations_to.clear
81 end
81 end
82 # issue is moved to another project
82 # issue is moved to another project
83 # reassign to the category with same name if any
83 # reassign to the category with same name if any
84 new_category = category.nil? ? nil : new_project.issue_categories.find_by_name(category.name)
84 new_category = category.nil? ? nil : new_project.issue_categories.find_by_name(category.name)
85 self.category = new_category
85 self.category = new_category
86 self.fixed_version = nil
86 self.fixed_version = nil
87 self.project = new_project
87 self.project = new_project
88 end
88 end
89 if new_tracker
89 if new_tracker
90 self.tracker = new_tracker
90 self.tracker = new_tracker
91 end
91 end
92 if save
92 if save
93 # Manually update project_id on related time entries
93 # Manually update project_id on related time entries
94 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
94 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
95 else
95 else
96 rollback_db_transaction
96 rollback_db_transaction
97 return false
97 return false
98 end
98 end
99 end
99 end
100 return true
100 return true
101 end
101 end
102
102
103 def priority_id=(pid)
103 def priority_id=(pid)
104 self.priority = nil
104 self.priority = nil
105 write_attribute(:priority_id, pid)
105 write_attribute(:priority_id, pid)
106 end
106 end
107
107
108 def estimated_hours=(h)
108 def estimated_hours=(h)
109 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
109 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
110 end
110 end
111
111
112 def validate
112 def validate
113 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
113 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
114 errors.add :due_date, :activerecord_error_not_a_date
114 errors.add :due_date, :activerecord_error_not_a_date
115 end
115 end
116
116
117 if self.due_date and self.start_date and self.due_date < self.start_date
117 if self.due_date and self.start_date and self.due_date < self.start_date
118 errors.add :due_date, :activerecord_error_greater_than_start_date
118 errors.add :due_date, :activerecord_error_greater_than_start_date
119 end
119 end
120
120
121 if start_date && soonest_start && start_date < soonest_start
121 if start_date && soonest_start && start_date < soonest_start
122 errors.add :start_date, :activerecord_error_invalid
122 errors.add :start_date, :activerecord_error_invalid
123 end
123 end
124 end
124 end
125
125
126 def validate_on_create
126 def validate_on_create
127 errors.add :tracker_id, :activerecord_error_invalid unless project.trackers.include?(tracker)
127 errors.add :tracker_id, :activerecord_error_invalid unless project.trackers.include?(tracker)
128 end
128 end
129
129
130 def before_create
130 def before_create
131 # default assignment based on category
131 # default assignment based on category
132 if assigned_to.nil? && category && category.assigned_to
132 if assigned_to.nil? && category && category.assigned_to
133 self.assigned_to = category.assigned_to
133 self.assigned_to = category.assigned_to
134 end
134 end
135 end
135 end
136
136
137 def before_save
137 def before_save
138 if @current_journal
138 if @current_journal
139 # attributes changes
139 # attributes changes
140 (Issue.column_names - %w(id description)).each {|c|
140 (Issue.column_names - %w(id description)).each {|c|
141 @current_journal.details << JournalDetail.new(:property => 'attr',
141 @current_journal.details << JournalDetail.new(:property => 'attr',
142 :prop_key => c,
142 :prop_key => c,
143 :old_value => @issue_before_change.send(c),
143 :old_value => @issue_before_change.send(c),
144 :value => send(c)) unless send(c)==@issue_before_change.send(c)
144 :value => send(c)) unless send(c)==@issue_before_change.send(c)
145 }
145 }
146 # custom fields changes
146 # custom fields changes
147 custom_values.each {|c|
147 custom_values.each {|c|
148 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
148 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
149 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
149 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
150 @current_journal.details << JournalDetail.new(:property => 'cf',
150 @current_journal.details << JournalDetail.new(:property => 'cf',
151 :prop_key => c.custom_field_id,
151 :prop_key => c.custom_field_id,
152 :old_value => @custom_values_before_change[c.custom_field_id],
152 :old_value => @custom_values_before_change[c.custom_field_id],
153 :value => c.value)
153 :value => c.value)
154 }
154 }
155 @current_journal.save
155 @current_journal.save
156 end
156 end
157 # Save the issue even if the journal is not saved (because empty)
157 # Save the issue even if the journal is not saved (because empty)
158 true
158 true
159 end
159 end
160
160
161 def after_save
161 def after_save
162 # Reload is needed in order to get the right status
162 # Reload is needed in order to get the right status
163 reload
163 reload
164
164
165 # Update start/due dates of following issues
165 # Update start/due dates of following issues
166 relations_from.each(&:set_issue_to_dates)
166 relations_from.each(&:set_issue_to_dates)
167
167
168 # Close duplicates if the issue was closed
168 # Close duplicates if the issue was closed
169 if @issue_before_change && !@issue_before_change.closed? && self.closed?
169 if @issue_before_change && !@issue_before_change.closed? && self.closed?
170 duplicates.each do |duplicate|
170 duplicates.each do |duplicate|
171 # Reload is need in case the duplicate was updated by a previous duplicate
171 # Reload is need in case the duplicate was updated by a previous duplicate
172 duplicate.reload
172 duplicate.reload
173 # Don't re-close it if it's already closed
173 # Don't re-close it if it's already closed
174 next if duplicate.closed?
174 next if duplicate.closed?
175 # Same user and notes
175 # Same user and notes
176 duplicate.init_journal(@current_journal.user, @current_journal.notes)
176 duplicate.init_journal(@current_journal.user, @current_journal.notes)
177 duplicate.update_attribute :status, self.status
177 duplicate.update_attribute :status, self.status
178 end
178 end
179 end
179 end
180 end
180 end
181
181
182 def init_journal(user, notes = "")
182 def init_journal(user, notes = "")
183 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
183 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
184 @issue_before_change = self.clone
184 @issue_before_change = self.clone
185 @issue_before_change.status = self.status
185 @issue_before_change.status = self.status
186 @custom_values_before_change = {}
186 @custom_values_before_change = {}
187 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
187 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
188 # Make sure updated_on is updated when adding a note.
188 # Make sure updated_on is updated when adding a note.
189 updated_on_will_change!
189 updated_on_will_change!
190 @current_journal
190 @current_journal
191 end
191 end
192
192
193 # Return true if the issue is closed, otherwise false
193 # Return true if the issue is closed, otherwise false
194 def closed?
194 def closed?
195 self.status.is_closed?
195 self.status.is_closed?
196 end
196 end
197
197
198 # Returns true if the issue is overdue
198 # Returns true if the issue is overdue
199 def overdue?
199 def overdue?
200 !due_date.nil? && (due_date < Date.today)
200 !due_date.nil? && (due_date < Date.today)
201 end
201 end
202
202
203 # Users the issue can be assigned to
203 # Users the issue can be assigned to
204 def assignable_users
204 def assignable_users
205 project.assignable_users
205 project.assignable_users
206 end
206 end
207
207
208 # Returns an array of status that user is able to apply
208 # Returns an array of status that user is able to apply
209 def new_statuses_allowed_to(user)
209 def new_statuses_allowed_to(user)
210 statuses = status.find_new_statuses_allowed_to(user.role_for_project(project), tracker)
210 statuses = status.find_new_statuses_allowed_to(user.role_for_project(project), tracker)
211 statuses << status unless statuses.empty?
211 statuses << status unless statuses.empty?
212 statuses.uniq.sort
212 statuses.uniq.sort
213 end
213 end
214
214
215 # Returns the mail adresses of users that should be notified for the issue
215 # Returns the mail adresses of users that should be notified for the issue
216 def recipients
216 def recipients
217 recipients = project.recipients
217 recipients = project.recipients
218 # Author and assignee are always notified unless they have been locked
218 # Author and assignee are always notified unless they have been locked
219 recipients << author.mail if author && author.active?
219 recipients << author.mail if author && author.active?
220 recipients << assigned_to.mail if assigned_to && assigned_to.active?
220 recipients << assigned_to.mail if assigned_to && assigned_to.active?
221 recipients.compact.uniq
221 recipients.compact.uniq
222 end
222 end
223
223
224 def spent_hours
224 def spent_hours
225 @spent_hours ||= time_entries.sum(:hours) || 0
225 @spent_hours ||= time_entries.sum(:hours) || 0
226 end
226 end
227
227
228 def relations
228 def relations
229 (relations_from + relations_to).sort
229 (relations_from + relations_to).sort
230 end
230 end
231
231
232 def all_dependent_issues
232 def all_dependent_issues
233 dependencies = []
233 dependencies = []
234 relations_from.each do |relation|
234 relations_from.each do |relation|
235 dependencies << relation.issue_to
235 dependencies << relation.issue_to
236 dependencies += relation.issue_to.all_dependent_issues
236 dependencies += relation.issue_to.all_dependent_issues
237 end
237 end
238 dependencies
238 dependencies
239 end
239 end
240
240
241 # Returns an array of issues that duplicate this one
241 # Returns an array of issues that duplicate this one
242 def duplicates
242 def duplicates
243 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
243 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
244 end
244 end
245
245
246 # Returns the due date or the target due date if any
246 # Returns the due date or the target due date if any
247 # Used on gantt chart
247 # Used on gantt chart
248 def due_before
248 def due_before
249 due_date || (fixed_version ? fixed_version.effective_date : nil)
249 due_date || (fixed_version ? fixed_version.effective_date : nil)
250 end
250 end
251
251
252 def duration
252 def duration
253 (start_date && due_date) ? due_date - start_date : 0
253 (start_date && due_date) ? due_date - start_date : 0
254 end
254 end
255
255
256 def soonest_start
256 def soonest_start
257 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
257 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
258 end
258 end
259
259
260 def self.visible_by(usr)
260 def self.visible_by(usr)
261 with_scope(:find => { :conditions => Project.visible_by(usr) }) do
261 with_scope(:find => { :conditions => Project.visible_by(usr) }) do
262 yield
262 yield
263 end
263 end
264 end
264 end
265
265
266 def to_s
266 def to_s
267 "#{tracker} ##{id}: #{subject}"
267 "#{tracker} ##{id}: #{subject}"
268 end
268 end
269
269
270 private
270 private
271
271
272 # Callback on attachment deletion
272 # Callback on attachment deletion
273 def attachment_removed(obj)
273 def attachment_removed(obj)
274 journal = init_journal(User.current)
274 journal = init_journal(User.current)
275 journal.details << JournalDetail.new(:property => 'attachment',
275 journal.details << JournalDetail.new(:property => 'attachment',
276 :prop_key => obj.id,
276 :prop_key => obj.id,
277 :old_value => obj.filename)
277 :old_value => obj.filename)
278 journal.save
278 journal.save
279 end
279 end
280 end
280 end
@@ -1,61 +1,61
1 <% if @issue.new_record? %>
1 <% if @issue.new_record? %>
2 <p><%= f.select :tracker_id, @project.trackers.collect {|t| [t.name, t.id]}, :required => true %></p>
2 <p><%= f.select :tracker_id, @project.trackers.collect {|t| [t.name, t.id]}, :required => true %></p>
3 <%= observe_field :issue_tracker_id, :url => { :action => :new },
3 <%= observe_field :issue_tracker_id, :url => { :action => :new },
4 :update => :content,
4 :update => :content,
5 :with => "Form.serialize('issue-form')" %>
5 :with => "Form.serialize('issue-form')" %>
6 <hr />
6 <hr />
7 <% end %>
7 <% end %>
8
8
9 <div id="issue_descr_fields" <%= 'style="display:none"' unless @issue.new_record? || @issue.errors.any? %>>
9 <div id="issue_descr_fields" <%= 'style="display:none"' unless @issue.new_record? || @issue.errors.any? %>>
10 <p><%= f.text_field :subject, :size => 80, :required => true %></p>
10 <p><%= f.text_field :subject, :size => 80, :required => true %></p>
11 <p><%= f.text_area :description, :required => true,
11 <p><%= f.text_area :description,
12 :cols => 60,
12 :cols => 60,
13 :rows => (@issue.description.blank? ? 10 : [[10, @issue.description.length / 50].max, 100].min),
13 :rows => (@issue.description.blank? ? 10 : [[10, @issue.description.length / 50].max, 100].min),
14 :accesskey => accesskey(:edit),
14 :accesskey => accesskey(:edit),
15 :class => 'wiki-edit' %></p>
15 :class => 'wiki-edit' %></p>
16 </div>
16 </div>
17
17
18 <div class="splitcontentleft">
18 <div class="splitcontentleft">
19 <% if @issue.new_record? || @allowed_statuses.any? %>
19 <% if @issue.new_record? || @allowed_statuses.any? %>
20 <p><%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), :required => true %></p>
20 <p><%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), :required => true %></p>
21 <% else %>
21 <% else %>
22 <p><label><%= l(:field_status) %></label> <%= @issue.status.name %></p>
22 <p><label><%= l(:field_status) %></label> <%= @issue.status.name %></p>
23 <% end %>
23 <% end %>
24
24
25 <p><%= f.select :priority_id, (@priorities.collect {|p| [p.name, p.id]}), :required => true %></p>
25 <p><%= f.select :priority_id, (@priorities.collect {|p| [p.name, p.id]}), :required => true %></p>
26 <p><%= f.select :assigned_to_id, (@issue.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true %></p>
26 <p><%= f.select :assigned_to_id, (@issue.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true %></p>
27 <p><%= f.select :category_id, (@project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true %>
27 <p><%= f.select :category_id, (@project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true %>
28 <%= prompt_to_remote(l(:label_issue_category_new),
28 <%= prompt_to_remote(l(:label_issue_category_new),
29 l(:label_issue_category_new), 'category[name]',
29 l(:label_issue_category_new), 'category[name]',
30 {:controller => 'projects', :action => 'add_issue_category', :id => @project},
30 {:controller => 'projects', :action => 'add_issue_category', :id => @project},
31 :class => 'small', :tabindex => 199) if authorize_for('projects', 'add_issue_category') %></p>
31 :class => 'small', :tabindex => 199) if authorize_for('projects', 'add_issue_category') %></p>
32 <%= content_tag('p', f.select(:fixed_version_id,
32 <%= content_tag('p', f.select(:fixed_version_id,
33 (@project.versions.sort.collect {|v| [v.name, v.id]}),
33 (@project.versions.sort.collect {|v| [v.name, v.id]}),
34 { :include_blank => true })) unless @project.versions.empty? %>
34 { :include_blank => true })) unless @project.versions.empty? %>
35 </div>
35 </div>
36
36
37 <div class="splitcontentright">
37 <div class="splitcontentright">
38 <p><%= f.text_field :start_date, :size => 10 %><%= calendar_for('issue_start_date') %></p>
38 <p><%= f.text_field :start_date, :size => 10 %><%= calendar_for('issue_start_date') %></p>
39 <p><%= f.text_field :due_date, :size => 10 %><%= calendar_for('issue_due_date') %></p>
39 <p><%= f.text_field :due_date, :size => 10 %><%= calendar_for('issue_due_date') %></p>
40 <p><%= f.text_field :estimated_hours, :size => 3 %> <%= l(:field_hours) %></p>
40 <p><%= f.text_field :estimated_hours, :size => 3 %> <%= l(:field_hours) %></p>
41 <p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></p>
41 <p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></p>
42 </div>
42 </div>
43
43
44 <div style="clear:both;"> </div>
44 <div style="clear:both;"> </div>
45 <%= render :partial => 'form_custom_fields' %>
45 <%= render :partial => 'form_custom_fields' %>
46
46
47 <% if @issue.new_record? %>
47 <% if @issue.new_record? %>
48 <p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form' %></p>
48 <p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form' %></p>
49 <% end %>
49 <% end %>
50
50
51 <% if @issue.new_record? && User.current.allowed_to?(:add_issue_watchers, @project) -%>
51 <% if @issue.new_record? && User.current.allowed_to?(:add_issue_watchers, @project) -%>
52 <p><label><%= l(:label_issue_watchers) %></label>
52 <p><label><%= l(:label_issue_watchers) %></label>
53 <% @issue.project.users.sort.each do |user| -%>
53 <% @issue.project.users.sort.each do |user| -%>
54 <label class="floating"><%= check_box_tag 'issue[watcher_user_ids][]', user.id, @issue.watcher_user_ids.include?(user.id) %> <%=h user %></label>
54 <label class="floating"><%= check_box_tag 'issue[watcher_user_ids][]', user.id, @issue.watcher_user_ids.include?(user.id) %> <%=h user %></label>
55 <% end -%>
55 <% end -%>
56 </p>
56 </p>
57 <% end %>
57 <% end %>
58
58
59 <%= call_hook(:view_issues_form_details_bottom, { :issue => @issue, :form => f }) %>
59 <%= call_hook(:view_issues_form_details_bottom, { :issue => @issue, :form => f }) %>
60
60
61 <%= wikitoolbar_for 'issue_description' %>
61 <%= wikitoolbar_for 'issue_description' %>
@@ -1,126 +1,126
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to_if_authorized(l(:button_update), {:controller => 'issues', :action => 'edit', :id => @issue }, :onclick => 'showAndScrollTo("update", "notes"); return false;', :class => 'icon icon-edit', :accesskey => accesskey(:edit)) %>
2 <%= link_to_if_authorized(l(:button_update), {:controller => 'issues', :action => 'edit', :id => @issue }, :onclick => 'showAndScrollTo("update", "notes"); return false;', :class => 'icon icon-edit', :accesskey => accesskey(:edit)) %>
3 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, :class => 'icon icon-time' %>
3 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, :class => 'icon icon-time' %>
4 <%= watcher_tag(@issue, User.current) %>
4 <%= watcher_tag(@issue, User.current) %>
5 <%= link_to_if_authorized l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue }, :class => 'icon icon-copy' %>
5 <%= link_to_if_authorized l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue }, :class => 'icon icon-copy' %>
6 <%= link_to_if_authorized l(:button_move), {:controller => 'issues', :action => 'move', :id => @issue }, :class => 'icon icon-move' %>
6 <%= link_to_if_authorized l(:button_move), {:controller => 'issues', :action => 'move', :id => @issue }, :class => 'icon icon-move' %>
7 <%= link_to_if_authorized l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
7 <%= link_to_if_authorized l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
8 </div>
8 </div>
9
9
10 <h2><%= @issue.tracker.name %> #<%= @issue.id %></h2>
10 <h2><%= @issue.tracker.name %> #<%= @issue.id %></h2>
11
11
12 <div class="<%= css_issue_classes(@issue) %>">
12 <div class="<%= css_issue_classes(@issue) %>">
13 <%= avatar(@issue.author, :size => "64") %>
13 <%= avatar(@issue.author, :size => "64") %>
14 <h3><%=h @issue.subject %></h3>
14 <h3><%=h @issue.subject %></h3>
15 <p class="author">
15 <p class="author">
16 <%= authoring @issue.created_on, @issue.author %>.
16 <%= authoring @issue.created_on, @issue.author %>.
17 <%= l(:label_updated_time, distance_of_time_in_words(Time.now, @issue.updated_on)) + '.' if @issue.created_on != @issue.updated_on %>
17 <%= l(:label_updated_time, distance_of_time_in_words(Time.now, @issue.updated_on)) + '.' if @issue.created_on != @issue.updated_on %>
18 </p>
18 </p>
19
19
20 <table width="100%">
20 <table width="100%">
21 <tr>
21 <tr>
22 <td style="width:15%" class="status"><b><%=l(:field_status)%>:</b></td><td style="width:35%" class="status status-<%= @issue.status.name %>"><%= @issue.status.name %></td>
22 <td style="width:15%" class="status"><b><%=l(:field_status)%>:</b></td><td style="width:35%" class="status status-<%= @issue.status.name %>"><%= @issue.status.name %></td>
23 <td style="width:15%" class="start-date"><b><%=l(:field_start_date)%>:</b></td><td style="width:35%"><%= format_date(@issue.start_date) %></td>
23 <td style="width:15%" class="start-date"><b><%=l(:field_start_date)%>:</b></td><td style="width:35%"><%= format_date(@issue.start_date) %></td>
24 </tr>
24 </tr>
25 <tr>
25 <tr>
26 <td class="priority"><b><%=l(:field_priority)%>:</b></td><td class="priority priority-<%= @issue.priority.name %>"><%= @issue.priority.name %></td>
26 <td class="priority"><b><%=l(:field_priority)%>:</b></td><td class="priority priority-<%= @issue.priority.name %>"><%= @issue.priority.name %></td>
27 <td class="due-date"><b><%=l(:field_due_date)%>:</b></td><td class="due-date"><%= format_date(@issue.due_date) %></td>
27 <td class="due-date"><b><%=l(:field_due_date)%>:</b></td><td class="due-date"><%= format_date(@issue.due_date) %></td>
28 </tr>
28 </tr>
29 <tr>
29 <tr>
30 <td class="assigned-to"><b><%=l(:field_assigned_to)%>:</b></td><td><%= avatar(@issue.assigned_to, :size => "14") %><%= @issue.assigned_to ? link_to_user(@issue.assigned_to) : "-" %></td>
30 <td class="assigned-to"><b><%=l(:field_assigned_to)%>:</b></td><td><%= avatar(@issue.assigned_to, :size => "14") %><%= @issue.assigned_to ? link_to_user(@issue.assigned_to) : "-" %></td>
31 <td class="progress"><b><%=l(:field_done_ratio)%>:</b></td><td class="progress"><%= progress_bar @issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%" %></td>
31 <td class="progress"><b><%=l(:field_done_ratio)%>:</b></td><td class="progress"><%= progress_bar @issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%" %></td>
32 </tr>
32 </tr>
33 <tr>
33 <tr>
34 <td class="category"><b><%=l(:field_category)%>:</b></td><td><%=h @issue.category ? @issue.category.name : "-" %></td>
34 <td class="category"><b><%=l(:field_category)%>:</b></td><td><%=h @issue.category ? @issue.category.name : "-" %></td>
35 <% if User.current.allowed_to?(:view_time_entries, @project) %>
35 <% if User.current.allowed_to?(:view_time_entries, @project) %>
36 <td class="spent-time"><b><%=l(:label_spent_time)%>:</b></td>
36 <td class="spent-time"><b><%=l(:label_spent_time)%>:</b></td>
37 <td class="spent-hours"><%= @issue.spent_hours > 0 ? (link_to lwr(:label_f_hour, @issue.spent_hours), {:controller => 'timelog', :action => 'details', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time') : "-" %></td>
37 <td class="spent-hours"><%= @issue.spent_hours > 0 ? (link_to lwr(:label_f_hour, @issue.spent_hours), {:controller => 'timelog', :action => 'details', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time') : "-" %></td>
38 <% end %>
38 <% end %>
39 </tr>
39 </tr>
40 <tr>
40 <tr>
41 <td class="fixed-version"><b><%=l(:field_fixed_version)%>:</b></td><td><%= @issue.fixed_version ? link_to_version(@issue.fixed_version) : "-" %></td>
41 <td class="fixed-version"><b><%=l(:field_fixed_version)%>:</b></td><td><%= @issue.fixed_version ? link_to_version(@issue.fixed_version) : "-" %></td>
42 <% if @issue.estimated_hours %>
42 <% if @issue.estimated_hours %>
43 <td class="estimated-hours"><b><%=l(:field_estimated_hours)%>:</b></td><td><%= lwr(:label_f_hour, @issue.estimated_hours) %></td>
43 <td class="estimated-hours"><b><%=l(:field_estimated_hours)%>:</b></td><td><%= lwr(:label_f_hour, @issue.estimated_hours) %></td>
44 <% end %>
44 <% end %>
45 </tr>
45 </tr>
46 <tr>
46 <tr>
47 <% n = 0 -%>
47 <% n = 0 -%>
48 <% @issue.custom_values.each do |value| -%>
48 <% @issue.custom_values.each do |value| -%>
49 <td valign="top"><b><%=h value.custom_field.name %>:</b></td><td valign="top"><%= simple_format(h(show_value(value))) %></td>
49 <td valign="top"><b><%=h value.custom_field.name %>:</b></td><td valign="top"><%= simple_format(h(show_value(value))) %></td>
50 <% n = n + 1
50 <% n = n + 1
51 if (n > 1)
51 if (n > 1)
52 n = 0 %>
52 n = 0 %>
53 </tr><tr>
53 </tr><tr>
54 <%end
54 <%end
55 end %>
55 end %>
56 </tr>
56 </tr>
57 <%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %>
57 <%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %>
58 </table>
58 </table>
59 <hr />
59 <hr />
60
60
61 <div class="contextual">
61 <div class="contextual">
62 <%= link_to_remote_if_authorized l(:button_quote), { :url => {:action => 'reply', :id => @issue} }, :class => 'icon icon-comment' %>
62 <%= link_to_remote_if_authorized(l(:button_quote), { :url => {:action => 'reply', :id => @issue} }, :class => 'icon icon-comment') unless @issue.description.blank? %>
63 </div>
63 </div>
64
64
65 <p><strong><%=l(:field_description)%></strong></p>
65 <p><strong><%=l(:field_description)%></strong></p>
66 <div class="wiki">
66 <div class="wiki">
67 <%= textilizable @issue, :description, :attachments => @issue.attachments %>
67 <%= textilizable @issue, :description, :attachments => @issue.attachments %>
68 </div>
68 </div>
69
69
70 <%= link_to_attachments @issue %>
70 <%= link_to_attachments @issue %>
71
71
72 <% if authorize_for('issue_relations', 'new') || @issue.relations.any? %>
72 <% if authorize_for('issue_relations', 'new') || @issue.relations.any? %>
73 <hr />
73 <hr />
74 <div id="relations">
74 <div id="relations">
75 <%= render :partial => 'relations' %>
75 <%= render :partial => 'relations' %>
76 </div>
76 </div>
77 <% end %>
77 <% end %>
78
78
79 <% if User.current.allowed_to?(:add_issue_watchers, @project) ||
79 <% if User.current.allowed_to?(:add_issue_watchers, @project) ||
80 (@issue.watchers.any? && User.current.allowed_to?(:view_issue_watchers, @project)) %>
80 (@issue.watchers.any? && User.current.allowed_to?(:view_issue_watchers, @project)) %>
81 <hr />
81 <hr />
82 <div id="watchers">
82 <div id="watchers">
83 <%= render :partial => 'watchers/watchers', :locals => {:watched => @issue} %>
83 <%= render :partial => 'watchers/watchers', :locals => {:watched => @issue} %>
84 </div>
84 </div>
85 <% end %>
85 <% end %>
86
86
87 </div>
87 </div>
88
88
89 <% if @issue.changesets.any? && User.current.allowed_to?(:view_changesets, @project) %>
89 <% if @issue.changesets.any? && User.current.allowed_to?(:view_changesets, @project) %>
90 <div id="issue-changesets">
90 <div id="issue-changesets">
91 <h3><%=l(:label_associated_revisions)%></h3>
91 <h3><%=l(:label_associated_revisions)%></h3>
92 <%= render :partial => 'changesets', :locals => { :changesets => @issue.changesets} %>
92 <%= render :partial => 'changesets', :locals => { :changesets => @issue.changesets} %>
93 </div>
93 </div>
94 <% end %>
94 <% end %>
95
95
96 <% if @journals.any? %>
96 <% if @journals.any? %>
97 <div id="history">
97 <div id="history">
98 <h3><%=l(:label_history)%></h3>
98 <h3><%=l(:label_history)%></h3>
99 <%= render :partial => 'history', :locals => { :journals => @journals } %>
99 <%= render :partial => 'history', :locals => { :journals => @journals } %>
100 </div>
100 </div>
101 <% end %>
101 <% end %>
102 <div style="clear: both;"></div>
102 <div style="clear: both;"></div>
103
103
104 <% if authorize_for('issues', 'edit') %>
104 <% if authorize_for('issues', 'edit') %>
105 <div id="update" style="display:none;">
105 <div id="update" style="display:none;">
106 <h3><%= l(:button_update) %></h3>
106 <h3><%= l(:button_update) %></h3>
107 <%= render :partial => 'edit' %>
107 <%= render :partial => 'edit' %>
108 </div>
108 </div>
109 <% end %>
109 <% end %>
110
110
111 <p class="other-formats">
111 <p class="other-formats">
112 <%= l(:label_export_to) %>
112 <%= l(:label_export_to) %>
113 <span><%= link_to 'Atom', {:format => 'atom', :key => User.current.rss_key}, :class => 'feed' %></span>
113 <span><%= link_to 'Atom', {:format => 'atom', :key => User.current.rss_key}, :class => 'feed' %></span>
114 <span><%= link_to 'PDF', {:format => 'pdf'}, :class => 'pdf' %></span>
114 <span><%= link_to 'PDF', {:format => 'pdf'}, :class => 'pdf' %></span>
115 </p>
115 </p>
116
116
117 <% html_title "#{@issue.tracker.name} ##{@issue.id}: #{@issue.subject}" %>
117 <% html_title "#{@issue.tracker.name} ##{@issue.id}: #{@issue.subject}" %>
118
118
119 <% content_for :sidebar do %>
119 <% content_for :sidebar do %>
120 <%= render :partial => 'issues/sidebar' %>
120 <%= render :partial => 'issues/sidebar' %>
121 <% end %>
121 <% end %>
122
122
123 <% content_for :header_tags do %>
123 <% content_for :header_tags do %>
124 <%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@issue.project} - #{@issue.tracker} ##{@issue.id}: #{@issue.subject}") %>
124 <%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@issue.project} - #{@issue.tracker} ##{@issue.id}: #{@issue.subject}") %>
125 <%= stylesheet_link_tag 'scm' %>
125 <%= stylesheet_link_tag 'scm' %>
126 <% end %>
126 <% end %>
@@ -1,200 +1,206
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.get_values('IPRI').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.get_values('IPRI').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
37 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'test_create')
38 assert issue.save
39 assert issue.description.nil?
40 end
41
36 def test_create_with_required_custom_field
42 def test_create_with_required_custom_field
37 field = IssueCustomField.find_by_name('Database')
43 field = IssueCustomField.find_by_name('Database')
38 field.update_attribute(:is_required, true)
44 field.update_attribute(:is_required, true)
39
45
40 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')
41 assert issue.available_custom_fields.include?(field)
47 assert issue.available_custom_fields.include?(field)
42 # No value for the custom field
48 # No value for the custom field
43 assert !issue.save
49 assert !issue.save
44 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
50 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
45 # Blank value
51 # Blank value
46 issue.custom_field_values = { field.id => '' }
52 issue.custom_field_values = { field.id => '' }
47 assert !issue.save
53 assert !issue.save
48 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
54 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
49 # Invalid value
55 # Invalid value
50 issue.custom_field_values = { field.id => 'SQLServer' }
56 issue.custom_field_values = { field.id => 'SQLServer' }
51 assert !issue.save
57 assert !issue.save
52 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
58 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
53 # Valid value
59 # Valid value
54 issue.custom_field_values = { field.id => 'PostgreSQL' }
60 issue.custom_field_values = { field.id => 'PostgreSQL' }
55 assert issue.save
61 assert issue.save
56 issue.reload
62 issue.reload
57 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
63 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
58 end
64 end
59
65
60 def test_update_issue_with_required_custom_field
66 def test_update_issue_with_required_custom_field
61 field = IssueCustomField.find_by_name('Database')
67 field = IssueCustomField.find_by_name('Database')
62 field.update_attribute(:is_required, true)
68 field.update_attribute(:is_required, true)
63
69
64 issue = Issue.find(1)
70 issue = Issue.find(1)
65 assert_nil issue.custom_value_for(field)
71 assert_nil issue.custom_value_for(field)
66 assert issue.available_custom_fields.include?(field)
72 assert issue.available_custom_fields.include?(field)
67 # No change to custom values, issue can be saved
73 # No change to custom values, issue can be saved
68 assert issue.save
74 assert issue.save
69 # Blank value
75 # Blank value
70 issue.custom_field_values = { field.id => '' }
76 issue.custom_field_values = { field.id => '' }
71 assert !issue.save
77 assert !issue.save
72 # Valid value
78 # Valid value
73 issue.custom_field_values = { field.id => 'PostgreSQL' }
79 issue.custom_field_values = { field.id => 'PostgreSQL' }
74 assert issue.save
80 assert issue.save
75 issue.reload
81 issue.reload
76 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
82 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
77 end
83 end
78
84
79 def test_should_not_update_attributes_if_custom_fields_validation_fails
85 def test_should_not_update_attributes_if_custom_fields_validation_fails
80 issue = Issue.find(1)
86 issue = Issue.find(1)
81 field = IssueCustomField.find_by_name('Database')
87 field = IssueCustomField.find_by_name('Database')
82 assert issue.available_custom_fields.include?(field)
88 assert issue.available_custom_fields.include?(field)
83
89
84 issue.custom_field_values = { field.id => 'Invalid' }
90 issue.custom_field_values = { field.id => 'Invalid' }
85 issue.subject = 'Should be not be saved'
91 issue.subject = 'Should be not be saved'
86 assert !issue.save
92 assert !issue.save
87
93
88 issue.reload
94 issue.reload
89 assert_equal "Can't print recipes", issue.subject
95 assert_equal "Can't print recipes", issue.subject
90 end
96 end
91
97
92 def test_should_not_recreate_custom_values_objects_on_update
98 def test_should_not_recreate_custom_values_objects_on_update
93 field = IssueCustomField.find_by_name('Database')
99 field = IssueCustomField.find_by_name('Database')
94
100
95 issue = Issue.find(1)
101 issue = Issue.find(1)
96 issue.custom_field_values = { field.id => 'PostgreSQL' }
102 issue.custom_field_values = { field.id => 'PostgreSQL' }
97 assert issue.save
103 assert issue.save
98 custom_value = issue.custom_value_for(field)
104 custom_value = issue.custom_value_for(field)
99 issue.reload
105 issue.reload
100 issue.custom_field_values = { field.id => 'MySQL' }
106 issue.custom_field_values = { field.id => 'MySQL' }
101 assert issue.save
107 assert issue.save
102 issue.reload
108 issue.reload
103 assert_equal custom_value.id, issue.custom_value_for(field).id
109 assert_equal custom_value.id, issue.custom_value_for(field).id
104 end
110 end
105
111
106 def test_category_based_assignment
112 def test_category_based_assignment
107 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
113 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
108 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
114 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
109 end
115 end
110
116
111 def test_copy
117 def test_copy
112 issue = Issue.new.copy_from(1)
118 issue = Issue.new.copy_from(1)
113 assert issue.save
119 assert issue.save
114 issue.reload
120 issue.reload
115 orig = Issue.find(1)
121 orig = Issue.find(1)
116 assert_equal orig.subject, issue.subject
122 assert_equal orig.subject, issue.subject
117 assert_equal orig.tracker, issue.tracker
123 assert_equal orig.tracker, issue.tracker
118 assert_equal orig.custom_values.first.value, issue.custom_values.first.value
124 assert_equal orig.custom_values.first.value, issue.custom_values.first.value
119 end
125 end
120
126
121 def test_should_close_duplicates
127 def test_should_close_duplicates
122 # Create 3 issues
128 # Create 3 issues
123 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Duplicates test', :description => 'Duplicates test')
129 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Duplicates test', :description => 'Duplicates test')
124 assert issue1.save
130 assert issue1.save
125 issue2 = issue1.clone
131 issue2 = issue1.clone
126 assert issue2.save
132 assert issue2.save
127 issue3 = issue1.clone
133 issue3 = issue1.clone
128 assert issue3.save
134 assert issue3.save
129
135
130 # 2 is a dupe of 1
136 # 2 is a dupe of 1
131 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
137 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
132 # And 3 is a dupe of 2
138 # And 3 is a dupe of 2
133 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
139 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
134 # And 3 is a dupe of 1 (circular duplicates)
140 # And 3 is a dupe of 1 (circular duplicates)
135 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
141 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
136
142
137 assert issue1.reload.duplicates.include?(issue2)
143 assert issue1.reload.duplicates.include?(issue2)
138
144
139 # Closing issue 1
145 # Closing issue 1
140 issue1.init_journal(User.find(:first), "Closing issue1")
146 issue1.init_journal(User.find(:first), "Closing issue1")
141 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
147 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
142 assert issue1.save
148 assert issue1.save
143 # 2 and 3 should be also closed
149 # 2 and 3 should be also closed
144 assert issue2.reload.closed?
150 assert issue2.reload.closed?
145 assert issue3.reload.closed?
151 assert issue3.reload.closed?
146 end
152 end
147
153
148 def test_should_not_close_duplicated_issue
154 def test_should_not_close_duplicated_issue
149 # Create 3 issues
155 # Create 3 issues
150 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Duplicates test', :description => 'Duplicates test')
156 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Duplicates test', :description => 'Duplicates test')
151 assert issue1.save
157 assert issue1.save
152 issue2 = issue1.clone
158 issue2 = issue1.clone
153 assert issue2.save
159 assert issue2.save
154
160
155 # 2 is a dupe of 1
161 # 2 is a dupe of 1
156 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
162 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
157 # 2 is a dup of 1 but 1 is not a duplicate of 2
163 # 2 is a dup of 1 but 1 is not a duplicate of 2
158 assert !issue2.reload.duplicates.include?(issue1)
164 assert !issue2.reload.duplicates.include?(issue1)
159
165
160 # Closing issue 2
166 # Closing issue 2
161 issue2.init_journal(User.find(:first), "Closing issue2")
167 issue2.init_journal(User.find(:first), "Closing issue2")
162 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
168 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
163 assert issue2.save
169 assert issue2.save
164 # 1 should not be also closed
170 # 1 should not be also closed
165 assert !issue1.reload.closed?
171 assert !issue1.reload.closed?
166 end
172 end
167
173
168 def test_move_to_another_project_with_same_category
174 def test_move_to_another_project_with_same_category
169 issue = Issue.find(1)
175 issue = Issue.find(1)
170 assert issue.move_to(Project.find(2))
176 assert issue.move_to(Project.find(2))
171 issue.reload
177 issue.reload
172 assert_equal 2, issue.project_id
178 assert_equal 2, issue.project_id
173 # Category changes
179 # Category changes
174 assert_equal 4, issue.category_id
180 assert_equal 4, issue.category_id
175 # Make sure time entries were move to the target project
181 # Make sure time entries were move to the target project
176 assert_equal 2, issue.time_entries.first.project_id
182 assert_equal 2, issue.time_entries.first.project_id
177 end
183 end
178
184
179 def test_move_to_another_project_without_same_category
185 def test_move_to_another_project_without_same_category
180 issue = Issue.find(2)
186 issue = Issue.find(2)
181 assert issue.move_to(Project.find(2))
187 assert issue.move_to(Project.find(2))
182 issue.reload
188 issue.reload
183 assert_equal 2, issue.project_id
189 assert_equal 2, issue.project_id
184 # Category cleared
190 # Category cleared
185 assert_nil issue.category_id
191 assert_nil issue.category_id
186 end
192 end
187
193
188 def test_issue_destroy
194 def test_issue_destroy
189 Issue.find(1).destroy
195 Issue.find(1).destroy
190 assert_nil Issue.find_by_id(1)
196 assert_nil Issue.find_by_id(1)
191 assert_nil TimeEntry.find_by_issue_id(1)
197 assert_nil TimeEntry.find_by_issue_id(1)
192 end
198 end
193
199
194 def test_overdue
200 def test_overdue
195 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
201 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
196 assert !Issue.new(:due_date => Date.today).overdue?
202 assert !Issue.new(:due_date => Date.today).overdue?
197 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
203 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
198 assert !Issue.new(:due_date => nil).overdue?
204 assert !Issue.new(:due_date => nil).overdue?
199 end
205 end
200 end
206 end
General Comments 0
You need to be logged in to leave comments. Login now