##// END OF EJS Templates
Merged r3357 from trunk....
Jean-Philippe Lang -
r3266:7c1e87720918
parent child
Show More
@@ -1,542 +1,542
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class IssuesController < ApplicationController
18 class IssuesController < ApplicationController
19 menu_item :new_issue, :only => :new
19 menu_item :new_issue, :only => :new
20 default_search_scope :issues
20 default_search_scope :issues
21
21
22 before_filter :find_issue, :only => [:show, :edit, :reply]
22 before_filter :find_issue, :only => [:show, :edit, :reply]
23 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
23 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
24 before_filter :find_project, :only => [:new, :update_form, :preview]
24 before_filter :find_project, :only => [:new, :update_form, :preview]
25 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :context_menu]
25 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :context_menu]
26 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
26 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
27 accept_key_auth :index, :show, :changes
27 accept_key_auth :index, :show, :changes
28
28
29 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
29 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
30
30
31 helper :journals
31 helper :journals
32 helper :projects
32 helper :projects
33 include ProjectsHelper
33 include ProjectsHelper
34 helper :custom_fields
34 helper :custom_fields
35 include CustomFieldsHelper
35 include CustomFieldsHelper
36 helper :issue_relations
36 helper :issue_relations
37 include IssueRelationsHelper
37 include IssueRelationsHelper
38 helper :watchers
38 helper :watchers
39 include WatchersHelper
39 include WatchersHelper
40 helper :attachments
40 helper :attachments
41 include AttachmentsHelper
41 include AttachmentsHelper
42 helper :queries
42 helper :queries
43 include QueriesHelper
43 include QueriesHelper
44 helper :sort
44 helper :sort
45 include SortHelper
45 include SortHelper
46 include IssuesHelper
46 include IssuesHelper
47 helper :timelog
47 helper :timelog
48 include Redmine::Export::PDF
48 include Redmine::Export::PDF
49
49
50 verify :method => :post,
50 verify :method => :post,
51 :only => :destroy,
51 :only => :destroy,
52 :render => { :nothing => true, :status => :method_not_allowed }
52 :render => { :nothing => true, :status => :method_not_allowed }
53
53
54 def index
54 def index
55 retrieve_query
55 retrieve_query
56 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
56 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
57 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
57 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
58
58
59 if @query.valid?
59 if @query.valid?
60 limit = per_page_option
60 limit = per_page_option
61 respond_to do |format|
61 respond_to do |format|
62 format.html { }
62 format.html { }
63 format.atom { limit = Setting.feeds_limit.to_i }
63 format.atom { limit = Setting.feeds_limit.to_i }
64 format.csv { limit = Setting.issues_export_limit.to_i }
64 format.csv { limit = Setting.issues_export_limit.to_i }
65 format.pdf { limit = Setting.issues_export_limit.to_i }
65 format.pdf { limit = Setting.issues_export_limit.to_i }
66 end
66 end
67
67
68 @issue_count = @query.issue_count
68 @issue_count = @query.issue_count
69 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
69 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
70 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
70 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
71 :order => sort_clause,
71 :order => sort_clause,
72 :offset => @issue_pages.current.offset,
72 :offset => @issue_pages.current.offset,
73 :limit => limit)
73 :limit => limit)
74 @issue_count_by_group = @query.issue_count_by_group
74 @issue_count_by_group = @query.issue_count_by_group
75
75
76 respond_to do |format|
76 respond_to do |format|
77 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
77 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
78 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
78 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
79 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
79 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
80 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
80 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
81 end
81 end
82 else
82 else
83 # Send html if the query is not valid
83 # Send html if the query is not valid
84 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
84 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
85 end
85 end
86 rescue ActiveRecord::RecordNotFound
86 rescue ActiveRecord::RecordNotFound
87 render_404
87 render_404
88 end
88 end
89
89
90 def changes
90 def changes
91 retrieve_query
91 retrieve_query
92 sort_init 'id', 'desc'
92 sort_init 'id', 'desc'
93 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
93 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
94
94
95 if @query.valid?
95 if @query.valid?
96 @journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
96 @journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
97 :limit => 25)
97 :limit => 25)
98 end
98 end
99 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
99 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
100 render :layout => false, :content_type => 'application/atom+xml'
100 render :layout => false, :content_type => 'application/atom+xml'
101 rescue ActiveRecord::RecordNotFound
101 rescue ActiveRecord::RecordNotFound
102 render_404
102 render_404
103 end
103 end
104
104
105 def show
105 def show
106 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
106 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
107 @journals.each_with_index {|j,i| j.indice = i+1}
107 @journals.each_with_index {|j,i| j.indice = i+1}
108 @journals.reverse! if User.current.wants_comments_in_reverse_order?
108 @journals.reverse! if User.current.wants_comments_in_reverse_order?
109 @changesets = @issue.changesets
109 @changesets = @issue.changesets.visible.all
110 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
110 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
111 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
111 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
112 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
112 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
113 @priorities = IssuePriority.all
113 @priorities = IssuePriority.all
114 @time_entry = TimeEntry.new
114 @time_entry = TimeEntry.new
115 respond_to do |format|
115 respond_to do |format|
116 format.html { render :template => 'issues/show.rhtml' }
116 format.html { render :template => 'issues/show.rhtml' }
117 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
117 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
118 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
118 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
119 end
119 end
120 end
120 end
121
121
122 # Add a new issue
122 # Add a new issue
123 # The new issue will be created from an existing one if copy_from parameter is given
123 # The new issue will be created from an existing one if copy_from parameter is given
124 def new
124 def new
125 @issue = Issue.new
125 @issue = Issue.new
126 @issue.copy_from(params[:copy_from]) if params[:copy_from]
126 @issue.copy_from(params[:copy_from]) if params[:copy_from]
127 @issue.project = @project
127 @issue.project = @project
128 # Tracker must be set before custom field values
128 # Tracker must be set before custom field values
129 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
129 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
130 if @issue.tracker.nil?
130 if @issue.tracker.nil?
131 render_error l(:error_no_tracker_in_project)
131 render_error l(:error_no_tracker_in_project)
132 return
132 return
133 end
133 end
134 if params[:issue].is_a?(Hash)
134 if params[:issue].is_a?(Hash)
135 @issue.attributes = params[:issue]
135 @issue.attributes = params[:issue]
136 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
136 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
137 end
137 end
138 @issue.author = User.current
138 @issue.author = User.current
139
139
140 default_status = IssueStatus.default
140 default_status = IssueStatus.default
141 unless default_status
141 unless default_status
142 render_error l(:error_no_default_issue_status)
142 render_error l(:error_no_default_issue_status)
143 return
143 return
144 end
144 end
145 @issue.status = default_status
145 @issue.status = default_status
146 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
146 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
147
147
148 if request.get? || request.xhr?
148 if request.get? || request.xhr?
149 @issue.start_date ||= Date.today
149 @issue.start_date ||= Date.today
150 else
150 else
151 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
151 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
152 # Check that the user is allowed to apply the requested status
152 # Check that the user is allowed to apply the requested status
153 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
153 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
154 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
154 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
155 if @issue.save
155 if @issue.save
156 attach_files(@issue, params[:attachments])
156 attach_files(@issue, params[:attachments])
157 flash[:notice] = l(:notice_successful_create)
157 flash[:notice] = l(:notice_successful_create)
158 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
158 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
159 redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
159 redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
160 { :action => 'show', :id => @issue })
160 { :action => 'show', :id => @issue })
161 return
161 return
162 end
162 end
163 end
163 end
164 @priorities = IssuePriority.all
164 @priorities = IssuePriority.all
165 render :layout => !request.xhr?
165 render :layout => !request.xhr?
166 end
166 end
167
167
168 # Attributes that can be updated on workflow transition (without :edit permission)
168 # Attributes that can be updated on workflow transition (without :edit permission)
169 # TODO: make it configurable (at least per role)
169 # TODO: make it configurable (at least per role)
170 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
170 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
171
171
172 def edit
172 def edit
173 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
173 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
174 @priorities = IssuePriority.all
174 @priorities = IssuePriority.all
175 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
175 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
176 @time_entry = TimeEntry.new
176 @time_entry = TimeEntry.new
177
177
178 @notes = params[:notes]
178 @notes = params[:notes]
179 journal = @issue.init_journal(User.current, @notes)
179 journal = @issue.init_journal(User.current, @notes)
180 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
180 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
181 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
181 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
182 attrs = params[:issue].dup
182 attrs = params[:issue].dup
183 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
183 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
184 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
184 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
185 @issue.attributes = attrs
185 @issue.attributes = attrs
186 end
186 end
187
187
188 if request.post?
188 if request.post?
189 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
189 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
190 @time_entry.attributes = params[:time_entry]
190 @time_entry.attributes = params[:time_entry]
191 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.valid?
191 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.valid?
192 attachments = attach_files(@issue, params[:attachments])
192 attachments = attach_files(@issue, params[:attachments])
193 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
193 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
194 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
194 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
195 if @issue.save
195 if @issue.save
196 # Log spend time
196 # Log spend time
197 if User.current.allowed_to?(:log_time, @project)
197 if User.current.allowed_to?(:log_time, @project)
198 @time_entry.save
198 @time_entry.save
199 end
199 end
200 if !journal.new_record?
200 if !journal.new_record?
201 # Only send notification if something was actually changed
201 # Only send notification if something was actually changed
202 flash[:notice] = l(:notice_successful_update)
202 flash[:notice] = l(:notice_successful_update)
203 end
203 end
204 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
204 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
205 redirect_back_or_default({:action => 'show', :id => @issue})
205 redirect_back_or_default({:action => 'show', :id => @issue})
206 end
206 end
207 end
207 end
208 end
208 end
209 rescue ActiveRecord::StaleObjectError
209 rescue ActiveRecord::StaleObjectError
210 # Optimistic locking exception
210 # Optimistic locking exception
211 flash.now[:error] = l(:notice_locking_conflict)
211 flash.now[:error] = l(:notice_locking_conflict)
212 # Remove the previously added attachments if issue was not updated
212 # Remove the previously added attachments if issue was not updated
213 attachments.each(&:destroy)
213 attachments.each(&:destroy)
214 end
214 end
215
215
216 def reply
216 def reply
217 journal = Journal.find(params[:journal_id]) if params[:journal_id]
217 journal = Journal.find(params[:journal_id]) if params[:journal_id]
218 if journal
218 if journal
219 user = journal.user
219 user = journal.user
220 text = journal.notes
220 text = journal.notes
221 else
221 else
222 user = @issue.author
222 user = @issue.author
223 text = @issue.description
223 text = @issue.description
224 end
224 end
225 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
225 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
226 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
226 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
227 render(:update) { |page|
227 render(:update) { |page|
228 page.<< "$('notes').value = \"#{content}\";"
228 page.<< "$('notes').value = \"#{content}\";"
229 page.show 'update'
229 page.show 'update'
230 page << "Form.Element.focus('notes');"
230 page << "Form.Element.focus('notes');"
231 page << "Element.scrollTo('update');"
231 page << "Element.scrollTo('update');"
232 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
232 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
233 }
233 }
234 end
234 end
235
235
236 # Bulk edit a set of issues
236 # Bulk edit a set of issues
237 def bulk_edit
237 def bulk_edit
238 if request.post?
238 if request.post?
239 tracker = params[:tracker_id].blank? ? nil : @project.trackers.find_by_id(params[:tracker_id])
239 tracker = params[:tracker_id].blank? ? nil : @project.trackers.find_by_id(params[:tracker_id])
240 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
240 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
241 priority = params[:priority_id].blank? ? nil : IssuePriority.find_by_id(params[:priority_id])
241 priority = params[:priority_id].blank? ? nil : IssuePriority.find_by_id(params[:priority_id])
242 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
242 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
243 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
243 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
244 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.shared_versions.find_by_id(params[:fixed_version_id])
244 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.shared_versions.find_by_id(params[:fixed_version_id])
245 custom_field_values = params[:custom_field_values] ? params[:custom_field_values].reject {|k,v| v.blank?} : nil
245 custom_field_values = params[:custom_field_values] ? params[:custom_field_values].reject {|k,v| v.blank?} : nil
246
246
247 unsaved_issue_ids = []
247 unsaved_issue_ids = []
248 @issues.each do |issue|
248 @issues.each do |issue|
249 journal = issue.init_journal(User.current, params[:notes])
249 journal = issue.init_journal(User.current, params[:notes])
250 issue.tracker = tracker if tracker
250 issue.tracker = tracker if tracker
251 issue.priority = priority if priority
251 issue.priority = priority if priority
252 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
252 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
253 issue.category = category if category || params[:category_id] == 'none'
253 issue.category = category if category || params[:category_id] == 'none'
254 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
254 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
255 issue.start_date = params[:start_date] unless params[:start_date].blank?
255 issue.start_date = params[:start_date] unless params[:start_date].blank?
256 issue.due_date = params[:due_date] unless params[:due_date].blank?
256 issue.due_date = params[:due_date] unless params[:due_date].blank?
257 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
257 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
258 issue.custom_field_values = custom_field_values if custom_field_values && !custom_field_values.empty?
258 issue.custom_field_values = custom_field_values if custom_field_values && !custom_field_values.empty?
259 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
259 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
260 # Don't save any change to the issue if the user is not authorized to apply the requested status
260 # Don't save any change to the issue if the user is not authorized to apply the requested status
261 unless (status.nil? || (issue.new_statuses_allowed_to(User.current).include?(status) && issue.status = status)) && issue.save
261 unless (status.nil? || (issue.new_statuses_allowed_to(User.current).include?(status) && issue.status = status)) && issue.save
262 # Keep unsaved issue ids to display them in flash error
262 # Keep unsaved issue ids to display them in flash error
263 unsaved_issue_ids << issue.id
263 unsaved_issue_ids << issue.id
264 end
264 end
265 end
265 end
266 if unsaved_issue_ids.empty?
266 if unsaved_issue_ids.empty?
267 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
267 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
268 else
268 else
269 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
269 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
270 :total => @issues.size,
270 :total => @issues.size,
271 :ids => '#' + unsaved_issue_ids.join(', #'))
271 :ids => '#' + unsaved_issue_ids.join(', #'))
272 end
272 end
273 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
273 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
274 return
274 return
275 end
275 end
276 @available_statuses = Workflow.available_statuses(@project)
276 @available_statuses = Workflow.available_statuses(@project)
277 @custom_fields = @project.all_issue_custom_fields
277 @custom_fields = @project.all_issue_custom_fields
278 end
278 end
279
279
280 def move
280 def move
281 @copy = params[:copy_options] && params[:copy_options][:copy]
281 @copy = params[:copy_options] && params[:copy_options][:copy]
282 @allowed_projects = []
282 @allowed_projects = []
283 # find projects to which the user is allowed to move the issue
283 # find projects to which the user is allowed to move the issue
284 if User.current.admin?
284 if User.current.admin?
285 # admin is allowed to move issues to any active (visible) project
285 # admin is allowed to move issues to any active (visible) project
286 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
286 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
287 else
287 else
288 User.current.memberships.each {|m| @allowed_projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
288 User.current.memberships.each {|m| @allowed_projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
289 end
289 end
290 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
290 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
291 @target_project ||= @project
291 @target_project ||= @project
292 @trackers = @target_project.trackers
292 @trackers = @target_project.trackers
293 @available_statuses = Workflow.available_statuses(@project)
293 @available_statuses = Workflow.available_statuses(@project)
294 if request.post?
294 if request.post?
295 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
295 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
296 unsaved_issue_ids = []
296 unsaved_issue_ids = []
297 moved_issues = []
297 moved_issues = []
298 @issues.each do |issue|
298 @issues.each do |issue|
299 changed_attributes = {}
299 changed_attributes = {}
300 [:assigned_to_id, :status_id, :start_date, :due_date].each do |valid_attribute|
300 [:assigned_to_id, :status_id, :start_date, :due_date].each do |valid_attribute|
301 unless params[valid_attribute].blank?
301 unless params[valid_attribute].blank?
302 changed_attributes[valid_attribute] = (params[valid_attribute] == 'none' ? nil : params[valid_attribute])
302 changed_attributes[valid_attribute] = (params[valid_attribute] == 'none' ? nil : params[valid_attribute])
303 end
303 end
304 end
304 end
305 issue.init_journal(User.current)
305 issue.init_journal(User.current)
306 if r = issue.move_to(@target_project, new_tracker, {:copy => @copy, :attributes => changed_attributes})
306 if r = issue.move_to(@target_project, new_tracker, {:copy => @copy, :attributes => changed_attributes})
307 moved_issues << r
307 moved_issues << r
308 else
308 else
309 unsaved_issue_ids << issue.id
309 unsaved_issue_ids << issue.id
310 end
310 end
311 end
311 end
312 if unsaved_issue_ids.empty?
312 if unsaved_issue_ids.empty?
313 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
313 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
314 else
314 else
315 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
315 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
316 :total => @issues.size,
316 :total => @issues.size,
317 :ids => '#' + unsaved_issue_ids.join(', #'))
317 :ids => '#' + unsaved_issue_ids.join(', #'))
318 end
318 end
319 if params[:follow]
319 if params[:follow]
320 if @issues.size == 1 && moved_issues.size == 1
320 if @issues.size == 1 && moved_issues.size == 1
321 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
321 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
322 else
322 else
323 redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project)
323 redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project)
324 end
324 end
325 else
325 else
326 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
326 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
327 end
327 end
328 return
328 return
329 end
329 end
330 render :layout => false if request.xhr?
330 render :layout => false if request.xhr?
331 end
331 end
332
332
333 def destroy
333 def destroy
334 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
334 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
335 if @hours > 0
335 if @hours > 0
336 case params[:todo]
336 case params[:todo]
337 when 'destroy'
337 when 'destroy'
338 # nothing to do
338 # nothing to do
339 when 'nullify'
339 when 'nullify'
340 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
340 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
341 when 'reassign'
341 when 'reassign'
342 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
342 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
343 if reassign_to.nil?
343 if reassign_to.nil?
344 flash.now[:error] = l(:error_issue_not_found_in_project)
344 flash.now[:error] = l(:error_issue_not_found_in_project)
345 return
345 return
346 else
346 else
347 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
347 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
348 end
348 end
349 else
349 else
350 # display the destroy form
350 # display the destroy form
351 return
351 return
352 end
352 end
353 end
353 end
354 @issues.each(&:destroy)
354 @issues.each(&:destroy)
355 redirect_to :action => 'index', :project_id => @project
355 redirect_to :action => 'index', :project_id => @project
356 end
356 end
357
357
358 def gantt
358 def gantt
359 @gantt = Redmine::Helpers::Gantt.new(params)
359 @gantt = Redmine::Helpers::Gantt.new(params)
360 retrieve_query
360 retrieve_query
361 if @query.valid?
361 if @query.valid?
362 events = []
362 events = []
363 # Issues that have start and due dates
363 # Issues that have start and due dates
364 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
364 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
365 :order => "start_date, due_date",
365 :order => "start_date, due_date",
366 :conditions => ["(((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]
366 :conditions => ["(((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]
367 )
367 )
368 # Issues that don't have a due date but that are assigned to a version with a date
368 # Issues that don't have a due date but that are assigned to a version with a date
369 events += @query.issues(:include => [:tracker, :assigned_to, :priority, :fixed_version],
369 events += @query.issues(:include => [:tracker, :assigned_to, :priority, :fixed_version],
370 :order => "start_date, effective_date",
370 :order => "start_date, effective_date",
371 :conditions => ["(((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]
371 :conditions => ["(((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]
372 )
372 )
373 # Versions
373 # Versions
374 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
374 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
375
375
376 @gantt.events = events
376 @gantt.events = events
377 end
377 end
378
378
379 basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
379 basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
380
380
381 respond_to do |format|
381 respond_to do |format|
382 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
382 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
383 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
383 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
384 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") }
384 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") }
385 end
385 end
386 end
386 end
387
387
388 def calendar
388 def calendar
389 if params[:year] and params[:year].to_i > 1900
389 if params[:year] and params[:year].to_i > 1900
390 @year = params[:year].to_i
390 @year = params[:year].to_i
391 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
391 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
392 @month = params[:month].to_i
392 @month = params[:month].to_i
393 end
393 end
394 end
394 end
395 @year ||= Date.today.year
395 @year ||= Date.today.year
396 @month ||= Date.today.month
396 @month ||= Date.today.month
397
397
398 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
398 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
399 retrieve_query
399 retrieve_query
400 if @query.valid?
400 if @query.valid?
401 events = []
401 events = []
402 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
402 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
403 :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
403 :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
404 )
404 )
405 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
405 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
406
406
407 @calendar.events = events
407 @calendar.events = events
408 end
408 end
409
409
410 render :layout => false if request.xhr?
410 render :layout => false if request.xhr?
411 end
411 end
412
412
413 def context_menu
413 def context_menu
414 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
414 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
415 if (@issues.size == 1)
415 if (@issues.size == 1)
416 @issue = @issues.first
416 @issue = @issues.first
417 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
417 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
418 end
418 end
419 projects = @issues.collect(&:project).compact.uniq
419 projects = @issues.collect(&:project).compact.uniq
420 @project = projects.first if projects.size == 1
420 @project = projects.first if projects.size == 1
421
421
422 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
422 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
423 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
423 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
424 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
424 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
425 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
425 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
426 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
426 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
427 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
427 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
428 }
428 }
429 if @project
429 if @project
430 @assignables = @project.assignable_users
430 @assignables = @project.assignable_users
431 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
431 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
432 @trackers = @project.trackers
432 @trackers = @project.trackers
433 end
433 end
434
434
435 @priorities = IssuePriority.all.reverse
435 @priorities = IssuePriority.all.reverse
436 @statuses = IssueStatus.find(:all, :order => 'position')
436 @statuses = IssueStatus.find(:all, :order => 'position')
437 @back = params[:back_url] || request.env['HTTP_REFERER']
437 @back = params[:back_url] || request.env['HTTP_REFERER']
438
438
439 render :layout => false
439 render :layout => false
440 end
440 end
441
441
442 def update_form
442 def update_form
443 if params[:id].blank?
443 if params[:id].blank?
444 @issue = Issue.new
444 @issue = Issue.new
445 @issue.project = @project
445 @issue.project = @project
446 else
446 else
447 @issue = @project.issues.visible.find(params[:id])
447 @issue = @project.issues.visible.find(params[:id])
448 end
448 end
449 @issue.attributes = params[:issue]
449 @issue.attributes = params[:issue]
450 @allowed_statuses = ([@issue.status] + @issue.status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
450 @allowed_statuses = ([@issue.status] + @issue.status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
451 @priorities = IssuePriority.all
451 @priorities = IssuePriority.all
452
452
453 render :partial => 'attributes'
453 render :partial => 'attributes'
454 end
454 end
455
455
456 def preview
456 def preview
457 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
457 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
458 @attachements = @issue.attachments if @issue
458 @attachements = @issue.attachments if @issue
459 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
459 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
460 render :partial => 'common/preview'
460 render :partial => 'common/preview'
461 end
461 end
462
462
463 private
463 private
464 def find_issue
464 def find_issue
465 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
465 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
466 @project = @issue.project
466 @project = @issue.project
467 rescue ActiveRecord::RecordNotFound
467 rescue ActiveRecord::RecordNotFound
468 render_404
468 render_404
469 end
469 end
470
470
471 # Filter for bulk operations
471 # Filter for bulk operations
472 def find_issues
472 def find_issues
473 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
473 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
474 raise ActiveRecord::RecordNotFound if @issues.empty?
474 raise ActiveRecord::RecordNotFound if @issues.empty?
475 projects = @issues.collect(&:project).compact.uniq
475 projects = @issues.collect(&:project).compact.uniq
476 if projects.size == 1
476 if projects.size == 1
477 @project = projects.first
477 @project = projects.first
478 else
478 else
479 # TODO: let users bulk edit/move/destroy issues from different projects
479 # TODO: let users bulk edit/move/destroy issues from different projects
480 render_error 'Can not bulk edit/move/destroy issues from different projects'
480 render_error 'Can not bulk edit/move/destroy issues from different projects'
481 return false
481 return false
482 end
482 end
483 rescue ActiveRecord::RecordNotFound
483 rescue ActiveRecord::RecordNotFound
484 render_404
484 render_404
485 end
485 end
486
486
487 def find_project
487 def find_project
488 @project = Project.find(params[:project_id])
488 @project = Project.find(params[:project_id])
489 rescue ActiveRecord::RecordNotFound
489 rescue ActiveRecord::RecordNotFound
490 render_404
490 render_404
491 end
491 end
492
492
493 def find_optional_project
493 def find_optional_project
494 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
494 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
495 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
495 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
496 allowed ? true : deny_access
496 allowed ? true : deny_access
497 rescue ActiveRecord::RecordNotFound
497 rescue ActiveRecord::RecordNotFound
498 render_404
498 render_404
499 end
499 end
500
500
501 # Retrieve query from session or build a new query
501 # Retrieve query from session or build a new query
502 def retrieve_query
502 def retrieve_query
503 if !params[:query_id].blank?
503 if !params[:query_id].blank?
504 cond = "project_id IS NULL"
504 cond = "project_id IS NULL"
505 cond << " OR project_id = #{@project.id}" if @project
505 cond << " OR project_id = #{@project.id}" if @project
506 @query = Query.find(params[:query_id], :conditions => cond)
506 @query = Query.find(params[:query_id], :conditions => cond)
507 @query.project = @project
507 @query.project = @project
508 session[:query] = {:id => @query.id, :project_id => @query.project_id}
508 session[:query] = {:id => @query.id, :project_id => @query.project_id}
509 sort_clear
509 sort_clear
510 else
510 else
511 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
511 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
512 # Give it a name, required to be valid
512 # Give it a name, required to be valid
513 @query = Query.new(:name => "_")
513 @query = Query.new(:name => "_")
514 @query.project = @project
514 @query.project = @project
515 if params[:fields] and params[:fields].is_a? Array
515 if params[:fields] and params[:fields].is_a? Array
516 params[:fields].each do |field|
516 params[:fields].each do |field|
517 @query.add_filter(field, params[:operators][field], params[:values][field])
517 @query.add_filter(field, params[:operators][field], params[:values][field])
518 end
518 end
519 else
519 else
520 @query.available_filters.keys.each do |field|
520 @query.available_filters.keys.each do |field|
521 @query.add_short_filter(field, params[field]) if params[field]
521 @query.add_short_filter(field, params[field]) if params[field]
522 end
522 end
523 end
523 end
524 @query.group_by = params[:group_by]
524 @query.group_by = params[:group_by]
525 @query.column_names = params[:query] && params[:query][:column_names]
525 @query.column_names = params[:query] && params[:query][:column_names]
526 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
526 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
527 else
527 else
528 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
528 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
529 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
529 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
530 @query.project = @project
530 @query.project = @project
531 end
531 end
532 end
532 end
533 end
533 end
534
534
535 # Rescues an invalid query statement. Just in case...
535 # Rescues an invalid query statement. Just in case...
536 def query_statement_invalid(exception)
536 def query_statement_invalid(exception)
537 logger.error "Query::StatementInvalid: #{exception.message}" if logger
537 logger.error "Query::StatementInvalid: #{exception.message}" if logger
538 session.delete(:query)
538 session.delete(:query)
539 sort_clear
539 sort_clear
540 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
540 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
541 end
541 end
542 end
542 end
@@ -1,170 +1,181
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'iconv'
18 require 'iconv'
19
19
20 class Changeset < ActiveRecord::Base
20 class Changeset < ActiveRecord::Base
21 belongs_to :repository
21 belongs_to :repository
22 belongs_to :user
22 belongs_to :user
23 has_many :changes, :dependent => :delete_all
23 has_many :changes, :dependent => :delete_all
24 has_and_belongs_to_many :issues
24 has_and_belongs_to_many :issues
25
25
26 acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
26 acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
27 :description => :long_comments,
27 :description => :long_comments,
28 :datetime => :committed_on,
28 :datetime => :committed_on,
29 :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.revision}}
29 :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.revision}}
30
30
31 acts_as_searchable :columns => 'comments',
31 acts_as_searchable :columns => 'comments',
32 :include => {:repository => :project},
32 :include => {:repository => :project},
33 :project_key => "#{Repository.table_name}.project_id",
33 :project_key => "#{Repository.table_name}.project_id",
34 :date_column => 'committed_on'
34 :date_column => 'committed_on'
35
35
36 acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
36 acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
37 :author_key => :user_id,
37 :author_key => :user_id,
38 :find_options => {:include => [:user, {:repository => :project}]}
38 :find_options => {:include => [:user, {:repository => :project}]}
39
39
40 validates_presence_of :repository_id, :revision, :committed_on, :commit_date
40 validates_presence_of :repository_id, :revision, :committed_on, :commit_date
41 validates_uniqueness_of :revision, :scope => :repository_id
41 validates_uniqueness_of :revision, :scope => :repository_id
42 validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
42 validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
43
43
44 named_scope :visible, lambda {|*args| { :include => {:repository => :project},
45 :conditions => Project.allowed_to_condition(args.first || User.current, :view_changesets) } }
46
44 def revision=(r)
47 def revision=(r)
45 write_attribute :revision, (r.nil? ? nil : r.to_s)
48 write_attribute :revision, (r.nil? ? nil : r.to_s)
46 end
49 end
47
50
48 def comments=(comment)
51 def comments=(comment)
49 write_attribute(:comments, Changeset.normalize_comments(comment))
52 write_attribute(:comments, Changeset.normalize_comments(comment))
50 end
53 end
51
54
52 def committed_on=(date)
55 def committed_on=(date)
53 self.commit_date = date
56 self.commit_date = date
54 super
57 super
55 end
58 end
56
59
57 def project
60 def project
58 repository.project
61 repository.project
59 end
62 end
60
63
61 def author
64 def author
62 user || committer.to_s.split('<').first
65 user || committer.to_s.split('<').first
63 end
66 end
64
67
65 def before_create
68 def before_create
66 self.user = repository.find_committer_user(committer)
69 self.user = repository.find_committer_user(committer)
67 end
70 end
68
71
69 def after_create
72 def after_create
70 scan_comment_for_issue_ids
73 scan_comment_for_issue_ids
71 end
74 end
72 require 'pp'
75 require 'pp'
73
76
74 def scan_comment_for_issue_ids
77 def scan_comment_for_issue_ids
75 return if comments.blank?
78 return if comments.blank?
76 # keywords used to reference issues
79 # keywords used to reference issues
77 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
80 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
78 # keywords used to fix issues
81 # keywords used to fix issues
79 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
82 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
80 # status and optional done ratio applied
83 # status and optional done ratio applied
81 fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id)
84 fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id)
82 done_ratio = Setting.commit_fix_done_ratio.blank? ? nil : Setting.commit_fix_done_ratio.to_i
85 done_ratio = Setting.commit_fix_done_ratio.blank? ? nil : Setting.commit_fix_done_ratio.to_i
83
86
84 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
87 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
85 return if kw_regexp.blank?
88 return if kw_regexp.blank?
86
89
87 referenced_issues = []
90 referenced_issues = []
88
91
89 if ref_keywords.delete('*')
92 if ref_keywords.delete('*')
90 # find any issue ID in the comments
93 # find any issue ID in the comments
91 target_issue_ids = []
94 target_issue_ids = []
92 comments.scan(%r{([\s\(\[,-]|^)#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] }
95 comments.scan(%r{([\s\(\[,-]|^)#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] }
93 referenced_issues += repository.project.issues.find_all_by_id(target_issue_ids)
96 referenced_issues += find_referenced_issues_by_id(target_issue_ids)
94 end
97 end
95
98
96 comments.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match|
99 comments.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match|
97 action = match[0]
100 action = match[0]
98 target_issue_ids = match[1].scan(/\d+/)
101 target_issue_ids = match[1].scan(/\d+/)
99 target_issues = repository.project.issues.find_all_by_id(target_issue_ids)
102 target_issues = find_referenced_issues_by_id(target_issue_ids)
100 if fix_status && fix_keywords.include?(action.downcase)
103 if fix_status && fix_keywords.include?(action.downcase)
101 # update status of issues
104 # update status of issues
102 logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug?
105 logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug?
103 target_issues.each do |issue|
106 target_issues.each do |issue|
104 # the issue may have been updated by the closure of another one (eg. duplicate)
107 # the issue may have been updated by the closure of another one (eg. duplicate)
105 issue.reload
108 issue.reload
106 # don't change the status is the issue is closed
109 # don't change the status is the issue is closed
107 next if issue.status.is_closed?
110 next if issue.status.is_closed?
108 csettext = "r#{self.revision}"
111 csettext = "r#{self.revision}"
109 if self.scmid && (! (csettext =~ /^r[0-9]+$/))
112 if self.scmid && (! (csettext =~ /^r[0-9]+$/))
110 csettext = "commit:\"#{self.scmid}\""
113 csettext = "commit:\"#{self.scmid}\""
111 end
114 end
112 journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, csettext))
115 journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, csettext))
113 issue.status = fix_status
116 issue.status = fix_status
114 issue.done_ratio = done_ratio if done_ratio
117 issue.done_ratio = done_ratio if done_ratio
115 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
118 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
116 { :changeset => self, :issue => issue })
119 { :changeset => self, :issue => issue })
117 issue.save
120 issue.save
118 end
121 end
119 end
122 end
120 referenced_issues += target_issues
123 referenced_issues += target_issues
121 end
124 end
122
125
123 self.issues = referenced_issues.uniq
126 self.issues = referenced_issues.uniq
124 end
127 end
125
128
126 def short_comments
129 def short_comments
127 @short_comments || split_comments.first
130 @short_comments || split_comments.first
128 end
131 end
129
132
130 def long_comments
133 def long_comments
131 @long_comments || split_comments.last
134 @long_comments || split_comments.last
132 end
135 end
133
136
134 # Returns the previous changeset
137 # Returns the previous changeset
135 def previous
138 def previous
136 @previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
139 @previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
137 end
140 end
138
141
139 # Returns the next changeset
142 # Returns the next changeset
140 def next
143 def next
141 @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
144 @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
142 end
145 end
143
146
144 # Strips and reencodes a commit log before insertion into the database
147 # Strips and reencodes a commit log before insertion into the database
145 def self.normalize_comments(str)
148 def self.normalize_comments(str)
146 to_utf8(str.to_s.strip)
149 to_utf8(str.to_s.strip)
147 end
150 end
148
151
149 private
152 private
150
153
154 # Finds issues that can be referenced by the commit message
155 # i.e. issues that belong to the repository project, a subproject or a parent project
156 def find_referenced_issues_by_id(ids)
157 Issue.find_all_by_id(ids, :include => :project).select {|issue|
158 project == issue.project || project.is_ancestor_of?(issue.project) || project.is_descendant_of?(issue.project)
159 }
160 end
161
151 def split_comments
162 def split_comments
152 comments =~ /\A(.+?)\r?\n(.*)$/m
163 comments =~ /\A(.+?)\r?\n(.*)$/m
153 @short_comments = $1 || comments
164 @short_comments = $1 || comments
154 @long_comments = $2.to_s.strip
165 @long_comments = $2.to_s.strip
155 return @short_comments, @long_comments
166 return @short_comments, @long_comments
156 end
167 end
157
168
158 def self.to_utf8(str)
169 def self.to_utf8(str)
159 return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
170 return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
160 encoding = Setting.commit_logs_encoding.to_s.strip
171 encoding = Setting.commit_logs_encoding.to_s.strip
161 unless encoding.blank? || encoding == 'UTF-8'
172 unless encoding.blank? || encoding == 'UTF-8'
162 begin
173 begin
163 return Iconv.conv('UTF-8', encoding, str)
174 return Iconv.conv('UTF-8', encoding, str)
164 rescue Iconv::Failure
175 rescue Iconv::Failure
165 # do nothing here
176 # do nothing here
166 end
177 end
167 end
178 end
168 str
179 str
169 end
180 end
170 end
181 end
@@ -1,115 +1,115
1 <%= render :partial => 'action_menu' %>
1 <%= render :partial => 'action_menu' %>
2
2
3 <h2><%= @issue.tracker.name %> #<%= @issue.id %></h2>
3 <h2><%= @issue.tracker.name %> #<%= @issue.id %></h2>
4
4
5 <div class="<%= @issue.css_classes %> details">
5 <div class="<%= @issue.css_classes %> details">
6 <%= avatar(@issue.author, :size => "50") %>
6 <%= avatar(@issue.author, :size => "50") %>
7 <h3><%=h @issue.subject %></h3>
7 <h3><%=h @issue.subject %></h3>
8 <p class="author">
8 <p class="author">
9 <%= authoring @issue.created_on, @issue.author %>.
9 <%= authoring @issue.created_on, @issue.author %>.
10 <% if @issue.created_on != @issue.updated_on %>
10 <% if @issue.created_on != @issue.updated_on %>
11 <%= l(:label_updated_time, time_tag(@issue.updated_on)) %>.
11 <%= l(:label_updated_time, time_tag(@issue.updated_on)) %>.
12 <% end %>
12 <% end %>
13 </p>
13 </p>
14
14
15 <table class="attributes">
15 <table class="attributes">
16 <tr>
16 <tr>
17 <th class="status"><%=l(:field_status)%>:</th><td class="status"><%= @issue.status.name %></td>
17 <th class="status"><%=l(:field_status)%>:</th><td class="status"><%= @issue.status.name %></td>
18 <th class="start-date"><%=l(:field_start_date)%>:</th><td class="start-date"><%= format_date(@issue.start_date) %></td>
18 <th class="start-date"><%=l(:field_start_date)%>:</th><td class="start-date"><%= format_date(@issue.start_date) %></td>
19 </tr>
19 </tr>
20 <tr>
20 <tr>
21 <th class="priority"><%=l(:field_priority)%>:</th><td class="priority"><%= @issue.priority.name %></td>
21 <th class="priority"><%=l(:field_priority)%>:</th><td class="priority"><%= @issue.priority.name %></td>
22 <th class="due-date"><%=l(:field_due_date)%>:</th><td class="due-date"><%= format_date(@issue.due_date) %></td>
22 <th class="due-date"><%=l(:field_due_date)%>:</th><td class="due-date"><%= format_date(@issue.due_date) %></td>
23 </tr>
23 </tr>
24 <tr>
24 <tr>
25 <th class="assigned-to"><%=l(:field_assigned_to)%>:</th><td class="assigned-to"><%= avatar(@issue.assigned_to, :size => "14") %><%= @issue.assigned_to ? link_to_user(@issue.assigned_to) : "-" %></td>
25 <th class="assigned-to"><%=l(:field_assigned_to)%>:</th><td class="assigned-to"><%= avatar(@issue.assigned_to, :size => "14") %><%= @issue.assigned_to ? link_to_user(@issue.assigned_to) : "-" %></td>
26 <th class="progress"><%=l(:field_done_ratio)%>:</th><td class="progress"><%= progress_bar @issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%" %></td>
26 <th class="progress"><%=l(:field_done_ratio)%>:</th><td class="progress"><%= progress_bar @issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%" %></td>
27 </tr>
27 </tr>
28 <tr>
28 <tr>
29 <th class="category"><%=l(:field_category)%>:</th><td class="category"><%=h @issue.category ? @issue.category.name : "-" %></td>
29 <th class="category"><%=l(:field_category)%>:</th><td class="category"><%=h @issue.category ? @issue.category.name : "-" %></td>
30 <% if User.current.allowed_to?(:view_time_entries, @project) %>
30 <% if User.current.allowed_to?(:view_time_entries, @project) %>
31 <th class="spent-time"><%=l(:label_spent_time)%>:</th>
31 <th class="spent-time"><%=l(:label_spent_time)%>:</th>
32 <td class="spent-time"><%= @issue.spent_hours > 0 ? (link_to l_hours(@issue.spent_hours), {:controller => 'timelog', :action => 'details', :project_id => @project, :issue_id => @issue}) : "-" %></td>
32 <td class="spent-time"><%= @issue.spent_hours > 0 ? (link_to l_hours(@issue.spent_hours), {:controller => 'timelog', :action => 'details', :project_id => @project, :issue_id => @issue}) : "-" %></td>
33 <% end %>
33 <% end %>
34 </tr>
34 </tr>
35 <tr>
35 <tr>
36 <th class="fixed-version"><%=l(:field_fixed_version)%>:</th><td class="fixed-version"><%= @issue.fixed_version ? link_to_version(@issue.fixed_version) : "-" %></td>
36 <th class="fixed-version"><%=l(:field_fixed_version)%>:</th><td class="fixed-version"><%= @issue.fixed_version ? link_to_version(@issue.fixed_version) : "-" %></td>
37 <% if @issue.estimated_hours %>
37 <% if @issue.estimated_hours %>
38 <th class="estimated-hours"><%=l(:field_estimated_hours)%>:</th><td class="estimated-hours"><%= l_hours(@issue.estimated_hours) %></td>
38 <th class="estimated-hours"><%=l(:field_estimated_hours)%>:</th><td class="estimated-hours"><%= l_hours(@issue.estimated_hours) %></td>
39 <% end %>
39 <% end %>
40 </tr>
40 </tr>
41 <%= render_custom_fields_rows(@issue) %>
41 <%= render_custom_fields_rows(@issue) %>
42 <%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %>
42 <%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %>
43 </table>
43 </table>
44 <hr />
44 <hr />
45
45
46 <div class="contextual">
46 <div class="contextual">
47 <%= link_to_remote_if_authorized(l(:button_quote), { :url => {:action => 'reply', :id => @issue} }, :class => 'icon icon-comment') unless @issue.description.blank? %>
47 <%= link_to_remote_if_authorized(l(:button_quote), { :url => {:action => 'reply', :id => @issue} }, :class => 'icon icon-comment') unless @issue.description.blank? %>
48 </div>
48 </div>
49
49
50 <p><strong><%=l(:field_description)%></strong></p>
50 <p><strong><%=l(:field_description)%></strong></p>
51 <div class="wiki">
51 <div class="wiki">
52 <%= textilizable @issue, :description, :attachments => @issue.attachments %>
52 <%= textilizable @issue, :description, :attachments => @issue.attachments %>
53 </div>
53 </div>
54
54
55 <%= link_to_attachments @issue %>
55 <%= link_to_attachments @issue %>
56
56
57 <%= call_hook(:view_issues_show_description_bottom, :issue => @issue) %>
57 <%= call_hook(:view_issues_show_description_bottom, :issue => @issue) %>
58
58
59 <% if authorize_for('issue_relations', 'new') || @issue.relations.any? %>
59 <% if authorize_for('issue_relations', 'new') || @issue.relations.any? %>
60 <hr />
60 <hr />
61 <div id="relations">
61 <div id="relations">
62 <%= render :partial => 'relations' %>
62 <%= render :partial => 'relations' %>
63 </div>
63 </div>
64 <% end %>
64 <% end %>
65
65
66 <% if User.current.allowed_to?(:add_issue_watchers, @project) ||
66 <% if User.current.allowed_to?(:add_issue_watchers, @project) ||
67 (@issue.watchers.any? && User.current.allowed_to?(:view_issue_watchers, @project)) %>
67 (@issue.watchers.any? && User.current.allowed_to?(:view_issue_watchers, @project)) %>
68 <hr />
68 <hr />
69 <div id="watchers">
69 <div id="watchers">
70 <%= render :partial => 'watchers/watchers', :locals => {:watched => @issue} %>
70 <%= render :partial => 'watchers/watchers', :locals => {:watched => @issue} %>
71 </div>
71 </div>
72 <% end %>
72 <% end %>
73
73
74 </div>
74 </div>
75
75
76 <% if @changesets.any? && User.current.allowed_to?(:view_changesets, @project) %>
76 <% if @changesets.any? %>
77 <div id="issue-changesets">
77 <div id="issue-changesets">
78 <h3><%=l(:label_associated_revisions)%></h3>
78 <h3><%=l(:label_associated_revisions)%></h3>
79 <%= render :partial => 'changesets', :locals => { :changesets => @changesets} %>
79 <%= render :partial => 'changesets', :locals => { :changesets => @changesets} %>
80 </div>
80 </div>
81 <% end %>
81 <% end %>
82
82
83 <% if @journals.any? %>
83 <% if @journals.any? %>
84 <div id="history">
84 <div id="history">
85 <h3><%=l(:label_history)%></h3>
85 <h3><%=l(:label_history)%></h3>
86 <%= render :partial => 'history', :locals => { :journals => @journals } %>
86 <%= render :partial => 'history', :locals => { :journals => @journals } %>
87 </div>
87 </div>
88 <% end %>
88 <% end %>
89
89
90 <%= render :partial => 'action_menu', :locals => {:replace_watcher => 'watcher2' } %>
90 <%= render :partial => 'action_menu', :locals => {:replace_watcher => 'watcher2' } %>
91
91
92 <div style="clear: both;"></div>
92 <div style="clear: both;"></div>
93
93
94 <% if authorize_for('issues', 'edit') %>
94 <% if authorize_for('issues', 'edit') %>
95 <div id="update" style="display:none;">
95 <div id="update" style="display:none;">
96 <h3><%= l(:button_update) %></h3>
96 <h3><%= l(:button_update) %></h3>
97 <%= render :partial => 'edit' %>
97 <%= render :partial => 'edit' %>
98 </div>
98 </div>
99 <% end %>
99 <% end %>
100
100
101 <% other_formats_links do |f| %>
101 <% other_formats_links do |f| %>
102 <%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
102 <%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
103 <%= f.link_to 'PDF' %>
103 <%= f.link_to 'PDF' %>
104 <% end %>
104 <% end %>
105
105
106 <% html_title "#{@issue.tracker.name} ##{@issue.id}: #{@issue.subject}" %>
106 <% html_title "#{@issue.tracker.name} ##{@issue.id}: #{@issue.subject}" %>
107
107
108 <% content_for :sidebar do %>
108 <% content_for :sidebar do %>
109 <%= render :partial => 'issues/sidebar' %>
109 <%= render :partial => 'issues/sidebar' %>
110 <% end %>
110 <% end %>
111
111
112 <% content_for :header_tags do %>
112 <% content_for :header_tags do %>
113 <%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@issue.project} - #{@issue.tracker} ##{@issue.id}: #{@issue.subject}") %>
113 <%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@issue.project} - #{@issue.tracker} ##{@issue.id}: #{@issue.subject}") %>
114 <%= stylesheet_link_tag 'scm' %>
114 <%= stylesheet_link_tag 'scm' %>
115 <% end %>
115 <% end %>
@@ -1,97 +1,120
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class ChangesetTest < ActiveSupport::TestCase
20 class ChangesetTest < ActiveSupport::TestCase
21 fixtures :projects, :repositories, :issues, :issue_statuses, :changesets, :changes, :issue_categories, :enumerations, :custom_fields, :custom_values, :users, :members, :member_roles, :trackers
21 fixtures :projects, :repositories, :issues, :issue_statuses, :changesets, :changes, :issue_categories, :enumerations, :custom_fields, :custom_values, :users, :members, :member_roles, :trackers
22
22
23 def setup
23 def setup
24 end
24 end
25
25
26 def test_ref_keywords_any
26 def test_ref_keywords_any
27 ActionMailer::Base.deliveries.clear
27 ActionMailer::Base.deliveries.clear
28 Setting.commit_fix_status_id = IssueStatus.find(:first, :conditions => ["is_closed = ?", true]).id
28 Setting.commit_fix_status_id = IssueStatus.find(:first, :conditions => ["is_closed = ?", true]).id
29 Setting.commit_fix_done_ratio = '90'
29 Setting.commit_fix_done_ratio = '90'
30 Setting.commit_ref_keywords = '*'
30 Setting.commit_ref_keywords = '*'
31 Setting.commit_fix_keywords = 'fixes , closes'
31 Setting.commit_fix_keywords = 'fixes , closes'
32
32
33 c = Changeset.new(:repository => Project.find(1).repository,
33 c = Changeset.new(:repository => Project.find(1).repository,
34 :committed_on => Time.now,
34 :committed_on => Time.now,
35 :comments => 'New commit (#2). Fixes #1')
35 :comments => 'New commit (#2). Fixes #1')
36 c.scan_comment_for_issue_ids
36 c.scan_comment_for_issue_ids
37
37
38 assert_equal [1, 2], c.issue_ids.sort
38 assert_equal [1, 2], c.issue_ids.sort
39 fixed = Issue.find(1)
39 fixed = Issue.find(1)
40 assert fixed.closed?
40 assert fixed.closed?
41 assert_equal 90, fixed.done_ratio
41 assert_equal 90, fixed.done_ratio
42 assert_equal 1, ActionMailer::Base.deliveries.size
42 assert_equal 1, ActionMailer::Base.deliveries.size
43 end
43 end
44
44
45 def test_ref_keywords_any_line_start
45 def test_ref_keywords_any_line_start
46 Setting.commit_ref_keywords = '*'
46 Setting.commit_ref_keywords = '*'
47
47
48 c = Changeset.new(:repository => Project.find(1).repository,
48 c = Changeset.new(:repository => Project.find(1).repository,
49 :committed_on => Time.now,
49 :committed_on => Time.now,
50 :comments => '#1 is the reason of this commit')
50 :comments => '#1 is the reason of this commit')
51 c.scan_comment_for_issue_ids
51 c.scan_comment_for_issue_ids
52
52
53 assert_equal [1], c.issue_ids.sort
53 assert_equal [1], c.issue_ids.sort
54 end
54 end
55
55
56 def test_ref_keywords_allow_brackets_around_a_issue_number
56 def test_ref_keywords_allow_brackets_around_a_issue_number
57 Setting.commit_ref_keywords = '*'
57 Setting.commit_ref_keywords = '*'
58
58
59 c = Changeset.new(:repository => Project.find(1).repository,
59 c = Changeset.new(:repository => Project.find(1).repository,
60 :committed_on => Time.now,
60 :committed_on => Time.now,
61 :comments => '[#1] Worked on this issue')
61 :comments => '[#1] Worked on this issue')
62 c.scan_comment_for_issue_ids
62 c.scan_comment_for_issue_ids
63
63
64 assert_equal [1], c.issue_ids.sort
64 assert_equal [1], c.issue_ids.sort
65 end
65 end
66
66
67 def test_ref_keywords_allow_brackets_around_multiple_issue_numbers
67 def test_ref_keywords_allow_brackets_around_multiple_issue_numbers
68 Setting.commit_ref_keywords = '*'
68 Setting.commit_ref_keywords = '*'
69
69
70 c = Changeset.new(:repository => Project.find(1).repository,
70 c = Changeset.new(:repository => Project.find(1).repository,
71 :committed_on => Time.now,
71 :committed_on => Time.now,
72 :comments => '[#1 #2, #3] Worked on these')
72 :comments => '[#1 #2, #3] Worked on these')
73 c.scan_comment_for_issue_ids
73 c.scan_comment_for_issue_ids
74
74
75 assert_equal [1,2,3], c.issue_ids.sort
75 assert_equal [1,2,3], c.issue_ids.sort
76 end
76 end
77
77
78 def test_commit_referencing_a_subproject_issue
79 c = Changeset.new(:repository => Project.find(1).repository,
80 :committed_on => Time.now,
81 :comments => 'refs #5, a subproject issue')
82 c.scan_comment_for_issue_ids
83
84 assert_equal [5], c.issue_ids.sort
85 assert c.issues.first.project != c.project
86 end
87
88 def test_commit_referencing_a_parent_project_issue
89 # repository of child project
90 r = Repository::Subversion.create!(:project => Project.find(3), :url => 'svn://localhost/test')
91
92 c = Changeset.new(:repository => r,
93 :committed_on => Time.now,
94 :comments => 'refs #2, an issue of a parent project')
95 c.scan_comment_for_issue_ids
96
97 assert_equal [2], c.issue_ids.sort
98 assert c.issues.first.project != c.project
99 end
100
78 def test_previous
101 def test_previous
79 changeset = Changeset.find_by_revision('3')
102 changeset = Changeset.find_by_revision('3')
80 assert_equal Changeset.find_by_revision('2'), changeset.previous
103 assert_equal Changeset.find_by_revision('2'), changeset.previous
81 end
104 end
82
105
83 def test_previous_nil
106 def test_previous_nil
84 changeset = Changeset.find_by_revision('1')
107 changeset = Changeset.find_by_revision('1')
85 assert_nil changeset.previous
108 assert_nil changeset.previous
86 end
109 end
87
110
88 def test_next
111 def test_next
89 changeset = Changeset.find_by_revision('2')
112 changeset = Changeset.find_by_revision('2')
90 assert_equal Changeset.find_by_revision('3'), changeset.next
113 assert_equal Changeset.find_by_revision('3'), changeset.next
91 end
114 end
92
115
93 def test_next_nil
116 def test_next_nil
94 changeset = Changeset.find_by_revision('10')
117 changeset = Changeset.find_by_revision('10')
95 assert_nil changeset.next
118 assert_nil changeset.next
96 end
119 end
97 end
120 end
General Comments 0
You need to be logged in to leave comments. Login now