##// END OF EJS Templates
Track project and tracker changes in issue history....
Jean-Philippe Lang -
r1551:9cfa233001b0
parent child
Show More
@@ -1,441 +1,442
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class IssuesController < ApplicationController
18 class IssuesController < ApplicationController
19 layout 'base'
19 layout 'base'
20 menu_item :new_issue, :only => :new
20 menu_item :new_issue, :only => :new
21
21
22 before_filter :find_issue, :only => [:show, :edit, :reply, :destroy_attachment]
22 before_filter :find_issue, :only => [:show, :edit, :reply, :destroy_attachment]
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, :preview, :update_form, :context_menu]
25 before_filter :authorize, :except => [:index, :changes, :preview, :update_form, :context_menu]
26 before_filter :find_optional_project, :only => [:index, :changes]
26 before_filter :find_optional_project, :only => [:index, :changes]
27 accept_key_auth :index, :changes
27 accept_key_auth :index, :changes
28
28
29 helper :journals
29 helper :journals
30 helper :projects
30 helper :projects
31 include ProjectsHelper
31 include ProjectsHelper
32 helper :custom_fields
32 helper :custom_fields
33 include CustomFieldsHelper
33 include CustomFieldsHelper
34 helper :ifpdf
34 helper :ifpdf
35 include IfpdfHelper
35 include IfpdfHelper
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 helper :sort
43 helper :sort
44 include SortHelper
44 include SortHelper
45 include IssuesHelper
45 include IssuesHelper
46
46
47 def index
47 def index
48 sort_init "#{Issue.table_name}.id", "desc"
48 sort_init "#{Issue.table_name}.id", "desc"
49 sort_update
49 sort_update
50 retrieve_query
50 retrieve_query
51 if @query.valid?
51 if @query.valid?
52 limit = per_page_option
52 limit = per_page_option
53 respond_to do |format|
53 respond_to do |format|
54 format.html { }
54 format.html { }
55 format.atom { }
55 format.atom { }
56 format.csv { limit = Setting.issues_export_limit.to_i }
56 format.csv { limit = Setting.issues_export_limit.to_i }
57 format.pdf { limit = Setting.issues_export_limit.to_i }
57 format.pdf { limit = Setting.issues_export_limit.to_i }
58 end
58 end
59 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
59 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
60 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
60 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
61 @issues = Issue.find :all, :order => sort_clause,
61 @issues = Issue.find :all, :order => sort_clause,
62 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
62 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
63 :conditions => @query.statement,
63 :conditions => @query.statement,
64 :limit => limit,
64 :limit => limit,
65 :offset => @issue_pages.current.offset
65 :offset => @issue_pages.current.offset
66 respond_to do |format|
66 respond_to do |format|
67 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
67 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
68 format.atom { render_feed(@issues, :title => l(:label_issue_plural)) }
68 format.atom { render_feed(@issues, :title => l(:label_issue_plural)) }
69 format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
69 format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
70 format.pdf { send_data(render(:template => 'issues/index.rfpdf', :layout => false), :type => 'application/pdf', :filename => 'export.pdf') }
70 format.pdf { send_data(render(:template => 'issues/index.rfpdf', :layout => false), :type => 'application/pdf', :filename => 'export.pdf') }
71 end
71 end
72 else
72 else
73 # Send html if the query is not valid
73 # Send html if the query is not valid
74 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
74 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
75 end
75 end
76 rescue ActiveRecord::RecordNotFound
76 rescue ActiveRecord::RecordNotFound
77 render_404
77 render_404
78 end
78 end
79
79
80 def changes
80 def changes
81 sort_init "#{Issue.table_name}.id", "desc"
81 sort_init "#{Issue.table_name}.id", "desc"
82 sort_update
82 sort_update
83 retrieve_query
83 retrieve_query
84 if @query.valid?
84 if @query.valid?
85 @journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
85 @journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
86 :conditions => @query.statement,
86 :conditions => @query.statement,
87 :limit => 25,
87 :limit => 25,
88 :order => "#{Journal.table_name}.created_on DESC"
88 :order => "#{Journal.table_name}.created_on DESC"
89 end
89 end
90 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
90 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
91 render :layout => false, :content_type => 'application/atom+xml'
91 render :layout => false, :content_type => 'application/atom+xml'
92 rescue ActiveRecord::RecordNotFound
92 rescue ActiveRecord::RecordNotFound
93 render_404
93 render_404
94 end
94 end
95
95
96 def show
96 def show
97 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| @issue.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x, :customized => @issue) }
97 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| @issue.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x, :customized => @issue) }
98 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
98 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
99 @journals.each_with_index {|j,i| j.indice = i+1}
99 @journals.each_with_index {|j,i| j.indice = i+1}
100 @journals.reverse! if User.current.wants_comments_in_reverse_order?
100 @journals.reverse! if User.current.wants_comments_in_reverse_order?
101 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
101 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
102 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
102 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
103 @activities = Enumeration::get_values('ACTI')
103 @activities = Enumeration::get_values('ACTI')
104 @priorities = Enumeration::get_values('IPRI')
104 @priorities = Enumeration::get_values('IPRI')
105 @time_entry = TimeEntry.new
105 @time_entry = TimeEntry.new
106 respond_to do |format|
106 respond_to do |format|
107 format.html { render :template => 'issues/show.rhtml' }
107 format.html { render :template => 'issues/show.rhtml' }
108 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
108 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
109 format.pdf { send_data(render(:template => 'issues/show.rfpdf', :layout => false), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
109 format.pdf { send_data(render(:template => 'issues/show.rfpdf', :layout => false), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
110 end
110 end
111 end
111 end
112
112
113 # Add a new issue
113 # Add a new issue
114 # The new issue will be created from an existing one if copy_from parameter is given
114 # The new issue will be created from an existing one if copy_from parameter is given
115 def new
115 def new
116 @issue = params[:copy_from] ? Issue.new.copy_from(params[:copy_from]) : Issue.new(params[:issue])
116 @issue = params[:copy_from] ? Issue.new.copy_from(params[:copy_from]) : Issue.new(params[:issue])
117 @issue.project = @project
117 @issue.project = @project
118 @issue.author = User.current
118 @issue.author = User.current
119 @issue.tracker ||= @project.trackers.find(params[:tracker_id] ? params[:tracker_id] : :first)
119 @issue.tracker ||= @project.trackers.find(params[:tracker_id] ? params[:tracker_id] : :first)
120 if @issue.tracker.nil?
120 if @issue.tracker.nil?
121 flash.now[:error] = 'No tracker is associated to this project. Please check the Project settings.'
121 flash.now[:error] = 'No tracker is associated to this project. Please check the Project settings.'
122 render :nothing => true, :layout => true
122 render :nothing => true, :layout => true
123 return
123 return
124 end
124 end
125
125
126 default_status = IssueStatus.default
126 default_status = IssueStatus.default
127 unless default_status
127 unless default_status
128 flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
128 flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
129 render :nothing => true, :layout => true
129 render :nothing => true, :layout => true
130 return
130 return
131 end
131 end
132 @issue.status = default_status
132 @issue.status = default_status
133 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)).uniq
133 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)).uniq
134
134
135 if request.get? || request.xhr?
135 if request.get? || request.xhr?
136 @issue.start_date ||= Date.today
136 @issue.start_date ||= Date.today
137 @custom_values = @issue.custom_values.empty? ?
137 @custom_values = @issue.custom_values.empty? ?
138 @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue) } :
138 @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue) } :
139 @issue.custom_values
139 @issue.custom_values
140 else
140 else
141 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
141 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
142 # Check that the user is allowed to apply the requested status
142 # Check that the user is allowed to apply the requested status
143 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
143 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
144 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x,
144 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x,
145 :customized => @issue,
145 :customized => @issue,
146 :value => (params[:custom_fields] ? params[:custom_fields][x.id.to_s] : nil)) }
146 :value => (params[:custom_fields] ? params[:custom_fields][x.id.to_s] : nil)) }
147 @issue.custom_values = @custom_values
147 @issue.custom_values = @custom_values
148 if @issue.save
148 if @issue.save
149 attach_files(@issue, params[:attachments])
149 attach_files(@issue, params[:attachments])
150 flash[:notice] = l(:notice_successful_create)
150 flash[:notice] = l(:notice_successful_create)
151 Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
151 Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
152 redirect_to :controller => 'issues', :action => 'show', :id => @issue
152 redirect_to :controller => 'issues', :action => 'show', :id => @issue
153 return
153 return
154 end
154 end
155 end
155 end
156 @priorities = Enumeration::get_values('IPRI')
156 @priorities = Enumeration::get_values('IPRI')
157 render :layout => !request.xhr?
157 render :layout => !request.xhr?
158 end
158 end
159
159
160 # Attributes that can be updated on workflow transition (without :edit permission)
160 # Attributes that can be updated on workflow transition (without :edit permission)
161 # TODO: make it configurable (at least per role)
161 # TODO: make it configurable (at least per role)
162 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
162 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
163
163
164 def edit
164 def edit
165 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
165 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
166 @activities = Enumeration::get_values('ACTI')
166 @activities = Enumeration::get_values('ACTI')
167 @priorities = Enumeration::get_values('IPRI')
167 @priorities = Enumeration::get_values('IPRI')
168 @custom_values = []
168 @custom_values = []
169 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
169 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
170
170
171 @notes = params[:notes]
171 @notes = params[:notes]
172 journal = @issue.init_journal(User.current, @notes)
172 journal = @issue.init_journal(User.current, @notes)
173 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
173 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
174 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
174 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
175 attrs = params[:issue].dup
175 attrs = params[:issue].dup
176 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
176 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
177 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
177 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
178 @issue.attributes = attrs
178 @issue.attributes = attrs
179 end
179 end
180
180
181 if request.get?
181 if request.get?
182 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| @issue.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x, :customized => @issue) }
182 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| @issue.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x, :customized => @issue) }
183 else
183 else
184 # Update custom fields if user has :edit permission
184 # Update custom fields if user has :edit permission
185 if @edit_allowed && params[:custom_fields]
185 if @edit_allowed && params[:custom_fields]
186 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) }
186 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) }
187 @issue.custom_values = @custom_values
187 @issue.custom_values = @custom_values
188 end
188 end
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 attachments = attach_files(@issue, params[:attachments])
191 attachments = attach_files(@issue, params[:attachments])
192 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
192 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
193 if @issue.save
193 if @issue.save
194 # Log spend time
194 # Log spend time
195 if current_role.allowed_to?(:log_time)
195 if current_role.allowed_to?(:log_time)
196 @time_entry.save
196 @time_entry.save
197 end
197 end
198 if !journal.new_record?
198 if !journal.new_record?
199 # Only send notification if something was actually changed
199 # Only send notification if something was actually changed
200 flash[:notice] = l(:notice_successful_update)
200 flash[:notice] = l(:notice_successful_update)
201 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
201 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
202 end
202 end
203 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
203 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
204 end
204 end
205 end
205 end
206 rescue ActiveRecord::StaleObjectError
206 rescue ActiveRecord::StaleObjectError
207 # Optimistic locking exception
207 # Optimistic locking exception
208 flash.now[:error] = l(:notice_locking_conflict)
208 flash.now[:error] = l(:notice_locking_conflict)
209 end
209 end
210
210
211 def reply
211 def reply
212 journal = Journal.find(params[:journal_id]) if params[:journal_id]
212 journal = Journal.find(params[:journal_id]) if params[:journal_id]
213 if journal
213 if journal
214 user = journal.user
214 user = journal.user
215 text = journal.notes
215 text = journal.notes
216 else
216 else
217 user = @issue.author
217 user = @issue.author
218 text = @issue.description
218 text = @issue.description
219 end
219 end
220 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
220 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
221 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
221 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
222 render(:update) { |page|
222 render(:update) { |page|
223 page.<< "$('notes').value = \"#{content}\";"
223 page.<< "$('notes').value = \"#{content}\";"
224 page.show 'update'
224 page.show 'update'
225 page << "Form.Element.focus('notes');"
225 page << "Form.Element.focus('notes');"
226 page << "Element.scrollTo('update');"
226 page << "Element.scrollTo('update');"
227 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
227 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
228 }
228 }
229 end
229 end
230
230
231 # Bulk edit a set of issues
231 # Bulk edit a set of issues
232 def bulk_edit
232 def bulk_edit
233 if request.post?
233 if request.post?
234 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
234 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
235 priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
235 priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
236 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
236 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
237 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
237 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
238 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
238 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
239
239
240 unsaved_issue_ids = []
240 unsaved_issue_ids = []
241 @issues.each do |issue|
241 @issues.each do |issue|
242 journal = issue.init_journal(User.current, params[:notes])
242 journal = issue.init_journal(User.current, params[:notes])
243 issue.priority = priority if priority
243 issue.priority = priority if priority
244 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
244 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
245 issue.category = category if category || params[:category_id] == 'none'
245 issue.category = category if category || params[:category_id] == 'none'
246 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
246 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
247 issue.start_date = params[:start_date] unless params[:start_date].blank?
247 issue.start_date = params[:start_date] unless params[:start_date].blank?
248 issue.due_date = params[:due_date] unless params[:due_date].blank?
248 issue.due_date = params[:due_date] unless params[:due_date].blank?
249 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
249 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
250 # Don't save any change to the issue if the user is not authorized to apply the requested status
250 # Don't save any change to the issue if the user is not authorized to apply the requested status
251 if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
251 if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
252 # Send notification for each issue (if changed)
252 # Send notification for each issue (if changed)
253 Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
253 Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
254 else
254 else
255 # Keep unsaved issue ids to display them in flash error
255 # Keep unsaved issue ids to display them in flash error
256 unsaved_issue_ids << issue.id
256 unsaved_issue_ids << issue.id
257 end
257 end
258 end
258 end
259 if unsaved_issue_ids.empty?
259 if unsaved_issue_ids.empty?
260 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
260 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
261 else
261 else
262 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
262 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
263 end
263 end
264 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
264 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
265 return
265 return
266 end
266 end
267 # Find potential statuses the user could be allowed to switch issues to
267 # Find potential statuses the user could be allowed to switch issues to
268 @available_statuses = Workflow.find(:all, :include => :new_status,
268 @available_statuses = Workflow.find(:all, :include => :new_status,
269 :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq
269 :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq
270 end
270 end
271
271
272 def move
272 def move
273 @allowed_projects = []
273 @allowed_projects = []
274 # find projects to which the user is allowed to move the issue
274 # find projects to which the user is allowed to move the issue
275 if User.current.admin?
275 if User.current.admin?
276 # admin is allowed to move issues to any active (visible) project
276 # admin is allowed to move issues to any active (visible) project
277 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name')
277 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name')
278 else
278 else
279 User.current.memberships.each {|m| @allowed_projects << m.project if m.role.allowed_to?(:move_issues)}
279 User.current.memberships.each {|m| @allowed_projects << m.project if m.role.allowed_to?(:move_issues)}
280 end
280 end
281 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
281 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
282 @target_project ||= @project
282 @target_project ||= @project
283 @trackers = @target_project.trackers
283 @trackers = @target_project.trackers
284 if request.post?
284 if request.post?
285 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
285 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
286 unsaved_issue_ids = []
286 unsaved_issue_ids = []
287 @issues.each do |issue|
287 @issues.each do |issue|
288 issue.init_journal(User.current)
288 unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker)
289 unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker)
289 end
290 end
290 if unsaved_issue_ids.empty?
291 if unsaved_issue_ids.empty?
291 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
292 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
292 else
293 else
293 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
294 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
294 end
295 end
295 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
296 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
296 return
297 return
297 end
298 end
298 render :layout => false if request.xhr?
299 render :layout => false if request.xhr?
299 end
300 end
300
301
301 def destroy
302 def destroy
302 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
303 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
303 if @hours > 0
304 if @hours > 0
304 case params[:todo]
305 case params[:todo]
305 when 'destroy'
306 when 'destroy'
306 # nothing to do
307 # nothing to do
307 when 'nullify'
308 when 'nullify'
308 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
309 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
309 when 'reassign'
310 when 'reassign'
310 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
311 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
311 if reassign_to.nil?
312 if reassign_to.nil?
312 flash.now[:error] = l(:error_issue_not_found_in_project)
313 flash.now[:error] = l(:error_issue_not_found_in_project)
313 return
314 return
314 else
315 else
315 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
316 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
316 end
317 end
317 else
318 else
318 # display the destroy form
319 # display the destroy form
319 return
320 return
320 end
321 end
321 end
322 end
322 @issues.each(&:destroy)
323 @issues.each(&:destroy)
323 redirect_to :action => 'index', :project_id => @project
324 redirect_to :action => 'index', :project_id => @project
324 end
325 end
325
326
326 def destroy_attachment
327 def destroy_attachment
327 a = @issue.attachments.find(params[:attachment_id])
328 a = @issue.attachments.find(params[:attachment_id])
328 a.destroy
329 a.destroy
329 journal = @issue.init_journal(User.current)
330 journal = @issue.init_journal(User.current)
330 journal.details << JournalDetail.new(:property => 'attachment',
331 journal.details << JournalDetail.new(:property => 'attachment',
331 :prop_key => a.id,
332 :prop_key => a.id,
332 :old_value => a.filename)
333 :old_value => a.filename)
333 journal.save
334 journal.save
334 redirect_to :action => 'show', :id => @issue
335 redirect_to :action => 'show', :id => @issue
335 end
336 end
336
337
337 def context_menu
338 def context_menu
338 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
339 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
339 if (@issues.size == 1)
340 if (@issues.size == 1)
340 @issue = @issues.first
341 @issue = @issues.first
341 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
342 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
342 @assignables = @issue.assignable_users
343 @assignables = @issue.assignable_users
343 @assignables << @issue.assigned_to if @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
344 @assignables << @issue.assigned_to if @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
344 end
345 end
345 projects = @issues.collect(&:project).compact.uniq
346 projects = @issues.collect(&:project).compact.uniq
346 @project = projects.first if projects.size == 1
347 @project = projects.first if projects.size == 1
347
348
348 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
349 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
349 :update => (@issue && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && !@allowed_statuses.empty?))),
350 :update => (@issue && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && !@allowed_statuses.empty?))),
350 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
351 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
351 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
352 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
352 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
353 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
353 }
354 }
354
355
355 @priorities = Enumeration.get_values('IPRI').reverse
356 @priorities = Enumeration.get_values('IPRI').reverse
356 @statuses = IssueStatus.find(:all, :order => 'position')
357 @statuses = IssueStatus.find(:all, :order => 'position')
357 @back = request.env['HTTP_REFERER']
358 @back = request.env['HTTP_REFERER']
358
359
359 render :layout => false
360 render :layout => false
360 end
361 end
361
362
362 def update_form
363 def update_form
363 @issue = Issue.new(params[:issue])
364 @issue = Issue.new(params[:issue])
364 render :action => :new, :layout => false
365 render :action => :new, :layout => false
365 end
366 end
366
367
367 def preview
368 def preview
368 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
369 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
369 @attachements = @issue.attachments if @issue
370 @attachements = @issue.attachments if @issue
370 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
371 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
371 render :partial => 'common/preview'
372 render :partial => 'common/preview'
372 end
373 end
373
374
374 private
375 private
375 def find_issue
376 def find_issue
376 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
377 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
377 @project = @issue.project
378 @project = @issue.project
378 rescue ActiveRecord::RecordNotFound
379 rescue ActiveRecord::RecordNotFound
379 render_404
380 render_404
380 end
381 end
381
382
382 # Filter for bulk operations
383 # Filter for bulk operations
383 def find_issues
384 def find_issues
384 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
385 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
385 raise ActiveRecord::RecordNotFound if @issues.empty?
386 raise ActiveRecord::RecordNotFound if @issues.empty?
386 projects = @issues.collect(&:project).compact.uniq
387 projects = @issues.collect(&:project).compact.uniq
387 if projects.size == 1
388 if projects.size == 1
388 @project = projects.first
389 @project = projects.first
389 else
390 else
390 # TODO: let users bulk edit/move/destroy issues from different projects
391 # TODO: let users bulk edit/move/destroy issues from different projects
391 render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
392 render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
392 end
393 end
393 rescue ActiveRecord::RecordNotFound
394 rescue ActiveRecord::RecordNotFound
394 render_404
395 render_404
395 end
396 end
396
397
397 def find_project
398 def find_project
398 @project = Project.find(params[:project_id])
399 @project = Project.find(params[:project_id])
399 rescue ActiveRecord::RecordNotFound
400 rescue ActiveRecord::RecordNotFound
400 render_404
401 render_404
401 end
402 end
402
403
403 def find_optional_project
404 def find_optional_project
404 return true unless params[:project_id]
405 return true unless params[:project_id]
405 @project = Project.find(params[:project_id])
406 @project = Project.find(params[:project_id])
406 authorize
407 authorize
407 rescue ActiveRecord::RecordNotFound
408 rescue ActiveRecord::RecordNotFound
408 render_404
409 render_404
409 end
410 end
410
411
411 # Retrieve query from session or build a new query
412 # Retrieve query from session or build a new query
412 def retrieve_query
413 def retrieve_query
413 if !params[:query_id].blank?
414 if !params[:query_id].blank?
414 cond = "project_id IS NULL"
415 cond = "project_id IS NULL"
415 cond << " OR project_id = #{@project.id}" if @project
416 cond << " OR project_id = #{@project.id}" if @project
416 @query = Query.find(params[:query_id], :conditions => cond)
417 @query = Query.find(params[:query_id], :conditions => cond)
417 @query.project = @project
418 @query.project = @project
418 session[:query] = {:id => @query.id, :project_id => @query.project_id}
419 session[:query] = {:id => @query.id, :project_id => @query.project_id}
419 else
420 else
420 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
421 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
421 # Give it a name, required to be valid
422 # Give it a name, required to be valid
422 @query = Query.new(:name => "_")
423 @query = Query.new(:name => "_")
423 @query.project = @project
424 @query.project = @project
424 if params[:fields] and params[:fields].is_a? Array
425 if params[:fields] and params[:fields].is_a? Array
425 params[:fields].each do |field|
426 params[:fields].each do |field|
426 @query.add_filter(field, params[:operators][field], params[:values][field])
427 @query.add_filter(field, params[:operators][field], params[:values][field])
427 end
428 end
428 else
429 else
429 @query.available_filters.keys.each do |field|
430 @query.available_filters.keys.each do |field|
430 @query.add_short_filter(field, params[field]) if params[field]
431 @query.add_short_filter(field, params[field]) if params[field]
431 end
432 end
432 end
433 end
433 session[:query] = {:project_id => @query.project_id, :filters => @query.filters}
434 session[:query] = {:project_id => @query.project_id, :filters => @query.filters}
434 else
435 else
435 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
436 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
436 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters])
437 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters])
437 @query.project = @project
438 @query.project = @project
438 end
439 end
439 end
440 end
440 end
441 end
441 end
442 end
@@ -1,177 +1,183
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'csv'
18 require 'csv'
19
19
20 module IssuesHelper
20 module IssuesHelper
21 include ApplicationHelper
21 include ApplicationHelper
22
22
23 def render_issue_tooltip(issue)
23 def render_issue_tooltip(issue)
24 @cached_label_start_date ||= l(:field_start_date)
24 @cached_label_start_date ||= l(:field_start_date)
25 @cached_label_due_date ||= l(:field_due_date)
25 @cached_label_due_date ||= l(:field_due_date)
26 @cached_label_assigned_to ||= l(:field_assigned_to)
26 @cached_label_assigned_to ||= l(:field_assigned_to)
27 @cached_label_priority ||= l(:field_priority)
27 @cached_label_priority ||= l(:field_priority)
28
28
29 link_to_issue(issue) + ": #{h(issue.subject)}<br /><br />" +
29 link_to_issue(issue) + ": #{h(issue.subject)}<br /><br />" +
30 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />" +
30 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />" +
31 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />" +
31 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />" +
32 "<strong>#{@cached_label_assigned_to}</strong>: #{issue.assigned_to}<br />" +
32 "<strong>#{@cached_label_assigned_to}</strong>: #{issue.assigned_to}<br />" +
33 "<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
33 "<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
34 end
34 end
35
35
36 def sidebar_queries
36 def sidebar_queries
37 unless @sidebar_queries
37 unless @sidebar_queries
38 # User can see public queries and his own queries
38 # User can see public queries and his own queries
39 visible = ARCondition.new(["is_public = ? OR user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
39 visible = ARCondition.new(["is_public = ? OR user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
40 # Project specific queries and global queries
40 # Project specific queries and global queries
41 visible << (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
41 visible << (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
42 @sidebar_queries = Query.find(:all,
42 @sidebar_queries = Query.find(:all,
43 :order => "name ASC",
43 :order => "name ASC",
44 :conditions => visible.conditions)
44 :conditions => visible.conditions)
45 end
45 end
46 @sidebar_queries
46 @sidebar_queries
47 end
47 end
48
48
49 def show_detail(detail, no_html=false)
49 def show_detail(detail, no_html=false)
50 case detail.property
50 case detail.property
51 when 'attr'
51 when 'attr'
52 label = l(("field_" + detail.prop_key.to_s.gsub(/\_id$/, "")).to_sym)
52 label = l(("field_" + detail.prop_key.to_s.gsub(/\_id$/, "")).to_sym)
53 case detail.prop_key
53 case detail.prop_key
54 when 'due_date', 'start_date'
54 when 'due_date', 'start_date'
55 value = format_date(detail.value.to_date) if detail.value
55 value = format_date(detail.value.to_date) if detail.value
56 old_value = format_date(detail.old_value.to_date) if detail.old_value
56 old_value = format_date(detail.old_value.to_date) if detail.old_value
57 when 'project_id'
58 p = Project.find_by_id(detail.value) and value = p.name if detail.value
59 p = Project.find_by_id(detail.old_value) and old_value = p.name if detail.old_value
57 when 'status_id'
60 when 'status_id'
58 s = IssueStatus.find_by_id(detail.value) and value = s.name if detail.value
61 s = IssueStatus.find_by_id(detail.value) and value = s.name if detail.value
59 s = IssueStatus.find_by_id(detail.old_value) and old_value = s.name if detail.old_value
62 s = IssueStatus.find_by_id(detail.old_value) and old_value = s.name if detail.old_value
63 when 'tracker_id'
64 t = Tracker.find_by_id(detail.value) and value = t.name if detail.value
65 t = Tracker.find_by_id(detail.old_value) and old_value = t.name if detail.old_value
60 when 'assigned_to_id'
66 when 'assigned_to_id'
61 u = User.find_by_id(detail.value) and value = u.name if detail.value
67 u = User.find_by_id(detail.value) and value = u.name if detail.value
62 u = User.find_by_id(detail.old_value) and old_value = u.name if detail.old_value
68 u = User.find_by_id(detail.old_value) and old_value = u.name if detail.old_value
63 when 'priority_id'
69 when 'priority_id'
64 e = Enumeration.find_by_id(detail.value) and value = e.name if detail.value
70 e = Enumeration.find_by_id(detail.value) and value = e.name if detail.value
65 e = Enumeration.find_by_id(detail.old_value) and old_value = e.name if detail.old_value
71 e = Enumeration.find_by_id(detail.old_value) and old_value = e.name if detail.old_value
66 when 'category_id'
72 when 'category_id'
67 c = IssueCategory.find_by_id(detail.value) and value = c.name if detail.value
73 c = IssueCategory.find_by_id(detail.value) and value = c.name if detail.value
68 c = IssueCategory.find_by_id(detail.old_value) and old_value = c.name if detail.old_value
74 c = IssueCategory.find_by_id(detail.old_value) and old_value = c.name if detail.old_value
69 when 'fixed_version_id'
75 when 'fixed_version_id'
70 v = Version.find_by_id(detail.value) and value = v.name if detail.value
76 v = Version.find_by_id(detail.value) and value = v.name if detail.value
71 v = Version.find_by_id(detail.old_value) and old_value = v.name if detail.old_value
77 v = Version.find_by_id(detail.old_value) and old_value = v.name if detail.old_value
72 end
78 end
73 when 'cf'
79 when 'cf'
74 custom_field = CustomField.find_by_id(detail.prop_key)
80 custom_field = CustomField.find_by_id(detail.prop_key)
75 if custom_field
81 if custom_field
76 label = custom_field.name
82 label = custom_field.name
77 value = format_value(detail.value, custom_field.field_format) if detail.value
83 value = format_value(detail.value, custom_field.field_format) if detail.value
78 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
84 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
79 end
85 end
80 when 'attachment'
86 when 'attachment'
81 label = l(:label_attachment)
87 label = l(:label_attachment)
82 end
88 end
83
89
84 label ||= detail.prop_key
90 label ||= detail.prop_key
85 value ||= detail.value
91 value ||= detail.value
86 old_value ||= detail.old_value
92 old_value ||= detail.old_value
87
93
88 unless no_html
94 unless no_html
89 label = content_tag('strong', label)
95 label = content_tag('strong', label)
90 old_value = content_tag("i", h(old_value)) if detail.old_value
96 old_value = content_tag("i", h(old_value)) if detail.old_value
91 old_value = content_tag("strike", old_value) if detail.old_value and (!detail.value or detail.value.empty?)
97 old_value = content_tag("strike", old_value) if detail.old_value and (!detail.value or detail.value.empty?)
92 if detail.property == 'attachment' && !value.blank? && Attachment.find_by_id(detail.prop_key)
98 if detail.property == 'attachment' && !value.blank? && Attachment.find_by_id(detail.prop_key)
93 # Link to the attachment if it has not been removed
99 # Link to the attachment if it has not been removed
94 value = link_to(value, :controller => 'attachments', :action => 'show', :id => detail.prop_key)
100 value = link_to(value, :controller => 'attachments', :action => 'show', :id => detail.prop_key)
95 else
101 else
96 value = content_tag("i", h(value)) if value
102 value = content_tag("i", h(value)) if value
97 end
103 end
98 end
104 end
99
105
100 if !detail.value.blank?
106 if !detail.value.blank?
101 case detail.property
107 case detail.property
102 when 'attr', 'cf'
108 when 'attr', 'cf'
103 if !detail.old_value.blank?
109 if !detail.old_value.blank?
104 label + " " + l(:text_journal_changed, old_value, value)
110 label + " " + l(:text_journal_changed, old_value, value)
105 else
111 else
106 label + " " + l(:text_journal_set_to, value)
112 label + " " + l(:text_journal_set_to, value)
107 end
113 end
108 when 'attachment'
114 when 'attachment'
109 "#{label} #{value} #{l(:label_added)}"
115 "#{label} #{value} #{l(:label_added)}"
110 end
116 end
111 else
117 else
112 case detail.property
118 case detail.property
113 when 'attr', 'cf'
119 when 'attr', 'cf'
114 label + " " + l(:text_journal_deleted) + " (#{old_value})"
120 label + " " + l(:text_journal_deleted) + " (#{old_value})"
115 when 'attachment'
121 when 'attachment'
116 "#{label} #{old_value} #{l(:label_deleted)}"
122 "#{label} #{old_value} #{l(:label_deleted)}"
117 end
123 end
118 end
124 end
119 end
125 end
120
126
121 def issues_to_csv(issues, project = nil)
127 def issues_to_csv(issues, project = nil)
122 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
128 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
123 export = StringIO.new
129 export = StringIO.new
124 CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
130 CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
125 # csv header fields
131 # csv header fields
126 headers = [ "#",
132 headers = [ "#",
127 l(:field_status),
133 l(:field_status),
128 l(:field_project),
134 l(:field_project),
129 l(:field_tracker),
135 l(:field_tracker),
130 l(:field_priority),
136 l(:field_priority),
131 l(:field_subject),
137 l(:field_subject),
132 l(:field_assigned_to),
138 l(:field_assigned_to),
133 l(:field_category),
139 l(:field_category),
134 l(:field_fixed_version),
140 l(:field_fixed_version),
135 l(:field_author),
141 l(:field_author),
136 l(:field_start_date),
142 l(:field_start_date),
137 l(:field_due_date),
143 l(:field_due_date),
138 l(:field_done_ratio),
144 l(:field_done_ratio),
139 l(:field_estimated_hours),
145 l(:field_estimated_hours),
140 l(:field_created_on),
146 l(:field_created_on),
141 l(:field_updated_on)
147 l(:field_updated_on)
142 ]
148 ]
143 # Export project custom fields if project is given
149 # Export project custom fields if project is given
144 # otherwise export custom fields marked as "For all projects"
150 # otherwise export custom fields marked as "For all projects"
145 custom_fields = project.nil? ? IssueCustomField.for_all : project.all_custom_fields
151 custom_fields = project.nil? ? IssueCustomField.for_all : project.all_custom_fields
146 custom_fields.each {|f| headers << f.name}
152 custom_fields.each {|f| headers << f.name}
147 # Description in the last column
153 # Description in the last column
148 headers << l(:field_description)
154 headers << l(:field_description)
149 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
155 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
150 # csv lines
156 # csv lines
151 issues.each do |issue|
157 issues.each do |issue|
152 fields = [issue.id,
158 fields = [issue.id,
153 issue.status.name,
159 issue.status.name,
154 issue.project.name,
160 issue.project.name,
155 issue.tracker.name,
161 issue.tracker.name,
156 issue.priority.name,
162 issue.priority.name,
157 issue.subject,
163 issue.subject,
158 issue.assigned_to,
164 issue.assigned_to,
159 issue.category,
165 issue.category,
160 issue.fixed_version,
166 issue.fixed_version,
161 issue.author.name,
167 issue.author.name,
162 format_date(issue.start_date),
168 format_date(issue.start_date),
163 format_date(issue.due_date),
169 format_date(issue.due_date),
164 issue.done_ratio,
170 issue.done_ratio,
165 issue.estimated_hours,
171 issue.estimated_hours,
166 format_time(issue.created_on),
172 format_time(issue.created_on),
167 format_time(issue.updated_on)
173 format_time(issue.updated_on)
168 ]
174 ]
169 custom_fields.each {|f| fields << show_value(issue.custom_value_for(f)) }
175 custom_fields.each {|f| fields << show_value(issue.custom_value_for(f)) }
170 fields << issue.description
176 fields << issue.description
171 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
177 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
172 end
178 end
173 end
179 end
174 export.rewind
180 export.rewind
175 export
181 export
176 end
182 end
177 end
183 end
General Comments 0
You need to be logged in to leave comments. Login now