##// END OF EJS Templates
Allow commits to reference issues of parent projects and subprojects (#4674)....
Jean-Philippe Lang -
r3243:d43c860448ce
parent child
Show More
@@ -1,572 +1,572
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, :delete],
50 verify :method => [:post, :delete],
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.xml { }
63 format.xml { }
64 format.atom { limit = Setting.feeds_limit.to_i }
64 format.atom { limit = Setting.feeds_limit.to_i }
65 format.csv { limit = Setting.issues_export_limit.to_i }
65 format.csv { limit = Setting.issues_export_limit.to_i }
66 format.pdf { limit = Setting.issues_export_limit.to_i }
66 format.pdf { limit = Setting.issues_export_limit.to_i }
67 end
67 end
68
68
69 @issue_count = @query.issue_count
69 @issue_count = @query.issue_count
70 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
70 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
71 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
71 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
72 :order => sort_clause,
72 :order => sort_clause,
73 :offset => @issue_pages.current.offset,
73 :offset => @issue_pages.current.offset,
74 :limit => limit)
74 :limit => limit)
75 @issue_count_by_group = @query.issue_count_by_group
75 @issue_count_by_group = @query.issue_count_by_group
76
76
77 respond_to do |format|
77 respond_to do |format|
78 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
78 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
79 format.xml { render :layout => false }
79 format.xml { render :layout => false }
80 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
80 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
81 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
81 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
82 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
82 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
83 end
83 end
84 else
84 else
85 # Send html if the query is not valid
85 # Send html if the query is not valid
86 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
86 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
87 end
87 end
88 rescue ActiveRecord::RecordNotFound
88 rescue ActiveRecord::RecordNotFound
89 render_404
89 render_404
90 end
90 end
91
91
92 def changes
92 def changes
93 retrieve_query
93 retrieve_query
94 sort_init 'id', 'desc'
94 sort_init 'id', 'desc'
95 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
95 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
96
96
97 if @query.valid?
97 if @query.valid?
98 @journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
98 @journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
99 :limit => 25)
99 :limit => 25)
100 end
100 end
101 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
101 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
102 render :layout => false, :content_type => 'application/atom+xml'
102 render :layout => false, :content_type => 'application/atom+xml'
103 rescue ActiveRecord::RecordNotFound
103 rescue ActiveRecord::RecordNotFound
104 render_404
104 render_404
105 end
105 end
106
106
107 def show
107 def show
108 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
108 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
109 @journals.each_with_index {|j,i| j.indice = i+1}
109 @journals.each_with_index {|j,i| j.indice = i+1}
110 @journals.reverse! if User.current.wants_comments_in_reverse_order?
110 @journals.reverse! if User.current.wants_comments_in_reverse_order?
111 @changesets = @issue.changesets
111 @changesets = @issue.changesets.visible.all
112 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
112 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
113 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
113 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
114 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
114 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
115 @priorities = IssuePriority.all
115 @priorities = IssuePriority.all
116 @time_entry = TimeEntry.new
116 @time_entry = TimeEntry.new
117 respond_to do |format|
117 respond_to do |format|
118 format.html { render :template => 'issues/show.rhtml' }
118 format.html { render :template => 'issues/show.rhtml' }
119 format.xml { render :layout => false }
119 format.xml { render :layout => false }
120 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
120 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
121 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
121 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
122 end
122 end
123 end
123 end
124
124
125 # Add a new issue
125 # Add a new issue
126 # The new issue will be created from an existing one if copy_from parameter is given
126 # The new issue will be created from an existing one if copy_from parameter is given
127 def new
127 def new
128 @issue = Issue.new
128 @issue = Issue.new
129 @issue.copy_from(params[:copy_from]) if params[:copy_from]
129 @issue.copy_from(params[:copy_from]) if params[:copy_from]
130 @issue.project = @project
130 @issue.project = @project
131 # Tracker must be set before custom field values
131 # Tracker must be set before custom field values
132 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
132 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
133 if @issue.tracker.nil?
133 if @issue.tracker.nil?
134 render_error l(:error_no_tracker_in_project)
134 render_error l(:error_no_tracker_in_project)
135 return
135 return
136 end
136 end
137 if params[:issue].is_a?(Hash)
137 if params[:issue].is_a?(Hash)
138 @issue.safe_attributes = params[:issue]
138 @issue.safe_attributes = params[:issue]
139 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
139 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
140 end
140 end
141 @issue.author = User.current
141 @issue.author = User.current
142
142
143 default_status = IssueStatus.default
143 default_status = IssueStatus.default
144 unless default_status
144 unless default_status
145 render_error l(:error_no_default_issue_status)
145 render_error l(:error_no_default_issue_status)
146 return
146 return
147 end
147 end
148 @issue.status = default_status
148 @issue.status = default_status
149 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
149 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
150
150
151 if request.get? || request.xhr?
151 if request.get? || request.xhr?
152 @issue.start_date ||= Date.today
152 @issue.start_date ||= Date.today
153 else
153 else
154 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
154 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
155 # Check that the user is allowed to apply the requested status
155 # Check that the user is allowed to apply the requested status
156 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
156 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
157 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
157 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
158 if @issue.save
158 if @issue.save
159 attach_files(@issue, params[:attachments])
159 attach_files(@issue, params[:attachments])
160 flash[:notice] = l(:notice_successful_create)
160 flash[:notice] = l(:notice_successful_create)
161 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
161 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
162 respond_to do |format|
162 respond_to do |format|
163 format.html {
163 format.html {
164 redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
164 redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
165 { :action => 'show', :id => @issue })
165 { :action => 'show', :id => @issue })
166 }
166 }
167 format.xml { render :action => 'show', :status => :created, :location => url_for(:controller => 'issues', :action => 'show', :id => @issue) }
167 format.xml { render :action => 'show', :status => :created, :location => url_for(:controller => 'issues', :action => 'show', :id => @issue) }
168 end
168 end
169 return
169 return
170 else
170 else
171 respond_to do |format|
171 respond_to do |format|
172 format.html { }
172 format.html { }
173 format.xml { render(:xml => @issue.errors, :status => :unprocessable_entity); return }
173 format.xml { render(:xml => @issue.errors, :status => :unprocessable_entity); return }
174 end
174 end
175 end
175 end
176 end
176 end
177 @priorities = IssuePriority.all
177 @priorities = IssuePriority.all
178 render :layout => !request.xhr?
178 render :layout => !request.xhr?
179 end
179 end
180
180
181 # Attributes that can be updated on workflow transition (without :edit permission)
181 # Attributes that can be updated on workflow transition (without :edit permission)
182 # TODO: make it configurable (at least per role)
182 # TODO: make it configurable (at least per role)
183 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
183 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
184
184
185 def edit
185 def edit
186 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
186 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
187 @priorities = IssuePriority.all
187 @priorities = IssuePriority.all
188 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
188 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
189 @time_entry = TimeEntry.new
189 @time_entry = TimeEntry.new
190
190
191 @notes = params[:notes]
191 @notes = params[:notes]
192 journal = @issue.init_journal(User.current, @notes)
192 journal = @issue.init_journal(User.current, @notes)
193 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
193 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
194 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
194 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
195 attrs = params[:issue].dup
195 attrs = params[:issue].dup
196 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
196 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
197 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
197 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
198 @issue.safe_attributes = attrs
198 @issue.safe_attributes = attrs
199 end
199 end
200
200
201 if request.get?
201 if request.get?
202 # nop
202 # nop
203 else
203 else
204 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
204 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
205 @time_entry.attributes = params[:time_entry]
205 @time_entry.attributes = params[:time_entry]
206 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.valid?
206 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.valid?
207 attachments = attach_files(@issue, params[:attachments])
207 attachments = attach_files(@issue, params[:attachments])
208 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
208 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
209 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
209 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
210 if @issue.save
210 if @issue.save
211 # Log spend time
211 # Log spend time
212 if User.current.allowed_to?(:log_time, @project)
212 if User.current.allowed_to?(:log_time, @project)
213 @time_entry.save
213 @time_entry.save
214 end
214 end
215 if !journal.new_record?
215 if !journal.new_record?
216 # Only send notification if something was actually changed
216 # Only send notification if something was actually changed
217 flash[:notice] = l(:notice_successful_update)
217 flash[:notice] = l(:notice_successful_update)
218 end
218 end
219 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
219 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
220 respond_to do |format|
220 respond_to do |format|
221 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
221 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
222 format.xml { head :ok }
222 format.xml { head :ok }
223 end
223 end
224 return
224 return
225 end
225 end
226 end
226 end
227 # failure
227 # failure
228 respond_to do |format|
228 respond_to do |format|
229 format.html { }
229 format.html { }
230 format.xml { render :xml => @issue.errors, :status => :unprocessable_entity }
230 format.xml { render :xml => @issue.errors, :status => :unprocessable_entity }
231 end
231 end
232 end
232 end
233 rescue ActiveRecord::StaleObjectError
233 rescue ActiveRecord::StaleObjectError
234 # Optimistic locking exception
234 # Optimistic locking exception
235 flash.now[:error] = l(:notice_locking_conflict)
235 flash.now[:error] = l(:notice_locking_conflict)
236 # Remove the previously added attachments if issue was not updated
236 # Remove the previously added attachments if issue was not updated
237 attachments.each(&:destroy)
237 attachments.each(&:destroy)
238 end
238 end
239
239
240 def reply
240 def reply
241 journal = Journal.find(params[:journal_id]) if params[:journal_id]
241 journal = Journal.find(params[:journal_id]) if params[:journal_id]
242 if journal
242 if journal
243 user = journal.user
243 user = journal.user
244 text = journal.notes
244 text = journal.notes
245 else
245 else
246 user = @issue.author
246 user = @issue.author
247 text = @issue.description
247 text = @issue.description
248 end
248 end
249 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
249 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
250 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
250 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
251 render(:update) { |page|
251 render(:update) { |page|
252 page.<< "$('notes').value = \"#{content}\";"
252 page.<< "$('notes').value = \"#{content}\";"
253 page.show 'update'
253 page.show 'update'
254 page << "Form.Element.focus('notes');"
254 page << "Form.Element.focus('notes');"
255 page << "Element.scrollTo('update');"
255 page << "Element.scrollTo('update');"
256 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
256 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
257 }
257 }
258 end
258 end
259
259
260 # Bulk edit a set of issues
260 # Bulk edit a set of issues
261 def bulk_edit
261 def bulk_edit
262 if request.post?
262 if request.post?
263 tracker = params[:tracker_id].blank? ? nil : @project.trackers.find_by_id(params[:tracker_id])
263 tracker = params[:tracker_id].blank? ? nil : @project.trackers.find_by_id(params[:tracker_id])
264 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
264 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
265 priority = params[:priority_id].blank? ? nil : IssuePriority.find_by_id(params[:priority_id])
265 priority = params[:priority_id].blank? ? nil : IssuePriority.find_by_id(params[:priority_id])
266 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
266 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
267 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
267 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
268 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.shared_versions.find_by_id(params[:fixed_version_id])
268 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.shared_versions.find_by_id(params[:fixed_version_id])
269 custom_field_values = params[:custom_field_values] ? params[:custom_field_values].reject {|k,v| v.blank?} : nil
269 custom_field_values = params[:custom_field_values] ? params[:custom_field_values].reject {|k,v| v.blank?} : nil
270
270
271 unsaved_issue_ids = []
271 unsaved_issue_ids = []
272 @issues.each do |issue|
272 @issues.each do |issue|
273 journal = issue.init_journal(User.current, params[:notes])
273 journal = issue.init_journal(User.current, params[:notes])
274 issue.tracker = tracker if tracker
274 issue.tracker = tracker if tracker
275 issue.priority = priority if priority
275 issue.priority = priority if priority
276 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
276 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
277 issue.category = category if category || params[:category_id] == 'none'
277 issue.category = category if category || params[:category_id] == 'none'
278 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
278 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
279 issue.start_date = params[:start_date] unless params[:start_date].blank?
279 issue.start_date = params[:start_date] unless params[:start_date].blank?
280 issue.due_date = params[:due_date] unless params[:due_date].blank?
280 issue.due_date = params[:due_date] unless params[:due_date].blank?
281 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
281 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
282 issue.custom_field_values = custom_field_values if custom_field_values && !custom_field_values.empty?
282 issue.custom_field_values = custom_field_values if custom_field_values && !custom_field_values.empty?
283 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
283 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
284 # Don't save any change to the issue if the user is not authorized to apply the requested status
284 # Don't save any change to the issue if the user is not authorized to apply the requested status
285 unless (status.nil? || (issue.new_statuses_allowed_to(User.current).include?(status) && issue.status = status)) && issue.save
285 unless (status.nil? || (issue.new_statuses_allowed_to(User.current).include?(status) && issue.status = status)) && issue.save
286 # Keep unsaved issue ids to display them in flash error
286 # Keep unsaved issue ids to display them in flash error
287 unsaved_issue_ids << issue.id
287 unsaved_issue_ids << issue.id
288 end
288 end
289 end
289 end
290 if unsaved_issue_ids.empty?
290 if unsaved_issue_ids.empty?
291 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
291 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
292 else
292 else
293 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
293 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
294 :total => @issues.size,
294 :total => @issues.size,
295 :ids => '#' + unsaved_issue_ids.join(', #'))
295 :ids => '#' + unsaved_issue_ids.join(', #'))
296 end
296 end
297 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
297 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
298 return
298 return
299 end
299 end
300 @available_statuses = Workflow.available_statuses(@project)
300 @available_statuses = Workflow.available_statuses(@project)
301 @custom_fields = @project.all_issue_custom_fields
301 @custom_fields = @project.all_issue_custom_fields
302 end
302 end
303
303
304 def move
304 def move
305 @copy = params[:copy_options] && params[:copy_options][:copy]
305 @copy = params[:copy_options] && params[:copy_options][:copy]
306 @allowed_projects = []
306 @allowed_projects = []
307 # find projects to which the user is allowed to move the issue
307 # find projects to which the user is allowed to move the issue
308 if User.current.admin?
308 if User.current.admin?
309 # admin is allowed to move issues to any active (visible) project
309 # admin is allowed to move issues to any active (visible) project
310 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
310 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
311 else
311 else
312 User.current.memberships.each {|m| @allowed_projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
312 User.current.memberships.each {|m| @allowed_projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
313 end
313 end
314 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
314 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
315 @target_project ||= @project
315 @target_project ||= @project
316 @trackers = @target_project.trackers
316 @trackers = @target_project.trackers
317 @available_statuses = Workflow.available_statuses(@project)
317 @available_statuses = Workflow.available_statuses(@project)
318 if request.post?
318 if request.post?
319 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
319 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
320 unsaved_issue_ids = []
320 unsaved_issue_ids = []
321 moved_issues = []
321 moved_issues = []
322 @issues.each do |issue|
322 @issues.each do |issue|
323 changed_attributes = {}
323 changed_attributes = {}
324 [:assigned_to_id, :status_id, :start_date, :due_date].each do |valid_attribute|
324 [:assigned_to_id, :status_id, :start_date, :due_date].each do |valid_attribute|
325 unless params[valid_attribute].blank?
325 unless params[valid_attribute].blank?
326 changed_attributes[valid_attribute] = (params[valid_attribute] == 'none' ? nil : params[valid_attribute])
326 changed_attributes[valid_attribute] = (params[valid_attribute] == 'none' ? nil : params[valid_attribute])
327 end
327 end
328 end
328 end
329 issue.init_journal(User.current)
329 issue.init_journal(User.current)
330 if r = issue.move_to(@target_project, new_tracker, {:copy => @copy, :attributes => changed_attributes})
330 if r = issue.move_to(@target_project, new_tracker, {:copy => @copy, :attributes => changed_attributes})
331 moved_issues << r
331 moved_issues << r
332 else
332 else
333 unsaved_issue_ids << issue.id
333 unsaved_issue_ids << issue.id
334 end
334 end
335 end
335 end
336 if unsaved_issue_ids.empty?
336 if unsaved_issue_ids.empty?
337 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
337 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
338 else
338 else
339 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
339 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
340 :total => @issues.size,
340 :total => @issues.size,
341 :ids => '#' + unsaved_issue_ids.join(', #'))
341 :ids => '#' + unsaved_issue_ids.join(', #'))
342 end
342 end
343 if params[:follow]
343 if params[:follow]
344 if @issues.size == 1 && moved_issues.size == 1
344 if @issues.size == 1 && moved_issues.size == 1
345 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
345 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
346 else
346 else
347 redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project)
347 redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project)
348 end
348 end
349 else
349 else
350 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
350 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
351 end
351 end
352 return
352 return
353 end
353 end
354 render :layout => false if request.xhr?
354 render :layout => false if request.xhr?
355 end
355 end
356
356
357 def destroy
357 def destroy
358 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
358 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
359 if @hours > 0
359 if @hours > 0
360 case params[:todo]
360 case params[:todo]
361 when 'destroy'
361 when 'destroy'
362 # nothing to do
362 # nothing to do
363 when 'nullify'
363 when 'nullify'
364 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
364 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
365 when 'reassign'
365 when 'reassign'
366 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
366 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
367 if reassign_to.nil?
367 if reassign_to.nil?
368 flash.now[:error] = l(:error_issue_not_found_in_project)
368 flash.now[:error] = l(:error_issue_not_found_in_project)
369 return
369 return
370 else
370 else
371 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
371 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
372 end
372 end
373 else
373 else
374 unless params[:format] == 'xml'
374 unless params[:format] == 'xml'
375 # display the destroy form if it's a user request
375 # display the destroy form if it's a user request
376 return
376 return
377 end
377 end
378 end
378 end
379 end
379 end
380 @issues.each(&:destroy)
380 @issues.each(&:destroy)
381 respond_to do |format|
381 respond_to do |format|
382 format.html { redirect_to :action => 'index', :project_id => @project }
382 format.html { redirect_to :action => 'index', :project_id => @project }
383 format.xml { head :ok }
383 format.xml { head :ok }
384 end
384 end
385 end
385 end
386
386
387 def gantt
387 def gantt
388 @gantt = Redmine::Helpers::Gantt.new(params)
388 @gantt = Redmine::Helpers::Gantt.new(params)
389 retrieve_query
389 retrieve_query
390 if @query.valid?
390 if @query.valid?
391 events = []
391 events = []
392 # Issues that have start and due dates
392 # Issues that have start and due dates
393 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
393 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
394 :order => "start_date, due_date",
394 :order => "start_date, due_date",
395 :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]
395 :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]
396 )
396 )
397 # Issues that don't have a due date but that are assigned to a version with a date
397 # Issues that don't have a due date but that are assigned to a version with a date
398 events += @query.issues(:include => [:tracker, :assigned_to, :priority, :fixed_version],
398 events += @query.issues(:include => [:tracker, :assigned_to, :priority, :fixed_version],
399 :order => "start_date, effective_date",
399 :order => "start_date, effective_date",
400 :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]
400 :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]
401 )
401 )
402 # Versions
402 # Versions
403 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
403 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
404
404
405 @gantt.events = events
405 @gantt.events = events
406 end
406 end
407
407
408 basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
408 basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
409
409
410 respond_to do |format|
410 respond_to do |format|
411 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
411 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
412 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
412 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
413 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") }
413 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") }
414 end
414 end
415 end
415 end
416
416
417 def calendar
417 def calendar
418 if params[:year] and params[:year].to_i > 1900
418 if params[:year] and params[:year].to_i > 1900
419 @year = params[:year].to_i
419 @year = params[:year].to_i
420 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
420 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
421 @month = params[:month].to_i
421 @month = params[:month].to_i
422 end
422 end
423 end
423 end
424 @year ||= Date.today.year
424 @year ||= Date.today.year
425 @month ||= Date.today.month
425 @month ||= Date.today.month
426
426
427 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
427 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
428 retrieve_query
428 retrieve_query
429 if @query.valid?
429 if @query.valid?
430 events = []
430 events = []
431 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
431 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
432 :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
432 :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
433 )
433 )
434 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
434 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
435
435
436 @calendar.events = events
436 @calendar.events = events
437 end
437 end
438
438
439 render :layout => false if request.xhr?
439 render :layout => false if request.xhr?
440 end
440 end
441
441
442 def context_menu
442 def context_menu
443 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
443 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
444 if (@issues.size == 1)
444 if (@issues.size == 1)
445 @issue = @issues.first
445 @issue = @issues.first
446 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
446 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
447 end
447 end
448 projects = @issues.collect(&:project).compact.uniq
448 projects = @issues.collect(&:project).compact.uniq
449 @project = projects.first if projects.size == 1
449 @project = projects.first if projects.size == 1
450
450
451 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
451 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
452 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
452 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
453 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
453 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
454 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
454 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
455 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
455 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
456 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
456 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
457 }
457 }
458 if @project
458 if @project
459 @assignables = @project.assignable_users
459 @assignables = @project.assignable_users
460 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
460 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
461 @trackers = @project.trackers
461 @trackers = @project.trackers
462 end
462 end
463
463
464 @priorities = IssuePriority.all.reverse
464 @priorities = IssuePriority.all.reverse
465 @statuses = IssueStatus.find(:all, :order => 'position')
465 @statuses = IssueStatus.find(:all, :order => 'position')
466 @back = params[:back_url] || request.env['HTTP_REFERER']
466 @back = params[:back_url] || request.env['HTTP_REFERER']
467
467
468 render :layout => false
468 render :layout => false
469 end
469 end
470
470
471 def update_form
471 def update_form
472 if params[:id].blank?
472 if params[:id].blank?
473 @issue = Issue.new
473 @issue = Issue.new
474 @issue.project = @project
474 @issue.project = @project
475 else
475 else
476 @issue = @project.issues.visible.find(params[:id])
476 @issue = @project.issues.visible.find(params[:id])
477 end
477 end
478 @issue.attributes = params[:issue]
478 @issue.attributes = params[:issue]
479 @allowed_statuses = ([@issue.status] + @issue.status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
479 @allowed_statuses = ([@issue.status] + @issue.status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
480 @priorities = IssuePriority.all
480 @priorities = IssuePriority.all
481
481
482 render :partial => 'attributes'
482 render :partial => 'attributes'
483 end
483 end
484
484
485 def preview
485 def preview
486 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
486 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
487 @attachements = @issue.attachments if @issue
487 @attachements = @issue.attachments if @issue
488 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
488 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
489 render :partial => 'common/preview'
489 render :partial => 'common/preview'
490 end
490 end
491
491
492 private
492 private
493 def find_issue
493 def find_issue
494 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
494 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
495 @project = @issue.project
495 @project = @issue.project
496 rescue ActiveRecord::RecordNotFound
496 rescue ActiveRecord::RecordNotFound
497 render_404
497 render_404
498 end
498 end
499
499
500 # Filter for bulk operations
500 # Filter for bulk operations
501 def find_issues
501 def find_issues
502 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
502 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
503 raise ActiveRecord::RecordNotFound if @issues.empty?
503 raise ActiveRecord::RecordNotFound if @issues.empty?
504 projects = @issues.collect(&:project).compact.uniq
504 projects = @issues.collect(&:project).compact.uniq
505 if projects.size == 1
505 if projects.size == 1
506 @project = projects.first
506 @project = projects.first
507 else
507 else
508 # TODO: let users bulk edit/move/destroy issues from different projects
508 # TODO: let users bulk edit/move/destroy issues from different projects
509 render_error 'Can not bulk edit/move/destroy issues from different projects'
509 render_error 'Can not bulk edit/move/destroy issues from different projects'
510 return false
510 return false
511 end
511 end
512 rescue ActiveRecord::RecordNotFound
512 rescue ActiveRecord::RecordNotFound
513 render_404
513 render_404
514 end
514 end
515
515
516 def find_project
516 def find_project
517 project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
517 project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
518 @project = Project.find(project_id)
518 @project = Project.find(project_id)
519 rescue ActiveRecord::RecordNotFound
519 rescue ActiveRecord::RecordNotFound
520 render_404
520 render_404
521 end
521 end
522
522
523 def find_optional_project
523 def find_optional_project
524 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
524 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
525 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
525 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
526 allowed ? true : deny_access
526 allowed ? true : deny_access
527 rescue ActiveRecord::RecordNotFound
527 rescue ActiveRecord::RecordNotFound
528 render_404
528 render_404
529 end
529 end
530
530
531 # Retrieve query from session or build a new query
531 # Retrieve query from session or build a new query
532 def retrieve_query
532 def retrieve_query
533 if !params[:query_id].blank?
533 if !params[:query_id].blank?
534 cond = "project_id IS NULL"
534 cond = "project_id IS NULL"
535 cond << " OR project_id = #{@project.id}" if @project
535 cond << " OR project_id = #{@project.id}" if @project
536 @query = Query.find(params[:query_id], :conditions => cond)
536 @query = Query.find(params[:query_id], :conditions => cond)
537 @query.project = @project
537 @query.project = @project
538 session[:query] = {:id => @query.id, :project_id => @query.project_id}
538 session[:query] = {:id => @query.id, :project_id => @query.project_id}
539 sort_clear
539 sort_clear
540 else
540 else
541 if api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
541 if api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
542 # Give it a name, required to be valid
542 # Give it a name, required to be valid
543 @query = Query.new(:name => "_")
543 @query = Query.new(:name => "_")
544 @query.project = @project
544 @query.project = @project
545 if params[:fields] and params[:fields].is_a? Array
545 if params[:fields] and params[:fields].is_a? Array
546 params[:fields].each do |field|
546 params[:fields].each do |field|
547 @query.add_filter(field, params[:operators][field], params[:values][field])
547 @query.add_filter(field, params[:operators][field], params[:values][field])
548 end
548 end
549 else
549 else
550 @query.available_filters.keys.each do |field|
550 @query.available_filters.keys.each do |field|
551 @query.add_short_filter(field, params[field]) if params[field]
551 @query.add_short_filter(field, params[field]) if params[field]
552 end
552 end
553 end
553 end
554 @query.group_by = params[:group_by]
554 @query.group_by = params[:group_by]
555 @query.column_names = params[:query] && params[:query][:column_names]
555 @query.column_names = params[:query] && params[:query][:column_names]
556 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
556 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
557 else
557 else
558 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
558 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
559 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
559 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
560 @query.project = @project
560 @query.project = @project
561 end
561 end
562 end
562 end
563 end
563 end
564
564
565 # Rescues an invalid query statement. Just in case...
565 # Rescues an invalid query statement. Just in case...
566 def query_statement_invalid(exception)
566 def query_statement_invalid(exception)
567 logger.error "Query::StatementInvalid: #{exception.message}" if logger
567 logger.error "Query::StatementInvalid: #{exception.message}" if logger
568 session.delete(:query)
568 session.delete(:query)
569 sort_clear
569 sort_clear
570 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
570 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
571 end
571 end
572 end
572 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