##// END OF EJS Templates
Adds a css class (overdue) to overdue issues on issue lists and detail views (#2337)....
Jean-Philippe Lang -
r2138:2564f05037b0
parent child
Show More
@@ -1,194 +1,195
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 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 'csv'
18 require 'csv'
19
19
20 module IssuesHelper
20 module IssuesHelper
21 include ApplicationHelper
21 include ApplicationHelper
22
22
23 def render_issue_tooltip(issue)
23 def render_issue_tooltip(issue)
24 @cached_label_start_date ||= l(:field_start_date)
24 @cached_label_start_date ||= l(:field_start_date)
25 @cached_label_due_date ||= l(:field_due_date)
25 @cached_label_due_date ||= l(:field_due_date)
26 @cached_label_assigned_to ||= l(:field_assigned_to)
26 @cached_label_assigned_to ||= l(:field_assigned_to)
27 @cached_label_priority ||= l(:field_priority)
27 @cached_label_priority ||= l(:field_priority)
28
28
29 link_to_issue(issue) + ": #{h(issue.subject)}<br /><br />" +
29 link_to_issue(issue) + ": #{h(issue.subject)}<br /><br />" +
30 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />" +
30 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />" +
31 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />" +
31 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />" +
32 "<strong>#{@cached_label_assigned_to}</strong>: #{issue.assigned_to}<br />" +
32 "<strong>#{@cached_label_assigned_to}</strong>: #{issue.assigned_to}<br />" +
33 "<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
33 "<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
34 end
34 end
35
35
36 # Returns a string of css classes that apply to the given issue
36 # Returns a string of css classes that apply to the given issue
37 def css_issue_classes(issue)
37 def css_issue_classes(issue)
38 s = "issue status-#{issue.status.position} priority-#{issue.priority.position}"
38 s = "issue status-#{issue.status.position} priority-#{issue.priority.position}"
39 s << ' overdue' if issue.overdue?
39 s
40 s
40 end
41 end
41
42
42 def sidebar_queries
43 def sidebar_queries
43 unless @sidebar_queries
44 unless @sidebar_queries
44 # User can see public queries and his own queries
45 # User can see public queries and his own queries
45 visible = ARCondition.new(["is_public = ? OR user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
46 visible = ARCondition.new(["is_public = ? OR user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
46 # Project specific queries and global queries
47 # Project specific queries and global queries
47 visible << (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
48 visible << (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
48 @sidebar_queries = Query.find(:all,
49 @sidebar_queries = Query.find(:all,
49 :order => "name ASC",
50 :order => "name ASC",
50 :conditions => visible.conditions)
51 :conditions => visible.conditions)
51 end
52 end
52 @sidebar_queries
53 @sidebar_queries
53 end
54 end
54
55
55 def show_detail(detail, no_html=false)
56 def show_detail(detail, no_html=false)
56 case detail.property
57 case detail.property
57 when 'attr'
58 when 'attr'
58 label = l(("field_" + detail.prop_key.to_s.gsub(/\_id$/, "")).to_sym)
59 label = l(("field_" + detail.prop_key.to_s.gsub(/\_id$/, "")).to_sym)
59 case detail.prop_key
60 case detail.prop_key
60 when 'due_date', 'start_date'
61 when 'due_date', 'start_date'
61 value = format_date(detail.value.to_date) if detail.value
62 value = format_date(detail.value.to_date) if detail.value
62 old_value = format_date(detail.old_value.to_date) if detail.old_value
63 old_value = format_date(detail.old_value.to_date) if detail.old_value
63 when 'project_id'
64 when 'project_id'
64 p = Project.find_by_id(detail.value) and value = p.name if detail.value
65 p = Project.find_by_id(detail.value) and value = p.name if detail.value
65 p = Project.find_by_id(detail.old_value) and old_value = p.name if detail.old_value
66 p = Project.find_by_id(detail.old_value) and old_value = p.name if detail.old_value
66 when 'status_id'
67 when 'status_id'
67 s = IssueStatus.find_by_id(detail.value) and value = s.name if detail.value
68 s = IssueStatus.find_by_id(detail.value) and value = s.name if detail.value
68 s = IssueStatus.find_by_id(detail.old_value) and old_value = s.name if detail.old_value
69 s = IssueStatus.find_by_id(detail.old_value) and old_value = s.name if detail.old_value
69 when 'tracker_id'
70 when 'tracker_id'
70 t = Tracker.find_by_id(detail.value) and value = t.name if detail.value
71 t = Tracker.find_by_id(detail.value) and value = t.name if detail.value
71 t = Tracker.find_by_id(detail.old_value) and old_value = t.name if detail.old_value
72 t = Tracker.find_by_id(detail.old_value) and old_value = t.name if detail.old_value
72 when 'assigned_to_id'
73 when 'assigned_to_id'
73 u = User.find_by_id(detail.value) and value = u.name if detail.value
74 u = User.find_by_id(detail.value) and value = u.name if detail.value
74 u = User.find_by_id(detail.old_value) and old_value = u.name if detail.old_value
75 u = User.find_by_id(detail.old_value) and old_value = u.name if detail.old_value
75 when 'priority_id'
76 when 'priority_id'
76 e = Enumeration.find_by_id(detail.value) and value = e.name if detail.value
77 e = Enumeration.find_by_id(detail.value) and value = e.name if detail.value
77 e = Enumeration.find_by_id(detail.old_value) and old_value = e.name if detail.old_value
78 e = Enumeration.find_by_id(detail.old_value) and old_value = e.name if detail.old_value
78 when 'category_id'
79 when 'category_id'
79 c = IssueCategory.find_by_id(detail.value) and value = c.name if detail.value
80 c = IssueCategory.find_by_id(detail.value) and value = c.name if detail.value
80 c = IssueCategory.find_by_id(detail.old_value) and old_value = c.name if detail.old_value
81 c = IssueCategory.find_by_id(detail.old_value) and old_value = c.name if detail.old_value
81 when 'fixed_version_id'
82 when 'fixed_version_id'
82 v = Version.find_by_id(detail.value) and value = v.name if detail.value
83 v = Version.find_by_id(detail.value) and value = v.name if detail.value
83 v = Version.find_by_id(detail.old_value) and old_value = v.name if detail.old_value
84 v = Version.find_by_id(detail.old_value) and old_value = v.name if detail.old_value
84 when 'estimated_hours'
85 when 'estimated_hours'
85 value = "%0.02f" % detail.value.to_f unless detail.value.blank?
86 value = "%0.02f" % detail.value.to_f unless detail.value.blank?
86 old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
87 old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
87 end
88 end
88 when 'cf'
89 when 'cf'
89 custom_field = CustomField.find_by_id(detail.prop_key)
90 custom_field = CustomField.find_by_id(detail.prop_key)
90 if custom_field
91 if custom_field
91 label = custom_field.name
92 label = custom_field.name
92 value = format_value(detail.value, custom_field.field_format) if detail.value
93 value = format_value(detail.value, custom_field.field_format) if detail.value
93 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
94 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
94 end
95 end
95 when 'attachment'
96 when 'attachment'
96 label = l(:label_attachment)
97 label = l(:label_attachment)
97 end
98 end
98 call_hook(:helper_issues_show_detail_after_setting, {:detail => detail, :label => label, :value => value, :old_value => old_value })
99 call_hook(:helper_issues_show_detail_after_setting, {:detail => detail, :label => label, :value => value, :old_value => old_value })
99
100
100 label ||= detail.prop_key
101 label ||= detail.prop_key
101 value ||= detail.value
102 value ||= detail.value
102 old_value ||= detail.old_value
103 old_value ||= detail.old_value
103
104
104 unless no_html
105 unless no_html
105 label = content_tag('strong', label)
106 label = content_tag('strong', label)
106 old_value = content_tag("i", h(old_value)) if detail.old_value
107 old_value = content_tag("i", h(old_value)) if detail.old_value
107 old_value = content_tag("strike", old_value) if detail.old_value and (!detail.value or detail.value.empty?)
108 old_value = content_tag("strike", old_value) if detail.old_value and (!detail.value or detail.value.empty?)
108 if detail.property == 'attachment' && !value.blank? && a = Attachment.find_by_id(detail.prop_key)
109 if detail.property == 'attachment' && !value.blank? && a = Attachment.find_by_id(detail.prop_key)
109 # Link to the attachment if it has not been removed
110 # Link to the attachment if it has not been removed
110 value = link_to_attachment(a)
111 value = link_to_attachment(a)
111 else
112 else
112 value = content_tag("i", h(value)) if value
113 value = content_tag("i", h(value)) if value
113 end
114 end
114 end
115 end
115
116
116 if !detail.value.blank?
117 if !detail.value.blank?
117 case detail.property
118 case detail.property
118 when 'attr', 'cf'
119 when 'attr', 'cf'
119 if !detail.old_value.blank?
120 if !detail.old_value.blank?
120 label + " " + l(:text_journal_changed, old_value, value)
121 label + " " + l(:text_journal_changed, old_value, value)
121 else
122 else
122 label + " " + l(:text_journal_set_to, value)
123 label + " " + l(:text_journal_set_to, value)
123 end
124 end
124 when 'attachment'
125 when 'attachment'
125 "#{label} #{value} #{l(:label_added)}"
126 "#{label} #{value} #{l(:label_added)}"
126 end
127 end
127 else
128 else
128 case detail.property
129 case detail.property
129 when 'attr', 'cf'
130 when 'attr', 'cf'
130 label + " " + l(:text_journal_deleted) + " (#{old_value})"
131 label + " " + l(:text_journal_deleted) + " (#{old_value})"
131 when 'attachment'
132 when 'attachment'
132 "#{label} #{old_value} #{l(:label_deleted)}"
133 "#{label} #{old_value} #{l(:label_deleted)}"
133 end
134 end
134 end
135 end
135 end
136 end
136
137
137 def issues_to_csv(issues, project = nil)
138 def issues_to_csv(issues, project = nil)
138 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
139 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
139 decimal_separator = l(:general_csv_decimal_separator)
140 decimal_separator = l(:general_csv_decimal_separator)
140 export = StringIO.new
141 export = StringIO.new
141 CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
142 CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
142 # csv header fields
143 # csv header fields
143 headers = [ "#",
144 headers = [ "#",
144 l(:field_status),
145 l(:field_status),
145 l(:field_project),
146 l(:field_project),
146 l(:field_tracker),
147 l(:field_tracker),
147 l(:field_priority),
148 l(:field_priority),
148 l(:field_subject),
149 l(:field_subject),
149 l(:field_assigned_to),
150 l(:field_assigned_to),
150 l(:field_category),
151 l(:field_category),
151 l(:field_fixed_version),
152 l(:field_fixed_version),
152 l(:field_author),
153 l(:field_author),
153 l(:field_start_date),
154 l(:field_start_date),
154 l(:field_due_date),
155 l(:field_due_date),
155 l(:field_done_ratio),
156 l(:field_done_ratio),
156 l(:field_estimated_hours),
157 l(:field_estimated_hours),
157 l(:field_created_on),
158 l(:field_created_on),
158 l(:field_updated_on)
159 l(:field_updated_on)
159 ]
160 ]
160 # Export project custom fields if project is given
161 # Export project custom fields if project is given
161 # otherwise export custom fields marked as "For all projects"
162 # otherwise export custom fields marked as "For all projects"
162 custom_fields = project.nil? ? IssueCustomField.for_all : project.all_issue_custom_fields
163 custom_fields = project.nil? ? IssueCustomField.for_all : project.all_issue_custom_fields
163 custom_fields.each {|f| headers << f.name}
164 custom_fields.each {|f| headers << f.name}
164 # Description in the last column
165 # Description in the last column
165 headers << l(:field_description)
166 headers << l(:field_description)
166 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
167 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
167 # csv lines
168 # csv lines
168 issues.each do |issue|
169 issues.each do |issue|
169 fields = [issue.id,
170 fields = [issue.id,
170 issue.status.name,
171 issue.status.name,
171 issue.project.name,
172 issue.project.name,
172 issue.tracker.name,
173 issue.tracker.name,
173 issue.priority.name,
174 issue.priority.name,
174 issue.subject,
175 issue.subject,
175 issue.assigned_to,
176 issue.assigned_to,
176 issue.category,
177 issue.category,
177 issue.fixed_version,
178 issue.fixed_version,
178 issue.author.name,
179 issue.author.name,
179 format_date(issue.start_date),
180 format_date(issue.start_date),
180 format_date(issue.due_date),
181 format_date(issue.due_date),
181 issue.done_ratio,
182 issue.done_ratio,
182 issue.estimated_hours.to_s.gsub('.', decimal_separator),
183 issue.estimated_hours.to_s.gsub('.', decimal_separator),
183 format_time(issue.created_on),
184 format_time(issue.created_on),
184 format_time(issue.updated_on)
185 format_time(issue.updated_on)
185 ]
186 ]
186 custom_fields.each {|f| fields << show_value(issue.custom_value_for(f)) }
187 custom_fields.each {|f| fields << show_value(issue.custom_value_for(f)) }
187 fields << issue.description
188 fields << issue.description
188 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
189 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
189 end
190 end
190 end
191 end
191 export.rewind
192 export.rewind
192 export
193 export
193 end
194 end
194 end
195 end
@@ -1,275 +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, :description, :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
199 def overdue?
200 !due_date.nil? && (due_date < Date.today)
201 end
202
198 # Users the issue can be assigned to
203 # Users the issue can be assigned to
199 def assignable_users
204 def assignable_users
200 project.assignable_users
205 project.assignable_users
201 end
206 end
202
207
203 # Returns an array of status that user is able to apply
208 # Returns an array of status that user is able to apply
204 def new_statuses_allowed_to(user)
209 def new_statuses_allowed_to(user)
205 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)
206 statuses << status unless statuses.empty?
211 statuses << status unless statuses.empty?
207 statuses.uniq.sort
212 statuses.uniq.sort
208 end
213 end
209
214
210 # 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
211 def recipients
216 def recipients
212 recipients = project.recipients
217 recipients = project.recipients
213 # Author and assignee are always notified unless they have been locked
218 # Author and assignee are always notified unless they have been locked
214 recipients << author.mail if author && author.active?
219 recipients << author.mail if author && author.active?
215 recipients << assigned_to.mail if assigned_to && assigned_to.active?
220 recipients << assigned_to.mail if assigned_to && assigned_to.active?
216 recipients.compact.uniq
221 recipients.compact.uniq
217 end
222 end
218
223
219 def spent_hours
224 def spent_hours
220 @spent_hours ||= time_entries.sum(:hours) || 0
225 @spent_hours ||= time_entries.sum(:hours) || 0
221 end
226 end
222
227
223 def relations
228 def relations
224 (relations_from + relations_to).sort
229 (relations_from + relations_to).sort
225 end
230 end
226
231
227 def all_dependent_issues
232 def all_dependent_issues
228 dependencies = []
233 dependencies = []
229 relations_from.each do |relation|
234 relations_from.each do |relation|
230 dependencies << relation.issue_to
235 dependencies << relation.issue_to
231 dependencies += relation.issue_to.all_dependent_issues
236 dependencies += relation.issue_to.all_dependent_issues
232 end
237 end
233 dependencies
238 dependencies
234 end
239 end
235
240
236 # Returns an array of issues that duplicate this one
241 # Returns an array of issues that duplicate this one
237 def duplicates
242 def duplicates
238 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}
239 end
244 end
240
245
241 # Returns the due date or the target due date if any
246 # Returns the due date or the target due date if any
242 # Used on gantt chart
247 # Used on gantt chart
243 def due_before
248 def due_before
244 due_date || (fixed_version ? fixed_version.effective_date : nil)
249 due_date || (fixed_version ? fixed_version.effective_date : nil)
245 end
250 end
246
251
247 def duration
252 def duration
248 (start_date && due_date) ? due_date - start_date : 0
253 (start_date && due_date) ? due_date - start_date : 0
249 end
254 end
250
255
251 def soonest_start
256 def soonest_start
252 @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
253 end
258 end
254
259
255 def self.visible_by(usr)
260 def self.visible_by(usr)
256 with_scope(:find => { :conditions => Project.visible_by(usr) }) do
261 with_scope(:find => { :conditions => Project.visible_by(usr) }) do
257 yield
262 yield
258 end
263 end
259 end
264 end
260
265
261 def to_s
266 def to_s
262 "#{tracker} ##{id}: #{subject}"
267 "#{tracker} ##{id}: #{subject}"
263 end
268 end
264
269
265 private
270 private
266
271
267 # Callback on attachment deletion
272 # Callback on attachment deletion
268 def attachment_removed(obj)
273 def attachment_removed(obj)
269 journal = init_journal(User.current)
274 journal = init_journal(User.current)
270 journal.details << JournalDetail.new(:property => 'attachment',
275 journal.details << JournalDetail.new(:property => 'attachment',
271 :prop_key => obj.id,
276 :prop_key => obj.id,
272 :old_value => obj.filename)
277 :old_value => obj.filename)
273 journal.save
278 journal.save
274 end
279 end
275 end
280 end
@@ -1,193 +1,200
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_with_required_custom_field
36 def test_create_with_required_custom_field
37 field = IssueCustomField.find_by_name('Database')
37 field = IssueCustomField.find_by_name('Database')
38 field.update_attribute(:is_required, true)
38 field.update_attribute(:is_required, true)
39
39
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')
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')
41 assert issue.available_custom_fields.include?(field)
41 assert issue.available_custom_fields.include?(field)
42 # No value for the custom field
42 # No value for the custom field
43 assert !issue.save
43 assert !issue.save
44 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
44 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
45 # Blank value
45 # Blank value
46 issue.custom_field_values = { field.id => '' }
46 issue.custom_field_values = { field.id => '' }
47 assert !issue.save
47 assert !issue.save
48 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
48 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
49 # Invalid value
49 # Invalid value
50 issue.custom_field_values = { field.id => 'SQLServer' }
50 issue.custom_field_values = { field.id => 'SQLServer' }
51 assert !issue.save
51 assert !issue.save
52 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
52 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
53 # Valid value
53 # Valid value
54 issue.custom_field_values = { field.id => 'PostgreSQL' }
54 issue.custom_field_values = { field.id => 'PostgreSQL' }
55 assert issue.save
55 assert issue.save
56 issue.reload
56 issue.reload
57 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
57 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
58 end
58 end
59
59
60 def test_update_issue_with_required_custom_field
60 def test_update_issue_with_required_custom_field
61 field = IssueCustomField.find_by_name('Database')
61 field = IssueCustomField.find_by_name('Database')
62 field.update_attribute(:is_required, true)
62 field.update_attribute(:is_required, true)
63
63
64 issue = Issue.find(1)
64 issue = Issue.find(1)
65 assert_nil issue.custom_value_for(field)
65 assert_nil issue.custom_value_for(field)
66 assert issue.available_custom_fields.include?(field)
66 assert issue.available_custom_fields.include?(field)
67 # No change to custom values, issue can be saved
67 # No change to custom values, issue can be saved
68 assert issue.save
68 assert issue.save
69 # Blank value
69 # Blank value
70 issue.custom_field_values = { field.id => '' }
70 issue.custom_field_values = { field.id => '' }
71 assert !issue.save
71 assert !issue.save
72 # Valid value
72 # Valid value
73 issue.custom_field_values = { field.id => 'PostgreSQL' }
73 issue.custom_field_values = { field.id => 'PostgreSQL' }
74 assert issue.save
74 assert issue.save
75 issue.reload
75 issue.reload
76 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
76 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
77 end
77 end
78
78
79 def test_should_not_update_attributes_if_custom_fields_validation_fails
79 def test_should_not_update_attributes_if_custom_fields_validation_fails
80 issue = Issue.find(1)
80 issue = Issue.find(1)
81 field = IssueCustomField.find_by_name('Database')
81 field = IssueCustomField.find_by_name('Database')
82 assert issue.available_custom_fields.include?(field)
82 assert issue.available_custom_fields.include?(field)
83
83
84 issue.custom_field_values = { field.id => 'Invalid' }
84 issue.custom_field_values = { field.id => 'Invalid' }
85 issue.subject = 'Should be not be saved'
85 issue.subject = 'Should be not be saved'
86 assert !issue.save
86 assert !issue.save
87
87
88 issue.reload
88 issue.reload
89 assert_equal "Can't print recipes", issue.subject
89 assert_equal "Can't print recipes", issue.subject
90 end
90 end
91
91
92 def test_should_not_recreate_custom_values_objects_on_update
92 def test_should_not_recreate_custom_values_objects_on_update
93 field = IssueCustomField.find_by_name('Database')
93 field = IssueCustomField.find_by_name('Database')
94
94
95 issue = Issue.find(1)
95 issue = Issue.find(1)
96 issue.custom_field_values = { field.id => 'PostgreSQL' }
96 issue.custom_field_values = { field.id => 'PostgreSQL' }
97 assert issue.save
97 assert issue.save
98 custom_value = issue.custom_value_for(field)
98 custom_value = issue.custom_value_for(field)
99 issue.reload
99 issue.reload
100 issue.custom_field_values = { field.id => 'MySQL' }
100 issue.custom_field_values = { field.id => 'MySQL' }
101 assert issue.save
101 assert issue.save
102 issue.reload
102 issue.reload
103 assert_equal custom_value.id, issue.custom_value_for(field).id
103 assert_equal custom_value.id, issue.custom_value_for(field).id
104 end
104 end
105
105
106 def test_category_based_assignment
106 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)
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)
108 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
108 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
109 end
109 end
110
110
111 def test_copy
111 def test_copy
112 issue = Issue.new.copy_from(1)
112 issue = Issue.new.copy_from(1)
113 assert issue.save
113 assert issue.save
114 issue.reload
114 issue.reload
115 orig = Issue.find(1)
115 orig = Issue.find(1)
116 assert_equal orig.subject, issue.subject
116 assert_equal orig.subject, issue.subject
117 assert_equal orig.tracker, issue.tracker
117 assert_equal orig.tracker, issue.tracker
118 assert_equal orig.custom_values.first.value, issue.custom_values.first.value
118 assert_equal orig.custom_values.first.value, issue.custom_values.first.value
119 end
119 end
120
120
121 def test_should_close_duplicates
121 def test_should_close_duplicates
122 # Create 3 issues
122 # 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')
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')
124 assert issue1.save
124 assert issue1.save
125 issue2 = issue1.clone
125 issue2 = issue1.clone
126 assert issue2.save
126 assert issue2.save
127 issue3 = issue1.clone
127 issue3 = issue1.clone
128 assert issue3.save
128 assert issue3.save
129
129
130 # 2 is a dupe of 1
130 # 2 is a dupe of 1
131 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
131 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
132 # And 3 is a dupe of 2
132 # And 3 is a dupe of 2
133 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
133 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
134 # And 3 is a dupe of 1 (circular duplicates)
134 # And 3 is a dupe of 1 (circular duplicates)
135 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
135 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
136
136
137 assert issue1.reload.duplicates.include?(issue2)
137 assert issue1.reload.duplicates.include?(issue2)
138
138
139 # Closing issue 1
139 # Closing issue 1
140 issue1.init_journal(User.find(:first), "Closing issue1")
140 issue1.init_journal(User.find(:first), "Closing issue1")
141 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
141 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
142 assert issue1.save
142 assert issue1.save
143 # 2 and 3 should be also closed
143 # 2 and 3 should be also closed
144 assert issue2.reload.closed?
144 assert issue2.reload.closed?
145 assert issue3.reload.closed?
145 assert issue3.reload.closed?
146 end
146 end
147
147
148 def test_should_not_close_duplicated_issue
148 def test_should_not_close_duplicated_issue
149 # Create 3 issues
149 # 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')
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')
151 assert issue1.save
151 assert issue1.save
152 issue2 = issue1.clone
152 issue2 = issue1.clone
153 assert issue2.save
153 assert issue2.save
154
154
155 # 2 is a dupe of 1
155 # 2 is a dupe of 1
156 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
156 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
157 # 2 is a dup of 1 but 1 is not a duplicate of 2
158 assert !issue2.reload.duplicates.include?(issue1)
158 assert !issue2.reload.duplicates.include?(issue1)
159
159
160 # Closing issue 2
160 # Closing issue 2
161 issue2.init_journal(User.find(:first), "Closing issue2")
161 issue2.init_journal(User.find(:first), "Closing issue2")
162 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
162 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
163 assert issue2.save
163 assert issue2.save
164 # 1 should not be also closed
164 # 1 should not be also closed
165 assert !issue1.reload.closed?
165 assert !issue1.reload.closed?
166 end
166 end
167
167
168 def test_move_to_another_project_with_same_category
168 def test_move_to_another_project_with_same_category
169 issue = Issue.find(1)
169 issue = Issue.find(1)
170 assert issue.move_to(Project.find(2))
170 assert issue.move_to(Project.find(2))
171 issue.reload
171 issue.reload
172 assert_equal 2, issue.project_id
172 assert_equal 2, issue.project_id
173 # Category changes
173 # Category changes
174 assert_equal 4, issue.category_id
174 assert_equal 4, issue.category_id
175 # Make sure time entries were move to the target project
175 # Make sure time entries were move to the target project
176 assert_equal 2, issue.time_entries.first.project_id
176 assert_equal 2, issue.time_entries.first.project_id
177 end
177 end
178
178
179 def test_move_to_another_project_without_same_category
179 def test_move_to_another_project_without_same_category
180 issue = Issue.find(2)
180 issue = Issue.find(2)
181 assert issue.move_to(Project.find(2))
181 assert issue.move_to(Project.find(2))
182 issue.reload
182 issue.reload
183 assert_equal 2, issue.project_id
183 assert_equal 2, issue.project_id
184 # Category cleared
184 # Category cleared
185 assert_nil issue.category_id
185 assert_nil issue.category_id
186 end
186 end
187
187
188 def test_issue_destroy
188 def test_issue_destroy
189 Issue.find(1).destroy
189 Issue.find(1).destroy
190 assert_nil Issue.find_by_id(1)
190 assert_nil Issue.find_by_id(1)
191 assert_nil TimeEntry.find_by_issue_id(1)
191 assert_nil TimeEntry.find_by_issue_id(1)
192 end
192 end
193
194 def test_overdue
195 assert Issue.new(:due_date => 1.day.ago).overdue?
196 assert !Issue.new(:due_date => Date.today).overdue?
197 assert !Issue.new(:due_date => 1.day.from_now).overdue?
198 assert !Issue.new(:due_date => nil).overdue?
199 end
193 end
200 end
General Comments 0
You need to be logged in to leave comments. Login now