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