##// END OF EJS Templates
Replaces Enumeration.get_values and Enumeration.default with named scopes....
Jean-Philippe Lang -
r2411:4601ed2f3aeb
parent child
Show More
@@ -1,88 +1,88
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 DocumentsController < ApplicationController
19 19 before_filter :find_project, :only => [:index, :new]
20 20 before_filter :find_document, :except => [:index, :new]
21 21 before_filter :authorize
22 22
23 23 helper :attachments
24 24
25 25 def index
26 26 @sort_by = %w(category date title author).include?(params[:sort_by]) ? params[:sort_by] : 'category'
27 27 documents = @project.documents.find :all, :include => [:attachments, :category]
28 28 case @sort_by
29 29 when 'date'
30 30 @grouped = documents.group_by {|d| d.created_on.to_date }
31 31 when 'title'
32 32 @grouped = documents.group_by {|d| d.title.first.upcase}
33 33 when 'author'
34 34 @grouped = documents.select{|d| d.attachments.any?}.group_by {|d| d.attachments.last.author}
35 35 else
36 36 @grouped = documents.group_by(&:category)
37 37 end
38 38 @document = @project.documents.build
39 39 render :layout => false if request.xhr?
40 40 end
41 41
42 42 def show
43 43 @attachments = @document.attachments.find(:all, :order => "created_on DESC")
44 44 end
45 45
46 46 def new
47 47 @document = @project.documents.build(params[:document])
48 48 if request.post? and @document.save
49 49 attach_files(@document, params[:attachments])
50 50 flash[:notice] = l(:notice_successful_create)
51 51 Mailer.deliver_document_added(@document) if Setting.notified_events.include?('document_added')
52 52 redirect_to :action => 'index', :project_id => @project
53 53 end
54 54 end
55 55
56 56 def edit
57 @categories = Enumeration::get_values('DCAT')
57 @categories = Enumeration.document_categories
58 58 if request.post? and @document.update_attributes(params[:document])
59 59 flash[:notice] = l(:notice_successful_update)
60 60 redirect_to :action => 'show', :id => @document
61 61 end
62 62 end
63 63
64 64 def destroy
65 65 @document.destroy
66 66 redirect_to :controller => 'documents', :action => 'index', :project_id => @project
67 67 end
68 68
69 69 def add_attachment
70 70 attachments = attach_files(@document, params[:attachments])
71 71 Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('document_added')
72 72 redirect_to :action => 'show', :id => @document
73 73 end
74 74
75 75 private
76 76 def find_project
77 77 @project = Project.find(params[:project_id])
78 78 rescue ActiveRecord::RecordNotFound
79 79 render_404
80 80 end
81 81
82 82 def find_document
83 83 @document = Document.find(params[:id])
84 84 @project = @document.project
85 85 rescue ActiveRecord::RecordNotFound
86 86 render_404
87 87 end
88 88 end
@@ -1,93 +1,93
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 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 EnumerationsController < ApplicationController
19 19 before_filter :require_admin
20 20
21 21 def index
22 22 list
23 23 render :action => 'list'
24 24 end
25 25
26 26 # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)
27 27 verify :method => :post, :only => [ :destroy, :create, :update ],
28 28 :redirect_to => { :action => :list }
29 29
30 30 def list
31 31 end
32 32
33 33 def new
34 34 @enumeration = Enumeration.new(:opt => params[:opt])
35 35 end
36 36
37 37 def create
38 38 @enumeration = Enumeration.new(params[:enumeration])
39 39 if @enumeration.save
40 40 flash[:notice] = l(:notice_successful_create)
41 41 redirect_to :action => 'list', :opt => @enumeration.opt
42 42 else
43 43 render :action => 'new'
44 44 end
45 45 end
46 46
47 47 def edit
48 48 @enumeration = Enumeration.find(params[:id])
49 49 end
50 50
51 51 def update
52 52 @enumeration = Enumeration.find(params[:id])
53 53 if @enumeration.update_attributes(params[:enumeration])
54 54 flash[:notice] = l(:notice_successful_update)
55 55 redirect_to :action => 'list', :opt => @enumeration.opt
56 56 else
57 57 render :action => 'edit'
58 58 end
59 59 end
60 60
61 61 def move
62 62 @enumeration = Enumeration.find(params[:id])
63 63 case params[:position]
64 64 when 'highest'
65 65 @enumeration.move_to_top
66 66 when 'higher'
67 67 @enumeration.move_higher
68 68 when 'lower'
69 69 @enumeration.move_lower
70 70 when 'lowest'
71 71 @enumeration.move_to_bottom
72 72 end if params[:position]
73 73 redirect_to :action => 'index'
74 74 end
75 75
76 76 def destroy
77 77 @enumeration = Enumeration.find(params[:id])
78 78 if !@enumeration.in_use?
79 79 # No associated objects
80 80 @enumeration.destroy
81 81 redirect_to :action => 'index'
82 82 elsif params[:reassign_to_id]
83 83 if reassign_to = Enumeration.find_by_opt_and_id(@enumeration.opt, params[:reassign_to_id])
84 84 @enumeration.destroy(reassign_to)
85 85 redirect_to :action => 'index'
86 86 end
87 87 end
88 @enumerations = Enumeration.get_values(@enumeration.opt) - [@enumeration]
88 @enumerations = Enumeration.values(@enumeration.opt) - [@enumeration]
89 89 #rescue
90 90 # flash[:error] = 'Unable to delete enumeration'
91 91 # redirect_to :action => 'index'
92 92 end
93 93 end
@@ -1,492 +1,492
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2008 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 IssuesController < ApplicationController
19 19 menu_item :new_issue, :only => :new
20 20
21 21 before_filter :find_issue, :only => [:show, :edit, :reply]
22 22 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
23 23 before_filter :find_project, :only => [:new, :update_form, :preview]
24 24 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu]
25 25 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
26 26 accept_key_auth :index, :changes
27 27
28 28 helper :journals
29 29 helper :projects
30 30 include ProjectsHelper
31 31 helper :custom_fields
32 32 include CustomFieldsHelper
33 33 helper :issue_relations
34 34 include IssueRelationsHelper
35 35 helper :watchers
36 36 include WatchersHelper
37 37 helper :attachments
38 38 include AttachmentsHelper
39 39 helper :queries
40 40 helper :sort
41 41 include SortHelper
42 42 include IssuesHelper
43 43 helper :timelog
44 44 include Redmine::Export::PDF
45 45
46 46 def index
47 47 retrieve_query
48 48 sort_init 'id', 'desc'
49 49 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
50 50
51 51 if @query.valid?
52 52 limit = per_page_option
53 53 respond_to do |format|
54 54 format.html { }
55 55 format.atom { }
56 56 format.csv { limit = Setting.issues_export_limit.to_i }
57 57 format.pdf { limit = Setting.issues_export_limit.to_i }
58 58 end
59 59 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
60 60 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
61 61 @issues = Issue.find :all, :order => sort_clause,
62 62 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
63 63 :conditions => @query.statement,
64 64 :limit => limit,
65 65 :offset => @issue_pages.current.offset
66 66 respond_to do |format|
67 67 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
68 68 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
69 69 format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
70 70 format.pdf { send_data(issues_to_pdf(@issues, @project), :type => 'application/pdf', :filename => 'export.pdf') }
71 71 end
72 72 else
73 73 # Send html if the query is not valid
74 74 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
75 75 end
76 76 rescue ActiveRecord::RecordNotFound
77 77 render_404
78 78 end
79 79
80 80 def changes
81 81 retrieve_query
82 82 sort_init 'id', 'desc'
83 83 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
84 84
85 85 if @query.valid?
86 86 @journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
87 87 :conditions => @query.statement,
88 88 :limit => 25,
89 89 :order => "#{Journal.table_name}.created_on DESC"
90 90 end
91 91 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
92 92 render :layout => false, :content_type => 'application/atom+xml'
93 93 rescue ActiveRecord::RecordNotFound
94 94 render_404
95 95 end
96 96
97 97 def show
98 98 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
99 99 @journals.each_with_index {|j,i| j.indice = i+1}
100 100 @journals.reverse! if User.current.wants_comments_in_reverse_order?
101 101 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
102 102 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
103 @priorities = Enumeration::get_values('IPRI')
103 @priorities = Enumeration.priorities
104 104 @time_entry = TimeEntry.new
105 105 respond_to do |format|
106 106 format.html { render :template => 'issues/show.rhtml' }
107 107 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
108 108 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
109 109 end
110 110 end
111 111
112 112 # Add a new issue
113 113 # The new issue will be created from an existing one if copy_from parameter is given
114 114 def new
115 115 @issue = Issue.new
116 116 @issue.copy_from(params[:copy_from]) if params[:copy_from]
117 117 @issue.project = @project
118 118 # Tracker must be set before custom field values
119 119 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
120 120 if @issue.tracker.nil?
121 121 flash.now[:error] = 'No tracker is associated to this project. Please check the Project settings.'
122 122 render :nothing => true, :layout => true
123 123 return
124 124 end
125 125 if params[:issue].is_a?(Hash)
126 126 @issue.attributes = params[:issue]
127 127 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
128 128 end
129 129 @issue.author = User.current
130 130
131 131 default_status = IssueStatus.default
132 132 unless default_status
133 133 flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
134 134 render :nothing => true, :layout => true
135 135 return
136 136 end
137 137 @issue.status = default_status
138 138 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)).uniq
139 139
140 140 if request.get? || request.xhr?
141 141 @issue.start_date ||= Date.today
142 142 else
143 143 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
144 144 # Check that the user is allowed to apply the requested status
145 145 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
146 146 if @issue.save
147 147 attach_files(@issue, params[:attachments])
148 148 flash[:notice] = l(:notice_successful_create)
149 149 Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
150 150 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
151 151 redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
152 152 { :action => 'show', :id => @issue })
153 153 return
154 154 end
155 155 end
156 @priorities = Enumeration::get_values('IPRI')
156 @priorities = Enumeration.priorities
157 157 render :layout => !request.xhr?
158 158 end
159 159
160 160 # Attributes that can be updated on workflow transition (without :edit permission)
161 161 # TODO: make it configurable (at least per role)
162 162 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
163 163
164 164 def edit
165 165 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
166 @priorities = Enumeration::get_values('IPRI')
166 @priorities = Enumeration.priorities
167 167 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
168 168 @time_entry = TimeEntry.new
169 169
170 170 @notes = params[:notes]
171 171 journal = @issue.init_journal(User.current, @notes)
172 172 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
173 173 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
174 174 attrs = params[:issue].dup
175 175 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
176 176 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
177 177 @issue.attributes = attrs
178 178 end
179 179
180 180 if request.post?
181 181 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
182 182 @time_entry.attributes = params[:time_entry]
183 183 attachments = attach_files(@issue, params[:attachments])
184 184 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
185 185
186 186 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
187 187
188 188 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
189 189 # Log spend time
190 190 if User.current.allowed_to?(:log_time, @project)
191 191 @time_entry.save
192 192 end
193 193 if !journal.new_record?
194 194 # Only send notification if something was actually changed
195 195 flash[:notice] = l(:notice_successful_update)
196 196 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
197 197 end
198 198 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
199 199 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
200 200 end
201 201 end
202 202 rescue ActiveRecord::StaleObjectError
203 203 # Optimistic locking exception
204 204 flash.now[:error] = l(:notice_locking_conflict)
205 205 end
206 206
207 207 def reply
208 208 journal = Journal.find(params[:journal_id]) if params[:journal_id]
209 209 if journal
210 210 user = journal.user
211 211 text = journal.notes
212 212 else
213 213 user = @issue.author
214 214 text = @issue.description
215 215 end
216 216 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
217 217 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
218 218 render(:update) { |page|
219 219 page.<< "$('notes').value = \"#{content}\";"
220 220 page.show 'update'
221 221 page << "Form.Element.focus('notes');"
222 222 page << "Element.scrollTo('update');"
223 223 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
224 224 }
225 225 end
226 226
227 227 # Bulk edit a set of issues
228 228 def bulk_edit
229 229 if request.post?
230 230 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
231 231 priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
232 232 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
233 233 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
234 234 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
235 235 custom_field_values = params[:custom_field_values] ? params[:custom_field_values].reject {|k,v| v.blank?} : nil
236 236
237 237 unsaved_issue_ids = []
238 238 @issues.each do |issue|
239 239 journal = issue.init_journal(User.current, params[:notes])
240 240 issue.priority = priority if priority
241 241 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
242 242 issue.category = category if category || params[:category_id] == 'none'
243 243 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
244 244 issue.start_date = params[:start_date] unless params[:start_date].blank?
245 245 issue.due_date = params[:due_date] unless params[:due_date].blank?
246 246 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
247 247 issue.custom_field_values = custom_field_values if custom_field_values && !custom_field_values.empty?
248 248 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
249 249 # Don't save any change to the issue if the user is not authorized to apply the requested status
250 250 if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
251 251 # Send notification for each issue (if changed)
252 252 Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
253 253 else
254 254 # Keep unsaved issue ids to display them in flash error
255 255 unsaved_issue_ids << issue.id
256 256 end
257 257 end
258 258 if unsaved_issue_ids.empty?
259 259 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
260 260 else
261 261 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
262 262 end
263 263 redirect_to(params[:back_to] || {:controller => 'issues', :action => 'index', :project_id => @project})
264 264 return
265 265 end
266 266 # Find potential statuses the user could be allowed to switch issues to
267 267 @available_statuses = Workflow.find(:all, :include => :new_status,
268 268 :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq.sort
269 269 @custom_fields = @project.issue_custom_fields.select {|f| f.field_format == 'list'}
270 270 end
271 271
272 272 def move
273 273 @allowed_projects = []
274 274 # find projects to which the user is allowed to move the issue
275 275 if User.current.admin?
276 276 # admin is allowed to move issues to any active (visible) project
277 277 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
278 278 else
279 279 User.current.memberships.each {|m| @allowed_projects << m.project if m.role.allowed_to?(:move_issues)}
280 280 end
281 281 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
282 282 @target_project ||= @project
283 283 @trackers = @target_project.trackers
284 284 if request.post?
285 285 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
286 286 unsaved_issue_ids = []
287 287 @issues.each do |issue|
288 288 issue.init_journal(User.current)
289 289 unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker, params[:copy_options])
290 290 end
291 291 if unsaved_issue_ids.empty?
292 292 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
293 293 else
294 294 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
295 295 end
296 296 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
297 297 return
298 298 end
299 299 render :layout => false if request.xhr?
300 300 end
301 301
302 302 def destroy
303 303 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
304 304 if @hours > 0
305 305 case params[:todo]
306 306 when 'destroy'
307 307 # nothing to do
308 308 when 'nullify'
309 309 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
310 310 when 'reassign'
311 311 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
312 312 if reassign_to.nil?
313 313 flash.now[:error] = l(:error_issue_not_found_in_project)
314 314 return
315 315 else
316 316 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
317 317 end
318 318 else
319 319 # display the destroy form
320 320 return
321 321 end
322 322 end
323 323 @issues.each(&:destroy)
324 324 redirect_to :action => 'index', :project_id => @project
325 325 end
326 326
327 327 def gantt
328 328 @gantt = Redmine::Helpers::Gantt.new(params)
329 329 retrieve_query
330 330 if @query.valid?
331 331 events = []
332 332 # Issues that have start and due dates
333 333 events += Issue.find(:all,
334 334 :order => "start_date, due_date",
335 335 :include => [:tracker, :status, :assigned_to, :priority, :project],
336 336 :conditions => ["(#{@query.statement}) AND (((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
337 337 )
338 338 # Issues that don't have a due date but that are assigned to a version with a date
339 339 events += Issue.find(:all,
340 340 :order => "start_date, effective_date",
341 341 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
342 342 :conditions => ["(#{@query.statement}) AND (((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
343 343 )
344 344 # Versions
345 345 events += Version.find(:all, :include => :project,
346 346 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
347 347
348 348 @gantt.events = events
349 349 end
350 350
351 351 respond_to do |format|
352 352 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
353 353 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.nil? ? '' : "#{@project.identifier}-" }gantt.png") } if @gantt.respond_to?('to_image')
354 354 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{@project.nil? ? '' : "#{@project.identifier}-" }gantt.pdf") }
355 355 end
356 356 end
357 357
358 358 def calendar
359 359 if params[:year] and params[:year].to_i > 1900
360 360 @year = params[:year].to_i
361 361 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
362 362 @month = params[:month].to_i
363 363 end
364 364 end
365 365 @year ||= Date.today.year
366 366 @month ||= Date.today.month
367 367
368 368 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
369 369 retrieve_query
370 370 if @query.valid?
371 371 events = []
372 372 events += Issue.find(:all,
373 373 :include => [:tracker, :status, :assigned_to, :priority, :project],
374 374 :conditions => ["(#{@query.statement}) AND ((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
375 375 )
376 376 events += Version.find(:all, :include => :project,
377 377 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
378 378
379 379 @calendar.events = events
380 380 end
381 381
382 382 render :layout => false if request.xhr?
383 383 end
384 384
385 385 def context_menu
386 386 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
387 387 if (@issues.size == 1)
388 388 @issue = @issues.first
389 389 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
390 390 end
391 391 projects = @issues.collect(&:project).compact.uniq
392 392 @project = projects.first if projects.size == 1
393 393
394 394 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
395 395 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
396 396 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
397 397 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
398 398 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
399 399 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
400 400 }
401 401 if @project
402 402 @assignables = @project.assignable_users
403 403 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
404 404 end
405 405
406 @priorities = Enumeration.get_values('IPRI').reverse
406 @priorities = Enumeration.priorities.reverse
407 407 @statuses = IssueStatus.find(:all, :order => 'position')
408 408 @back = request.env['HTTP_REFERER']
409 409
410 410 render :layout => false
411 411 end
412 412
413 413 def update_form
414 414 @issue = Issue.new(params[:issue])
415 415 render :action => :new, :layout => false
416 416 end
417 417
418 418 def preview
419 419 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
420 420 @attachements = @issue.attachments if @issue
421 421 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
422 422 render :partial => 'common/preview'
423 423 end
424 424
425 425 private
426 426 def find_issue
427 427 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
428 428 @project = @issue.project
429 429 rescue ActiveRecord::RecordNotFound
430 430 render_404
431 431 end
432 432
433 433 # Filter for bulk operations
434 434 def find_issues
435 435 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
436 436 raise ActiveRecord::RecordNotFound if @issues.empty?
437 437 projects = @issues.collect(&:project).compact.uniq
438 438 if projects.size == 1
439 439 @project = projects.first
440 440 else
441 441 # TODO: let users bulk edit/move/destroy issues from different projects
442 442 render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
443 443 end
444 444 rescue ActiveRecord::RecordNotFound
445 445 render_404
446 446 end
447 447
448 448 def find_project
449 449 @project = Project.find(params[:project_id])
450 450 rescue ActiveRecord::RecordNotFound
451 451 render_404
452 452 end
453 453
454 454 def find_optional_project
455 455 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
456 456 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
457 457 allowed ? true : deny_access
458 458 rescue ActiveRecord::RecordNotFound
459 459 render_404
460 460 end
461 461
462 462 # Retrieve query from session or build a new query
463 463 def retrieve_query
464 464 if !params[:query_id].blank?
465 465 cond = "project_id IS NULL"
466 466 cond << " OR project_id = #{@project.id}" if @project
467 467 @query = Query.find(params[:query_id], :conditions => cond)
468 468 @query.project = @project
469 469 session[:query] = {:id => @query.id, :project_id => @query.project_id}
470 470 else
471 471 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
472 472 # Give it a name, required to be valid
473 473 @query = Query.new(:name => "_")
474 474 @query.project = @project
475 475 if params[:fields] and params[:fields].is_a? Array
476 476 params[:fields].each do |field|
477 477 @query.add_filter(field, params[:operators][field], params[:values][field])
478 478 end
479 479 else
480 480 @query.available_filters.keys.each do |field|
481 481 @query.add_short_filter(field, params[field]) if params[field]
482 482 end
483 483 end
484 484 session[:query] = {:project_id => @query.project_id, :filters => @query.filters}
485 485 else
486 486 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
487 487 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters])
488 488 @query.project = @project
489 489 end
490 490 end
491 491 end
492 492 end
@@ -1,236 +1,236
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 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 ReportsController < ApplicationController
19 19 menu_item :issues
20 20 before_filter :find_project, :authorize
21 21
22 22 def issue_report
23 23 @statuses = IssueStatus.find(:all, :order => 'position')
24 24
25 25 case params[:detail]
26 26 when "tracker"
27 27 @field = "tracker_id"
28 28 @rows = @project.trackers
29 29 @data = issues_by_tracker
30 30 @report_title = l(:field_tracker)
31 31 render :template => "reports/issue_report_details"
32 32 when "version"
33 33 @field = "fixed_version_id"
34 34 @rows = @project.versions.sort
35 35 @data = issues_by_version
36 36 @report_title = l(:field_version)
37 37 render :template => "reports/issue_report_details"
38 38 when "priority"
39 39 @field = "priority_id"
40 @rows = Enumeration::get_values('IPRI')
40 @rows = Enumeration.priorities
41 41 @data = issues_by_priority
42 42 @report_title = l(:field_priority)
43 43 render :template => "reports/issue_report_details"
44 44 when "category"
45 45 @field = "category_id"
46 46 @rows = @project.issue_categories
47 47 @data = issues_by_category
48 48 @report_title = l(:field_category)
49 49 render :template => "reports/issue_report_details"
50 50 when "assigned_to"
51 51 @field = "assigned_to_id"
52 52 @rows = @project.members.collect { |m| m.user }
53 53 @data = issues_by_assigned_to
54 54 @report_title = l(:field_assigned_to)
55 55 render :template => "reports/issue_report_details"
56 56 when "author"
57 57 @field = "author_id"
58 58 @rows = @project.members.collect { |m| m.user }
59 59 @data = issues_by_author
60 60 @report_title = l(:field_author)
61 61 render :template => "reports/issue_report_details"
62 62 when "subproject"
63 63 @field = "project_id"
64 64 @rows = @project.descendants.active
65 65 @data = issues_by_subproject
66 66 @report_title = l(:field_subproject)
67 67 render :template => "reports/issue_report_details"
68 68 else
69 69 @trackers = @project.trackers
70 70 @versions = @project.versions.sort
71 @priorities = Enumeration::get_values('IPRI')
71 @priorities = Enumeration.priorities
72 72 @categories = @project.issue_categories
73 73 @assignees = @project.members.collect { |m| m.user }
74 74 @authors = @project.members.collect { |m| m.user }
75 75 @subprojects = @project.descendants.active
76 76 issues_by_tracker
77 77 issues_by_version
78 78 issues_by_priority
79 79 issues_by_category
80 80 issues_by_assigned_to
81 81 issues_by_author
82 82 issues_by_subproject
83 83
84 84 render :template => "reports/issue_report"
85 85 end
86 86 end
87 87
88 88 def delays
89 89 @trackers = Tracker.find(:all)
90 90 if request.get?
91 91 @selected_tracker_ids = @trackers.collect {|t| t.id.to_s }
92 92 else
93 93 @selected_tracker_ids = params[:tracker_ids].collect { |id| id.to_i.to_s } if params[:tracker_ids] and params[:tracker_ids].is_a? Array
94 94 end
95 95 @selected_tracker_ids ||= []
96 96 @raw =
97 97 ActiveRecord::Base.connection.select_all("SELECT datediff( a.created_on, b.created_on ) as delay, count(a.id) as total
98 98 FROM issue_histories a, issue_histories b, issues i
99 99 WHERE a.status_id =5
100 100 AND a.issue_id = b.issue_id
101 101 AND a.issue_id = i.id
102 102 AND i.tracker_id in (#{@selected_tracker_ids.join(',')})
103 103 AND b.id = (
104 104 SELECT min( c.id )
105 105 FROM issue_histories c
106 106 WHERE b.issue_id = c.issue_id )
107 107 GROUP BY delay") unless @selected_tracker_ids.empty?
108 108 @raw ||=[]
109 109
110 110 @x_from = 0
111 111 @x_to = 0
112 112 @y_from = 0
113 113 @y_to = 0
114 114 @sum_total = 0
115 115 @sum_delay = 0
116 116 @raw.each do |r|
117 117 @x_to = [r['delay'].to_i, @x_to].max
118 118 @y_to = [r['total'].to_i, @y_to].max
119 119 @sum_total = @sum_total + r['total'].to_i
120 120 @sum_delay = @sum_delay + r['total'].to_i * r['delay'].to_i
121 121 end
122 122 end
123 123
124 124 private
125 125 # Find project of id params[:id]
126 126 def find_project
127 127 @project = Project.find(params[:id])
128 128 rescue ActiveRecord::RecordNotFound
129 129 render_404
130 130 end
131 131
132 132 def issues_by_tracker
133 133 @issues_by_tracker ||=
134 134 ActiveRecord::Base.connection.select_all("select s.id as status_id,
135 135 s.is_closed as closed,
136 136 t.id as tracker_id,
137 137 count(i.id) as total
138 138 from
139 139 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Tracker.table_name} t
140 140 where
141 141 i.status_id=s.id
142 142 and i.tracker_id=t.id
143 143 and i.project_id=#{@project.id}
144 144 group by s.id, s.is_closed, t.id")
145 145 end
146 146
147 147 def issues_by_version
148 148 @issues_by_version ||=
149 149 ActiveRecord::Base.connection.select_all("select s.id as status_id,
150 150 s.is_closed as closed,
151 151 v.id as fixed_version_id,
152 152 count(i.id) as total
153 153 from
154 154 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Version.table_name} v
155 155 where
156 156 i.status_id=s.id
157 157 and i.fixed_version_id=v.id
158 158 and i.project_id=#{@project.id}
159 159 group by s.id, s.is_closed, v.id")
160 160 end
161 161
162 162 def issues_by_priority
163 163 @issues_by_priority ||=
164 164 ActiveRecord::Base.connection.select_all("select s.id as status_id,
165 165 s.is_closed as closed,
166 166 p.id as priority_id,
167 167 count(i.id) as total
168 168 from
169 169 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Enumeration.table_name} p
170 170 where
171 171 i.status_id=s.id
172 172 and i.priority_id=p.id
173 173 and i.project_id=#{@project.id}
174 174 group by s.id, s.is_closed, p.id")
175 175 end
176 176
177 177 def issues_by_category
178 178 @issues_by_category ||=
179 179 ActiveRecord::Base.connection.select_all("select s.id as status_id,
180 180 s.is_closed as closed,
181 181 c.id as category_id,
182 182 count(i.id) as total
183 183 from
184 184 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{IssueCategory.table_name} c
185 185 where
186 186 i.status_id=s.id
187 187 and i.category_id=c.id
188 188 and i.project_id=#{@project.id}
189 189 group by s.id, s.is_closed, c.id")
190 190 end
191 191
192 192 def issues_by_assigned_to
193 193 @issues_by_assigned_to ||=
194 194 ActiveRecord::Base.connection.select_all("select s.id as status_id,
195 195 s.is_closed as closed,
196 196 a.id as assigned_to_id,
197 197 count(i.id) as total
198 198 from
199 199 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{User.table_name} a
200 200 where
201 201 i.status_id=s.id
202 202 and i.assigned_to_id=a.id
203 203 and i.project_id=#{@project.id}
204 204 group by s.id, s.is_closed, a.id")
205 205 end
206 206
207 207 def issues_by_author
208 208 @issues_by_author ||=
209 209 ActiveRecord::Base.connection.select_all("select s.id as status_id,
210 210 s.is_closed as closed,
211 211 a.id as author_id,
212 212 count(i.id) as total
213 213 from
214 214 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{User.table_name} a
215 215 where
216 216 i.status_id=s.id
217 217 and i.author_id=a.id
218 218 and i.project_id=#{@project.id}
219 219 group by s.id, s.is_closed, a.id")
220 220 end
221 221
222 222 def issues_by_subproject
223 223 @issues_by_subproject ||=
224 224 ActiveRecord::Base.connection.select_all("select s.id as status_id,
225 225 s.is_closed as closed,
226 226 i.project_id as project_id,
227 227 count(i.id) as total
228 228 from
229 229 #{Issue.table_name} i, #{IssueStatus.table_name} s
230 230 where
231 231 i.status_id=s.id
232 232 and i.project_id IN (#{@project.descendants.active.collect{|p| p.id}.join(',')})
233 233 group by s.id, s.is_closed, i.project_id") if @project.descendants.active.any?
234 234 @issues_by_subproject ||= []
235 235 end
236 236 end
@@ -1,160 +1,160
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 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 module TimelogHelper
19 19 include ApplicationHelper
20 20
21 21 def render_timelog_breadcrumb
22 22 links = []
23 23 links << link_to(l(:label_project_all), {:project_id => nil, :issue_id => nil})
24 24 links << link_to(h(@project), {:project_id => @project, :issue_id => nil}) if @project
25 25 links << link_to_issue(@issue) if @issue
26 26 breadcrumb links
27 27 end
28 28
29 29 def activity_collection_for_select_options
30 activities = Enumeration::get_values('ACTI')
30 activities = Enumeration.activities
31 31 collection = []
32 32 collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ] unless activities.detect(&:is_default)
33 33 activities.each { |a| collection << [a.name, a.id] }
34 34 collection
35 35 end
36 36
37 37 def select_hours(data, criteria, value)
38 38 data.select {|row| row[criteria] == value}
39 39 end
40 40
41 41 def sum_hours(data)
42 42 sum = 0
43 43 data.each do |row|
44 44 sum += row['hours'].to_f
45 45 end
46 46 sum
47 47 end
48 48
49 49 def options_for_period_select(value)
50 50 options_for_select([[l(:label_all_time), 'all'],
51 51 [l(:label_today), 'today'],
52 52 [l(:label_yesterday), 'yesterday'],
53 53 [l(:label_this_week), 'current_week'],
54 54 [l(:label_last_week), 'last_week'],
55 55 [l(:label_last_n_days, 7), '7_days'],
56 56 [l(:label_this_month), 'current_month'],
57 57 [l(:label_last_month), 'last_month'],
58 58 [l(:label_last_n_days, 30), '30_days'],
59 59 [l(:label_this_year), 'current_year']],
60 60 value)
61 61 end
62 62
63 63 def entries_to_csv(entries)
64 64 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
65 65 decimal_separator = l(:general_csv_decimal_separator)
66 66 custom_fields = TimeEntryCustomField.find(:all)
67 67 export = StringIO.new
68 68 CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
69 69 # csv header fields
70 70 headers = [l(:field_spent_on),
71 71 l(:field_user),
72 72 l(:field_activity),
73 73 l(:field_project),
74 74 l(:field_issue),
75 75 l(:field_tracker),
76 76 l(:field_subject),
77 77 l(:field_hours),
78 78 l(:field_comments)
79 79 ]
80 80 # Export custom fields
81 81 headers += custom_fields.collect(&:name)
82 82
83 83 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
84 84 # csv lines
85 85 entries.each do |entry|
86 86 fields = [format_date(entry.spent_on),
87 87 entry.user,
88 88 entry.activity,
89 89 entry.project,
90 90 (entry.issue ? entry.issue.id : nil),
91 91 (entry.issue ? entry.issue.tracker : nil),
92 92 (entry.issue ? entry.issue.subject : nil),
93 93 entry.hours.to_s.gsub('.', decimal_separator),
94 94 entry.comments
95 95 ]
96 96 fields += custom_fields.collect {|f| show_value(entry.custom_value_for(f)) }
97 97
98 98 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
99 99 end
100 100 end
101 101 export.rewind
102 102 export
103 103 end
104 104
105 105 def format_criteria_value(criteria, value)
106 106 value.blank? ? l(:label_none) : ((k = @available_criterias[criteria][:klass]) ? k.find_by_id(value.to_i) : format_value(value, @available_criterias[criteria][:format]))
107 107 end
108 108
109 109 def report_to_csv(criterias, periods, hours)
110 110 export = StringIO.new
111 111 CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
112 112 # Column headers
113 113 headers = criterias.collect {|criteria| l(@available_criterias[criteria][:label]) }
114 114 headers += periods
115 115 headers << l(:label_total)
116 116 csv << headers.collect {|c| to_utf8(c) }
117 117 # Content
118 118 report_criteria_to_csv(csv, criterias, periods, hours)
119 119 # Total row
120 120 row = [ l(:label_total) ] + [''] * (criterias.size - 1)
121 121 total = 0
122 122 periods.each do |period|
123 123 sum = sum_hours(select_hours(hours, @columns, period.to_s))
124 124 total += sum
125 125 row << (sum > 0 ? "%.2f" % sum : '')
126 126 end
127 127 row << "%.2f" %total
128 128 csv << row
129 129 end
130 130 export.rewind
131 131 export
132 132 end
133 133
134 134 def report_criteria_to_csv(csv, criterias, periods, hours, level=0)
135 135 hours.collect {|h| h[criterias[level]].to_s}.uniq.each do |value|
136 136 hours_for_value = select_hours(hours, criterias[level], value)
137 137 next if hours_for_value.empty?
138 138 row = [''] * level
139 139 row << to_utf8(format_criteria_value(criterias[level], value))
140 140 row += [''] * (criterias.length - level - 1)
141 141 total = 0
142 142 periods.each do |period|
143 143 sum = sum_hours(select_hours(hours_for_value, @columns, period.to_s))
144 144 total += sum
145 145 row << (sum > 0 ? "%.2f" % sum : '')
146 146 end
147 147 row << "%.2f" %total
148 148 csv << row
149 149
150 150 if criterias.length > level + 1
151 151 report_criteria_to_csv(csv, criterias, periods, hours_for_value, level + 1)
152 152 end
153 153 end
154 154 end
155 155
156 156 def to_utf8(s)
157 157 @ic ||= Iconv.new(l(:general_csv_encoding), 'UTF-8')
158 158 begin; @ic.iconv(s.to_s); rescue; s.to_s; end
159 159 end
160 160 end
@@ -1,37 +1,37
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 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 Document < ActiveRecord::Base
19 19 belongs_to :project
20 20 belongs_to :category, :class_name => "Enumeration", :foreign_key => "category_id"
21 21 acts_as_attachable :delete_permission => :manage_documents
22 22
23 23 acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
24 24 acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
25 25 :author => Proc.new {|o| (a = o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC")) ? a.author : nil },
26 26 :url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}}
27 27 acts_as_activity_provider :find_options => {:include => :project}
28 28
29 29 validates_presence_of :project, :title, :category
30 30 validates_length_of :title, :maximum => 60
31 31
32 32 def after_initialize
33 33 if new_record?
34 self.category ||= Enumeration.default('DCAT')
34 self.category ||= Enumeration.document_categories.default
35 35 end
36 36 end
37 37 end
@@ -1,81 +1,93
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 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 Enumeration < ActiveRecord::Base
19 19 acts_as_list :scope => 'opt = \'#{opt}\''
20 20
21 21 before_destroy :check_integrity
22 22
23 23 validates_presence_of :opt, :name
24 24 validates_uniqueness_of :name, :scope => [:opt]
25 25 validates_length_of :name, :maximum => 30
26 26
27 27 # Single table inheritance would be an option
28 28 OPTIONS = {
29 "IPRI" => {:label => :enumeration_issue_priorities, :model => Issue, :foreign_key => :priority_id},
30 "DCAT" => {:label => :enumeration_doc_categories, :model => Document, :foreign_key => :category_id},
31 "ACTI" => {:label => :enumeration_activities, :model => TimeEntry, :foreign_key => :activity_id}
29 "IPRI" => {:label => :enumeration_issue_priorities, :model => Issue, :foreign_key => :priority_id, :scope => :priorities},
30 "DCAT" => {:label => :enumeration_doc_categories, :model => Document, :foreign_key => :category_id, :scope => :document_categories},
31 "ACTI" => {:label => :enumeration_activities, :model => TimeEntry, :foreign_key => :activity_id, :scope => :activities}
32 32 }.freeze
33 33
34 def self.get_values(option)
35 find(:all, :conditions => {:opt => option}, :order => 'position')
34 # Creates a named scope for each type of value. The scope has a +default+ method
35 # that returns the default value, or nil if no value is set as default.
36 # Example:
37 # Enumeration.priorities
38 # Enumeration.priorities.default
39 OPTIONS.each do |k, v|
40 next unless v[:scope]
41 named_scope v[:scope], :conditions => { :opt => k }, :order => 'position' do
42 def default
43 find(:first, :conditions => { :is_default => true })
44 end
45 end
36 46 end
37 47
38 def self.default(option)
39 find(:first, :conditions => {:opt => option, :is_default => true}, :order => 'position')
48 named_scope :values, lambda {|opt| { :conditions => { :opt => opt }, :order => 'position' } } do
49 def default
50 find(:first, :conditions => { :is_default => true })
51 end
40 52 end
41 53
42 54 def option_name
43 55 OPTIONS[self.opt][:label]
44 56 end
45 57
46 58 def before_save
47 59 if is_default? && is_default_changed?
48 60 Enumeration.update_all("is_default = #{connection.quoted_false}", {:opt => opt})
49 61 end
50 62 end
51 63
52 64 def objects_count
53 65 OPTIONS[self.opt][:model].count(:conditions => "#{OPTIONS[self.opt][:foreign_key]} = #{id}")
54 66 end
55 67
56 68 def in_use?
57 69 self.objects_count != 0
58 70 end
59 71
60 72 alias :destroy_without_reassign :destroy
61 73
62 74 # Destroy the enumeration
63 75 # If a enumeration is specified, objects are reassigned
64 76 def destroy(reassign_to = nil)
65 77 if reassign_to && reassign_to.is_a?(Enumeration)
66 78 OPTIONS[self.opt][:model].update_all("#{OPTIONS[self.opt][:foreign_key]} = #{reassign_to.id}", "#{OPTIONS[self.opt][:foreign_key]} = #{id}")
67 79 end
68 80 destroy_without_reassign
69 81 end
70 82
71 83 def <=>(enumeration)
72 84 position <=> enumeration.position
73 85 end
74 86
75 87 def to_s; name end
76 88
77 89 private
78 90 def check_integrity
79 91 raise "Can't delete enumeration" if self.in_use?
80 92 end
81 93 end
@@ -1,292 +1,292
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 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
45 45
46 46 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
47 47 :author_key => :author_id
48 48
49 49 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
50 50 validates_length_of :subject, :maximum => 255
51 51 validates_inclusion_of :done_ratio, :in => 0..100
52 52 validates_numericality_of :estimated_hours, :allow_nil => true
53 53
54 54 named_scope :visible, lambda {|*args| { :include => :project,
55 55 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
56 56
57 57 # Returns true if usr or current user is allowed to view the issue
58 58 def visible?(usr=nil)
59 59 (usr || User.current).allowed_to?(:view_issues, self.project)
60 60 end
61 61
62 62 def after_initialize
63 63 if new_record?
64 64 # set default values for new records only
65 65 self.status ||= IssueStatus.default
66 self.priority ||= Enumeration.default('IPRI')
66 self.priority ||= Enumeration.priorities.default
67 67 end
68 68 end
69 69
70 70 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
71 71 def available_custom_fields
72 72 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
73 73 end
74 74
75 75 def copy_from(arg)
76 76 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
77 77 self.attributes = issue.attributes.dup
78 78 self.custom_values = issue.custom_values.collect {|v| v.clone}
79 79 self
80 80 end
81 81
82 82 # Moves/copies an issue to a new project and tracker
83 83 # Returns the moved/copied issue on success, false on failure
84 84 def move_to(new_project, new_tracker = nil, options = {})
85 85 options ||= {}
86 86 issue = options[:copy] ? self.clone : self
87 87 transaction do
88 88 if new_project && issue.project_id != new_project.id
89 89 # delete issue relations
90 90 unless Setting.cross_project_issue_relations?
91 91 issue.relations_from.clear
92 92 issue.relations_to.clear
93 93 end
94 94 # issue is moved to another project
95 95 # reassign to the category with same name if any
96 96 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
97 97 issue.category = new_category
98 98 issue.fixed_version = nil
99 99 issue.project = new_project
100 100 end
101 101 if new_tracker
102 102 issue.tracker = new_tracker
103 103 end
104 104 if options[:copy]
105 105 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
106 106 issue.status = self.status
107 107 end
108 108 if issue.save
109 109 unless options[:copy]
110 110 # Manually update project_id on related time entries
111 111 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
112 112 end
113 113 else
114 114 Issue.connection.rollback_db_transaction
115 115 return false
116 116 end
117 117 end
118 118 return issue
119 119 end
120 120
121 121 def priority_id=(pid)
122 122 self.priority = nil
123 123 write_attribute(:priority_id, pid)
124 124 end
125 125
126 126 def estimated_hours=(h)
127 127 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
128 128 end
129 129
130 130 def validate
131 131 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
132 132 errors.add :due_date, :activerecord_error_not_a_date
133 133 end
134 134
135 135 if self.due_date and self.start_date and self.due_date < self.start_date
136 136 errors.add :due_date, :activerecord_error_greater_than_start_date
137 137 end
138 138
139 139 if start_date && soonest_start && start_date < soonest_start
140 140 errors.add :start_date, :activerecord_error_invalid
141 141 end
142 142 end
143 143
144 144 def validate_on_create
145 145 errors.add :tracker_id, :activerecord_error_invalid unless project.trackers.include?(tracker)
146 146 end
147 147
148 148 def before_create
149 149 # default assignment based on category
150 150 if assigned_to.nil? && category && category.assigned_to
151 151 self.assigned_to = category.assigned_to
152 152 end
153 153 end
154 154
155 155 def before_save
156 156 if @current_journal
157 157 # attributes changes
158 158 (Issue.column_names - %w(id description)).each {|c|
159 159 @current_journal.details << JournalDetail.new(:property => 'attr',
160 160 :prop_key => c,
161 161 :old_value => @issue_before_change.send(c),
162 162 :value => send(c)) unless send(c)==@issue_before_change.send(c)
163 163 }
164 164 # custom fields changes
165 165 custom_values.each {|c|
166 166 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
167 167 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
168 168 @current_journal.details << JournalDetail.new(:property => 'cf',
169 169 :prop_key => c.custom_field_id,
170 170 :old_value => @custom_values_before_change[c.custom_field_id],
171 171 :value => c.value)
172 172 }
173 173 @current_journal.save
174 174 end
175 175 # Save the issue even if the journal is not saved (because empty)
176 176 true
177 177 end
178 178
179 179 def after_save
180 180 # Reload is needed in order to get the right status
181 181 reload
182 182
183 183 # Update start/due dates of following issues
184 184 relations_from.each(&:set_issue_to_dates)
185 185
186 186 # Close duplicates if the issue was closed
187 187 if @issue_before_change && !@issue_before_change.closed? && self.closed?
188 188 duplicates.each do |duplicate|
189 189 # Reload is need in case the duplicate was updated by a previous duplicate
190 190 duplicate.reload
191 191 # Don't re-close it if it's already closed
192 192 next if duplicate.closed?
193 193 # Same user and notes
194 194 duplicate.init_journal(@current_journal.user, @current_journal.notes)
195 195 duplicate.update_attribute :status, self.status
196 196 end
197 197 end
198 198 end
199 199
200 200 def init_journal(user, notes = "")
201 201 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
202 202 @issue_before_change = self.clone
203 203 @issue_before_change.status = self.status
204 204 @custom_values_before_change = {}
205 205 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
206 206 # Make sure updated_on is updated when adding a note.
207 207 updated_on_will_change!
208 208 @current_journal
209 209 end
210 210
211 211 # Return true if the issue is closed, otherwise false
212 212 def closed?
213 213 self.status.is_closed?
214 214 end
215 215
216 216 # Returns true if the issue is overdue
217 217 def overdue?
218 218 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
219 219 end
220 220
221 221 # Users the issue can be assigned to
222 222 def assignable_users
223 223 project.assignable_users
224 224 end
225 225
226 226 # Returns an array of status that user is able to apply
227 227 def new_statuses_allowed_to(user)
228 228 statuses = status.find_new_statuses_allowed_to(user.role_for_project(project), tracker)
229 229 statuses << status unless statuses.empty?
230 230 statuses.uniq.sort
231 231 end
232 232
233 233 # Returns the mail adresses of users that should be notified for the issue
234 234 def recipients
235 235 recipients = project.recipients
236 236 # Author and assignee are always notified unless they have been locked
237 237 recipients << author.mail if author && author.active?
238 238 recipients << assigned_to.mail if assigned_to && assigned_to.active?
239 239 recipients.compact.uniq
240 240 end
241 241
242 242 def spent_hours
243 243 @spent_hours ||= time_entries.sum(:hours) || 0
244 244 end
245 245
246 246 def relations
247 247 (relations_from + relations_to).sort
248 248 end
249 249
250 250 def all_dependent_issues
251 251 dependencies = []
252 252 relations_from.each do |relation|
253 253 dependencies << relation.issue_to
254 254 dependencies += relation.issue_to.all_dependent_issues
255 255 end
256 256 dependencies
257 257 end
258 258
259 259 # Returns an array of issues that duplicate this one
260 260 def duplicates
261 261 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
262 262 end
263 263
264 264 # Returns the due date or the target due date if any
265 265 # Used on gantt chart
266 266 def due_before
267 267 due_date || (fixed_version ? fixed_version.effective_date : nil)
268 268 end
269 269
270 270 def duration
271 271 (start_date && due_date) ? due_date - start_date : 0
272 272 end
273 273
274 274 def soonest_start
275 275 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
276 276 end
277 277
278 278 def to_s
279 279 "#{tracker} ##{id}: #{subject}"
280 280 end
281 281
282 282 private
283 283
284 284 # Callback on attachment deletion
285 285 def attachment_removed(obj)
286 286 journal = init_journal(User.current)
287 287 journal.details << JournalDetail.new(:property => 'attachment',
288 288 :prop_key => obj.id,
289 289 :old_value => obj.filename)
290 290 journal.save
291 291 end
292 292 end
@@ -1,79 +1,79
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2008 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 TimeEntry < ActiveRecord::Base
19 19 # could have used polymorphic association
20 20 # project association here allows easy loading of time entries at project level with one database trip
21 21 belongs_to :project
22 22 belongs_to :issue
23 23 belongs_to :user
24 24 belongs_to :activity, :class_name => 'Enumeration', :foreign_key => :activity_id
25 25
26 26 attr_protected :project_id, :user_id, :tyear, :tmonth, :tweek
27 27
28 28 acts_as_customizable
29 29 acts_as_event :title => Proc.new {|o| "#{o.user}: #{lwr(:label_f_hour, o.hours)} (#{(o.issue || o.project).event_title})"},
30 30 :url => Proc.new {|o| {:controller => 'timelog', :action => 'details', :project_id => o.project}},
31 31 :author => :user,
32 32 :description => :comments
33 33
34 34 validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on
35 35 validates_numericality_of :hours, :allow_nil => true, :message => :activerecord_error_invalid
36 36 validates_length_of :comments, :maximum => 255, :allow_nil => true
37 37
38 38 def after_initialize
39 39 if new_record? && self.activity.nil?
40 if default_activity = Enumeration.default('ACTI')
40 if default_activity = Enumeration.activities.default
41 41 self.activity_id = default_activity.id
42 42 end
43 43 end
44 44 end
45 45
46 46 def before_validation
47 47 self.project = issue.project if issue && project.nil?
48 48 end
49 49
50 50 def validate
51 51 errors.add :hours, :activerecord_error_invalid if hours && (hours < 0 || hours >= 1000)
52 52 errors.add :project_id, :activerecord_error_invalid if project.nil?
53 53 errors.add :issue_id, :activerecord_error_invalid if (issue_id && !issue) || (issue && project!=issue.project)
54 54 end
55 55
56 56 def hours=(h)
57 57 write_attribute :hours, (h.is_a?(String) ? (h.to_hours || h) : h)
58 58 end
59 59
60 60 # tyear, tmonth, tweek assigned where setting spent_on attributes
61 61 # these attributes make time aggregations easier
62 62 def spent_on=(date)
63 63 super
64 64 self.tyear = spent_on ? spent_on.year : nil
65 65 self.tmonth = spent_on ? spent_on.month : nil
66 66 self.tweek = spent_on ? Date.civil(spent_on.year, spent_on.month, spent_on.day).cweek : nil
67 67 end
68 68
69 69 # Returns true if the time entry can be edited by usr, otherwise false
70 70 def editable_by?(usr)
71 71 (usr == user && usr.allowed_to?(:edit_own_time_entries, project)) || usr.allowed_to?(:edit_time_entries, project)
72 72 end
73 73
74 74 def self.visible_by(usr)
75 75 with_scope(:find => { :conditions => Project.allowed_to_condition(usr, :view_time_entries) }) do
76 76 yield
77 77 end
78 78 end
79 79 end
@@ -1,15 +1,15
1 1 <%= error_messages_for 'document' %>
2 2 <div class="box">
3 3 <!--[form:document]-->
4 4 <p><label for="document_category_id"><%=l(:field_category)%></label>
5 <%= select('document', 'category_id', Enumeration.get_values('DCAT').collect {|c| [c.name, c.id]}) %></p>
5 <%= select('document', 'category_id', Enumeration.document_categories.collect {|c| [c.name, c.id]}) %></p>
6 6
7 7 <p><label for="document_title"><%=l(:field_title)%> <span class="required">*</span></label>
8 8 <%= text_field 'document', 'title', :size => 60 %></p>
9 9
10 10 <p><label for="document_description"><%=l(:field_description)%></label>
11 11 <%= text_area 'document', 'description', :cols => 60, :rows => 15, :class => 'wiki-edit' %></p>
12 12 <!--[eoform:document]-->
13 13 </div>
14 14
15 15 <%= wikitoolbar_for 'document_description' %>
@@ -1,31 +1,31
1 1 <h2><%=l(:label_enumerations)%></h2>
2 2
3 3 <% Enumeration::OPTIONS.each do |option, params| %>
4 4 <h3><%= l(params[:label]) %></h3>
5 5
6 <% enumerations = Enumeration.get_values(option) %>
6 <% enumerations = Enumeration.values(option) %>
7 7 <% if enumerations.any? %>
8 8 <table class="list">
9 9 <% enumerations.each do |enumeration| %>
10 10 <tr class="<%= cycle('odd', 'even') %>">
11 11 <td><%= link_to h(enumeration), :action => 'edit', :id => enumeration %></td>
12 12 <td style="width:15%;"><%= image_tag('true.png') if enumeration.is_default? %></td>
13 13 <td style="width:15%;">
14 14 <%= link_to image_tag('2uparrow.png', :alt => l(:label_sort_highest)), {:action => 'move', :id => enumeration, :position => 'highest'}, :method => :post, :title => l(:label_sort_highest) %>
15 15 <%= link_to image_tag('1uparrow.png', :alt => l(:label_sort_higher)), {:action => 'move', :id => enumeration, :position => 'higher'}, :method => :post, :title => l(:label_sort_higher) %> -
16 16 <%= link_to image_tag('1downarrow.png', :alt => l(:label_sort_lower)), {:action => 'move', :id => enumeration, :position => 'lower'}, :method => :post, :title => l(:label_sort_lower) %>
17 17 <%= link_to image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), {:action => 'move', :id => enumeration, :position => 'lowest'}, :method => :post, :title => l(:label_sort_lowest) %>
18 18 </td>
19 19 <td align="center" style="width:10%;">
20 20 <%= link_to l(:button_delete), { :action => 'destroy', :id => enumeration }, :method => :post, :confirm => l(:text_are_you_sure), :class => "icon icon-del" %>
21 21 </td>
22 22 </tr>
23 23 <% end %>
24 24 </table>
25 25 <% reset_cycle %>
26 26 <% end %>
27 27
28 28 <p><%= link_to l(:label_enumeration_new), { :action => 'new', :opt => option } %></p>
29 29 <% end %>
30 30
31 31 <% html_title(l(:label_enumerations)) -%>
@@ -1,58 +1,58
1 1 <h2><%= l(:label_bulk_edit_selected_issues) %></h2>
2 2
3 3 <ul><%= @issues.collect {|i| content_tag('li', link_to(h("#{i.tracker} ##{i.id}"), { :action => 'show', :id => i }) + h(": #{i.subject}")) }.join("\n") %></ul>
4 4
5 5 <% form_tag() do %>
6 6 <%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join %>
7 7 <div class="box">
8 8 <fieldset>
9 9 <legend><%= l(:label_change_properties) %></legend>
10 10 <p>
11 11 <% if @available_statuses.any? %>
12 12 <label><%= l(:field_status) %>:
13 13 <%= select_tag('status_id', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@available_statuses, :id, :name)) %></label>
14 14 <% end %>
15 15 <label><%= l(:field_priority) %>:
16 <%= select_tag('priority_id', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(Enumeration.get_values('IPRI'), :id, :name)) %></label>
16 <%= select_tag('priority_id', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(Enumeration.priorities, :id, :name)) %></label>
17 17 <label><%= l(:field_category) %>:
18 18 <%= select_tag('category_id', content_tag('option', l(:label_no_change_option), :value => '') +
19 19 content_tag('option', l(:label_none), :value => 'none') +
20 20 options_from_collection_for_select(@project.issue_categories, :id, :name)) %></label>
21 21 </p>
22 22 <p>
23 23 <label><%= l(:field_assigned_to) %>:
24 24 <%= select_tag('assigned_to_id', content_tag('option', l(:label_no_change_option), :value => '') +
25 25 content_tag('option', l(:label_nobody), :value => 'none') +
26 26 options_from_collection_for_select(@project.assignable_users, :id, :name)) %></label>
27 27 <label><%= l(:field_fixed_version) %>:
28 28 <%= select_tag('fixed_version_id', content_tag('option', l(:label_no_change_option), :value => '') +
29 29 content_tag('option', l(:label_none), :value => 'none') +
30 30 options_from_collection_for_select(@project.versions.sort, :id, :name)) %></label>
31 31 </p>
32 32
33 33 <p>
34 34 <label><%= l(:field_start_date) %>:
35 35 <%= text_field_tag 'start_date', '', :size => 10 %><%= calendar_for('start_date') %></label>
36 36 <label><%= l(:field_due_date) %>:
37 37 <%= text_field_tag 'due_date', '', :size => 10 %><%= calendar_for('due_date') %></label>
38 38 <label><%= l(:field_done_ratio) %>:
39 39 <%= select_tag 'done_ratio', options_for_select([[l(:label_no_change_option), '']] + (0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></label>
40 40 </p>
41 41
42 42 <% @custom_fields.each do |custom_field| %>
43 43 <p><label><%= h(custom_field.name) %></label>
44 44 <%= select_tag "custom_field_values[#{custom_field.id}]", options_for_select([[l(:label_no_change_option), '']] + custom_field.possible_values) %></label>
45 45 </p>
46 46 <% end %>
47 47
48 48 <%= call_hook(:view_issues_bulk_edit_details_bottom, { :issues => @issues }) %>
49 49 </fieldset>
50 50
51 51 <fieldset><legend><%= l(:field_notes) %></legend>
52 52 <%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
53 53 <%= wikitoolbar_for 'notes' %>
54 54 </fieldset>
55 55 </div>
56 56
57 57 <p><%= submit_tag l(:button_submit) %>
58 58 <% end %>
@@ -1,505 +1,505
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 desc 'Mantis migration script'
19 19
20 20 require 'active_record'
21 21 require 'iconv'
22 22 require 'pp'
23 23
24 24 namespace :redmine do
25 25 task :migrate_from_mantis => :environment do
26 26
27 27 module MantisMigrate
28 28
29 29 DEFAULT_STATUS = IssueStatus.default
30 30 assigned_status = IssueStatus.find_by_position(2)
31 31 resolved_status = IssueStatus.find_by_position(3)
32 32 feedback_status = IssueStatus.find_by_position(4)
33 33 closed_status = IssueStatus.find :first, :conditions => { :is_closed => true }
34 34 STATUS_MAPPING = {10 => DEFAULT_STATUS, # new
35 35 20 => feedback_status, # feedback
36 36 30 => DEFAULT_STATUS, # acknowledged
37 37 40 => DEFAULT_STATUS, # confirmed
38 38 50 => assigned_status, # assigned
39 39 80 => resolved_status, # resolved
40 40 90 => closed_status # closed
41 41 }
42 42
43 priorities = Enumeration.get_values('IPRI')
43 priorities = Enumeration.priorities
44 44 DEFAULT_PRIORITY = priorities[2]
45 45 PRIORITY_MAPPING = {10 => priorities[1], # none
46 46 20 => priorities[1], # low
47 47 30 => priorities[2], # normal
48 48 40 => priorities[3], # high
49 49 50 => priorities[4], # urgent
50 50 60 => priorities[5] # immediate
51 51 }
52 52
53 53 TRACKER_BUG = Tracker.find_by_position(1)
54 54 TRACKER_FEATURE = Tracker.find_by_position(2)
55 55
56 56 roles = Role.find(:all, :conditions => {:builtin => 0}, :order => 'position ASC')
57 57 manager_role = roles[0]
58 58 developer_role = roles[1]
59 59 DEFAULT_ROLE = roles.last
60 60 ROLE_MAPPING = {10 => DEFAULT_ROLE, # viewer
61 61 25 => DEFAULT_ROLE, # reporter
62 62 40 => DEFAULT_ROLE, # updater
63 63 55 => developer_role, # developer
64 64 70 => manager_role, # manager
65 65 90 => manager_role # administrator
66 66 }
67 67
68 68 CUSTOM_FIELD_TYPE_MAPPING = {0 => 'string', # String
69 69 1 => 'int', # Numeric
70 70 2 => 'int', # Float
71 71 3 => 'list', # Enumeration
72 72 4 => 'string', # Email
73 73 5 => 'bool', # Checkbox
74 74 6 => 'list', # List
75 75 7 => 'list', # Multiselection list
76 76 8 => 'date', # Date
77 77 }
78 78
79 79 RELATION_TYPE_MAPPING = {1 => IssueRelation::TYPE_RELATES, # related to
80 80 2 => IssueRelation::TYPE_RELATES, # parent of
81 81 3 => IssueRelation::TYPE_RELATES, # child of
82 82 0 => IssueRelation::TYPE_DUPLICATES, # duplicate of
83 83 4 => IssueRelation::TYPE_DUPLICATES # has duplicate
84 84 }
85 85
86 86 class MantisUser < ActiveRecord::Base
87 87 set_table_name :mantis_user_table
88 88
89 89 def firstname
90 90 @firstname = realname.blank? ? username : realname.split.first[0..29]
91 91 @firstname.gsub!(/[^\w\s\'\-]/i, '')
92 92 @firstname
93 93 end
94 94
95 95 def lastname
96 96 @lastname = realname.blank? ? '-' : realname.split[1..-1].join(' ')[0..29]
97 97 @lastname.gsub!(/[^\w\s\'\-]/i, '')
98 98 @lastname = '-' if @lastname.blank?
99 99 @lastname
100 100 end
101 101
102 102 def email
103 103 if read_attribute(:email).match(/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i) &&
104 104 !User.find_by_mail(read_attribute(:email))
105 105 @email = read_attribute(:email)
106 106 else
107 107 @email = "#{username}@foo.bar"
108 108 end
109 109 end
110 110
111 111 def username
112 112 read_attribute(:username)[0..29].gsub(/[^a-zA-Z0-9_\-@\.]/, '-')
113 113 end
114 114 end
115 115
116 116 class MantisProject < ActiveRecord::Base
117 117 set_table_name :mantis_project_table
118 118 has_many :versions, :class_name => "MantisVersion", :foreign_key => :project_id
119 119 has_many :categories, :class_name => "MantisCategory", :foreign_key => :project_id
120 120 has_many :news, :class_name => "MantisNews", :foreign_key => :project_id
121 121 has_many :members, :class_name => "MantisProjectUser", :foreign_key => :project_id
122 122
123 123 def name
124 124 read_attribute(:name)[0..29]
125 125 end
126 126
127 127 def identifier
128 128 read_attribute(:name).underscore[0..19].gsub(/[^a-z0-9\-]/, '-')
129 129 end
130 130 end
131 131
132 132 class MantisVersion < ActiveRecord::Base
133 133 set_table_name :mantis_project_version_table
134 134
135 135 def version
136 136 read_attribute(:version)[0..29]
137 137 end
138 138
139 139 def description
140 140 read_attribute(:description)[0..254]
141 141 end
142 142 end
143 143
144 144 class MantisCategory < ActiveRecord::Base
145 145 set_table_name :mantis_project_category_table
146 146 end
147 147
148 148 class MantisProjectUser < ActiveRecord::Base
149 149 set_table_name :mantis_project_user_list_table
150 150 end
151 151
152 152 class MantisBug < ActiveRecord::Base
153 153 set_table_name :mantis_bug_table
154 154 belongs_to :bug_text, :class_name => "MantisBugText", :foreign_key => :bug_text_id
155 155 has_many :bug_notes, :class_name => "MantisBugNote", :foreign_key => :bug_id
156 156 has_many :bug_files, :class_name => "MantisBugFile", :foreign_key => :bug_id
157 157 has_many :bug_monitors, :class_name => "MantisBugMonitor", :foreign_key => :bug_id
158 158 end
159 159
160 160 class MantisBugText < ActiveRecord::Base
161 161 set_table_name :mantis_bug_text_table
162 162
163 163 # Adds Mantis steps_to_reproduce and additional_information fields
164 164 # to description if any
165 165 def full_description
166 166 full_description = description
167 167 full_description += "\n\n*Steps to reproduce:*\n\n#{steps_to_reproduce}" unless steps_to_reproduce.blank?
168 168 full_description += "\n\n*Additional information:*\n\n#{additional_information}" unless additional_information.blank?
169 169 full_description
170 170 end
171 171 end
172 172
173 173 class MantisBugNote < ActiveRecord::Base
174 174 set_table_name :mantis_bugnote_table
175 175 belongs_to :bug, :class_name => "MantisBug", :foreign_key => :bug_id
176 176 belongs_to :bug_note_text, :class_name => "MantisBugNoteText", :foreign_key => :bugnote_text_id
177 177 end
178 178
179 179 class MantisBugNoteText < ActiveRecord::Base
180 180 set_table_name :mantis_bugnote_text_table
181 181 end
182 182
183 183 class MantisBugFile < ActiveRecord::Base
184 184 set_table_name :mantis_bug_file_table
185 185
186 186 def size
187 187 filesize
188 188 end
189 189
190 190 def original_filename
191 191 MantisMigrate.encode(filename)
192 192 end
193 193
194 194 def content_type
195 195 file_type
196 196 end
197 197
198 198 def read
199 199 content
200 200 end
201 201 end
202 202
203 203 class MantisBugRelationship < ActiveRecord::Base
204 204 set_table_name :mantis_bug_relationship_table
205 205 end
206 206
207 207 class MantisBugMonitor < ActiveRecord::Base
208 208 set_table_name :mantis_bug_monitor_table
209 209 end
210 210
211 211 class MantisNews < ActiveRecord::Base
212 212 set_table_name :mantis_news_table
213 213 end
214 214
215 215 class MantisCustomField < ActiveRecord::Base
216 216 set_table_name :mantis_custom_field_table
217 217 set_inheritance_column :none
218 218 has_many :values, :class_name => "MantisCustomFieldString", :foreign_key => :field_id
219 219 has_many :projects, :class_name => "MantisCustomFieldProject", :foreign_key => :field_id
220 220
221 221 def format
222 222 read_attribute :type
223 223 end
224 224
225 225 def name
226 226 read_attribute(:name)[0..29].gsub(/[^\w\s\'\-]/, '-')
227 227 end
228 228 end
229 229
230 230 class MantisCustomFieldProject < ActiveRecord::Base
231 231 set_table_name :mantis_custom_field_project_table
232 232 end
233 233
234 234 class MantisCustomFieldString < ActiveRecord::Base
235 235 set_table_name :mantis_custom_field_string_table
236 236 end
237 237
238 238
239 239 def self.migrate
240 240
241 241 # Users
242 242 print "Migrating users"
243 243 User.delete_all "login <> 'admin'"
244 244 users_map = {}
245 245 users_migrated = 0
246 246 MantisUser.find(:all).each do |user|
247 247 u = User.new :firstname => encode(user.firstname),
248 248 :lastname => encode(user.lastname),
249 249 :mail => user.email,
250 250 :last_login_on => user.last_visit
251 251 u.login = user.username
252 252 u.password = 'mantis'
253 253 u.status = User::STATUS_LOCKED if user.enabled != 1
254 254 u.admin = true if user.access_level == 90
255 255 next unless u.save!
256 256 users_migrated += 1
257 257 users_map[user.id] = u.id
258 258 print '.'
259 259 end
260 260 puts
261 261
262 262 # Projects
263 263 print "Migrating projects"
264 264 Project.destroy_all
265 265 projects_map = {}
266 266 versions_map = {}
267 267 categories_map = {}
268 268 MantisProject.find(:all).each do |project|
269 269 p = Project.new :name => encode(project.name),
270 270 :description => encode(project.description)
271 271 p.identifier = project.identifier
272 272 next unless p.save
273 273 projects_map[project.id] = p.id
274 274 p.enabled_module_names = ['issue_tracking', 'news', 'wiki']
275 275 p.trackers << TRACKER_BUG
276 276 p.trackers << TRACKER_FEATURE
277 277 print '.'
278 278
279 279 # Project members
280 280 project.members.each do |member|
281 281 m = Member.new :user => User.find_by_id(users_map[member.user_id]),
282 282 :role => ROLE_MAPPING[member.access_level] || DEFAULT_ROLE
283 283 m.project = p
284 284 m.save
285 285 end
286 286
287 287 # Project versions
288 288 project.versions.each do |version|
289 289 v = Version.new :name => encode(version.version),
290 290 :description => encode(version.description),
291 291 :effective_date => version.date_order.to_date
292 292 v.project = p
293 293 v.save
294 294 versions_map[version.id] = v.id
295 295 end
296 296
297 297 # Project categories
298 298 project.categories.each do |category|
299 299 g = IssueCategory.new :name => category.category[0,30]
300 300 g.project = p
301 301 g.save
302 302 categories_map[category.category] = g.id
303 303 end
304 304 end
305 305 puts
306 306
307 307 # Bugs
308 308 print "Migrating bugs"
309 309 Issue.destroy_all
310 310 issues_map = {}
311 311 keep_bug_ids = (Issue.count == 0)
312 312 MantisBug.find(:all, :order => 'id ASC').each do |bug|
313 313 next unless projects_map[bug.project_id] && users_map[bug.reporter_id]
314 314 i = Issue.new :project_id => projects_map[bug.project_id],
315 315 :subject => encode(bug.summary),
316 316 :description => encode(bug.bug_text.full_description),
317 317 :priority => PRIORITY_MAPPING[bug.priority] || DEFAULT_PRIORITY,
318 318 :created_on => bug.date_submitted,
319 319 :updated_on => bug.last_updated
320 320 i.author = User.find_by_id(users_map[bug.reporter_id])
321 321 i.category = IssueCategory.find_by_project_id_and_name(i.project_id, bug.category[0,30]) unless bug.category.blank?
322 322 i.fixed_version = Version.find_by_project_id_and_name(i.project_id, bug.fixed_in_version) unless bug.fixed_in_version.blank?
323 323 i.status = STATUS_MAPPING[bug.status] || DEFAULT_STATUS
324 324 i.tracker = (bug.severity == 10 ? TRACKER_FEATURE : TRACKER_BUG)
325 325 i.id = bug.id if keep_bug_ids
326 326 next unless i.save
327 327 issues_map[bug.id] = i.id
328 328 print '.'
329 329
330 330 # Assignee
331 331 # Redmine checks that the assignee is a project member
332 332 if (bug.handler_id && users_map[bug.handler_id])
333 333 i.assigned_to = User.find_by_id(users_map[bug.handler_id])
334 334 i.save_with_validation(false)
335 335 end
336 336
337 337 # Bug notes
338 338 bug.bug_notes.each do |note|
339 339 next unless users_map[note.reporter_id]
340 340 n = Journal.new :notes => encode(note.bug_note_text.note),
341 341 :created_on => note.date_submitted
342 342 n.user = User.find_by_id(users_map[note.reporter_id])
343 343 n.journalized = i
344 344 n.save
345 345 end
346 346
347 347 # Bug files
348 348 bug.bug_files.each do |file|
349 349 a = Attachment.new :created_on => file.date_added
350 350 a.file = file
351 351 a.author = User.find :first
352 352 a.container = i
353 353 a.save
354 354 end
355 355
356 356 # Bug monitors
357 357 bug.bug_monitors.each do |monitor|
358 358 next unless users_map[monitor.user_id]
359 359 i.add_watcher(User.find_by_id(users_map[monitor.user_id]))
360 360 end
361 361 end
362 362
363 363 # update issue id sequence if needed (postgresql)
364 364 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
365 365 puts
366 366
367 367 # Bug relationships
368 368 print "Migrating bug relations"
369 369 MantisBugRelationship.find(:all).each do |relation|
370 370 next unless issues_map[relation.source_bug_id] && issues_map[relation.destination_bug_id]
371 371 r = IssueRelation.new :relation_type => RELATION_TYPE_MAPPING[relation.relationship_type]
372 372 r.issue_from = Issue.find_by_id(issues_map[relation.source_bug_id])
373 373 r.issue_to = Issue.find_by_id(issues_map[relation.destination_bug_id])
374 374 pp r unless r.save
375 375 print '.'
376 376 end
377 377 puts
378 378
379 379 # News
380 380 print "Migrating news"
381 381 News.destroy_all
382 382 MantisNews.find(:all, :conditions => 'project_id > 0').each do |news|
383 383 next unless projects_map[news.project_id]
384 384 n = News.new :project_id => projects_map[news.project_id],
385 385 :title => encode(news.headline[0..59]),
386 386 :description => encode(news.body),
387 387 :created_on => news.date_posted
388 388 n.author = User.find_by_id(users_map[news.poster_id])
389 389 n.save
390 390 print '.'
391 391 end
392 392 puts
393 393
394 394 # Custom fields
395 395 print "Migrating custom fields"
396 396 IssueCustomField.destroy_all
397 397 MantisCustomField.find(:all).each do |field|
398 398 f = IssueCustomField.new :name => field.name[0..29],
399 399 :field_format => CUSTOM_FIELD_TYPE_MAPPING[field.format],
400 400 :min_length => field.length_min,
401 401 :max_length => field.length_max,
402 402 :regexp => field.valid_regexp,
403 403 :possible_values => field.possible_values.split('|'),
404 404 :is_required => field.require_report?
405 405 next unless f.save
406 406 print '.'
407 407
408 408 # Trackers association
409 409 f.trackers = Tracker.find :all
410 410
411 411 # Projects association
412 412 field.projects.each do |project|
413 413 f.projects << Project.find_by_id(projects_map[project.project_id]) if projects_map[project.project_id]
414 414 end
415 415
416 416 # Values
417 417 field.values.each do |value|
418 418 v = CustomValue.new :custom_field_id => f.id,
419 419 :value => value.value
420 420 v.customized = Issue.find_by_id(issues_map[value.bug_id]) if issues_map[value.bug_id]
421 421 v.save
422 422 end unless f.new_record?
423 423 end
424 424 puts
425 425
426 426 puts
427 427 puts "Users: #{users_migrated}/#{MantisUser.count}"
428 428 puts "Projects: #{Project.count}/#{MantisProject.count}"
429 429 puts "Memberships: #{Member.count}/#{MantisProjectUser.count}"
430 430 puts "Versions: #{Version.count}/#{MantisVersion.count}"
431 431 puts "Categories: #{IssueCategory.count}/#{MantisCategory.count}"
432 432 puts "Bugs: #{Issue.count}/#{MantisBug.count}"
433 433 puts "Bug notes: #{Journal.count}/#{MantisBugNote.count}"
434 434 puts "Bug files: #{Attachment.count}/#{MantisBugFile.count}"
435 435 puts "Bug relations: #{IssueRelation.count}/#{MantisBugRelationship.count}"
436 436 puts "Bug monitors: #{Watcher.count}/#{MantisBugMonitor.count}"
437 437 puts "News: #{News.count}/#{MantisNews.count}"
438 438 puts "Custom fields: #{IssueCustomField.count}/#{MantisCustomField.count}"
439 439 end
440 440
441 441 def self.encoding(charset)
442 442 @ic = Iconv.new('UTF-8', charset)
443 443 rescue Iconv::InvalidEncoding
444 444 return false
445 445 end
446 446
447 447 def self.establish_connection(params)
448 448 constants.each do |const|
449 449 klass = const_get(const)
450 450 next unless klass.respond_to? 'establish_connection'
451 451 klass.establish_connection params
452 452 end
453 453 end
454 454
455 455 def self.encode(text)
456 456 @ic.iconv text
457 457 rescue
458 458 text
459 459 end
460 460 end
461 461
462 462 puts
463 463 if Redmine::DefaultData::Loader.no_data?
464 464 puts "Redmine configuration need to be loaded before importing data."
465 465 puts "Please, run this first:"
466 466 puts
467 467 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
468 468 exit
469 469 end
470 470
471 471 puts "WARNING: Your Redmine data will be deleted during this process."
472 472 print "Are you sure you want to continue ? [y/N] "
473 473 break unless STDIN.gets.match(/^y$/i)
474 474
475 475 # Default Mantis database settings
476 476 db_params = {:adapter => 'mysql',
477 477 :database => 'bugtracker',
478 478 :host => 'localhost',
479 479 :username => 'root',
480 480 :password => '' }
481 481
482 482 puts
483 483 puts "Please enter settings for your Mantis database"
484 484 [:adapter, :host, :database, :username, :password].each do |param|
485 485 print "#{param} [#{db_params[param]}]: "
486 486 value = STDIN.gets.chomp!
487 487 db_params[param] = value unless value.blank?
488 488 end
489 489
490 490 while true
491 491 print "encoding [UTF-8]: "
492 492 encoding = STDIN.gets.chomp!
493 493 encoding = 'UTF-8' if encoding.blank?
494 494 break if MantisMigrate.encoding encoding
495 495 puts "Invalid encoding!"
496 496 end
497 497 puts
498 498
499 499 # Make sure bugs can refer bugs in other projects
500 500 Setting.cross_project_issue_relations = 1 if Setting.respond_to? 'cross_project_issue_relations'
501 501
502 502 MantisMigrate.establish_connection db_params
503 503 MantisMigrate.migrate
504 504 end
505 505 end
@@ -1,750 +1,750
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 'active_record'
19 19 require 'iconv'
20 20 require 'pp'
21 21
22 22 namespace :redmine do
23 23 desc 'Trac migration script'
24 24 task :migrate_from_trac => :environment do
25 25
26 26 module TracMigrate
27 27 TICKET_MAP = []
28 28
29 29 DEFAULT_STATUS = IssueStatus.default
30 30 assigned_status = IssueStatus.find_by_position(2)
31 31 resolved_status = IssueStatus.find_by_position(3)
32 32 feedback_status = IssueStatus.find_by_position(4)
33 33 closed_status = IssueStatus.find :first, :conditions => { :is_closed => true }
34 34 STATUS_MAPPING = {'new' => DEFAULT_STATUS,
35 35 'reopened' => feedback_status,
36 36 'assigned' => assigned_status,
37 37 'closed' => closed_status
38 38 }
39 39
40 priorities = Enumeration.get_values('IPRI')
40 priorities = Enumeration.priorities
41 41 DEFAULT_PRIORITY = priorities[0]
42 42 PRIORITY_MAPPING = {'lowest' => priorities[0],
43 43 'low' => priorities[0],
44 44 'normal' => priorities[1],
45 45 'high' => priorities[2],
46 46 'highest' => priorities[3],
47 47 # ---
48 48 'trivial' => priorities[0],
49 49 'minor' => priorities[1],
50 50 'major' => priorities[2],
51 51 'critical' => priorities[3],
52 52 'blocker' => priorities[4]
53 53 }
54 54
55 55 TRACKER_BUG = Tracker.find_by_position(1)
56 56 TRACKER_FEATURE = Tracker.find_by_position(2)
57 57 DEFAULT_TRACKER = TRACKER_BUG
58 58 TRACKER_MAPPING = {'defect' => TRACKER_BUG,
59 59 'enhancement' => TRACKER_FEATURE,
60 60 'task' => TRACKER_FEATURE,
61 61 'patch' =>TRACKER_FEATURE
62 62 }
63 63
64 64 roles = Role.find(:all, :conditions => {:builtin => 0}, :order => 'position ASC')
65 65 manager_role = roles[0]
66 66 developer_role = roles[1]
67 67 DEFAULT_ROLE = roles.last
68 68 ROLE_MAPPING = {'admin' => manager_role,
69 69 'developer' => developer_role
70 70 }
71 71
72 72 class ::Time
73 73 class << self
74 74 alias :real_now :now
75 75 def now
76 76 real_now - @fake_diff.to_i
77 77 end
78 78 def fake(time)
79 79 @fake_diff = real_now - time
80 80 res = yield
81 81 @fake_diff = 0
82 82 res
83 83 end
84 84 end
85 85 end
86 86
87 87 class TracComponent < ActiveRecord::Base
88 88 set_table_name :component
89 89 end
90 90
91 91 class TracMilestone < ActiveRecord::Base
92 92 set_table_name :milestone
93 93 # If this attribute is set a milestone has a defined target timepoint
94 94 def due
95 95 if read_attribute(:due) && read_attribute(:due) > 0
96 96 Time.at(read_attribute(:due)).to_date
97 97 else
98 98 nil
99 99 end
100 100 end
101 101 # This is the real timepoint at which the milestone has finished.
102 102 def completed
103 103 if read_attribute(:completed) && read_attribute(:completed) > 0
104 104 Time.at(read_attribute(:completed)).to_date
105 105 else
106 106 nil
107 107 end
108 108 end
109 109
110 110 def description
111 111 # Attribute is named descr in Trac v0.8.x
112 112 has_attribute?(:descr) ? read_attribute(:descr) : read_attribute(:description)
113 113 end
114 114 end
115 115
116 116 class TracTicketCustom < ActiveRecord::Base
117 117 set_table_name :ticket_custom
118 118 end
119 119
120 120 class TracAttachment < ActiveRecord::Base
121 121 set_table_name :attachment
122 122 set_inheritance_column :none
123 123
124 124 def time; Time.at(read_attribute(:time)) end
125 125
126 126 def original_filename
127 127 filename
128 128 end
129 129
130 130 def content_type
131 131 Redmine::MimeType.of(filename) || ''
132 132 end
133 133
134 134 def exist?
135 135 File.file? trac_fullpath
136 136 end
137 137
138 138 def read
139 139 File.open("#{trac_fullpath}", 'rb').read
140 140 end
141 141
142 142 def description
143 143 read_attribute(:description).to_s.slice(0,255)
144 144 end
145 145
146 146 private
147 147 def trac_fullpath
148 148 attachment_type = read_attribute(:type)
149 149 trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) {|x| sprintf('%%%02x', x[0]) }
150 150 "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}"
151 151 end
152 152 end
153 153
154 154 class TracTicket < ActiveRecord::Base
155 155 set_table_name :ticket
156 156 set_inheritance_column :none
157 157
158 158 # ticket changes: only migrate status changes and comments
159 159 has_many :changes, :class_name => "TracTicketChange", :foreign_key => :ticket
160 160 has_many :attachments, :class_name => "TracAttachment",
161 161 :finder_sql => "SELECT DISTINCT attachment.* FROM #{TracMigrate::TracAttachment.table_name}" +
162 162 " WHERE #{TracMigrate::TracAttachment.table_name}.type = 'ticket'" +
163 163 ' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{id}\''
164 164 has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
165 165
166 166 def ticket_type
167 167 read_attribute(:type)
168 168 end
169 169
170 170 def summary
171 171 read_attribute(:summary).blank? ? "(no subject)" : read_attribute(:summary)
172 172 end
173 173
174 174 def description
175 175 read_attribute(:description).blank? ? summary : read_attribute(:description)
176 176 end
177 177
178 178 def time; Time.at(read_attribute(:time)) end
179 179 def changetime; Time.at(read_attribute(:changetime)) end
180 180 end
181 181
182 182 class TracTicketChange < ActiveRecord::Base
183 183 set_table_name :ticket_change
184 184
185 185 def time; Time.at(read_attribute(:time)) end
186 186 end
187 187
188 188 TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
189 189 TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
190 190 TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
191 191 TracReports TracRevisionLog TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \
192 192 TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
193 193 WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
194 194 CamelCase TitleIndex)
195 195
196 196 class TracWikiPage < ActiveRecord::Base
197 197 set_table_name :wiki
198 198 set_primary_key :name
199 199
200 200 has_many :attachments, :class_name => "TracAttachment",
201 201 :finder_sql => "SELECT DISTINCT attachment.* FROM #{TracMigrate::TracAttachment.table_name}" +
202 202 " WHERE #{TracMigrate::TracAttachment.table_name}.type = 'wiki'" +
203 203 ' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{id}\''
204 204
205 205 def self.columns
206 206 # Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0)
207 207 super.select {|column| column.name.to_s != 'readonly'}
208 208 end
209 209
210 210 def time; Time.at(read_attribute(:time)) end
211 211 end
212 212
213 213 class TracPermission < ActiveRecord::Base
214 214 set_table_name :permission
215 215 end
216 216
217 217 class TracSessionAttribute < ActiveRecord::Base
218 218 set_table_name :session_attribute
219 219 end
220 220
221 221 def self.find_or_create_user(username, project_member = false)
222 222 return User.anonymous if username.blank?
223 223
224 224 u = User.find_by_login(username)
225 225 if !u
226 226 # Create a new user if not found
227 227 mail = username[0,limit_for(User, 'mail')]
228 228 if mail_attr = TracSessionAttribute.find_by_sid_and_name(username, 'email')
229 229 mail = mail_attr.value
230 230 end
231 231 mail = "#{mail}@foo.bar" unless mail.include?("@")
232 232
233 233 name = username
234 234 if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name')
235 235 name = name_attr.value
236 236 end
237 237 name =~ (/(.*)(\s+\w+)?/)
238 238 fn = $1.strip
239 239 ln = ($2 || '-').strip
240 240
241 241 u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'),
242 242 :firstname => fn[0, limit_for(User, 'firstname')].gsub(/[^\w\s\'\-]/i, '-'),
243 243 :lastname => ln[0, limit_for(User, 'lastname')].gsub(/[^\w\s\'\-]/i, '-')
244 244
245 245 u.login = username[0,limit_for(User, 'login')].gsub(/[^a-z0-9_\-@\.]/i, '-')
246 246 u.password = 'trac'
247 247 u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
248 248 # finally, a default user is used if the new user is not valid
249 249 u = User.find(:first) unless u.save
250 250 end
251 251 # Make sure he is a member of the project
252 252 if project_member && !u.member_of?(@target_project)
253 253 role = DEFAULT_ROLE
254 254 if u.admin
255 255 role = ROLE_MAPPING['admin']
256 256 elsif TracPermission.find_by_username_and_action(username, 'developer')
257 257 role = ROLE_MAPPING['developer']
258 258 end
259 259 Member.create(:user => u, :project => @target_project, :role => role)
260 260 u.reload
261 261 end
262 262 u
263 263 end
264 264
265 265 # Basic wiki syntax conversion
266 266 def self.convert_wiki_text(text)
267 267 # Titles
268 268 text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"}
269 269 # External Links
270 270 text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"}
271 271 # Ticket links:
272 272 # [ticket:234 Text],[ticket:234 This is a test]
273 273 text = text.gsub(/\[ticket\:([^\ ]+)\ (.+?)\]/, '"\2":/issues/show/\1')
274 274 # ticket:1234
275 275 # #1 is working cause Redmine uses the same syntax.
276 276 text = text.gsub(/ticket\:([^\ ]+)/, '#\1')
277 277 # Milestone links:
278 278 # [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)]
279 279 # The text "Milestone 0.1.0 (Mercury)" is not converted,
280 280 # cause Redmine's wiki does not support this.
281 281 text = text.gsub(/\[milestone\:\"([^\"]+)\"\ (.+?)\]/, 'version:"\1"')
282 282 # [milestone:"0.1.0 Mercury"]
283 283 text = text.gsub(/\[milestone\:\"([^\"]+)\"\]/, 'version:"\1"')
284 284 text = text.gsub(/milestone\:\"([^\"]+)\"/, 'version:"\1"')
285 285 # milestone:0.1.0
286 286 text = text.gsub(/\[milestone\:([^\ ]+)\]/, 'version:\1')
287 287 text = text.gsub(/milestone\:([^\ ]+)/, 'version:\1')
288 288 # Internal Links
289 289 text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below
290 290 text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
291 291 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
292 292 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
293 293 text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
294 294 text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"}
295 295
296 296 # Links to pages UsingJustWikiCaps
297 297 text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
298 298 # Normalize things that were supposed to not be links
299 299 # like !NotALink
300 300 text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
301 301 # Revisions links
302 302 text = text.gsub(/\[(\d+)\]/, 'r\1')
303 303 # Ticket number re-writing
304 304 text = text.gsub(/#(\d+)/) do |s|
305 305 if $1.length < 10
306 306 TICKET_MAP[$1.to_i] ||= $1
307 307 "\##{TICKET_MAP[$1.to_i] || $1}"
308 308 else
309 309 s
310 310 end
311 311 end
312 312 # We would like to convert the Code highlighting too
313 313 # This will go into the next line.
314 314 shebang_line = false
315 315 # Reguar expression for start of code
316 316 pre_re = /\{\{\{/
317 317 # Code hightlighing...
318 318 shebang_re = /^\#\!([a-z]+)/
319 319 # Regular expression for end of code
320 320 pre_end_re = /\}\}\}/
321 321
322 322 # Go through the whole text..extract it line by line
323 323 text = text.gsub(/^(.*)$/) do |line|
324 324 m_pre = pre_re.match(line)
325 325 if m_pre
326 326 line = '<pre>'
327 327 else
328 328 m_sl = shebang_re.match(line)
329 329 if m_sl
330 330 shebang_line = true
331 331 line = '<code class="' + m_sl[1] + '">'
332 332 end
333 333 m_pre_end = pre_end_re.match(line)
334 334 if m_pre_end
335 335 line = '</pre>'
336 336 if shebang_line
337 337 line = '</code>' + line
338 338 end
339 339 end
340 340 end
341 341 line
342 342 end
343 343
344 344 # Highlighting
345 345 text = text.gsub(/'''''([^\s])/, '_*\1')
346 346 text = text.gsub(/([^\s])'''''/, '\1*_')
347 347 text = text.gsub(/'''/, '*')
348 348 text = text.gsub(/''/, '_')
349 349 text = text.gsub(/__/, '+')
350 350 text = text.gsub(/~~/, '-')
351 351 text = text.gsub(/`/, '@')
352 352 text = text.gsub(/,,/, '~')
353 353 # Lists
354 354 text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
355 355
356 356 text
357 357 end
358 358
359 359 def self.migrate
360 360 establish_connection
361 361
362 362 # Quick database test
363 363 TracComponent.count
364 364
365 365 migrated_components = 0
366 366 migrated_milestones = 0
367 367 migrated_tickets = 0
368 368 migrated_custom_values = 0
369 369 migrated_ticket_attachments = 0
370 370 migrated_wiki_edits = 0
371 371 migrated_wiki_attachments = 0
372 372
373 373 #Wiki system initializing...
374 374 @target_project.wiki.destroy if @target_project.wiki
375 375 @target_project.reload
376 376 wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
377 377 wiki_edit_count = 0
378 378
379 379 # Components
380 380 print "Migrating components"
381 381 issues_category_map = {}
382 382 TracComponent.find(:all).each do |component|
383 383 print '.'
384 384 STDOUT.flush
385 385 c = IssueCategory.new :project => @target_project,
386 386 :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
387 387 next unless c.save
388 388 issues_category_map[component.name] = c
389 389 migrated_components += 1
390 390 end
391 391 puts
392 392
393 393 # Milestones
394 394 print "Migrating milestones"
395 395 version_map = {}
396 396 TracMilestone.find(:all).each do |milestone|
397 397 print '.'
398 398 STDOUT.flush
399 399 # First we try to find the wiki page...
400 400 p = wiki.find_or_new_page(milestone.name.to_s)
401 401 p.content = WikiContent.new(:page => p) if p.new_record?
402 402 p.content.text = milestone.description.to_s
403 403 p.content.author = find_or_create_user('trac')
404 404 p.content.comments = 'Milestone'
405 405 p.save
406 406
407 407 v = Version.new :project => @target_project,
408 408 :name => encode(milestone.name[0, limit_for(Version, 'name')]),
409 409 :description => nil,
410 410 :wiki_page_title => milestone.name.to_s,
411 411 :effective_date => milestone.completed
412 412
413 413 next unless v.save
414 414 version_map[milestone.name] = v
415 415 migrated_milestones += 1
416 416 end
417 417 puts
418 418
419 419 # Custom fields
420 420 # TODO: read trac.ini instead
421 421 print "Migrating custom fields"
422 422 custom_field_map = {}
423 423 TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
424 424 print '.'
425 425 STDOUT.flush
426 426 # Redmine custom field name
427 427 field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
428 428 # Find if the custom already exists in Redmine
429 429 f = IssueCustomField.find_by_name(field_name)
430 430 # Or create a new one
431 431 f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
432 432 :field_format => 'string')
433 433
434 434 next if f.new_record?
435 435 f.trackers = Tracker.find(:all)
436 436 f.projects << @target_project
437 437 custom_field_map[field.name] = f
438 438 end
439 439 puts
440 440
441 441 # Trac 'resolution' field as a Redmine custom field
442 442 r = IssueCustomField.find(:first, :conditions => { :name => "Resolution" })
443 443 r = IssueCustomField.new(:name => 'Resolution',
444 444 :field_format => 'list',
445 445 :is_filter => true) if r.nil?
446 446 r.trackers = Tracker.find(:all)
447 447 r.projects << @target_project
448 448 r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq
449 449 r.save!
450 450 custom_field_map['resolution'] = r
451 451
452 452 # Tickets
453 453 print "Migrating tickets"
454 454 TracTicket.find(:all, :order => 'id ASC').each do |ticket|
455 455 print '.'
456 456 STDOUT.flush
457 457 i = Issue.new :project => @target_project,
458 458 :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
459 459 :description => convert_wiki_text(encode(ticket.description)),
460 460 :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
461 461 :created_on => ticket.time
462 462 i.author = find_or_create_user(ticket.reporter)
463 463 i.category = issues_category_map[ticket.component] unless ticket.component.blank?
464 464 i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
465 465 i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
466 466 i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
467 467 i.id = ticket.id unless Issue.exists?(ticket.id)
468 468 next unless Time.fake(ticket.changetime) { i.save }
469 469 TICKET_MAP[ticket.id] = i.id
470 470 migrated_tickets += 1
471 471
472 472 # Owner
473 473 unless ticket.owner.blank?
474 474 i.assigned_to = find_or_create_user(ticket.owner, true)
475 475 Time.fake(ticket.changetime) { i.save }
476 476 end
477 477
478 478 # Comments and status/resolution changes
479 479 ticket.changes.group_by(&:time).each do |time, changeset|
480 480 status_change = changeset.select {|change| change.field == 'status'}.first
481 481 resolution_change = changeset.select {|change| change.field == 'resolution'}.first
482 482 comment_change = changeset.select {|change| change.field == 'comment'}.first
483 483
484 484 n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
485 485 :created_on => time
486 486 n.user = find_or_create_user(changeset.first.author)
487 487 n.journalized = i
488 488 if status_change &&
489 489 STATUS_MAPPING[status_change.oldvalue] &&
490 490 STATUS_MAPPING[status_change.newvalue] &&
491 491 (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
492 492 n.details << JournalDetail.new(:property => 'attr',
493 493 :prop_key => 'status_id',
494 494 :old_value => STATUS_MAPPING[status_change.oldvalue].id,
495 495 :value => STATUS_MAPPING[status_change.newvalue].id)
496 496 end
497 497 if resolution_change
498 498 n.details << JournalDetail.new(:property => 'cf',
499 499 :prop_key => custom_field_map['resolution'].id,
500 500 :old_value => resolution_change.oldvalue,
501 501 :value => resolution_change.newvalue)
502 502 end
503 503 n.save unless n.details.empty? && n.notes.blank?
504 504 end
505 505
506 506 # Attachments
507 507 ticket.attachments.each do |attachment|
508 508 next unless attachment.exist?
509 509 a = Attachment.new :created_on => attachment.time
510 510 a.file = attachment
511 511 a.author = find_or_create_user(attachment.author)
512 512 a.container = i
513 513 a.description = attachment.description
514 514 migrated_ticket_attachments += 1 if a.save
515 515 end
516 516
517 517 # Custom fields
518 518 custom_values = ticket.customs.inject({}) do |h, custom|
519 519 if custom_field = custom_field_map[custom.name]
520 520 h[custom_field.id] = custom.value
521 521 migrated_custom_values += 1
522 522 end
523 523 h
524 524 end
525 525 if custom_field_map['resolution'] && !ticket.resolution.blank?
526 526 custom_values[custom_field_map['resolution'].id] = ticket.resolution
527 527 end
528 528 i.custom_field_values = custom_values
529 529 i.save_custom_field_values
530 530 end
531 531
532 532 # update issue id sequence if needed (postgresql)
533 533 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
534 534 puts
535 535
536 536 # Wiki
537 537 print "Migrating wiki"
538 538 if wiki.save
539 539 TracWikiPage.find(:all, :order => 'name, version').each do |page|
540 540 # Do not migrate Trac manual wiki pages
541 541 next if TRAC_WIKI_PAGES.include?(page.name)
542 542 wiki_edit_count += 1
543 543 print '.'
544 544 STDOUT.flush
545 545 p = wiki.find_or_new_page(page.name)
546 546 p.content = WikiContent.new(:page => p) if p.new_record?
547 547 p.content.text = page.text
548 548 p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac'
549 549 p.content.comments = page.comment
550 550 Time.fake(page.time) { p.new_record? ? p.save : p.content.save }
551 551
552 552 next if p.content.new_record?
553 553 migrated_wiki_edits += 1
554 554
555 555 # Attachments
556 556 page.attachments.each do |attachment|
557 557 next unless attachment.exist?
558 558 next if p.attachments.find_by_filename(attachment.filename.gsub(/^.*(\\|\/)/, '').gsub(/[^\w\.\-]/,'_')) #add only once per page
559 559 a = Attachment.new :created_on => attachment.time
560 560 a.file = attachment
561 561 a.author = find_or_create_user(attachment.author)
562 562 a.description = attachment.description
563 563 a.container = p
564 564 migrated_wiki_attachments += 1 if a.save
565 565 end
566 566 end
567 567
568 568 wiki.reload
569 569 wiki.pages.each do |page|
570 570 page.content.text = convert_wiki_text(page.content.text)
571 571 Time.fake(page.content.updated_on) { page.content.save }
572 572 end
573 573 end
574 574 puts
575 575
576 576 puts
577 577 puts "Components: #{migrated_components}/#{TracComponent.count}"
578 578 puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}"
579 579 puts "Tickets: #{migrated_tickets}/#{TracTicket.count}"
580 580 puts "Ticket files: #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s
581 581 puts "Custom values: #{migrated_custom_values}/#{TracTicketCustom.count}"
582 582 puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}"
583 583 puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
584 584 end
585 585
586 586 def self.limit_for(klass, attribute)
587 587 klass.columns_hash[attribute.to_s].limit
588 588 end
589 589
590 590 def self.encoding(charset)
591 591 @ic = Iconv.new('UTF-8', charset)
592 592 rescue Iconv::InvalidEncoding
593 593 puts "Invalid encoding!"
594 594 return false
595 595 end
596 596
597 597 def self.set_trac_directory(path)
598 598 @@trac_directory = path
599 599 raise "This directory doesn't exist!" unless File.directory?(path)
600 600 raise "#{trac_attachments_directory} doesn't exist!" unless File.directory?(trac_attachments_directory)
601 601 @@trac_directory
602 602 rescue Exception => e
603 603 puts e
604 604 return false
605 605 end
606 606
607 607 def self.trac_directory
608 608 @@trac_directory
609 609 end
610 610
611 611 def self.set_trac_adapter(adapter)
612 612 return false if adapter.blank?
613 613 raise "Unknown adapter: #{adapter}!" unless %w(sqlite sqlite3 mysql postgresql).include?(adapter)
614 614 # If adapter is sqlite or sqlite3, make sure that trac.db exists
615 615 raise "#{trac_db_path} doesn't exist!" if %w(sqlite sqlite3).include?(adapter) && !File.exist?(trac_db_path)
616 616 @@trac_adapter = adapter
617 617 rescue Exception => e
618 618 puts e
619 619 return false
620 620 end
621 621
622 622 def self.set_trac_db_host(host)
623 623 return nil if host.blank?
624 624 @@trac_db_host = host
625 625 end
626 626
627 627 def self.set_trac_db_port(port)
628 628 return nil if port.to_i == 0
629 629 @@trac_db_port = port.to_i
630 630 end
631 631
632 632 def self.set_trac_db_name(name)
633 633 return nil if name.blank?
634 634 @@trac_db_name = name
635 635 end
636 636
637 637 def self.set_trac_db_username(username)
638 638 @@trac_db_username = username
639 639 end
640 640
641 641 def self.set_trac_db_password(password)
642 642 @@trac_db_password = password
643 643 end
644 644
645 645 def self.set_trac_db_schema(schema)
646 646 @@trac_db_schema = schema
647 647 end
648 648
649 649 mattr_reader :trac_directory, :trac_adapter, :trac_db_host, :trac_db_port, :trac_db_name, :trac_db_schema, :trac_db_username, :trac_db_password
650 650
651 651 def self.trac_db_path; "#{trac_directory}/db/trac.db" end
652 652 def self.trac_attachments_directory; "#{trac_directory}/attachments" end
653 653
654 654 def self.target_project_identifier(identifier)
655 655 project = Project.find_by_identifier(identifier)
656 656 if !project
657 657 # create the target project
658 658 project = Project.new :name => identifier.humanize,
659 659 :description => ''
660 660 project.identifier = identifier
661 661 puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
662 662 # enable issues and wiki for the created project
663 663 project.enabled_module_names = ['issue_tracking', 'wiki']
664 664 else
665 665 puts
666 666 puts "This project already exists in your Redmine database."
667 667 print "Are you sure you want to append data to this project ? [Y/n] "
668 668 exit if STDIN.gets.match(/^n$/i)
669 669 end
670 670 project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
671 671 project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
672 672 @target_project = project.new_record? ? nil : project
673 673 end
674 674
675 675 def self.connection_params
676 676 if %w(sqlite sqlite3).include?(trac_adapter)
677 677 {:adapter => trac_adapter,
678 678 :database => trac_db_path}
679 679 else
680 680 {:adapter => trac_adapter,
681 681 :database => trac_db_name,
682 682 :host => trac_db_host,
683 683 :port => trac_db_port,
684 684 :username => trac_db_username,
685 685 :password => trac_db_password,
686 686 :schema_search_path => trac_db_schema
687 687 }
688 688 end
689 689 end
690 690
691 691 def self.establish_connection
692 692 constants.each do |const|
693 693 klass = const_get(const)
694 694 next unless klass.respond_to? 'establish_connection'
695 695 klass.establish_connection connection_params
696 696 end
697 697 end
698 698
699 699 private
700 700 def self.encode(text)
701 701 @ic.iconv text
702 702 rescue
703 703 text
704 704 end
705 705 end
706 706
707 707 puts
708 708 if Redmine::DefaultData::Loader.no_data?
709 709 puts "Redmine configuration need to be loaded before importing data."
710 710 puts "Please, run this first:"
711 711 puts
712 712 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
713 713 exit
714 714 end
715 715
716 716 puts "WARNING: a new project will be added to Redmine during this process."
717 717 print "Are you sure you want to continue ? [y/N] "
718 718 break unless STDIN.gets.match(/^y$/i)
719 719 puts
720 720
721 721 def prompt(text, options = {}, &block)
722 722 default = options[:default] || ''
723 723 while true
724 724 print "#{text} [#{default}]: "
725 725 value = STDIN.gets.chomp!
726 726 value = default if value.blank?
727 727 break if yield value
728 728 end
729 729 end
730 730
731 731 DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
732 732
733 733 prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
734 734 prompt('Trac database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite') {|adapter| TracMigrate.set_trac_adapter adapter}
735 735 unless %w(sqlite sqlite3).include?(TracMigrate.trac_adapter)
736 736 prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
737 737 prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
738 738 prompt('Trac database name') {|name| TracMigrate.set_trac_db_name name}
739 739 prompt('Trac database schema', :default => 'public') {|schema| TracMigrate.set_trac_db_schema schema}
740 740 prompt('Trac database username') {|username| TracMigrate.set_trac_db_username username}
741 741 prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
742 742 end
743 743 prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
744 744 prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
745 745 puts
746 746
747 747 TracMigrate.migrate
748 748 end
749 749 end
750 750
@@ -1,963 +1,963
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2008 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 require 'issues_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class IssuesController; def rescue_action(e) raise e end; end
23 23
24 24 class IssuesControllerTest < Test::Unit::TestCase
25 25 fixtures :projects,
26 26 :users,
27 27 :roles,
28 28 :members,
29 29 :issues,
30 30 :issue_statuses,
31 31 :versions,
32 32 :trackers,
33 33 :projects_trackers,
34 34 :issue_categories,
35 35 :enabled_modules,
36 36 :enumerations,
37 37 :attachments,
38 38 :workflows,
39 39 :custom_fields,
40 40 :custom_values,
41 41 :custom_fields_trackers,
42 42 :time_entries,
43 43 :journals,
44 44 :journal_details
45 45
46 46 def setup
47 47 @controller = IssuesController.new
48 48 @request = ActionController::TestRequest.new
49 49 @response = ActionController::TestResponse.new
50 50 User.current = nil
51 51 end
52 52
53 53 def test_index_routing
54 54 assert_routing(
55 55 {:method => :get, :path => '/issues'},
56 56 :controller => 'issues', :action => 'index'
57 57 )
58 58 end
59 59
60 60 def test_index
61 61 get :index
62 62 assert_response :success
63 63 assert_template 'index.rhtml'
64 64 assert_not_nil assigns(:issues)
65 65 assert_nil assigns(:project)
66 66 assert_tag :tag => 'a', :content => /Can't print recipes/
67 67 assert_tag :tag => 'a', :content => /Subproject issue/
68 68 # private projects hidden
69 69 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
70 70 assert_no_tag :tag => 'a', :content => /Issue on project 2/
71 71 end
72 72
73 73 def test_index_should_not_list_issues_when_module_disabled
74 74 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
75 75 get :index
76 76 assert_response :success
77 77 assert_template 'index.rhtml'
78 78 assert_not_nil assigns(:issues)
79 79 assert_nil assigns(:project)
80 80 assert_no_tag :tag => 'a', :content => /Can't print recipes/
81 81 assert_tag :tag => 'a', :content => /Subproject issue/
82 82 end
83 83
84 84 def test_index_with_project_routing
85 85 assert_routing(
86 86 {:method => :get, :path => '/projects/23/issues'},
87 87 :controller => 'issues', :action => 'index', :project_id => '23'
88 88 )
89 89 end
90 90
91 91 def test_index_should_not_list_issues_when_module_disabled
92 92 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
93 93 get :index
94 94 assert_response :success
95 95 assert_template 'index.rhtml'
96 96 assert_not_nil assigns(:issues)
97 97 assert_nil assigns(:project)
98 98 assert_no_tag :tag => 'a', :content => /Can't print recipes/
99 99 assert_tag :tag => 'a', :content => /Subproject issue/
100 100 end
101 101
102 102 def test_index_with_project_routing
103 103 assert_routing(
104 104 {:method => :get, :path => 'projects/23/issues'},
105 105 :controller => 'issues', :action => 'index', :project_id => '23'
106 106 )
107 107 end
108 108
109 109 def test_index_with_project
110 110 Setting.display_subprojects_issues = 0
111 111 get :index, :project_id => 1
112 112 assert_response :success
113 113 assert_template 'index.rhtml'
114 114 assert_not_nil assigns(:issues)
115 115 assert_tag :tag => 'a', :content => /Can't print recipes/
116 116 assert_no_tag :tag => 'a', :content => /Subproject issue/
117 117 end
118 118
119 119 def test_index_with_project_and_subprojects
120 120 Setting.display_subprojects_issues = 1
121 121 get :index, :project_id => 1
122 122 assert_response :success
123 123 assert_template 'index.rhtml'
124 124 assert_not_nil assigns(:issues)
125 125 assert_tag :tag => 'a', :content => /Can't print recipes/
126 126 assert_tag :tag => 'a', :content => /Subproject issue/
127 127 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
128 128 end
129 129
130 130 def test_index_with_project_and_subprojects_should_show_private_subprojects
131 131 @request.session[:user_id] = 2
132 132 Setting.display_subprojects_issues = 1
133 133 get :index, :project_id => 1
134 134 assert_response :success
135 135 assert_template 'index.rhtml'
136 136 assert_not_nil assigns(:issues)
137 137 assert_tag :tag => 'a', :content => /Can't print recipes/
138 138 assert_tag :tag => 'a', :content => /Subproject issue/
139 139 assert_tag :tag => 'a', :content => /Issue of a private subproject/
140 140 end
141 141
142 142 def test_index_with_project_routing_formatted
143 143 assert_routing(
144 144 {:method => :get, :path => 'projects/23/issues.pdf'},
145 145 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'pdf'
146 146 )
147 147 assert_routing(
148 148 {:method => :get, :path => 'projects/23/issues.atom'},
149 149 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'atom'
150 150 )
151 151 end
152 152
153 153 def test_index_with_project_and_filter
154 154 get :index, :project_id => 1, :set_filter => 1
155 155 assert_response :success
156 156 assert_template 'index.rhtml'
157 157 assert_not_nil assigns(:issues)
158 158 end
159 159
160 160 def test_index_csv_with_project
161 161 get :index, :format => 'csv'
162 162 assert_response :success
163 163 assert_not_nil assigns(:issues)
164 164 assert_equal 'text/csv', @response.content_type
165 165
166 166 get :index, :project_id => 1, :format => 'csv'
167 167 assert_response :success
168 168 assert_not_nil assigns(:issues)
169 169 assert_equal 'text/csv', @response.content_type
170 170 end
171 171
172 172 def test_index_formatted
173 173 assert_routing(
174 174 {:method => :get, :path => 'issues.pdf'},
175 175 :controller => 'issues', :action => 'index', :format => 'pdf'
176 176 )
177 177 assert_routing(
178 178 {:method => :get, :path => 'issues.atom'},
179 179 :controller => 'issues', :action => 'index', :format => 'atom'
180 180 )
181 181 end
182 182
183 183 def test_index_pdf
184 184 get :index, :format => 'pdf'
185 185 assert_response :success
186 186 assert_not_nil assigns(:issues)
187 187 assert_equal 'application/pdf', @response.content_type
188 188
189 189 get :index, :project_id => 1, :format => 'pdf'
190 190 assert_response :success
191 191 assert_not_nil assigns(:issues)
192 192 assert_equal 'application/pdf', @response.content_type
193 193 end
194 194
195 195 def test_index_sort
196 196 get :index, :sort_key => 'tracker'
197 197 assert_response :success
198 198
199 199 sort_params = @request.session['issuesindex_sort']
200 200 assert sort_params.is_a?(Hash)
201 201 assert_equal 'tracker', sort_params[:key]
202 202 assert_equal 'ASC', sort_params[:order]
203 203 end
204 204
205 205 def test_gantt
206 206 get :gantt, :project_id => 1
207 207 assert_response :success
208 208 assert_template 'gantt.rhtml'
209 209 assert_not_nil assigns(:gantt)
210 210 events = assigns(:gantt).events
211 211 assert_not_nil events
212 212 # Issue with start and due dates
213 213 i = Issue.find(1)
214 214 assert_not_nil i.due_date
215 215 assert events.include?(Issue.find(1))
216 216 # Issue with without due date but targeted to a version with date
217 217 i = Issue.find(2)
218 218 assert_nil i.due_date
219 219 assert events.include?(i)
220 220 end
221 221
222 222 def test_cross_project_gantt
223 223 get :gantt
224 224 assert_response :success
225 225 assert_template 'gantt.rhtml'
226 226 assert_not_nil assigns(:gantt)
227 227 events = assigns(:gantt).events
228 228 assert_not_nil events
229 229 end
230 230
231 231 def test_gantt_export_to_pdf
232 232 get :gantt, :project_id => 1, :format => 'pdf'
233 233 assert_response :success
234 234 assert_equal 'application/pdf', @response.content_type
235 235 assert @response.body.starts_with?('%PDF')
236 236 assert_not_nil assigns(:gantt)
237 237 end
238 238
239 239 def test_cross_project_gantt_export_to_pdf
240 240 get :gantt, :format => 'pdf'
241 241 assert_response :success
242 242 assert_equal 'application/pdf', @response.content_type
243 243 assert @response.body.starts_with?('%PDF')
244 244 assert_not_nil assigns(:gantt)
245 245 end
246 246
247 247 if Object.const_defined?(:Magick)
248 248 def test_gantt_image
249 249 get :gantt, :project_id => 1, :format => 'png'
250 250 assert_response :success
251 251 assert_equal 'image/png', @response.content_type
252 252 end
253 253 else
254 254 puts "RMagick not installed. Skipping tests !!!"
255 255 end
256 256
257 257 def test_calendar
258 258 get :calendar, :project_id => 1
259 259 assert_response :success
260 260 assert_template 'calendar'
261 261 assert_not_nil assigns(:calendar)
262 262 end
263 263
264 264 def test_cross_project_calendar
265 265 get :calendar
266 266 assert_response :success
267 267 assert_template 'calendar'
268 268 assert_not_nil assigns(:calendar)
269 269 end
270 270
271 271 def test_changes
272 272 get :changes, :project_id => 1
273 273 assert_response :success
274 274 assert_not_nil assigns(:journals)
275 275 assert_equal 'application/atom+xml', @response.content_type
276 276 end
277 277
278 278 def test_show_routing
279 279 assert_routing(
280 280 {:method => :get, :path => '/issues/64'},
281 281 :controller => 'issues', :action => 'show', :id => '64'
282 282 )
283 283 end
284 284
285 285 def test_show_routing_formatted
286 286 assert_routing(
287 287 {:method => :get, :path => '/issues/2332.pdf'},
288 288 :controller => 'issues', :action => 'show', :id => '2332', :format => 'pdf'
289 289 )
290 290 assert_routing(
291 291 {:method => :get, :path => '/issues/23123.atom'},
292 292 :controller => 'issues', :action => 'show', :id => '23123', :format => 'atom'
293 293 )
294 294 end
295 295
296 296 def test_show_by_anonymous
297 297 get :show, :id => 1
298 298 assert_response :success
299 299 assert_template 'show.rhtml'
300 300 assert_not_nil assigns(:issue)
301 301 assert_equal Issue.find(1), assigns(:issue)
302 302
303 303 # anonymous role is allowed to add a note
304 304 assert_tag :tag => 'form',
305 305 :descendant => { :tag => 'fieldset',
306 306 :child => { :tag => 'legend',
307 307 :content => /Notes/ } }
308 308 end
309 309
310 310 def test_show_by_manager
311 311 @request.session[:user_id] = 2
312 312 get :show, :id => 1
313 313 assert_response :success
314 314
315 315 assert_tag :tag => 'form',
316 316 :descendant => { :tag => 'fieldset',
317 317 :child => { :tag => 'legend',
318 318 :content => /Change properties/ } },
319 319 :descendant => { :tag => 'fieldset',
320 320 :child => { :tag => 'legend',
321 321 :content => /Log time/ } },
322 322 :descendant => { :tag => 'fieldset',
323 323 :child => { :tag => 'legend',
324 324 :content => /Notes/ } }
325 325 end
326 326
327 327 def test_show_should_not_disclose_relations_to_invisible_issues
328 328 Setting.cross_project_issue_relations = '1'
329 329 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
330 330 # Relation to a private project issue
331 331 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
332 332
333 333 get :show, :id => 1
334 334 assert_response :success
335 335
336 336 assert_tag :div, :attributes => { :id => 'relations' },
337 337 :descendant => { :tag => 'a', :content => /#2$/ }
338 338 assert_no_tag :div, :attributes => { :id => 'relations' },
339 339 :descendant => { :tag => 'a', :content => /#4$/ }
340 340 end
341 341
342 342 def test_new_routing
343 343 assert_routing(
344 344 {:method => :get, :path => '/projects/1/issues/new'},
345 345 :controller => 'issues', :action => 'new', :project_id => '1'
346 346 )
347 347 assert_recognizes(
348 348 {:controller => 'issues', :action => 'new', :project_id => '1'},
349 349 {:method => :post, :path => '/projects/1/issues'}
350 350 )
351 351 end
352 352
353 353 def test_show_export_to_pdf
354 354 get :show, :id => 3, :format => 'pdf'
355 355 assert_response :success
356 356 assert_equal 'application/pdf', @response.content_type
357 357 assert @response.body.starts_with?('%PDF')
358 358 assert_not_nil assigns(:issue)
359 359 end
360 360
361 361 def test_get_new
362 362 @request.session[:user_id] = 2
363 363 get :new, :project_id => 1, :tracker_id => 1
364 364 assert_response :success
365 365 assert_template 'new'
366 366
367 367 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
368 368 :value => 'Default string' }
369 369 end
370 370
371 371 def test_get_new_without_tracker_id
372 372 @request.session[:user_id] = 2
373 373 get :new, :project_id => 1
374 374 assert_response :success
375 375 assert_template 'new'
376 376
377 377 issue = assigns(:issue)
378 378 assert_not_nil issue
379 379 assert_equal Project.find(1).trackers.first, issue.tracker
380 380 end
381 381
382 382 def test_update_new_form
383 383 @request.session[:user_id] = 2
384 384 xhr :post, :new, :project_id => 1,
385 385 :issue => {:tracker_id => 2,
386 386 :subject => 'This is the test_new issue',
387 387 :description => 'This is the description',
388 388 :priority_id => 5}
389 389 assert_response :success
390 390 assert_template 'new'
391 391 end
392 392
393 393 def test_post_new
394 394 @request.session[:user_id] = 2
395 395 post :new, :project_id => 1,
396 396 :issue => {:tracker_id => 3,
397 397 :subject => 'This is the test_new issue',
398 398 :description => 'This is the description',
399 399 :priority_id => 5,
400 400 :estimated_hours => '',
401 401 :custom_field_values => {'2' => 'Value for field 2'}}
402 402 assert_redirected_to :action => 'show'
403 403
404 404 issue = Issue.find_by_subject('This is the test_new issue')
405 405 assert_not_nil issue
406 406 assert_equal 2, issue.author_id
407 407 assert_equal 3, issue.tracker_id
408 408 assert_nil issue.estimated_hours
409 409 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
410 410 assert_not_nil v
411 411 assert_equal 'Value for field 2', v.value
412 412 end
413 413
414 414 def test_post_new_and_continue
415 415 @request.session[:user_id] = 2
416 416 post :new, :project_id => 1,
417 417 :issue => {:tracker_id => 3,
418 418 :subject => 'This is first issue',
419 419 :priority_id => 5},
420 420 :continue => ''
421 421 assert_redirected_to :controller => 'issues', :action => 'new', :tracker_id => 3
422 422 end
423 423
424 424 def test_post_new_without_custom_fields_param
425 425 @request.session[:user_id] = 2
426 426 post :new, :project_id => 1,
427 427 :issue => {:tracker_id => 1,
428 428 :subject => 'This is the test_new issue',
429 429 :description => 'This is the description',
430 430 :priority_id => 5}
431 431 assert_redirected_to :action => 'show'
432 432 end
433 433
434 434 def test_post_new_with_required_custom_field_and_without_custom_fields_param
435 435 field = IssueCustomField.find_by_name('Database')
436 436 field.update_attribute(:is_required, true)
437 437
438 438 @request.session[:user_id] = 2
439 439 post :new, :project_id => 1,
440 440 :issue => {:tracker_id => 1,
441 441 :subject => 'This is the test_new issue',
442 442 :description => 'This is the description',
443 443 :priority_id => 5}
444 444 assert_response :success
445 445 assert_template 'new'
446 446 issue = assigns(:issue)
447 447 assert_not_nil issue
448 448 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
449 449 end
450 450
451 451 def test_post_new_with_watchers
452 452 @request.session[:user_id] = 2
453 453 ActionMailer::Base.deliveries.clear
454 454
455 455 assert_difference 'Watcher.count', 2 do
456 456 post :new, :project_id => 1,
457 457 :issue => {:tracker_id => 1,
458 458 :subject => 'This is a new issue with watchers',
459 459 :description => 'This is the description',
460 460 :priority_id => 5,
461 461 :watcher_user_ids => ['2', '3']}
462 462 end
463 463 issue = Issue.find_by_subject('This is a new issue with watchers')
464 464 assert_not_nil issue
465 465 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
466 466
467 467 # Watchers added
468 468 assert_equal [2, 3], issue.watcher_user_ids.sort
469 469 assert issue.watched_by?(User.find(3))
470 470 # Watchers notified
471 471 mail = ActionMailer::Base.deliveries.last
472 472 assert_kind_of TMail::Mail, mail
473 473 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
474 474 end
475 475
476 476 def test_post_should_preserve_fields_values_on_validation_failure
477 477 @request.session[:user_id] = 2
478 478 post :new, :project_id => 1,
479 479 :issue => {:tracker_id => 1,
480 480 # empty subject
481 481 :subject => '',
482 482 :description => 'This is a description',
483 483 :priority_id => 6,
484 484 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
485 485 assert_response :success
486 486 assert_template 'new'
487 487
488 488 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
489 489 :content => 'This is a description'
490 490 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
491 491 :child => { :tag => 'option', :attributes => { :selected => 'selected',
492 492 :value => '6' },
493 493 :content => 'High' }
494 494 # Custom fields
495 495 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
496 496 :child => { :tag => 'option', :attributes => { :selected => 'selected',
497 497 :value => 'Oracle' },
498 498 :content => 'Oracle' }
499 499 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
500 500 :value => 'Value for field 2'}
501 501 end
502 502
503 503 def test_copy_routing
504 504 assert_routing(
505 505 {:method => :get, :path => '/projects/world_domination/issues/567/copy'},
506 506 :controller => 'issues', :action => 'new', :project_id => 'world_domination', :copy_from => '567'
507 507 )
508 508 end
509 509
510 510 def test_copy_issue
511 511 @request.session[:user_id] = 2
512 512 get :new, :project_id => 1, :copy_from => 1
513 513 assert_template 'new'
514 514 assert_not_nil assigns(:issue)
515 515 orig = Issue.find(1)
516 516 assert_equal orig.subject, assigns(:issue).subject
517 517 end
518 518
519 519 def test_edit_routing
520 520 assert_routing(
521 521 {:method => :get, :path => '/issues/1/edit'},
522 522 :controller => 'issues', :action => 'edit', :id => '1'
523 523 )
524 524 assert_recognizes( #TODO: use a PUT on the issue URI isntead, need to adjust form
525 525 {:controller => 'issues', :action => 'edit', :id => '1'},
526 526 {:method => :post, :path => '/issues/1/edit'}
527 527 )
528 528 end
529 529
530 530 def test_get_edit
531 531 @request.session[:user_id] = 2
532 532 get :edit, :id => 1
533 533 assert_response :success
534 534 assert_template 'edit'
535 535 assert_not_nil assigns(:issue)
536 536 assert_equal Issue.find(1), assigns(:issue)
537 537 end
538 538
539 539 def test_get_edit_with_params
540 540 @request.session[:user_id] = 2
541 541 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
542 542 assert_response :success
543 543 assert_template 'edit'
544 544
545 545 issue = assigns(:issue)
546 546 assert_not_nil issue
547 547
548 548 assert_equal 5, issue.status_id
549 549 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
550 550 :child => { :tag => 'option',
551 551 :content => 'Closed',
552 552 :attributes => { :selected => 'selected' } }
553 553
554 554 assert_equal 7, issue.priority_id
555 555 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
556 556 :child => { :tag => 'option',
557 557 :content => 'Urgent',
558 558 :attributes => { :selected => 'selected' } }
559 559 end
560 560
561 561 def test_reply_routing
562 562 assert_routing(
563 563 {:method => :post, :path => '/issues/1/quoted'},
564 564 :controller => 'issues', :action => 'reply', :id => '1'
565 565 )
566 566 end
567 567
568 568 def test_reply_to_issue
569 569 @request.session[:user_id] = 2
570 570 get :reply, :id => 1
571 571 assert_response :success
572 572 assert_select_rjs :show, "update"
573 573 end
574 574
575 575 def test_reply_to_note
576 576 @request.session[:user_id] = 2
577 577 get :reply, :id => 1, :journal_id => 2
578 578 assert_response :success
579 579 assert_select_rjs :show, "update"
580 580 end
581 581
582 582 def test_post_edit_without_custom_fields_param
583 583 @request.session[:user_id] = 2
584 584 ActionMailer::Base.deliveries.clear
585 585
586 586 issue = Issue.find(1)
587 587 assert_equal '125', issue.custom_value_for(2).value
588 588 old_subject = issue.subject
589 589 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
590 590
591 591 assert_difference('Journal.count') do
592 592 assert_difference('JournalDetail.count', 2) do
593 593 post :edit, :id => 1, :issue => {:subject => new_subject,
594 594 :priority_id => '6',
595 595 :category_id => '1' # no change
596 596 }
597 597 end
598 598 end
599 599 assert_redirected_to :action => 'show', :id => '1'
600 600 issue.reload
601 601 assert_equal new_subject, issue.subject
602 602 # Make sure custom fields were not cleared
603 603 assert_equal '125', issue.custom_value_for(2).value
604 604
605 605 mail = ActionMailer::Base.deliveries.last
606 606 assert_kind_of TMail::Mail, mail
607 607 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
608 608 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
609 609 end
610 610
611 611 def test_post_edit_with_custom_field_change
612 612 @request.session[:user_id] = 2
613 613 issue = Issue.find(1)
614 614 assert_equal '125', issue.custom_value_for(2).value
615 615
616 616 assert_difference('Journal.count') do
617 617 assert_difference('JournalDetail.count', 3) do
618 618 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
619 619 :priority_id => '6',
620 620 :category_id => '1', # no change
621 621 :custom_field_values => { '2' => 'New custom value' }
622 622 }
623 623 end
624 624 end
625 625 assert_redirected_to :action => 'show', :id => '1'
626 626 issue.reload
627 627 assert_equal 'New custom value', issue.custom_value_for(2).value
628 628
629 629 mail = ActionMailer::Base.deliveries.last
630 630 assert_kind_of TMail::Mail, mail
631 631 assert mail.body.include?("Searchable field changed from 125 to New custom value")
632 632 end
633 633
634 634 def test_post_edit_with_status_and_assignee_change
635 635 issue = Issue.find(1)
636 636 assert_equal 1, issue.status_id
637 637 @request.session[:user_id] = 2
638 638 assert_difference('TimeEntry.count', 0) do
639 639 post :edit,
640 640 :id => 1,
641 641 :issue => { :status_id => 2, :assigned_to_id => 3 },
642 642 :notes => 'Assigned to dlopper',
643 :time_entry => { :hours => '', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
643 :time_entry => { :hours => '', :comments => '', :activity_id => Enumeration.activities.first }
644 644 end
645 645 assert_redirected_to :action => 'show', :id => '1'
646 646 issue.reload
647 647 assert_equal 2, issue.status_id
648 648 j = issue.journals.find(:first, :order => 'id DESC')
649 649 assert_equal 'Assigned to dlopper', j.notes
650 650 assert_equal 2, j.details.size
651 651
652 652 mail = ActionMailer::Base.deliveries.last
653 653 assert mail.body.include?("Status changed from New to Assigned")
654 654 end
655 655
656 656 def test_post_edit_with_note_only
657 657 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
658 658 # anonymous user
659 659 post :edit,
660 660 :id => 1,
661 661 :notes => notes
662 662 assert_redirected_to :action => 'show', :id => '1'
663 663 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
664 664 assert_equal notes, j.notes
665 665 assert_equal 0, j.details.size
666 666 assert_equal User.anonymous, j.user
667 667
668 668 mail = ActionMailer::Base.deliveries.last
669 669 assert mail.body.include?(notes)
670 670 end
671 671
672 672 def test_post_edit_with_note_and_spent_time
673 673 @request.session[:user_id] = 2
674 674 spent_hours_before = Issue.find(1).spent_hours
675 675 assert_difference('TimeEntry.count') do
676 676 post :edit,
677 677 :id => 1,
678 678 :notes => '2.5 hours added',
679 :time_entry => { :hours => '2.5', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
679 :time_entry => { :hours => '2.5', :comments => '', :activity_id => Enumeration.activities.first }
680 680 end
681 681 assert_redirected_to :action => 'show', :id => '1'
682 682
683 683 issue = Issue.find(1)
684 684
685 685 j = issue.journals.find(:first, :order => 'id DESC')
686 686 assert_equal '2.5 hours added', j.notes
687 687 assert_equal 0, j.details.size
688 688
689 689 t = issue.time_entries.find(:first, :order => 'id DESC')
690 690 assert_not_nil t
691 691 assert_equal 2.5, t.hours
692 692 assert_equal spent_hours_before + 2.5, issue.spent_hours
693 693 end
694 694
695 695 def test_post_edit_with_attachment_only
696 696 set_tmp_attachments_directory
697 697
698 698 # Delete all fixtured journals, a race condition can occur causing the wrong
699 699 # journal to get fetched in the next find.
700 700 Journal.delete_all
701 701
702 702 # anonymous user
703 703 post :edit,
704 704 :id => 1,
705 705 :notes => '',
706 706 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
707 707 assert_redirected_to :action => 'show', :id => '1'
708 708 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
709 709 assert j.notes.blank?
710 710 assert_equal 1, j.details.size
711 711 assert_equal 'testfile.txt', j.details.first.value
712 712 assert_equal User.anonymous, j.user
713 713
714 714 mail = ActionMailer::Base.deliveries.last
715 715 assert mail.body.include?('testfile.txt')
716 716 end
717 717
718 718 def test_post_edit_with_no_change
719 719 issue = Issue.find(1)
720 720 issue.journals.clear
721 721 ActionMailer::Base.deliveries.clear
722 722
723 723 post :edit,
724 724 :id => 1,
725 725 :notes => ''
726 726 assert_redirected_to :action => 'show', :id => '1'
727 727
728 728 issue.reload
729 729 assert issue.journals.empty?
730 730 # No email should be sent
731 731 assert ActionMailer::Base.deliveries.empty?
732 732 end
733 733
734 734 def test_post_edit_with_invalid_spent_time
735 735 @request.session[:user_id] = 2
736 736 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
737 737
738 738 assert_no_difference('Journal.count') do
739 739 post :edit,
740 740 :id => 1,
741 741 :notes => notes,
742 742 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
743 743 end
744 744 assert_response :success
745 745 assert_template 'edit'
746 746
747 747 assert_tag :textarea, :attributes => { :name => 'notes' },
748 748 :content => notes
749 749 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
750 750 end
751 751
752 752 def test_bulk_edit
753 753 @request.session[:user_id] = 2
754 754 # update issues priority
755 755 post :bulk_edit, :ids => [1, 2], :priority_id => 7,
756 756 :assigned_to_id => '',
757 757 :custom_field_values => {'2' => ''},
758 758 :notes => 'Bulk editing'
759 759 assert_response 302
760 760 # check that the issues were updated
761 761 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
762 762
763 763 issue = Issue.find(1)
764 764 journal = issue.journals.find(:first, :order => 'created_on DESC')
765 765 assert_equal '125', issue.custom_value_for(2).value
766 766 assert_equal 'Bulk editing', journal.notes
767 767 assert_equal 1, journal.details.size
768 768 end
769 769
770 770 def test_bulk_edit_custom_field
771 771 @request.session[:user_id] = 2
772 772 # update issues priority
773 773 post :bulk_edit, :ids => [1, 2], :priority_id => '',
774 774 :assigned_to_id => '',
775 775 :custom_field_values => {'2' => '777'},
776 776 :notes => 'Bulk editing custom field'
777 777 assert_response 302
778 778
779 779 issue = Issue.find(1)
780 780 journal = issue.journals.find(:first, :order => 'created_on DESC')
781 781 assert_equal '777', issue.custom_value_for(2).value
782 782 assert_equal 1, journal.details.size
783 783 assert_equal '125', journal.details.first.old_value
784 784 assert_equal '777', journal.details.first.value
785 785 end
786 786
787 787 def test_bulk_unassign
788 788 assert_not_nil Issue.find(2).assigned_to
789 789 @request.session[:user_id] = 2
790 790 # unassign issues
791 791 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
792 792 assert_response 302
793 793 # check that the issues were updated
794 794 assert_nil Issue.find(2).assigned_to
795 795 end
796 796
797 797 def test_move_routing
798 798 assert_routing(
799 799 {:method => :get, :path => '/issues/1/move'},
800 800 :controller => 'issues', :action => 'move', :id => '1'
801 801 )
802 802 assert_recognizes(
803 803 {:controller => 'issues', :action => 'move', :id => '1'},
804 804 {:method => :post, :path => '/issues/1/move'}
805 805 )
806 806 end
807 807
808 808 def test_move_one_issue_to_another_project
809 809 @request.session[:user_id] = 1
810 810 post :move, :id => 1, :new_project_id => 2
811 811 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
812 812 assert_equal 2, Issue.find(1).project_id
813 813 end
814 814
815 815 def test_bulk_move_to_another_project
816 816 @request.session[:user_id] = 1
817 817 post :move, :ids => [1, 2], :new_project_id => 2
818 818 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
819 819 # Issues moved to project 2
820 820 assert_equal 2, Issue.find(1).project_id
821 821 assert_equal 2, Issue.find(2).project_id
822 822 # No tracker change
823 823 assert_equal 1, Issue.find(1).tracker_id
824 824 assert_equal 2, Issue.find(2).tracker_id
825 825 end
826 826
827 827 def test_bulk_move_to_another_tracker
828 828 @request.session[:user_id] = 1
829 829 post :move, :ids => [1, 2], :new_tracker_id => 2
830 830 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
831 831 assert_equal 2, Issue.find(1).tracker_id
832 832 assert_equal 2, Issue.find(2).tracker_id
833 833 end
834 834
835 835 def test_bulk_copy_to_another_project
836 836 @request.session[:user_id] = 1
837 837 assert_difference 'Issue.count', 2 do
838 838 assert_no_difference 'Project.find(1).issues.count' do
839 839 post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}
840 840 end
841 841 end
842 842 assert_redirected_to 'projects/ecookbook/issues'
843 843 end
844 844
845 845 def test_context_menu_one_issue
846 846 @request.session[:user_id] = 2
847 847 get :context_menu, :ids => [1]
848 848 assert_response :success
849 849 assert_template 'context_menu'
850 850 assert_tag :tag => 'a', :content => 'Edit',
851 851 :attributes => { :href => '/issues/1/edit',
852 852 :class => 'icon-edit' }
853 853 assert_tag :tag => 'a', :content => 'Closed',
854 854 :attributes => { :href => '/issues/1/edit?issue%5Bstatus_id%5D=5',
855 855 :class => '' }
856 856 assert_tag :tag => 'a', :content => 'Immediate',
857 857 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
858 858 :class => '' }
859 859 assert_tag :tag => 'a', :content => 'Dave Lopper',
860 860 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
861 861 :class => '' }
862 862 assert_tag :tag => 'a', :content => 'Copy',
863 863 :attributes => { :href => '/projects/ecookbook/issues/1/copy',
864 864 :class => 'icon-copy' }
865 865 assert_tag :tag => 'a', :content => 'Move',
866 866 :attributes => { :href => '/issues/move?ids%5B%5D=1',
867 867 :class => 'icon-move' }
868 868 assert_tag :tag => 'a', :content => 'Delete',
869 869 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
870 870 :class => 'icon-del' }
871 871 end
872 872
873 873 def test_context_menu_one_issue_by_anonymous
874 874 get :context_menu, :ids => [1]
875 875 assert_response :success
876 876 assert_template 'context_menu'
877 877 assert_tag :tag => 'a', :content => 'Delete',
878 878 :attributes => { :href => '#',
879 879 :class => 'icon-del disabled' }
880 880 end
881 881
882 882 def test_context_menu_multiple_issues_of_same_project
883 883 @request.session[:user_id] = 2
884 884 get :context_menu, :ids => [1, 2]
885 885 assert_response :success
886 886 assert_template 'context_menu'
887 887 assert_tag :tag => 'a', :content => 'Edit',
888 888 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
889 889 :class => 'icon-edit' }
890 890 assert_tag :tag => 'a', :content => 'Immediate',
891 891 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
892 892 :class => '' }
893 893 assert_tag :tag => 'a', :content => 'Dave Lopper',
894 894 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
895 895 :class => '' }
896 896 assert_tag :tag => 'a', :content => 'Move',
897 897 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
898 898 :class => 'icon-move' }
899 899 assert_tag :tag => 'a', :content => 'Delete',
900 900 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
901 901 :class => 'icon-del' }
902 902 end
903 903
904 904 def test_context_menu_multiple_issues_of_different_project
905 905 @request.session[:user_id] = 2
906 906 get :context_menu, :ids => [1, 2, 4]
907 907 assert_response :success
908 908 assert_template 'context_menu'
909 909 assert_tag :tag => 'a', :content => 'Delete',
910 910 :attributes => { :href => '#',
911 911 :class => 'icon-del disabled' }
912 912 end
913 913
914 914 def test_destroy_routing
915 915 assert_recognizes( #TODO: use DELETE on issue URI (need to change forms)
916 916 {:controller => 'issues', :action => 'destroy', :id => '1'},
917 917 {:method => :post, :path => '/issues/1/destroy'}
918 918 )
919 919 end
920 920
921 921 def test_destroy_issue_with_no_time_entries
922 922 assert_nil TimeEntry.find_by_issue_id(2)
923 923 @request.session[:user_id] = 2
924 924 post :destroy, :id => 2
925 925 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
926 926 assert_nil Issue.find_by_id(2)
927 927 end
928 928
929 929 def test_destroy_issues_with_time_entries
930 930 @request.session[:user_id] = 2
931 931 post :destroy, :ids => [1, 3]
932 932 assert_response :success
933 933 assert_template 'destroy'
934 934 assert_not_nil assigns(:hours)
935 935 assert Issue.find_by_id(1) && Issue.find_by_id(3)
936 936 end
937 937
938 938 def test_destroy_issues_and_destroy_time_entries
939 939 @request.session[:user_id] = 2
940 940 post :destroy, :ids => [1, 3], :todo => 'destroy'
941 941 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
942 942 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
943 943 assert_nil TimeEntry.find_by_id([1, 2])
944 944 end
945 945
946 946 def test_destroy_issues_and_assign_time_entries_to_project
947 947 @request.session[:user_id] = 2
948 948 post :destroy, :ids => [1, 3], :todo => 'nullify'
949 949 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
950 950 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
951 951 assert_nil TimeEntry.find(1).issue_id
952 952 assert_nil TimeEntry.find(2).issue_id
953 953 end
954 954
955 955 def test_destroy_issues_and_reassign_time_entries_to_another_issue
956 956 @request.session[:user_id] = 2
957 957 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
958 958 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
959 959 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
960 960 assert_equal 2, TimeEntry.find(1).issue_id
961 961 assert_equal 2, TimeEntry.find(2).issue_id
962 962 end
963 963 end
@@ -1,82 +1,82
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2008 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 EnumerationTest < Test::Unit::TestCase
21 21 fixtures :enumerations, :issues
22 22
23 23 def setup
24 24 end
25 25
26 26 def test_objects_count
27 27 # low priority
28 28 assert_equal 5, Enumeration.find(4).objects_count
29 29 # urgent
30 30 assert_equal 0, Enumeration.find(7).objects_count
31 31 end
32 32
33 33 def test_in_use
34 34 # low priority
35 35 assert Enumeration.find(4).in_use?
36 36 # urgent
37 37 assert !Enumeration.find(7).in_use?
38 38 end
39 39
40 40 def test_default
41 e = Enumeration.default('IPRI')
41 e = Enumeration.priorities.default
42 42 assert e.is_a?(Enumeration)
43 43 assert e.is_default?
44 44 assert_equal 'Normal', e.name
45 45 end
46 46
47 47 def test_create
48 48 e = Enumeration.new(:opt => 'IPRI', :name => 'Very urgent', :is_default => false)
49 49 assert e.save
50 assert_equal 'Normal', Enumeration.default('IPRI').name
50 assert_equal 'Normal', Enumeration.priorities.default.name
51 51 end
52 52
53 53 def test_create_as_default
54 54 e = Enumeration.new(:opt => 'IPRI', :name => 'Very urgent', :is_default => true)
55 55 assert e.save
56 assert_equal e, Enumeration.default('IPRI')
56 assert_equal e, Enumeration.priorities.default
57 57 end
58 58
59 59 def test_update_default
60 e = Enumeration.default('IPRI')
60 e = Enumeration.priorities.default
61 61 e.update_attributes(:name => 'Changed', :is_default => true)
62 assert_equal e, Enumeration.default('IPRI')
62 assert_equal e, Enumeration.priorities.default
63 63 end
64 64
65 65 def test_update_default_to_non_default
66 e = Enumeration.default('IPRI')
66 e = Enumeration.priorities.default
67 67 e.update_attributes(:name => 'Changed', :is_default => false)
68 assert_nil Enumeration.default('IPRI')
68 assert_nil Enumeration.priorities.default
69 69 end
70 70
71 71 def test_change_default
72 72 e = Enumeration.find_by_name('Urgent')
73 73 e.update_attributes(:name => 'Urgent', :is_default => true)
74 assert_equal e, Enumeration.default('IPRI')
74 assert_equal e, Enumeration.priorities.default
75 75 end
76 76
77 77 def test_destroy_with_reassign
78 78 Enumeration.find(4).destroy(Enumeration.find(6))
79 79 assert_nil Issue.find(:first, :conditions => {:priority_id => 4})
80 80 assert_equal 5, Enumeration.find(6).objects_count
81 81 end
82 82 end
@@ -1,231 +1,231
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 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.priorities.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 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')
37 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.priorities.first, :subject => 'test_create')
38 38 assert issue.save
39 39 assert issue.description.nil?
40 40 end
41 41
42 42 def test_create_with_required_custom_field
43 43 field = IssueCustomField.find_by_name('Database')
44 44 field.update_attribute(:is_required, true)
45 45
46 46 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
47 47 assert issue.available_custom_fields.include?(field)
48 48 # No value for the custom field
49 49 assert !issue.save
50 50 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
51 51 # Blank value
52 52 issue.custom_field_values = { field.id => '' }
53 53 assert !issue.save
54 54 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
55 55 # Invalid value
56 56 issue.custom_field_values = { field.id => 'SQLServer' }
57 57 assert !issue.save
58 58 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
59 59 # Valid value
60 60 issue.custom_field_values = { field.id => 'PostgreSQL' }
61 61 assert issue.save
62 62 issue.reload
63 63 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
64 64 end
65 65
66 66 def test_update_issue_with_required_custom_field
67 67 field = IssueCustomField.find_by_name('Database')
68 68 field.update_attribute(:is_required, true)
69 69
70 70 issue = Issue.find(1)
71 71 assert_nil issue.custom_value_for(field)
72 72 assert issue.available_custom_fields.include?(field)
73 73 # No change to custom values, issue can be saved
74 74 assert issue.save
75 75 # Blank value
76 76 issue.custom_field_values = { field.id => '' }
77 77 assert !issue.save
78 78 # Valid value
79 79 issue.custom_field_values = { field.id => 'PostgreSQL' }
80 80 assert issue.save
81 81 issue.reload
82 82 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
83 83 end
84 84
85 85 def test_should_not_update_attributes_if_custom_fields_validation_fails
86 86 issue = Issue.find(1)
87 87 field = IssueCustomField.find_by_name('Database')
88 88 assert issue.available_custom_fields.include?(field)
89 89
90 90 issue.custom_field_values = { field.id => 'Invalid' }
91 91 issue.subject = 'Should be not be saved'
92 92 assert !issue.save
93 93
94 94 issue.reload
95 95 assert_equal "Can't print recipes", issue.subject
96 96 end
97 97
98 98 def test_should_not_recreate_custom_values_objects_on_update
99 99 field = IssueCustomField.find_by_name('Database')
100 100
101 101 issue = Issue.find(1)
102 102 issue.custom_field_values = { field.id => 'PostgreSQL' }
103 103 assert issue.save
104 104 custom_value = issue.custom_value_for(field)
105 105 issue.reload
106 106 issue.custom_field_values = { field.id => 'MySQL' }
107 107 assert issue.save
108 108 issue.reload
109 109 assert_equal custom_value.id, issue.custom_value_for(field).id
110 110 end
111 111
112 112 def test_category_based_assignment
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)
113 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.priorities.first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
114 114 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
115 115 end
116 116
117 117 def test_copy
118 118 issue = Issue.new.copy_from(1)
119 119 assert issue.save
120 120 issue.reload
121 121 orig = Issue.find(1)
122 122 assert_equal orig.subject, issue.subject
123 123 assert_equal orig.tracker, issue.tracker
124 124 assert_equal orig.custom_values.first.value, issue.custom_values.first.value
125 125 end
126 126
127 127 def test_should_close_duplicates
128 128 # Create 3 issues
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')
129 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.priorities.first, :subject => 'Duplicates test', :description => 'Duplicates test')
130 130 assert issue1.save
131 131 issue2 = issue1.clone
132 132 assert issue2.save
133 133 issue3 = issue1.clone
134 134 assert issue3.save
135 135
136 136 # 2 is a dupe of 1
137 137 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
138 138 # And 3 is a dupe of 2
139 139 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
140 140 # And 3 is a dupe of 1 (circular duplicates)
141 141 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
142 142
143 143 assert issue1.reload.duplicates.include?(issue2)
144 144
145 145 # Closing issue 1
146 146 issue1.init_journal(User.find(:first), "Closing issue1")
147 147 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
148 148 assert issue1.save
149 149 # 2 and 3 should be also closed
150 150 assert issue2.reload.closed?
151 151 assert issue3.reload.closed?
152 152 end
153 153
154 154 def test_should_not_close_duplicated_issue
155 155 # Create 3 issues
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')
156 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.priorities.first, :subject => 'Duplicates test', :description => 'Duplicates test')
157 157 assert issue1.save
158 158 issue2 = issue1.clone
159 159 assert issue2.save
160 160
161 161 # 2 is a dupe of 1
162 162 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
163 163 # 2 is a dup of 1 but 1 is not a duplicate of 2
164 164 assert !issue2.reload.duplicates.include?(issue1)
165 165
166 166 # Closing issue 2
167 167 issue2.init_journal(User.find(:first), "Closing issue2")
168 168 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
169 169 assert issue2.save
170 170 # 1 should not be also closed
171 171 assert !issue1.reload.closed?
172 172 end
173 173
174 174 def test_move_to_another_project_with_same_category
175 175 issue = Issue.find(1)
176 176 assert issue.move_to(Project.find(2))
177 177 issue.reload
178 178 assert_equal 2, issue.project_id
179 179 # Category changes
180 180 assert_equal 4, issue.category_id
181 181 # Make sure time entries were move to the target project
182 182 assert_equal 2, issue.time_entries.first.project_id
183 183 end
184 184
185 185 def test_move_to_another_project_without_same_category
186 186 issue = Issue.find(2)
187 187 assert issue.move_to(Project.find(2))
188 188 issue.reload
189 189 assert_equal 2, issue.project_id
190 190 # Category cleared
191 191 assert_nil issue.category_id
192 192 end
193 193
194 194 def test_copy_to_the_same_project
195 195 issue = Issue.find(1)
196 196 copy = nil
197 197 assert_difference 'Issue.count' do
198 198 copy = issue.move_to(issue.project, nil, :copy => true)
199 199 end
200 200 assert_kind_of Issue, copy
201 201 assert_equal issue.project, copy.project
202 202 assert_equal "125", copy.custom_value_for(2).value
203 203 end
204 204
205 205 def test_copy_to_another_project_and_tracker
206 206 issue = Issue.find(1)
207 207 copy = nil
208 208 assert_difference 'Issue.count' do
209 209 copy = issue.move_to(Project.find(3), Tracker.find(2), :copy => true)
210 210 end
211 211 assert_kind_of Issue, copy
212 212 assert_equal Project.find(3), copy.project
213 213 assert_equal Tracker.find(2), copy.tracker
214 214 # Custom field #2 is not associated with target tracker
215 215 assert_nil copy.custom_value_for(2)
216 216 end
217 217
218 218 def test_issue_destroy
219 219 Issue.find(1).destroy
220 220 assert_nil Issue.find_by_id(1)
221 221 assert_nil TimeEntry.find_by_issue_id(1)
222 222 end
223 223
224 224 def test_overdue
225 225 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
226 226 assert !Issue.new(:due_date => Date.today).overdue?
227 227 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
228 228 assert !Issue.new(:due_date => nil).overdue?
229 229 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
230 230 end
231 231 end
General Comments 0
You need to be logged in to leave comments. Login now