##// END OF EJS Templates
Preserve field values on bulk edit failure (#13943)....
Jean-Philippe Lang -
r11557:70bdb86c534f
parent child
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,447 +1,450
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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, :create]
19 menu_item :new_issue, :only => [:new, :create]
20 default_search_scope :issues
20 default_search_scope :issues
21
21
22 before_filter :find_issue, :only => [:show, :edit, :update]
22 before_filter :find_issue, :only => [:show, :edit, :update]
23 before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :destroy]
23 before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :destroy]
24 before_filter :find_project, :only => [:new, :create, :update_form]
24 before_filter :find_project, :only => [:new, :create, :update_form]
25 before_filter :authorize, :except => [:index]
25 before_filter :authorize, :except => [:index]
26 before_filter :find_optional_project, :only => [:index]
26 before_filter :find_optional_project, :only => [:index]
27 before_filter :check_for_default_issue_status, :only => [:new, :create]
27 before_filter :check_for_default_issue_status, :only => [:new, :create]
28 before_filter :build_new_issue_from_params, :only => [:new, :create, :update_form]
28 before_filter :build_new_issue_from_params, :only => [:new, :create, :update_form]
29 accept_rss_auth :index, :show
29 accept_rss_auth :index, :show
30 accept_api_auth :index, :show, :create, :update, :destroy
30 accept_api_auth :index, :show, :create, :update, :destroy
31
31
32 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
32 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
33
33
34 helper :journals
34 helper :journals
35 helper :projects
35 helper :projects
36 include ProjectsHelper
36 include ProjectsHelper
37 helper :custom_fields
37 helper :custom_fields
38 include CustomFieldsHelper
38 include CustomFieldsHelper
39 helper :issue_relations
39 helper :issue_relations
40 include IssueRelationsHelper
40 include IssueRelationsHelper
41 helper :watchers
41 helper :watchers
42 include WatchersHelper
42 include WatchersHelper
43 helper :attachments
43 helper :attachments
44 include AttachmentsHelper
44 include AttachmentsHelper
45 helper :queries
45 helper :queries
46 include QueriesHelper
46 include QueriesHelper
47 helper :repositories
47 helper :repositories
48 include RepositoriesHelper
48 include RepositoriesHelper
49 helper :sort
49 helper :sort
50 include SortHelper
50 include SortHelper
51 include IssuesHelper
51 include IssuesHelper
52 helper :timelog
52 helper :timelog
53 include Redmine::Export::PDF
53 include Redmine::Export::PDF
54
54
55 def index
55 def index
56 retrieve_query
56 retrieve_query
57 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
57 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
58 sort_update(@query.sortable_columns)
58 sort_update(@query.sortable_columns)
59 @query.sort_criteria = sort_criteria.to_a
59 @query.sort_criteria = sort_criteria.to_a
60
60
61 if @query.valid?
61 if @query.valid?
62 case params[:format]
62 case params[:format]
63 when 'csv', 'pdf'
63 when 'csv', 'pdf'
64 @limit = Setting.issues_export_limit.to_i
64 @limit = Setting.issues_export_limit.to_i
65 when 'atom'
65 when 'atom'
66 @limit = Setting.feeds_limit.to_i
66 @limit = Setting.feeds_limit.to_i
67 when 'xml', 'json'
67 when 'xml', 'json'
68 @offset, @limit = api_offset_and_limit
68 @offset, @limit = api_offset_and_limit
69 else
69 else
70 @limit = per_page_option
70 @limit = per_page_option
71 end
71 end
72
72
73 @issue_count = @query.issue_count
73 @issue_count = @query.issue_count
74 @issue_pages = Paginator.new @issue_count, @limit, params['page']
74 @issue_pages = Paginator.new @issue_count, @limit, params['page']
75 @offset ||= @issue_pages.offset
75 @offset ||= @issue_pages.offset
76 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
76 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
77 :order => sort_clause,
77 :order => sort_clause,
78 :offset => @offset,
78 :offset => @offset,
79 :limit => @limit)
79 :limit => @limit)
80 @issue_count_by_group = @query.issue_count_by_group
80 @issue_count_by_group = @query.issue_count_by_group
81
81
82 respond_to do |format|
82 respond_to do |format|
83 format.html { render :template => 'issues/index', :layout => !request.xhr? }
83 format.html { render :template => 'issues/index', :layout => !request.xhr? }
84 format.api {
84 format.api {
85 Issue.load_visible_relations(@issues) if include_in_api_response?('relations')
85 Issue.load_visible_relations(@issues) if include_in_api_response?('relations')
86 }
86 }
87 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
87 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
88 format.csv { send_data(query_to_csv(@issues, @query, params), :type => 'text/csv; header=present', :filename => 'issues.csv') }
88 format.csv { send_data(query_to_csv(@issues, @query, params), :type => 'text/csv; header=present', :filename => 'issues.csv') }
89 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'issues.pdf') }
89 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'issues.pdf') }
90 end
90 end
91 else
91 else
92 respond_to do |format|
92 respond_to do |format|
93 format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
93 format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
94 format.any(:atom, :csv, :pdf) { render(:nothing => true) }
94 format.any(:atom, :csv, :pdf) { render(:nothing => true) }
95 format.api { render_validation_errors(@query) }
95 format.api { render_validation_errors(@query) }
96 end
96 end
97 end
97 end
98 rescue ActiveRecord::RecordNotFound
98 rescue ActiveRecord::RecordNotFound
99 render_404
99 render_404
100 end
100 end
101
101
102 def show
102 def show
103 @journals = @issue.journals.includes(:user, :details).reorder("#{Journal.table_name}.id ASC").all
103 @journals = @issue.journals.includes(:user, :details).reorder("#{Journal.table_name}.id ASC").all
104 @journals.each_with_index {|j,i| j.indice = i+1}
104 @journals.each_with_index {|j,i| j.indice = i+1}
105 @journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
105 @journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
106 @journals.reverse! if User.current.wants_comments_in_reverse_order?
106 @journals.reverse! if User.current.wants_comments_in_reverse_order?
107
107
108 @changesets = @issue.changesets.visible.all
108 @changesets = @issue.changesets.visible.all
109 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
109 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
110
110
111 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
111 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
112 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
112 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
113 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
113 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
114 @priorities = IssuePriority.active
114 @priorities = IssuePriority.active
115 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
115 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
116 @relation = IssueRelation.new
116 @relation = IssueRelation.new
117
117
118 respond_to do |format|
118 respond_to do |format|
119 format.html {
119 format.html {
120 retrieve_previous_and_next_issue_ids
120 retrieve_previous_and_next_issue_ids
121 render :template => 'issues/show'
121 render :template => 'issues/show'
122 }
122 }
123 format.api
123 format.api
124 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
124 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
125 format.pdf {
125 format.pdf {
126 pdf = issue_to_pdf(@issue, :journals => @journals)
126 pdf = issue_to_pdf(@issue, :journals => @journals)
127 send_data(pdf, :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf")
127 send_data(pdf, :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf")
128 }
128 }
129 end
129 end
130 end
130 end
131
131
132 # Add a new issue
132 # Add a new issue
133 # The new issue will be created from an existing one if copy_from parameter is given
133 # The new issue will be created from an existing one if copy_from parameter is given
134 def new
134 def new
135 respond_to do |format|
135 respond_to do |format|
136 format.html { render :action => 'new', :layout => !request.xhr? }
136 format.html { render :action => 'new', :layout => !request.xhr? }
137 end
137 end
138 end
138 end
139
139
140 def create
140 def create
141 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
141 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
142 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
142 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
143 if @issue.save
143 if @issue.save
144 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
144 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
145 respond_to do |format|
145 respond_to do |format|
146 format.html {
146 format.html {
147 render_attachment_warning_if_needed(@issue)
147 render_attachment_warning_if_needed(@issue)
148 flash[:notice] = l(:notice_issue_successful_create, :id => view_context.link_to("##{@issue.id}", issue_path(@issue), :title => @issue.subject))
148 flash[:notice] = l(:notice_issue_successful_create, :id => view_context.link_to("##{@issue.id}", issue_path(@issue), :title => @issue.subject))
149 if params[:continue]
149 if params[:continue]
150 attrs = {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?}
150 attrs = {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?}
151 redirect_to new_project_issue_path(@issue.project, :issue => attrs)
151 redirect_to new_project_issue_path(@issue.project, :issue => attrs)
152 else
152 else
153 redirect_to issue_path(@issue)
153 redirect_to issue_path(@issue)
154 end
154 end
155 }
155 }
156 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
156 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
157 end
157 end
158 return
158 return
159 else
159 else
160 respond_to do |format|
160 respond_to do |format|
161 format.html { render :action => 'new' }
161 format.html { render :action => 'new' }
162 format.api { render_validation_errors(@issue) }
162 format.api { render_validation_errors(@issue) }
163 end
163 end
164 end
164 end
165 end
165 end
166
166
167 def edit
167 def edit
168 return unless update_issue_from_params
168 return unless update_issue_from_params
169
169
170 respond_to do |format|
170 respond_to do |format|
171 format.html { }
171 format.html { }
172 format.xml { }
172 format.xml { }
173 end
173 end
174 end
174 end
175
175
176 def update
176 def update
177 return unless update_issue_from_params
177 return unless update_issue_from_params
178 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
178 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
179 saved = false
179 saved = false
180 begin
180 begin
181 saved = @issue.save_issue_with_child_records(params, @time_entry)
181 saved = @issue.save_issue_with_child_records(params, @time_entry)
182 rescue ActiveRecord::StaleObjectError
182 rescue ActiveRecord::StaleObjectError
183 @conflict = true
183 @conflict = true
184 if params[:last_journal_id]
184 if params[:last_journal_id]
185 @conflict_journals = @issue.journals_after(params[:last_journal_id]).all
185 @conflict_journals = @issue.journals_after(params[:last_journal_id]).all
186 @conflict_journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
186 @conflict_journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
187 end
187 end
188 end
188 end
189
189
190 if saved
190 if saved
191 render_attachment_warning_if_needed(@issue)
191 render_attachment_warning_if_needed(@issue)
192 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
192 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
193
193
194 respond_to do |format|
194 respond_to do |format|
195 format.html { redirect_back_or_default issue_path(@issue) }
195 format.html { redirect_back_or_default issue_path(@issue) }
196 format.api { render_api_ok }
196 format.api { render_api_ok }
197 end
197 end
198 else
198 else
199 respond_to do |format|
199 respond_to do |format|
200 format.html { render :action => 'edit' }
200 format.html { render :action => 'edit' }
201 format.api { render_validation_errors(@issue) }
201 format.api { render_validation_errors(@issue) }
202 end
202 end
203 end
203 end
204 end
204 end
205
205
206 # Updates the issue form when changing the project, status or tracker
206 # Updates the issue form when changing the project, status or tracker
207 # on issue creation/update
207 # on issue creation/update
208 def update_form
208 def update_form
209 end
209 end
210
210
211 # Bulk edit/copy a set of issues
211 # Bulk edit/copy a set of issues
212 def bulk_edit
212 def bulk_edit
213 @issues.sort!
213 @issues.sort!
214 @copy = params[:copy].present?
214 @copy = params[:copy].present?
215 @notes = params[:notes]
215 @notes = params[:notes]
216
216
217 if User.current.allowed_to?(:move_issues, @projects)
217 if User.current.allowed_to?(:move_issues, @projects)
218 @allowed_projects = Issue.allowed_target_projects_on_move
218 @allowed_projects = Issue.allowed_target_projects_on_move
219 if params[:issue]
219 if params[:issue]
220 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id].to_s}
220 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id].to_s}
221 if @target_project
221 if @target_project
222 target_projects = [@target_project]
222 target_projects = [@target_project]
223 end
223 end
224 end
224 end
225 end
225 end
226 target_projects ||= @projects
226 target_projects ||= @projects
227
227
228 if @copy
228 if @copy
229 @available_statuses = [IssueStatus.default]
229 @available_statuses = [IssueStatus.default]
230 else
230 else
231 @available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
231 @available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
232 end
232 end
233 @custom_fields = target_projects.map{|p|p.all_issue_custom_fields}.reduce(:&)
233 @custom_fields = target_projects.map{|p|p.all_issue_custom_fields}.reduce(:&)
234 @assignables = target_projects.map(&:assignable_users).reduce(:&)
234 @assignables = target_projects.map(&:assignable_users).reduce(:&)
235 @trackers = target_projects.map(&:trackers).reduce(:&)
235 @trackers = target_projects.map(&:trackers).reduce(:&)
236 @versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
236 @versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
237 @categories = target_projects.map {|p| p.issue_categories}.reduce(:&)
237 @categories = target_projects.map {|p| p.issue_categories}.reduce(:&)
238 if @copy
238 if @copy
239 @attachments_present = @issues.detect {|i| i.attachments.any?}.present?
239 @attachments_present = @issues.detect {|i| i.attachments.any?}.present?
240 @subtasks_present = @issues.detect {|i| !i.leaf?}.present?
240 @subtasks_present = @issues.detect {|i| !i.leaf?}.present?
241 end
241 end
242
242
243 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
243 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
244
245 @issue_params = params[:issue] || {}
246 @issue_params[:custom_field_values] ||= {}
244 end
247 end
245
248
246 def bulk_update
249 def bulk_update
247 @issues.sort!
250 @issues.sort!
248 @copy = params[:copy].present?
251 @copy = params[:copy].present?
249 attributes = parse_params_for_bulk_issue_attributes(params)
252 attributes = parse_params_for_bulk_issue_attributes(params)
250
253
251 unsaved_issues = []
254 unsaved_issues = []
252 saved_issues = []
255 saved_issues = []
253
256
254 if @copy && params[:copy_subtasks].present?
257 if @copy && params[:copy_subtasks].present?
255 # Descendant issues will be copied with the parent task
258 # Descendant issues will be copied with the parent task
256 # Don't copy them twice
259 # Don't copy them twice
257 @issues.reject! {|issue| @issues.detect {|other| issue.is_descendant_of?(other)}}
260 @issues.reject! {|issue| @issues.detect {|other| issue.is_descendant_of?(other)}}
258 end
261 end
259
262
260 @issues.each do |issue|
263 @issues.each do |issue|
261 issue.reload
264 issue.reload
262 if @copy
265 if @copy
263 issue = issue.copy({},
266 issue = issue.copy({},
264 :attachments => params[:copy_attachments].present?,
267 :attachments => params[:copy_attachments].present?,
265 :subtasks => params[:copy_subtasks].present?
268 :subtasks => params[:copy_subtasks].present?
266 )
269 )
267 end
270 end
268 journal = issue.init_journal(User.current, params[:notes])
271 journal = issue.init_journal(User.current, params[:notes])
269 issue.safe_attributes = attributes
272 issue.safe_attributes = attributes
270 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
273 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
271 if issue.save
274 if issue.save
272 saved_issues << issue
275 saved_issues << issue
273 else
276 else
274 unsaved_issues << issue
277 unsaved_issues << issue
275 end
278 end
276 end
279 end
277
280
278 if unsaved_issues.empty?
281 if unsaved_issues.empty?
279 flash[:notice] = l(:notice_successful_update) unless saved_issues.empty?
282 flash[:notice] = l(:notice_successful_update) unless saved_issues.empty?
280 if params[:follow]
283 if params[:follow]
281 if @issues.size == 1 && saved_issues.size == 1
284 if @issues.size == 1 && saved_issues.size == 1
282 redirect_to issue_path(saved_issues.first)
285 redirect_to issue_path(saved_issues.first)
283 elsif saved_issues.map(&:project).uniq.size == 1
286 elsif saved_issues.map(&:project).uniq.size == 1
284 redirect_to project_issues_path(saved_issues.map(&:project).first)
287 redirect_to project_issues_path(saved_issues.map(&:project).first)
285 end
288 end
286 else
289 else
287 redirect_back_or_default _project_issues_path(@project)
290 redirect_back_or_default _project_issues_path(@project)
288 end
291 end
289 else
292 else
290 @saved_issues = @issues
293 @saved_issues = @issues
291 @unsaved_issues = unsaved_issues
294 @unsaved_issues = unsaved_issues
292 @issues = Issue.visible.find_all_by_id(@unsaved_issues.map(&:id))
295 @issues = Issue.visible.find_all_by_id(@unsaved_issues.map(&:id))
293 bulk_edit
296 bulk_edit
294 render :action => 'bulk_edit'
297 render :action => 'bulk_edit'
295 end
298 end
296 end
299 end
297
300
298 def destroy
301 def destroy
299 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
302 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
300 if @hours > 0
303 if @hours > 0
301 case params[:todo]
304 case params[:todo]
302 when 'destroy'
305 when 'destroy'
303 # nothing to do
306 # nothing to do
304 when 'nullify'
307 when 'nullify'
305 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
308 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
306 when 'reassign'
309 when 'reassign'
307 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
310 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
308 if reassign_to.nil?
311 if reassign_to.nil?
309 flash.now[:error] = l(:error_issue_not_found_in_project)
312 flash.now[:error] = l(:error_issue_not_found_in_project)
310 return
313 return
311 else
314 else
312 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
315 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
313 end
316 end
314 else
317 else
315 # display the destroy form if it's a user request
318 # display the destroy form if it's a user request
316 return unless api_request?
319 return unless api_request?
317 end
320 end
318 end
321 end
319 @issues.each do |issue|
322 @issues.each do |issue|
320 begin
323 begin
321 issue.reload.destroy
324 issue.reload.destroy
322 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
325 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
323 # nothing to do, issue was already deleted (eg. by a parent)
326 # nothing to do, issue was already deleted (eg. by a parent)
324 end
327 end
325 end
328 end
326 respond_to do |format|
329 respond_to do |format|
327 format.html { redirect_back_or_default _project_issues_path(@project) }
330 format.html { redirect_back_or_default _project_issues_path(@project) }
328 format.api { render_api_ok }
331 format.api { render_api_ok }
329 end
332 end
330 end
333 end
331
334
332 private
335 private
333
336
334 def find_project
337 def find_project
335 project_id = params[:project_id] || (params[:issue] && params[:issue][:project_id])
338 project_id = params[:project_id] || (params[:issue] && params[:issue][:project_id])
336 @project = Project.find(project_id)
339 @project = Project.find(project_id)
337 rescue ActiveRecord::RecordNotFound
340 rescue ActiveRecord::RecordNotFound
338 render_404
341 render_404
339 end
342 end
340
343
341 def retrieve_previous_and_next_issue_ids
344 def retrieve_previous_and_next_issue_ids
342 retrieve_query_from_session
345 retrieve_query_from_session
343 if @query
346 if @query
344 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
347 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
345 sort_update(@query.sortable_columns, 'issues_index_sort')
348 sort_update(@query.sortable_columns, 'issues_index_sort')
346 limit = 500
349 limit = 500
347 issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
350 issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
348 if (idx = issue_ids.index(@issue.id)) && idx < limit
351 if (idx = issue_ids.index(@issue.id)) && idx < limit
349 if issue_ids.size < 500
352 if issue_ids.size < 500
350 @issue_position = idx + 1
353 @issue_position = idx + 1
351 @issue_count = issue_ids.size
354 @issue_count = issue_ids.size
352 end
355 end
353 @prev_issue_id = issue_ids[idx - 1] if idx > 0
356 @prev_issue_id = issue_ids[idx - 1] if idx > 0
354 @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
357 @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
355 end
358 end
356 end
359 end
357 end
360 end
358
361
359 # Used by #edit and #update to set some common instance variables
362 # Used by #edit and #update to set some common instance variables
360 # from the params
363 # from the params
361 # TODO: Refactor, not everything in here is needed by #edit
364 # TODO: Refactor, not everything in here is needed by #edit
362 def update_issue_from_params
365 def update_issue_from_params
363 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
366 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
364 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
367 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
365 @time_entry.attributes = params[:time_entry]
368 @time_entry.attributes = params[:time_entry]
366
369
367 @issue.init_journal(User.current)
370 @issue.init_journal(User.current)
368
371
369 issue_attributes = params[:issue]
372 issue_attributes = params[:issue]
370 if issue_attributes && params[:conflict_resolution]
373 if issue_attributes && params[:conflict_resolution]
371 case params[:conflict_resolution]
374 case params[:conflict_resolution]
372 when 'overwrite'
375 when 'overwrite'
373 issue_attributes = issue_attributes.dup
376 issue_attributes = issue_attributes.dup
374 issue_attributes.delete(:lock_version)
377 issue_attributes.delete(:lock_version)
375 when 'add_notes'
378 when 'add_notes'
376 issue_attributes = issue_attributes.slice(:notes)
379 issue_attributes = issue_attributes.slice(:notes)
377 when 'cancel'
380 when 'cancel'
378 redirect_to issue_path(@issue)
381 redirect_to issue_path(@issue)
379 return false
382 return false
380 end
383 end
381 end
384 end
382 @issue.safe_attributes = issue_attributes
385 @issue.safe_attributes = issue_attributes
383 @priorities = IssuePriority.active
386 @priorities = IssuePriority.active
384 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
387 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
385 true
388 true
386 end
389 end
387
390
388 # TODO: Refactor, lots of extra code in here
391 # TODO: Refactor, lots of extra code in here
389 # TODO: Changing tracker on an existing issue should not trigger this
392 # TODO: Changing tracker on an existing issue should not trigger this
390 def build_new_issue_from_params
393 def build_new_issue_from_params
391 if params[:id].blank?
394 if params[:id].blank?
392 @issue = Issue.new
395 @issue = Issue.new
393 if params[:copy_from]
396 if params[:copy_from]
394 begin
397 begin
395 @copy_from = Issue.visible.find(params[:copy_from])
398 @copy_from = Issue.visible.find(params[:copy_from])
396 @copy_attachments = params[:copy_attachments].present? || request.get?
399 @copy_attachments = params[:copy_attachments].present? || request.get?
397 @copy_subtasks = params[:copy_subtasks].present? || request.get?
400 @copy_subtasks = params[:copy_subtasks].present? || request.get?
398 @issue.copy_from(@copy_from, :attachments => @copy_attachments, :subtasks => @copy_subtasks)
401 @issue.copy_from(@copy_from, :attachments => @copy_attachments, :subtasks => @copy_subtasks)
399 rescue ActiveRecord::RecordNotFound
402 rescue ActiveRecord::RecordNotFound
400 render_404
403 render_404
401 return
404 return
402 end
405 end
403 end
406 end
404 @issue.project = @project
407 @issue.project = @project
405 else
408 else
406 @issue = @project.issues.visible.find(params[:id])
409 @issue = @project.issues.visible.find(params[:id])
407 end
410 end
408
411
409 @issue.project = @project
412 @issue.project = @project
410 @issue.author ||= User.current
413 @issue.author ||= User.current
411 # Tracker must be set before custom field values
414 # Tracker must be set before custom field values
412 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
415 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
413 if @issue.tracker.nil?
416 if @issue.tracker.nil?
414 render_error l(:error_no_tracker_in_project)
417 render_error l(:error_no_tracker_in_project)
415 return false
418 return false
416 end
419 end
417 @issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
420 @issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
418 @issue.safe_attributes = params[:issue]
421 @issue.safe_attributes = params[:issue]
419
422
420 @priorities = IssuePriority.active
423 @priorities = IssuePriority.active
421 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
424 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
422 @available_watchers = (@issue.project.users.sort + @issue.watcher_users).uniq
425 @available_watchers = (@issue.project.users.sort + @issue.watcher_users).uniq
423 end
426 end
424
427
425 def check_for_default_issue_status
428 def check_for_default_issue_status
426 if IssueStatus.default.nil?
429 if IssueStatus.default.nil?
427 render_error l(:error_no_default_issue_status)
430 render_error l(:error_no_default_issue_status)
428 return false
431 return false
429 end
432 end
430 end
433 end
431
434
432 def parse_params_for_bulk_issue_attributes(params)
435 def parse_params_for_bulk_issue_attributes(params)
433 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
436 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
434 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
437 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
435 if custom = attributes[:custom_field_values]
438 if custom = attributes[:custom_field_values]
436 custom.reject! {|k,v| v.blank?}
439 custom.reject! {|k,v| v.blank?}
437 custom.keys.each do |k|
440 custom.keys.each do |k|
438 if custom[k].is_a?(Array)
441 if custom[k].is_a?(Array)
439 custom[k] << '' if custom[k].delete('__none__')
442 custom[k] << '' if custom[k].delete('__none__')
440 else
443 else
441 custom[k] = '' if custom[k] == '__none__'
444 custom[k] = '' if custom[k] == '__none__'
442 end
445 end
443 end
446 end
444 end
447 end
445 attributes
448 attributes
446 end
449 end
447 end
450 end
@@ -1,1244 +1,1248
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require 'forwardable'
20 require 'forwardable'
21 require 'cgi'
21 require 'cgi'
22
22
23 module ApplicationHelper
23 module ApplicationHelper
24 include Redmine::WikiFormatting::Macros::Definitions
24 include Redmine::WikiFormatting::Macros::Definitions
25 include Redmine::I18n
25 include Redmine::I18n
26 include GravatarHelper::PublicMethods
26 include GravatarHelper::PublicMethods
27 include Redmine::Pagination::Helper
27 include Redmine::Pagination::Helper
28
28
29 extend Forwardable
29 extend Forwardable
30 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
30 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
31
31
32 # Return true if user is authorized for controller/action, otherwise false
32 # Return true if user is authorized for controller/action, otherwise false
33 def authorize_for(controller, action)
33 def authorize_for(controller, action)
34 User.current.allowed_to?({:controller => controller, :action => action}, @project)
34 User.current.allowed_to?({:controller => controller, :action => action}, @project)
35 end
35 end
36
36
37 # Display a link if user is authorized
37 # Display a link if user is authorized
38 #
38 #
39 # @param [String] name Anchor text (passed to link_to)
39 # @param [String] name Anchor text (passed to link_to)
40 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
40 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
41 # @param [optional, Hash] html_options Options passed to link_to
41 # @param [optional, Hash] html_options Options passed to link_to
42 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
42 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
43 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
43 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
44 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
44 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
45 end
45 end
46
46
47 # Displays a link to user's account page if active
47 # Displays a link to user's account page if active
48 def link_to_user(user, options={})
48 def link_to_user(user, options={})
49 if user.is_a?(User)
49 if user.is_a?(User)
50 name = h(user.name(options[:format]))
50 name = h(user.name(options[:format]))
51 if user.active? || (User.current.admin? && user.logged?)
51 if user.active? || (User.current.admin? && user.logged?)
52 link_to name, user_path(user), :class => user.css_classes
52 link_to name, user_path(user), :class => user.css_classes
53 else
53 else
54 name
54 name
55 end
55 end
56 else
56 else
57 h(user.to_s)
57 h(user.to_s)
58 end
58 end
59 end
59 end
60
60
61 # Displays a link to +issue+ with its subject.
61 # Displays a link to +issue+ with its subject.
62 # Examples:
62 # Examples:
63 #
63 #
64 # link_to_issue(issue) # => Defect #6: This is the subject
64 # link_to_issue(issue) # => Defect #6: This is the subject
65 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
65 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
66 # link_to_issue(issue, :subject => false) # => Defect #6
66 # link_to_issue(issue, :subject => false) # => Defect #6
67 # link_to_issue(issue, :project => true) # => Foo - Defect #6
67 # link_to_issue(issue, :project => true) # => Foo - Defect #6
68 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
68 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
69 #
69 #
70 def link_to_issue(issue, options={})
70 def link_to_issue(issue, options={})
71 title = nil
71 title = nil
72 subject = nil
72 subject = nil
73 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
73 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
74 if options[:subject] == false
74 if options[:subject] == false
75 title = truncate(issue.subject, :length => 60)
75 title = truncate(issue.subject, :length => 60)
76 else
76 else
77 subject = issue.subject
77 subject = issue.subject
78 if options[:truncate]
78 if options[:truncate]
79 subject = truncate(subject, :length => options[:truncate])
79 subject = truncate(subject, :length => options[:truncate])
80 end
80 end
81 end
81 end
82 s = link_to text, issue_path(issue), :class => issue.css_classes, :title => title
82 s = link_to text, issue_path(issue), :class => issue.css_classes, :title => title
83 s << h(": #{subject}") if subject
83 s << h(": #{subject}") if subject
84 s = h("#{issue.project} - ") + s if options[:project]
84 s = h("#{issue.project} - ") + s if options[:project]
85 s
85 s
86 end
86 end
87
87
88 # Generates a link to an attachment.
88 # Generates a link to an attachment.
89 # Options:
89 # Options:
90 # * :text - Link text (default to attachment filename)
90 # * :text - Link text (default to attachment filename)
91 # * :download - Force download (default: false)
91 # * :download - Force download (default: false)
92 def link_to_attachment(attachment, options={})
92 def link_to_attachment(attachment, options={})
93 text = options.delete(:text) || attachment.filename
93 text = options.delete(:text) || attachment.filename
94 route_method = options.delete(:download) ? :download_named_attachment_path : :named_attachment_path
94 route_method = options.delete(:download) ? :download_named_attachment_path : :named_attachment_path
95 html_options = options.slice!(:only_path)
95 html_options = options.slice!(:only_path)
96 url = send(route_method, attachment, attachment.filename, options)
96 url = send(route_method, attachment, attachment.filename, options)
97 link_to text, url, html_options
97 link_to text, url, html_options
98 end
98 end
99
99
100 # Generates a link to a SCM revision
100 # Generates a link to a SCM revision
101 # Options:
101 # Options:
102 # * :text - Link text (default to the formatted revision)
102 # * :text - Link text (default to the formatted revision)
103 def link_to_revision(revision, repository, options={})
103 def link_to_revision(revision, repository, options={})
104 if repository.is_a?(Project)
104 if repository.is_a?(Project)
105 repository = repository.repository
105 repository = repository.repository
106 end
106 end
107 text = options.delete(:text) || format_revision(revision)
107 text = options.delete(:text) || format_revision(revision)
108 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
108 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
109 link_to(
109 link_to(
110 h(text),
110 h(text),
111 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
111 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
112 :title => l(:label_revision_id, format_revision(revision))
112 :title => l(:label_revision_id, format_revision(revision))
113 )
113 )
114 end
114 end
115
115
116 # Generates a link to a message
116 # Generates a link to a message
117 def link_to_message(message, options={}, html_options = nil)
117 def link_to_message(message, options={}, html_options = nil)
118 link_to(
118 link_to(
119 truncate(message.subject, :length => 60),
119 truncate(message.subject, :length => 60),
120 board_message_path(message.board_id, message.parent_id || message.id, {
120 board_message_path(message.board_id, message.parent_id || message.id, {
121 :r => (message.parent_id && message.id),
121 :r => (message.parent_id && message.id),
122 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
122 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
123 }.merge(options)),
123 }.merge(options)),
124 html_options
124 html_options
125 )
125 )
126 end
126 end
127
127
128 # Generates a link to a project if active
128 # Generates a link to a project if active
129 # Examples:
129 # Examples:
130 #
130 #
131 # link_to_project(project) # => link to the specified project overview
131 # link_to_project(project) # => link to the specified project overview
132 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
132 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
133 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
133 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
134 #
134 #
135 def link_to_project(project, options={}, html_options = nil)
135 def link_to_project(project, options={}, html_options = nil)
136 if project.archived?
136 if project.archived?
137 h(project.name)
137 h(project.name)
138 elsif options.key?(:action)
138 elsif options.key?(:action)
139 ActiveSupport::Deprecation.warn "#link_to_project with :action option is deprecated and will be removed in Redmine 3.0."
139 ActiveSupport::Deprecation.warn "#link_to_project with :action option is deprecated and will be removed in Redmine 3.0."
140 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
140 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
141 link_to project.name, url, html_options
141 link_to project.name, url, html_options
142 else
142 else
143 link_to project.name, project_path(project, options), html_options
143 link_to project.name, project_path(project, options), html_options
144 end
144 end
145 end
145 end
146
146
147 # Generates a link to a project settings if active
147 # Generates a link to a project settings if active
148 def link_to_project_settings(project, options={}, html_options=nil)
148 def link_to_project_settings(project, options={}, html_options=nil)
149 if project.active?
149 if project.active?
150 link_to project.name, settings_project_path(project, options), html_options
150 link_to project.name, settings_project_path(project, options), html_options
151 elsif project.archived?
151 elsif project.archived?
152 h(project.name)
152 h(project.name)
153 else
153 else
154 link_to project.name, project_path(project, options), html_options
154 link_to project.name, project_path(project, options), html_options
155 end
155 end
156 end
156 end
157
157
158 def wiki_page_path(page, options={})
158 def wiki_page_path(page, options={})
159 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
159 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
160 end
160 end
161
161
162 def thumbnail_tag(attachment)
162 def thumbnail_tag(attachment)
163 link_to image_tag(thumbnail_path(attachment)),
163 link_to image_tag(thumbnail_path(attachment)),
164 named_attachment_path(attachment, attachment.filename),
164 named_attachment_path(attachment, attachment.filename),
165 :title => attachment.filename
165 :title => attachment.filename
166 end
166 end
167
167
168 def toggle_link(name, id, options={})
168 def toggle_link(name, id, options={})
169 onclick = "$('##{id}').toggle(); "
169 onclick = "$('##{id}').toggle(); "
170 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
170 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
171 onclick << "return false;"
171 onclick << "return false;"
172 link_to(name, "#", :onclick => onclick)
172 link_to(name, "#", :onclick => onclick)
173 end
173 end
174
174
175 def image_to_function(name, function, html_options = {})
175 def image_to_function(name, function, html_options = {})
176 html_options.symbolize_keys!
176 html_options.symbolize_keys!
177 tag(:input, html_options.merge({
177 tag(:input, html_options.merge({
178 :type => "image", :src => image_path(name),
178 :type => "image", :src => image_path(name),
179 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
179 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
180 }))
180 }))
181 end
181 end
182
182
183 def format_activity_title(text)
183 def format_activity_title(text)
184 h(truncate_single_line(text, :length => 100))
184 h(truncate_single_line(text, :length => 100))
185 end
185 end
186
186
187 def format_activity_day(date)
187 def format_activity_day(date)
188 date == User.current.today ? l(:label_today).titleize : format_date(date)
188 date == User.current.today ? l(:label_today).titleize : format_date(date)
189 end
189 end
190
190
191 def format_activity_description(text)
191 def format_activity_description(text)
192 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
192 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
193 ).gsub(/[\r\n]+/, "<br />").html_safe
193 ).gsub(/[\r\n]+/, "<br />").html_safe
194 end
194 end
195
195
196 def format_version_name(version)
196 def format_version_name(version)
197 if version.project == @project
197 if version.project == @project
198 h(version)
198 h(version)
199 else
199 else
200 h("#{version.project} - #{version}")
200 h("#{version.project} - #{version}")
201 end
201 end
202 end
202 end
203
203
204 def due_date_distance_in_words(date)
204 def due_date_distance_in_words(date)
205 if date
205 if date
206 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
206 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
207 end
207 end
208 end
208 end
209
209
210 # Renders a tree of projects as a nested set of unordered lists
210 # Renders a tree of projects as a nested set of unordered lists
211 # The given collection may be a subset of the whole project tree
211 # The given collection may be a subset of the whole project tree
212 # (eg. some intermediate nodes are private and can not be seen)
212 # (eg. some intermediate nodes are private and can not be seen)
213 def render_project_nested_lists(projects)
213 def render_project_nested_lists(projects)
214 s = ''
214 s = ''
215 if projects.any?
215 if projects.any?
216 ancestors = []
216 ancestors = []
217 original_project = @project
217 original_project = @project
218 projects.sort_by(&:lft).each do |project|
218 projects.sort_by(&:lft).each do |project|
219 # set the project environment to please macros.
219 # set the project environment to please macros.
220 @project = project
220 @project = project
221 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
221 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
222 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
222 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
223 else
223 else
224 ancestors.pop
224 ancestors.pop
225 s << "</li>"
225 s << "</li>"
226 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
226 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
227 ancestors.pop
227 ancestors.pop
228 s << "</ul></li>\n"
228 s << "</ul></li>\n"
229 end
229 end
230 end
230 end
231 classes = (ancestors.empty? ? 'root' : 'child')
231 classes = (ancestors.empty? ? 'root' : 'child')
232 s << "<li class='#{classes}'><div class='#{classes}'>"
232 s << "<li class='#{classes}'><div class='#{classes}'>"
233 s << h(block_given? ? yield(project) : project.name)
233 s << h(block_given? ? yield(project) : project.name)
234 s << "</div>\n"
234 s << "</div>\n"
235 ancestors << project
235 ancestors << project
236 end
236 end
237 s << ("</li></ul>\n" * ancestors.size)
237 s << ("</li></ul>\n" * ancestors.size)
238 @project = original_project
238 @project = original_project
239 end
239 end
240 s.html_safe
240 s.html_safe
241 end
241 end
242
242
243 def render_page_hierarchy(pages, node=nil, options={})
243 def render_page_hierarchy(pages, node=nil, options={})
244 content = ''
244 content = ''
245 if pages[node]
245 if pages[node]
246 content << "<ul class=\"pages-hierarchy\">\n"
246 content << "<ul class=\"pages-hierarchy\">\n"
247 pages[node].each do |page|
247 pages[node].each do |page|
248 content << "<li>"
248 content << "<li>"
249 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
249 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
250 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
250 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
251 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
251 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
252 content << "</li>\n"
252 content << "</li>\n"
253 end
253 end
254 content << "</ul>\n"
254 content << "</ul>\n"
255 end
255 end
256 content.html_safe
256 content.html_safe
257 end
257 end
258
258
259 # Renders flash messages
259 # Renders flash messages
260 def render_flash_messages
260 def render_flash_messages
261 s = ''
261 s = ''
262 flash.each do |k,v|
262 flash.each do |k,v|
263 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
263 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
264 end
264 end
265 s.html_safe
265 s.html_safe
266 end
266 end
267
267
268 # Renders tabs and their content
268 # Renders tabs and their content
269 def render_tabs(tabs)
269 def render_tabs(tabs)
270 if tabs.any?
270 if tabs.any?
271 render :partial => 'common/tabs', :locals => {:tabs => tabs}
271 render :partial => 'common/tabs', :locals => {:tabs => tabs}
272 else
272 else
273 content_tag 'p', l(:label_no_data), :class => "nodata"
273 content_tag 'p', l(:label_no_data), :class => "nodata"
274 end
274 end
275 end
275 end
276
276
277 # Renders the project quick-jump box
277 # Renders the project quick-jump box
278 def render_project_jump_box
278 def render_project_jump_box
279 return unless User.current.logged?
279 return unless User.current.logged?
280 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
280 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
281 if projects.any?
281 if projects.any?
282 options =
282 options =
283 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
283 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
284 '<option value="" disabled="disabled">---</option>').html_safe
284 '<option value="" disabled="disabled">---</option>').html_safe
285
285
286 options << project_tree_options_for_select(projects, :selected => @project) do |p|
286 options << project_tree_options_for_select(projects, :selected => @project) do |p|
287 { :value => project_path(:id => p, :jump => current_menu_item) }
287 { :value => project_path(:id => p, :jump => current_menu_item) }
288 end
288 end
289
289
290 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
290 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
291 end
291 end
292 end
292 end
293
293
294 def project_tree_options_for_select(projects, options = {})
294 def project_tree_options_for_select(projects, options = {})
295 s = ''
295 s = ''
296 project_tree(projects) do |project, level|
296 project_tree(projects) do |project, level|
297 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
297 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
298 tag_options = {:value => project.id}
298 tag_options = {:value => project.id}
299 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
299 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
300 tag_options[:selected] = 'selected'
300 tag_options[:selected] = 'selected'
301 else
301 else
302 tag_options[:selected] = nil
302 tag_options[:selected] = nil
303 end
303 end
304 tag_options.merge!(yield(project)) if block_given?
304 tag_options.merge!(yield(project)) if block_given?
305 s << content_tag('option', name_prefix + h(project), tag_options)
305 s << content_tag('option', name_prefix + h(project), tag_options)
306 end
306 end
307 s.html_safe
307 s.html_safe
308 end
308 end
309
309
310 # Yields the given block for each project with its level in the tree
310 # Yields the given block for each project with its level in the tree
311 #
311 #
312 # Wrapper for Project#project_tree
312 # Wrapper for Project#project_tree
313 def project_tree(projects, &block)
313 def project_tree(projects, &block)
314 Project.project_tree(projects, &block)
314 Project.project_tree(projects, &block)
315 end
315 end
316
316
317 def principals_check_box_tags(name, principals)
317 def principals_check_box_tags(name, principals)
318 s = ''
318 s = ''
319 principals.each do |principal|
319 principals.each do |principal|
320 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
320 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
321 end
321 end
322 s.html_safe
322 s.html_safe
323 end
323 end
324
324
325 # Returns a string for users/groups option tags
325 # Returns a string for users/groups option tags
326 def principals_options_for_select(collection, selected=nil)
326 def principals_options_for_select(collection, selected=nil)
327 s = ''
327 s = ''
328 if collection.include?(User.current)
328 if collection.include?(User.current)
329 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
329 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
330 end
330 end
331 groups = ''
331 groups = ''
332 collection.sort.each do |element|
332 collection.sort.each do |element|
333 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
333 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
334 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
334 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
335 end
335 end
336 unless groups.empty?
336 unless groups.empty?
337 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
337 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
338 end
338 end
339 s.html_safe
339 s.html_safe
340 end
340 end
341
341
342 # Options for the new membership projects combo-box
342 # Options for the new membership projects combo-box
343 def options_for_membership_project_select(principal, projects)
343 def options_for_membership_project_select(principal, projects)
344 options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
344 options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
345 options << project_tree_options_for_select(projects) do |p|
345 options << project_tree_options_for_select(projects) do |p|
346 {:disabled => principal.projects.to_a.include?(p)}
346 {:disabled => principal.projects.to_a.include?(p)}
347 end
347 end
348 options
348 options
349 end
349 end
350
350
351 def option_tag(name, text, value, selected=nil, options={})
352 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
353 end
354
351 # Truncates and returns the string as a single line
355 # Truncates and returns the string as a single line
352 def truncate_single_line(string, *args)
356 def truncate_single_line(string, *args)
353 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
357 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
354 end
358 end
355
359
356 # Truncates at line break after 250 characters or options[:length]
360 # Truncates at line break after 250 characters or options[:length]
357 def truncate_lines(string, options={})
361 def truncate_lines(string, options={})
358 length = options[:length] || 250
362 length = options[:length] || 250
359 if string.to_s =~ /\A(.{#{length}}.*?)$/m
363 if string.to_s =~ /\A(.{#{length}}.*?)$/m
360 "#{$1}..."
364 "#{$1}..."
361 else
365 else
362 string
366 string
363 end
367 end
364 end
368 end
365
369
366 def anchor(text)
370 def anchor(text)
367 text.to_s.gsub(' ', '_')
371 text.to_s.gsub(' ', '_')
368 end
372 end
369
373
370 def html_hours(text)
374 def html_hours(text)
371 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
375 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
372 end
376 end
373
377
374 def authoring(created, author, options={})
378 def authoring(created, author, options={})
375 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
379 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
376 end
380 end
377
381
378 def time_tag(time)
382 def time_tag(time)
379 text = distance_of_time_in_words(Time.now, time)
383 text = distance_of_time_in_words(Time.now, time)
380 if @project
384 if @project
381 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
385 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
382 else
386 else
383 content_tag('acronym', text, :title => format_time(time))
387 content_tag('acronym', text, :title => format_time(time))
384 end
388 end
385 end
389 end
386
390
387 def syntax_highlight_lines(name, content)
391 def syntax_highlight_lines(name, content)
388 lines = []
392 lines = []
389 syntax_highlight(name, content).each_line { |line| lines << line }
393 syntax_highlight(name, content).each_line { |line| lines << line }
390 lines
394 lines
391 end
395 end
392
396
393 def syntax_highlight(name, content)
397 def syntax_highlight(name, content)
394 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
398 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
395 end
399 end
396
400
397 def to_path_param(path)
401 def to_path_param(path)
398 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
402 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
399 str.blank? ? nil : str
403 str.blank? ? nil : str
400 end
404 end
401
405
402 def reorder_links(name, url, method = :post)
406 def reorder_links(name, url, method = :post)
403 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
407 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
404 url.merge({"#{name}[move_to]" => 'highest'}),
408 url.merge({"#{name}[move_to]" => 'highest'}),
405 :method => method, :title => l(:label_sort_highest)) +
409 :method => method, :title => l(:label_sort_highest)) +
406 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
410 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
407 url.merge({"#{name}[move_to]" => 'higher'}),
411 url.merge({"#{name}[move_to]" => 'higher'}),
408 :method => method, :title => l(:label_sort_higher)) +
412 :method => method, :title => l(:label_sort_higher)) +
409 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
413 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
410 url.merge({"#{name}[move_to]" => 'lower'}),
414 url.merge({"#{name}[move_to]" => 'lower'}),
411 :method => method, :title => l(:label_sort_lower)) +
415 :method => method, :title => l(:label_sort_lower)) +
412 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
416 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
413 url.merge({"#{name}[move_to]" => 'lowest'}),
417 url.merge({"#{name}[move_to]" => 'lowest'}),
414 :method => method, :title => l(:label_sort_lowest))
418 :method => method, :title => l(:label_sort_lowest))
415 end
419 end
416
420
417 def breadcrumb(*args)
421 def breadcrumb(*args)
418 elements = args.flatten
422 elements = args.flatten
419 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
423 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
420 end
424 end
421
425
422 def other_formats_links(&block)
426 def other_formats_links(&block)
423 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
427 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
424 yield Redmine::Views::OtherFormatsBuilder.new(self)
428 yield Redmine::Views::OtherFormatsBuilder.new(self)
425 concat('</p>'.html_safe)
429 concat('</p>'.html_safe)
426 end
430 end
427
431
428 def page_header_title
432 def page_header_title
429 if @project.nil? || @project.new_record?
433 if @project.nil? || @project.new_record?
430 h(Setting.app_title)
434 h(Setting.app_title)
431 else
435 else
432 b = []
436 b = []
433 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
437 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
434 if ancestors.any?
438 if ancestors.any?
435 root = ancestors.shift
439 root = ancestors.shift
436 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
440 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
437 if ancestors.size > 2
441 if ancestors.size > 2
438 b << "\xe2\x80\xa6"
442 b << "\xe2\x80\xa6"
439 ancestors = ancestors[-2, 2]
443 ancestors = ancestors[-2, 2]
440 end
444 end
441 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
445 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
442 end
446 end
443 b << h(@project)
447 b << h(@project)
444 b.join(" \xc2\xbb ").html_safe
448 b.join(" \xc2\xbb ").html_safe
445 end
449 end
446 end
450 end
447
451
448 def html_title(*args)
452 def html_title(*args)
449 if args.empty?
453 if args.empty?
450 title = @html_title || []
454 title = @html_title || []
451 title << @project.name if @project
455 title << @project.name if @project
452 title << Setting.app_title unless Setting.app_title == title.last
456 title << Setting.app_title unless Setting.app_title == title.last
453 title.select {|t| !t.blank? }.join(' - ')
457 title.select {|t| !t.blank? }.join(' - ')
454 else
458 else
455 @html_title ||= []
459 @html_title ||= []
456 @html_title += args
460 @html_title += args
457 end
461 end
458 end
462 end
459
463
460 # Returns the theme, controller name, and action as css classes for the
464 # Returns the theme, controller name, and action as css classes for the
461 # HTML body.
465 # HTML body.
462 def body_css_classes
466 def body_css_classes
463 css = []
467 css = []
464 if theme = Redmine::Themes.theme(Setting.ui_theme)
468 if theme = Redmine::Themes.theme(Setting.ui_theme)
465 css << 'theme-' + theme.name
469 css << 'theme-' + theme.name
466 end
470 end
467
471
468 css << 'controller-' + controller_name
472 css << 'controller-' + controller_name
469 css << 'action-' + action_name
473 css << 'action-' + action_name
470 css.join(' ')
474 css.join(' ')
471 end
475 end
472
476
473 def accesskey(s)
477 def accesskey(s)
474 @used_accesskeys ||= []
478 @used_accesskeys ||= []
475 key = Redmine::AccessKeys.key_for(s)
479 key = Redmine::AccessKeys.key_for(s)
476 return nil if @used_accesskeys.include?(key)
480 return nil if @used_accesskeys.include?(key)
477 @used_accesskeys << key
481 @used_accesskeys << key
478 key
482 key
479 end
483 end
480
484
481 # Formats text according to system settings.
485 # Formats text according to system settings.
482 # 2 ways to call this method:
486 # 2 ways to call this method:
483 # * with a String: textilizable(text, options)
487 # * with a String: textilizable(text, options)
484 # * with an object and one of its attribute: textilizable(issue, :description, options)
488 # * with an object and one of its attribute: textilizable(issue, :description, options)
485 def textilizable(*args)
489 def textilizable(*args)
486 options = args.last.is_a?(Hash) ? args.pop : {}
490 options = args.last.is_a?(Hash) ? args.pop : {}
487 case args.size
491 case args.size
488 when 1
492 when 1
489 obj = options[:object]
493 obj = options[:object]
490 text = args.shift
494 text = args.shift
491 when 2
495 when 2
492 obj = args.shift
496 obj = args.shift
493 attr = args.shift
497 attr = args.shift
494 text = obj.send(attr).to_s
498 text = obj.send(attr).to_s
495 else
499 else
496 raise ArgumentError, 'invalid arguments to textilizable'
500 raise ArgumentError, 'invalid arguments to textilizable'
497 end
501 end
498 return '' if text.blank?
502 return '' if text.blank?
499 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
503 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
500 only_path = options.delete(:only_path) == false ? false : true
504 only_path = options.delete(:only_path) == false ? false : true
501
505
502 text = text.dup
506 text = text.dup
503 macros = catch_macros(text)
507 macros = catch_macros(text)
504 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
508 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
505
509
506 @parsed_headings = []
510 @parsed_headings = []
507 @heading_anchors = {}
511 @heading_anchors = {}
508 @current_section = 0 if options[:edit_section_links]
512 @current_section = 0 if options[:edit_section_links]
509
513
510 parse_sections(text, project, obj, attr, only_path, options)
514 parse_sections(text, project, obj, attr, only_path, options)
511 text = parse_non_pre_blocks(text, obj, macros) do |text|
515 text = parse_non_pre_blocks(text, obj, macros) do |text|
512 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
516 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
513 send method_name, text, project, obj, attr, only_path, options
517 send method_name, text, project, obj, attr, only_path, options
514 end
518 end
515 end
519 end
516 parse_headings(text, project, obj, attr, only_path, options)
520 parse_headings(text, project, obj, attr, only_path, options)
517
521
518 if @parsed_headings.any?
522 if @parsed_headings.any?
519 replace_toc(text, @parsed_headings)
523 replace_toc(text, @parsed_headings)
520 end
524 end
521
525
522 text.html_safe
526 text.html_safe
523 end
527 end
524
528
525 def parse_non_pre_blocks(text, obj, macros)
529 def parse_non_pre_blocks(text, obj, macros)
526 s = StringScanner.new(text)
530 s = StringScanner.new(text)
527 tags = []
531 tags = []
528 parsed = ''
532 parsed = ''
529 while !s.eos?
533 while !s.eos?
530 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
534 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
531 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
535 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
532 if tags.empty?
536 if tags.empty?
533 yield text
537 yield text
534 inject_macros(text, obj, macros) if macros.any?
538 inject_macros(text, obj, macros) if macros.any?
535 else
539 else
536 inject_macros(text, obj, macros, false) if macros.any?
540 inject_macros(text, obj, macros, false) if macros.any?
537 end
541 end
538 parsed << text
542 parsed << text
539 if tag
543 if tag
540 if closing
544 if closing
541 if tags.last == tag.downcase
545 if tags.last == tag.downcase
542 tags.pop
546 tags.pop
543 end
547 end
544 else
548 else
545 tags << tag.downcase
549 tags << tag.downcase
546 end
550 end
547 parsed << full_tag
551 parsed << full_tag
548 end
552 end
549 end
553 end
550 # Close any non closing tags
554 # Close any non closing tags
551 while tag = tags.pop
555 while tag = tags.pop
552 parsed << "</#{tag}>"
556 parsed << "</#{tag}>"
553 end
557 end
554 parsed
558 parsed
555 end
559 end
556
560
557 def parse_inline_attachments(text, project, obj, attr, only_path, options)
561 def parse_inline_attachments(text, project, obj, attr, only_path, options)
558 # when using an image link, try to use an attachment, if possible
562 # when using an image link, try to use an attachment, if possible
559 attachments = options[:attachments] || []
563 attachments = options[:attachments] || []
560 attachments += obj.attachments if obj.respond_to?(:attachments)
564 attachments += obj.attachments if obj.respond_to?(:attachments)
561 if attachments.present?
565 if attachments.present?
562 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
566 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
563 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
567 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
564 # search for the picture in attachments
568 # search for the picture in attachments
565 if found = Attachment.latest_attach(attachments, filename)
569 if found = Attachment.latest_attach(attachments, filename)
566 image_url = download_named_attachment_path(found, found.filename, :only_path => only_path)
570 image_url = download_named_attachment_path(found, found.filename, :only_path => only_path)
567 desc = found.description.to_s.gsub('"', '')
571 desc = found.description.to_s.gsub('"', '')
568 if !desc.blank? && alttext.blank?
572 if !desc.blank? && alttext.blank?
569 alt = " title=\"#{desc}\" alt=\"#{desc}\""
573 alt = " title=\"#{desc}\" alt=\"#{desc}\""
570 end
574 end
571 "src=\"#{image_url}\"#{alt}"
575 "src=\"#{image_url}\"#{alt}"
572 else
576 else
573 m
577 m
574 end
578 end
575 end
579 end
576 end
580 end
577 end
581 end
578
582
579 # Wiki links
583 # Wiki links
580 #
584 #
581 # Examples:
585 # Examples:
582 # [[mypage]]
586 # [[mypage]]
583 # [[mypage|mytext]]
587 # [[mypage|mytext]]
584 # wiki links can refer other project wikis, using project name or identifier:
588 # wiki links can refer other project wikis, using project name or identifier:
585 # [[project:]] -> wiki starting page
589 # [[project:]] -> wiki starting page
586 # [[project:|mytext]]
590 # [[project:|mytext]]
587 # [[project:mypage]]
591 # [[project:mypage]]
588 # [[project:mypage|mytext]]
592 # [[project:mypage|mytext]]
589 def parse_wiki_links(text, project, obj, attr, only_path, options)
593 def parse_wiki_links(text, project, obj, attr, only_path, options)
590 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
594 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
591 link_project = project
595 link_project = project
592 esc, all, page, title = $1, $2, $3, $5
596 esc, all, page, title = $1, $2, $3, $5
593 if esc.nil?
597 if esc.nil?
594 if page =~ /^([^\:]+)\:(.*)$/
598 if page =~ /^([^\:]+)\:(.*)$/
595 identifier, page = $1, $2
599 identifier, page = $1, $2
596 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
600 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
597 title ||= identifier if page.blank?
601 title ||= identifier if page.blank?
598 end
602 end
599
603
600 if link_project && link_project.wiki
604 if link_project && link_project.wiki
601 # extract anchor
605 # extract anchor
602 anchor = nil
606 anchor = nil
603 if page =~ /^(.+?)\#(.+)$/
607 if page =~ /^(.+?)\#(.+)$/
604 page, anchor = $1, $2
608 page, anchor = $1, $2
605 end
609 end
606 anchor = sanitize_anchor_name(anchor) if anchor.present?
610 anchor = sanitize_anchor_name(anchor) if anchor.present?
607 # check if page exists
611 # check if page exists
608 wiki_page = link_project.wiki.find_page(page)
612 wiki_page = link_project.wiki.find_page(page)
609 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
613 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
610 "##{anchor}"
614 "##{anchor}"
611 else
615 else
612 case options[:wiki_links]
616 case options[:wiki_links]
613 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
617 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
614 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
618 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
615 else
619 else
616 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
620 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
617 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
621 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
618 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
622 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
619 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
623 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
620 end
624 end
621 end
625 end
622 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
626 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
623 else
627 else
624 # project or wiki doesn't exist
628 # project or wiki doesn't exist
625 all
629 all
626 end
630 end
627 else
631 else
628 all
632 all
629 end
633 end
630 end
634 end
631 end
635 end
632
636
633 # Redmine links
637 # Redmine links
634 #
638 #
635 # Examples:
639 # Examples:
636 # Issues:
640 # Issues:
637 # #52 -> Link to issue #52
641 # #52 -> Link to issue #52
638 # Changesets:
642 # Changesets:
639 # r52 -> Link to revision 52
643 # r52 -> Link to revision 52
640 # commit:a85130f -> Link to scmid starting with a85130f
644 # commit:a85130f -> Link to scmid starting with a85130f
641 # Documents:
645 # Documents:
642 # document#17 -> Link to document with id 17
646 # document#17 -> Link to document with id 17
643 # document:Greetings -> Link to the document with title "Greetings"
647 # document:Greetings -> Link to the document with title "Greetings"
644 # document:"Some document" -> Link to the document with title "Some document"
648 # document:"Some document" -> Link to the document with title "Some document"
645 # Versions:
649 # Versions:
646 # version#3 -> Link to version with id 3
650 # version#3 -> Link to version with id 3
647 # version:1.0.0 -> Link to version named "1.0.0"
651 # version:1.0.0 -> Link to version named "1.0.0"
648 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
652 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
649 # Attachments:
653 # Attachments:
650 # attachment:file.zip -> Link to the attachment of the current object named file.zip
654 # attachment:file.zip -> Link to the attachment of the current object named file.zip
651 # Source files:
655 # Source files:
652 # source:some/file -> Link to the file located at /some/file in the project's repository
656 # source:some/file -> Link to the file located at /some/file in the project's repository
653 # source:some/file@52 -> Link to the file's revision 52
657 # source:some/file@52 -> Link to the file's revision 52
654 # source:some/file#L120 -> Link to line 120 of the file
658 # source:some/file#L120 -> Link to line 120 of the file
655 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
659 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
656 # export:some/file -> Force the download of the file
660 # export:some/file -> Force the download of the file
657 # Forum messages:
661 # Forum messages:
658 # message#1218 -> Link to message with id 1218
662 # message#1218 -> Link to message with id 1218
659 #
663 #
660 # Links can refer other objects from other projects, using project identifier:
664 # Links can refer other objects from other projects, using project identifier:
661 # identifier:r52
665 # identifier:r52
662 # identifier:document:"Some document"
666 # identifier:document:"Some document"
663 # identifier:version:1.0.0
667 # identifier:version:1.0.0
664 # identifier:source:some/file
668 # identifier:source:some/file
665 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
669 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
666 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
670 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
667 leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17
671 leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17
668 link = nil
672 link = nil
669 project = default_project
673 project = default_project
670 if project_identifier
674 if project_identifier
671 project = Project.visible.find_by_identifier(project_identifier)
675 project = Project.visible.find_by_identifier(project_identifier)
672 end
676 end
673 if esc.nil?
677 if esc.nil?
674 if prefix.nil? && sep == 'r'
678 if prefix.nil? && sep == 'r'
675 if project
679 if project
676 repository = nil
680 repository = nil
677 if repo_identifier
681 if repo_identifier
678 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
682 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
679 else
683 else
680 repository = project.repository
684 repository = project.repository
681 end
685 end
682 # project.changesets.visible raises an SQL error because of a double join on repositories
686 # project.changesets.visible raises an SQL error because of a double join on repositories
683 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
687 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
684 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision},
688 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision},
685 :class => 'changeset',
689 :class => 'changeset',
686 :title => truncate_single_line(changeset.comments, :length => 100))
690 :title => truncate_single_line(changeset.comments, :length => 100))
687 end
691 end
688 end
692 end
689 elsif sep == '#'
693 elsif sep == '#'
690 oid = identifier.to_i
694 oid = identifier.to_i
691 case prefix
695 case prefix
692 when nil
696 when nil
693 if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status)
697 if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status)
694 anchor = comment_id ? "note-#{comment_id}" : nil
698 anchor = comment_id ? "note-#{comment_id}" : nil
695 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
699 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
696 :class => issue.css_classes,
700 :class => issue.css_classes,
697 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
701 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
698 end
702 end
699 when 'document'
703 when 'document'
700 if document = Document.visible.find_by_id(oid)
704 if document = Document.visible.find_by_id(oid)
701 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
705 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
702 :class => 'document'
706 :class => 'document'
703 end
707 end
704 when 'version'
708 when 'version'
705 if version = Version.visible.find_by_id(oid)
709 if version = Version.visible.find_by_id(oid)
706 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
710 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
707 :class => 'version'
711 :class => 'version'
708 end
712 end
709 when 'message'
713 when 'message'
710 if message = Message.visible.find_by_id(oid, :include => :parent)
714 if message = Message.visible.find_by_id(oid, :include => :parent)
711 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
715 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
712 end
716 end
713 when 'forum'
717 when 'forum'
714 if board = Board.visible.find_by_id(oid)
718 if board = Board.visible.find_by_id(oid)
715 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
719 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
716 :class => 'board'
720 :class => 'board'
717 end
721 end
718 when 'news'
722 when 'news'
719 if news = News.visible.find_by_id(oid)
723 if news = News.visible.find_by_id(oid)
720 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
724 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
721 :class => 'news'
725 :class => 'news'
722 end
726 end
723 when 'project'
727 when 'project'
724 if p = Project.visible.find_by_id(oid)
728 if p = Project.visible.find_by_id(oid)
725 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
729 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
726 end
730 end
727 end
731 end
728 elsif sep == ':'
732 elsif sep == ':'
729 # removes the double quotes if any
733 # removes the double quotes if any
730 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
734 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
731 case prefix
735 case prefix
732 when 'document'
736 when 'document'
733 if project && document = project.documents.visible.find_by_title(name)
737 if project && document = project.documents.visible.find_by_title(name)
734 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
738 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
735 :class => 'document'
739 :class => 'document'
736 end
740 end
737 when 'version'
741 when 'version'
738 if project && version = project.versions.visible.find_by_name(name)
742 if project && version = project.versions.visible.find_by_name(name)
739 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
743 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
740 :class => 'version'
744 :class => 'version'
741 end
745 end
742 when 'forum'
746 when 'forum'
743 if project && board = project.boards.visible.find_by_name(name)
747 if project && board = project.boards.visible.find_by_name(name)
744 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
748 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
745 :class => 'board'
749 :class => 'board'
746 end
750 end
747 when 'news'
751 when 'news'
748 if project && news = project.news.visible.find_by_title(name)
752 if project && news = project.news.visible.find_by_title(name)
749 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
753 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
750 :class => 'news'
754 :class => 'news'
751 end
755 end
752 when 'commit', 'source', 'export'
756 when 'commit', 'source', 'export'
753 if project
757 if project
754 repository = nil
758 repository = nil
755 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
759 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
756 repo_prefix, repo_identifier, name = $1, $2, $3
760 repo_prefix, repo_identifier, name = $1, $2, $3
757 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
761 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
758 else
762 else
759 repository = project.repository
763 repository = project.repository
760 end
764 end
761 if prefix == 'commit'
765 if prefix == 'commit'
762 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
766 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
763 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
767 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
764 :class => 'changeset',
768 :class => 'changeset',
765 :title => truncate_single_line(changeset.comments, :length => 100)
769 :title => truncate_single_line(changeset.comments, :length => 100)
766 end
770 end
767 else
771 else
768 if repository && User.current.allowed_to?(:browse_repository, project)
772 if repository && User.current.allowed_to?(:browse_repository, project)
769 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
773 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
770 path, rev, anchor = $1, $3, $5
774 path, rev, anchor = $1, $3, $5
771 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
775 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
772 :path => to_path_param(path),
776 :path => to_path_param(path),
773 :rev => rev,
777 :rev => rev,
774 :anchor => anchor},
778 :anchor => anchor},
775 :class => (prefix == 'export' ? 'source download' : 'source')
779 :class => (prefix == 'export' ? 'source download' : 'source')
776 end
780 end
777 end
781 end
778 repo_prefix = nil
782 repo_prefix = nil
779 end
783 end
780 when 'attachment'
784 when 'attachment'
781 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
785 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
782 if attachments && attachment = Attachment.latest_attach(attachments, name)
786 if attachments && attachment = Attachment.latest_attach(attachments, name)
783 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
787 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
784 end
788 end
785 when 'project'
789 when 'project'
786 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
790 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
787 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
791 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
788 end
792 end
789 end
793 end
790 end
794 end
791 end
795 end
792 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
796 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
793 end
797 end
794 end
798 end
795
799
796 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
800 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
797
801
798 def parse_sections(text, project, obj, attr, only_path, options)
802 def parse_sections(text, project, obj, attr, only_path, options)
799 return unless options[:edit_section_links]
803 return unless options[:edit_section_links]
800 text.gsub!(HEADING_RE) do
804 text.gsub!(HEADING_RE) do
801 heading = $1
805 heading = $1
802 @current_section += 1
806 @current_section += 1
803 if @current_section > 1
807 if @current_section > 1
804 content_tag('div',
808 content_tag('div',
805 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
809 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
806 :class => 'contextual',
810 :class => 'contextual',
807 :title => l(:button_edit_section)) + heading.html_safe
811 :title => l(:button_edit_section)) + heading.html_safe
808 else
812 else
809 heading
813 heading
810 end
814 end
811 end
815 end
812 end
816 end
813
817
814 # Headings and TOC
818 # Headings and TOC
815 # Adds ids and links to headings unless options[:headings] is set to false
819 # Adds ids and links to headings unless options[:headings] is set to false
816 def parse_headings(text, project, obj, attr, only_path, options)
820 def parse_headings(text, project, obj, attr, only_path, options)
817 return if options[:headings] == false
821 return if options[:headings] == false
818
822
819 text.gsub!(HEADING_RE) do
823 text.gsub!(HEADING_RE) do
820 level, attrs, content = $2.to_i, $3, $4
824 level, attrs, content = $2.to_i, $3, $4
821 item = strip_tags(content).strip
825 item = strip_tags(content).strip
822 anchor = sanitize_anchor_name(item)
826 anchor = sanitize_anchor_name(item)
823 # used for single-file wiki export
827 # used for single-file wiki export
824 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
828 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
825 @heading_anchors[anchor] ||= 0
829 @heading_anchors[anchor] ||= 0
826 idx = (@heading_anchors[anchor] += 1)
830 idx = (@heading_anchors[anchor] += 1)
827 if idx > 1
831 if idx > 1
828 anchor = "#{anchor}-#{idx}"
832 anchor = "#{anchor}-#{idx}"
829 end
833 end
830 @parsed_headings << [level, anchor, item]
834 @parsed_headings << [level, anchor, item]
831 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
835 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
832 end
836 end
833 end
837 end
834
838
835 MACROS_RE = /(
839 MACROS_RE = /(
836 (!)? # escaping
840 (!)? # escaping
837 (
841 (
838 \{\{ # opening tag
842 \{\{ # opening tag
839 ([\w]+) # macro name
843 ([\w]+) # macro name
840 (\(([^\n\r]*?)\))? # optional arguments
844 (\(([^\n\r]*?)\))? # optional arguments
841 ([\n\r].*?[\n\r])? # optional block of text
845 ([\n\r].*?[\n\r])? # optional block of text
842 \}\} # closing tag
846 \}\} # closing tag
843 )
847 )
844 )/mx unless const_defined?(:MACROS_RE)
848 )/mx unless const_defined?(:MACROS_RE)
845
849
846 MACRO_SUB_RE = /(
850 MACRO_SUB_RE = /(
847 \{\{
851 \{\{
848 macro\((\d+)\)
852 macro\((\d+)\)
849 \}\}
853 \}\}
850 )/x unless const_defined?(:MACRO_SUB_RE)
854 )/x unless const_defined?(:MACRO_SUB_RE)
851
855
852 # Extracts macros from text
856 # Extracts macros from text
853 def catch_macros(text)
857 def catch_macros(text)
854 macros = {}
858 macros = {}
855 text.gsub!(MACROS_RE) do
859 text.gsub!(MACROS_RE) do
856 all, macro = $1, $4.downcase
860 all, macro = $1, $4.downcase
857 if macro_exists?(macro) || all =~ MACRO_SUB_RE
861 if macro_exists?(macro) || all =~ MACRO_SUB_RE
858 index = macros.size
862 index = macros.size
859 macros[index] = all
863 macros[index] = all
860 "{{macro(#{index})}}"
864 "{{macro(#{index})}}"
861 else
865 else
862 all
866 all
863 end
867 end
864 end
868 end
865 macros
869 macros
866 end
870 end
867
871
868 # Executes and replaces macros in text
872 # Executes and replaces macros in text
869 def inject_macros(text, obj, macros, execute=true)
873 def inject_macros(text, obj, macros, execute=true)
870 text.gsub!(MACRO_SUB_RE) do
874 text.gsub!(MACRO_SUB_RE) do
871 all, index = $1, $2.to_i
875 all, index = $1, $2.to_i
872 orig = macros.delete(index)
876 orig = macros.delete(index)
873 if execute && orig && orig =~ MACROS_RE
877 if execute && orig && orig =~ MACROS_RE
874 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
878 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
875 if esc.nil?
879 if esc.nil?
876 h(exec_macro(macro, obj, args, block) || all)
880 h(exec_macro(macro, obj, args, block) || all)
877 else
881 else
878 h(all)
882 h(all)
879 end
883 end
880 elsif orig
884 elsif orig
881 h(orig)
885 h(orig)
882 else
886 else
883 h(all)
887 h(all)
884 end
888 end
885 end
889 end
886 end
890 end
887
891
888 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
892 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
889
893
890 # Renders the TOC with given headings
894 # Renders the TOC with given headings
891 def replace_toc(text, headings)
895 def replace_toc(text, headings)
892 text.gsub!(TOC_RE) do
896 text.gsub!(TOC_RE) do
893 # Keep only the 4 first levels
897 # Keep only the 4 first levels
894 headings = headings.select{|level, anchor, item| level <= 4}
898 headings = headings.select{|level, anchor, item| level <= 4}
895 if headings.empty?
899 if headings.empty?
896 ''
900 ''
897 else
901 else
898 div_class = 'toc'
902 div_class = 'toc'
899 div_class << ' right' if $1 == '>'
903 div_class << ' right' if $1 == '>'
900 div_class << ' left' if $1 == '<'
904 div_class << ' left' if $1 == '<'
901 out = "<ul class=\"#{div_class}\"><li>"
905 out = "<ul class=\"#{div_class}\"><li>"
902 root = headings.map(&:first).min
906 root = headings.map(&:first).min
903 current = root
907 current = root
904 started = false
908 started = false
905 headings.each do |level, anchor, item|
909 headings.each do |level, anchor, item|
906 if level > current
910 if level > current
907 out << '<ul><li>' * (level - current)
911 out << '<ul><li>' * (level - current)
908 elsif level < current
912 elsif level < current
909 out << "</li></ul>\n" * (current - level) + "</li><li>"
913 out << "</li></ul>\n" * (current - level) + "</li><li>"
910 elsif started
914 elsif started
911 out << '</li><li>'
915 out << '</li><li>'
912 end
916 end
913 out << "<a href=\"##{anchor}\">#{item}</a>"
917 out << "<a href=\"##{anchor}\">#{item}</a>"
914 current = level
918 current = level
915 started = true
919 started = true
916 end
920 end
917 out << '</li></ul>' * (current - root)
921 out << '</li></ul>' * (current - root)
918 out << '</li></ul>'
922 out << '</li></ul>'
919 end
923 end
920 end
924 end
921 end
925 end
922
926
923 # Same as Rails' simple_format helper without using paragraphs
927 # Same as Rails' simple_format helper without using paragraphs
924 def simple_format_without_paragraph(text)
928 def simple_format_without_paragraph(text)
925 text.to_s.
929 text.to_s.
926 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
930 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
927 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
931 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
928 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
932 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
929 html_safe
933 html_safe
930 end
934 end
931
935
932 def lang_options_for_select(blank=true)
936 def lang_options_for_select(blank=true)
933 (blank ? [["(auto)", ""]] : []) + languages_options
937 (blank ? [["(auto)", ""]] : []) + languages_options
934 end
938 end
935
939
936 def label_tag_for(name, option_tags = nil, options = {})
940 def label_tag_for(name, option_tags = nil, options = {})
937 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
941 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
938 content_tag("label", label_text)
942 content_tag("label", label_text)
939 end
943 end
940
944
941 def labelled_form_for(*args, &proc)
945 def labelled_form_for(*args, &proc)
942 args << {} unless args.last.is_a?(Hash)
946 args << {} unless args.last.is_a?(Hash)
943 options = args.last
947 options = args.last
944 if args.first.is_a?(Symbol)
948 if args.first.is_a?(Symbol)
945 options.merge!(:as => args.shift)
949 options.merge!(:as => args.shift)
946 end
950 end
947 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
951 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
948 form_for(*args, &proc)
952 form_for(*args, &proc)
949 end
953 end
950
954
951 def labelled_fields_for(*args, &proc)
955 def labelled_fields_for(*args, &proc)
952 args << {} unless args.last.is_a?(Hash)
956 args << {} unless args.last.is_a?(Hash)
953 options = args.last
957 options = args.last
954 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
958 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
955 fields_for(*args, &proc)
959 fields_for(*args, &proc)
956 end
960 end
957
961
958 def labelled_remote_form_for(*args, &proc)
962 def labelled_remote_form_for(*args, &proc)
959 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
963 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
960 args << {} unless args.last.is_a?(Hash)
964 args << {} unless args.last.is_a?(Hash)
961 options = args.last
965 options = args.last
962 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
966 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
963 form_for(*args, &proc)
967 form_for(*args, &proc)
964 end
968 end
965
969
966 def error_messages_for(*objects)
970 def error_messages_for(*objects)
967 html = ""
971 html = ""
968 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
972 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
969 errors = objects.map {|o| o.errors.full_messages}.flatten
973 errors = objects.map {|o| o.errors.full_messages}.flatten
970 if errors.any?
974 if errors.any?
971 html << "<div id='errorExplanation'><ul>\n"
975 html << "<div id='errorExplanation'><ul>\n"
972 errors.each do |error|
976 errors.each do |error|
973 html << "<li>#{h error}</li>\n"
977 html << "<li>#{h error}</li>\n"
974 end
978 end
975 html << "</ul></div>\n"
979 html << "</ul></div>\n"
976 end
980 end
977 html.html_safe
981 html.html_safe
978 end
982 end
979
983
980 def delete_link(url, options={})
984 def delete_link(url, options={})
981 options = {
985 options = {
982 :method => :delete,
986 :method => :delete,
983 :data => {:confirm => l(:text_are_you_sure)},
987 :data => {:confirm => l(:text_are_you_sure)},
984 :class => 'icon icon-del'
988 :class => 'icon icon-del'
985 }.merge(options)
989 }.merge(options)
986
990
987 link_to l(:button_delete), url, options
991 link_to l(:button_delete), url, options
988 end
992 end
989
993
990 def preview_link(url, form, target='preview', options={})
994 def preview_link(url, form, target='preview', options={})
991 content_tag 'a', l(:label_preview), {
995 content_tag 'a', l(:label_preview), {
992 :href => "#",
996 :href => "#",
993 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
997 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
994 :accesskey => accesskey(:preview)
998 :accesskey => accesskey(:preview)
995 }.merge(options)
999 }.merge(options)
996 end
1000 end
997
1001
998 def link_to_function(name, function, html_options={})
1002 def link_to_function(name, function, html_options={})
999 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1003 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1000 end
1004 end
1001
1005
1002 # Helper to render JSON in views
1006 # Helper to render JSON in views
1003 def raw_json(arg)
1007 def raw_json(arg)
1004 arg.to_json.to_s.gsub('/', '\/').html_safe
1008 arg.to_json.to_s.gsub('/', '\/').html_safe
1005 end
1009 end
1006
1010
1007 def back_url
1011 def back_url
1008 url = params[:back_url]
1012 url = params[:back_url]
1009 if url.nil? && referer = request.env['HTTP_REFERER']
1013 if url.nil? && referer = request.env['HTTP_REFERER']
1010 url = CGI.unescape(referer.to_s)
1014 url = CGI.unescape(referer.to_s)
1011 end
1015 end
1012 url
1016 url
1013 end
1017 end
1014
1018
1015 def back_url_hidden_field_tag
1019 def back_url_hidden_field_tag
1016 url = back_url
1020 url = back_url
1017 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1021 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1018 end
1022 end
1019
1023
1020 def check_all_links(form_name)
1024 def check_all_links(form_name)
1021 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1025 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1022 " | ".html_safe +
1026 " | ".html_safe +
1023 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1027 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1024 end
1028 end
1025
1029
1026 def progress_bar(pcts, options={})
1030 def progress_bar(pcts, options={})
1027 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1031 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1028 pcts = pcts.collect(&:round)
1032 pcts = pcts.collect(&:round)
1029 pcts[1] = pcts[1] - pcts[0]
1033 pcts[1] = pcts[1] - pcts[0]
1030 pcts << (100 - pcts[1] - pcts[0])
1034 pcts << (100 - pcts[1] - pcts[0])
1031 width = options[:width] || '100px;'
1035 width = options[:width] || '100px;'
1032 legend = options[:legend] || ''
1036 legend = options[:legend] || ''
1033 content_tag('table',
1037 content_tag('table',
1034 content_tag('tr',
1038 content_tag('tr',
1035 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1039 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1036 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1040 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1037 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1041 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1038 ), :class => 'progress', :style => "width: #{width};").html_safe +
1042 ), :class => 'progress', :style => "width: #{width};").html_safe +
1039 content_tag('p', legend, :class => 'percent').html_safe
1043 content_tag('p', legend, :class => 'percent').html_safe
1040 end
1044 end
1041
1045
1042 def checked_image(checked=true)
1046 def checked_image(checked=true)
1043 if checked
1047 if checked
1044 image_tag 'toggle_check.png'
1048 image_tag 'toggle_check.png'
1045 end
1049 end
1046 end
1050 end
1047
1051
1048 def context_menu(url)
1052 def context_menu(url)
1049 unless @context_menu_included
1053 unless @context_menu_included
1050 content_for :header_tags do
1054 content_for :header_tags do
1051 javascript_include_tag('context_menu') +
1055 javascript_include_tag('context_menu') +
1052 stylesheet_link_tag('context_menu')
1056 stylesheet_link_tag('context_menu')
1053 end
1057 end
1054 if l(:direction) == 'rtl'
1058 if l(:direction) == 'rtl'
1055 content_for :header_tags do
1059 content_for :header_tags do
1056 stylesheet_link_tag('context_menu_rtl')
1060 stylesheet_link_tag('context_menu_rtl')
1057 end
1061 end
1058 end
1062 end
1059 @context_menu_included = true
1063 @context_menu_included = true
1060 end
1064 end
1061 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1065 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1062 end
1066 end
1063
1067
1064 def calendar_for(field_id)
1068 def calendar_for(field_id)
1065 include_calendar_headers_tags
1069 include_calendar_headers_tags
1066 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1070 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1067 end
1071 end
1068
1072
1069 def include_calendar_headers_tags
1073 def include_calendar_headers_tags
1070 unless @calendar_headers_tags_included
1074 unless @calendar_headers_tags_included
1071 @calendar_headers_tags_included = true
1075 @calendar_headers_tags_included = true
1072 content_for :header_tags do
1076 content_for :header_tags do
1073 start_of_week = Setting.start_of_week
1077 start_of_week = Setting.start_of_week
1074 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1078 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1075 # Redmine uses 1..7 (monday..sunday) in settings and locales
1079 # Redmine uses 1..7 (monday..sunday) in settings and locales
1076 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1080 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1077 start_of_week = start_of_week.to_i % 7
1081 start_of_week = start_of_week.to_i % 7
1078
1082
1079 tags = javascript_tag(
1083 tags = javascript_tag(
1080 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1084 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1081 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1085 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1082 path_to_image('/images/calendar.png') +
1086 path_to_image('/images/calendar.png') +
1083 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, selectOtherMonths: true, changeMonth: true, changeYear: true};")
1087 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, selectOtherMonths: true, changeMonth: true, changeYear: true};")
1084 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1088 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1085 unless jquery_locale == 'en'
1089 unless jquery_locale == 'en'
1086 tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1090 tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1087 end
1091 end
1088 tags
1092 tags
1089 end
1093 end
1090 end
1094 end
1091 end
1095 end
1092
1096
1093 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1097 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1094 # Examples:
1098 # Examples:
1095 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1099 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1096 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1100 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1097 #
1101 #
1098 def stylesheet_link_tag(*sources)
1102 def stylesheet_link_tag(*sources)
1099 options = sources.last.is_a?(Hash) ? sources.pop : {}
1103 options = sources.last.is_a?(Hash) ? sources.pop : {}
1100 plugin = options.delete(:plugin)
1104 plugin = options.delete(:plugin)
1101 sources = sources.map do |source|
1105 sources = sources.map do |source|
1102 if plugin
1106 if plugin
1103 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1107 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1104 elsif current_theme && current_theme.stylesheets.include?(source)
1108 elsif current_theme && current_theme.stylesheets.include?(source)
1105 current_theme.stylesheet_path(source)
1109 current_theme.stylesheet_path(source)
1106 else
1110 else
1107 source
1111 source
1108 end
1112 end
1109 end
1113 end
1110 super sources, options
1114 super sources, options
1111 end
1115 end
1112
1116
1113 # Overrides Rails' image_tag with themes and plugins support.
1117 # Overrides Rails' image_tag with themes and plugins support.
1114 # Examples:
1118 # Examples:
1115 # image_tag('image.png') # => picks image.png from the current theme or defaults
1119 # image_tag('image.png') # => picks image.png from the current theme or defaults
1116 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1120 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1117 #
1121 #
1118 def image_tag(source, options={})
1122 def image_tag(source, options={})
1119 if plugin = options.delete(:plugin)
1123 if plugin = options.delete(:plugin)
1120 source = "/plugin_assets/#{plugin}/images/#{source}"
1124 source = "/plugin_assets/#{plugin}/images/#{source}"
1121 elsif current_theme && current_theme.images.include?(source)
1125 elsif current_theme && current_theme.images.include?(source)
1122 source = current_theme.image_path(source)
1126 source = current_theme.image_path(source)
1123 end
1127 end
1124 super source, options
1128 super source, options
1125 end
1129 end
1126
1130
1127 # Overrides Rails' javascript_include_tag with plugins support
1131 # Overrides Rails' javascript_include_tag with plugins support
1128 # Examples:
1132 # Examples:
1129 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1133 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1130 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1134 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1131 #
1135 #
1132 def javascript_include_tag(*sources)
1136 def javascript_include_tag(*sources)
1133 options = sources.last.is_a?(Hash) ? sources.pop : {}
1137 options = sources.last.is_a?(Hash) ? sources.pop : {}
1134 if plugin = options.delete(:plugin)
1138 if plugin = options.delete(:plugin)
1135 sources = sources.map do |source|
1139 sources = sources.map do |source|
1136 if plugin
1140 if plugin
1137 "/plugin_assets/#{plugin}/javascripts/#{source}"
1141 "/plugin_assets/#{plugin}/javascripts/#{source}"
1138 else
1142 else
1139 source
1143 source
1140 end
1144 end
1141 end
1145 end
1142 end
1146 end
1143 super sources, options
1147 super sources, options
1144 end
1148 end
1145
1149
1146 def content_for(name, content = nil, &block)
1150 def content_for(name, content = nil, &block)
1147 @has_content ||= {}
1151 @has_content ||= {}
1148 @has_content[name] = true
1152 @has_content[name] = true
1149 super(name, content, &block)
1153 super(name, content, &block)
1150 end
1154 end
1151
1155
1152 def has_content?(name)
1156 def has_content?(name)
1153 (@has_content && @has_content[name]) || false
1157 (@has_content && @has_content[name]) || false
1154 end
1158 end
1155
1159
1156 def sidebar_content?
1160 def sidebar_content?
1157 has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1161 has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1158 end
1162 end
1159
1163
1160 def view_layouts_base_sidebar_hook_response
1164 def view_layouts_base_sidebar_hook_response
1161 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1165 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1162 end
1166 end
1163
1167
1164 def email_delivery_enabled?
1168 def email_delivery_enabled?
1165 !!ActionMailer::Base.perform_deliveries
1169 !!ActionMailer::Base.perform_deliveries
1166 end
1170 end
1167
1171
1168 # Returns the avatar image tag for the given +user+ if avatars are enabled
1172 # Returns the avatar image tag for the given +user+ if avatars are enabled
1169 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1173 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1170 def avatar(user, options = { })
1174 def avatar(user, options = { })
1171 if Setting.gravatar_enabled?
1175 if Setting.gravatar_enabled?
1172 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1176 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1173 email = nil
1177 email = nil
1174 if user.respond_to?(:mail)
1178 if user.respond_to?(:mail)
1175 email = user.mail
1179 email = user.mail
1176 elsif user.to_s =~ %r{<(.+?)>}
1180 elsif user.to_s =~ %r{<(.+?)>}
1177 email = $1
1181 email = $1
1178 end
1182 end
1179 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1183 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1180 else
1184 else
1181 ''
1185 ''
1182 end
1186 end
1183 end
1187 end
1184
1188
1185 def sanitize_anchor_name(anchor)
1189 def sanitize_anchor_name(anchor)
1186 if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1190 if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1187 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1191 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1188 else
1192 else
1189 # TODO: remove when ruby1.8 is no longer supported
1193 # TODO: remove when ruby1.8 is no longer supported
1190 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1194 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1191 end
1195 end
1192 end
1196 end
1193
1197
1194 # Returns the javascript tags that are included in the html layout head
1198 # Returns the javascript tags that are included in the html layout head
1195 def javascript_heads
1199 def javascript_heads
1196 tags = javascript_include_tag('jquery-1.8.3-ui-1.9.2-ujs-2.0.3', 'application')
1200 tags = javascript_include_tag('jquery-1.8.3-ui-1.9.2-ujs-2.0.3', 'application')
1197 unless User.current.pref.warn_on_leaving_unsaved == '0'
1201 unless User.current.pref.warn_on_leaving_unsaved == '0'
1198 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1202 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1199 end
1203 end
1200 tags
1204 tags
1201 end
1205 end
1202
1206
1203 def favicon
1207 def favicon
1204 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1208 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1205 end
1209 end
1206
1210
1207 def robot_exclusion_tag
1211 def robot_exclusion_tag
1208 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1212 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1209 end
1213 end
1210
1214
1211 # Returns true if arg is expected in the API response
1215 # Returns true if arg is expected in the API response
1212 def include_in_api_response?(arg)
1216 def include_in_api_response?(arg)
1213 unless @included_in_api_response
1217 unless @included_in_api_response
1214 param = params[:include]
1218 param = params[:include]
1215 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1219 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1216 @included_in_api_response.collect!(&:strip)
1220 @included_in_api_response.collect!(&:strip)
1217 end
1221 end
1218 @included_in_api_response.include?(arg.to_s)
1222 @included_in_api_response.include?(arg.to_s)
1219 end
1223 end
1220
1224
1221 # Returns options or nil if nometa param or X-Redmine-Nometa header
1225 # Returns options or nil if nometa param or X-Redmine-Nometa header
1222 # was set in the request
1226 # was set in the request
1223 def api_meta(options)
1227 def api_meta(options)
1224 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1228 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1225 # compatibility mode for activeresource clients that raise
1229 # compatibility mode for activeresource clients that raise
1226 # an error when unserializing an array with attributes
1230 # an error when unserializing an array with attributes
1227 nil
1231 nil
1228 else
1232 else
1229 options
1233 options
1230 end
1234 end
1231 end
1235 end
1232
1236
1233 private
1237 private
1234
1238
1235 def wiki_helper
1239 def wiki_helper
1236 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1240 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1237 extend helper
1241 extend helper
1238 return self
1242 return self
1239 end
1243 end
1240
1244
1241 def link_to_content_update(text, url_params = {}, html_options = {})
1245 def link_to_content_update(text, url_params = {}, html_options = {})
1242 link_to(text, url_params, html_options)
1246 link_to(text, url_params, html_options)
1243 end
1247 end
1244 end
1248 end
@@ -1,149 +1,149
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 module CustomFieldsHelper
20 module CustomFieldsHelper
21
21
22 def custom_fields_tabs
22 def custom_fields_tabs
23 CustomField::CUSTOM_FIELDS_TABS
23 CustomField::CUSTOM_FIELDS_TABS
24 end
24 end
25
25
26 # Return custom field html tag corresponding to its format
26 # Return custom field html tag corresponding to its format
27 def custom_field_tag(name, custom_value)
27 def custom_field_tag(name, custom_value)
28 custom_field = custom_value.custom_field
28 custom_field = custom_value.custom_field
29 field_name = "#{name}[custom_field_values][#{custom_field.id}]"
29 field_name = "#{name}[custom_field_values][#{custom_field.id}]"
30 field_name << "[]" if custom_field.multiple?
30 field_name << "[]" if custom_field.multiple?
31 field_id = "#{name}_custom_field_values_#{custom_field.id}"
31 field_id = "#{name}_custom_field_values_#{custom_field.id}"
32
32
33 tag_options = {:id => field_id, :class => "#{custom_field.field_format}_cf"}
33 tag_options = {:id => field_id, :class => "#{custom_field.field_format}_cf"}
34
34
35 field_format = Redmine::CustomFieldFormat.find_by_name(custom_field.field_format)
35 field_format = Redmine::CustomFieldFormat.find_by_name(custom_field.field_format)
36 case field_format.try(:edit_as)
36 case field_format.try(:edit_as)
37 when "date"
37 when "date"
38 text_field_tag(field_name, custom_value.value, tag_options.merge(:size => 10)) +
38 text_field_tag(field_name, custom_value.value, tag_options.merge(:size => 10)) +
39 calendar_for(field_id)
39 calendar_for(field_id)
40 when "text"
40 when "text"
41 text_area_tag(field_name, custom_value.value, tag_options.merge(:rows => 3))
41 text_area_tag(field_name, custom_value.value, tag_options.merge(:rows => 3))
42 when "bool"
42 when "bool"
43 hidden_field_tag(field_name, '0') + check_box_tag(field_name, '1', custom_value.true?, tag_options)
43 hidden_field_tag(field_name, '0') + check_box_tag(field_name, '1', custom_value.true?, tag_options)
44 when "list"
44 when "list"
45 blank_option = ''.html_safe
45 blank_option = ''.html_safe
46 unless custom_field.multiple?
46 unless custom_field.multiple?
47 if custom_field.is_required?
47 if custom_field.is_required?
48 unless custom_field.default_value.present?
48 unless custom_field.default_value.present?
49 blank_option = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '')
49 blank_option = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '')
50 end
50 end
51 else
51 else
52 blank_option = content_tag('option')
52 blank_option = content_tag('option')
53 end
53 end
54 end
54 end
55 s = select_tag(field_name, blank_option + options_for_select(custom_field.possible_values_options(custom_value.customized), custom_value.value),
55 s = select_tag(field_name, blank_option + options_for_select(custom_field.possible_values_options(custom_value.customized), custom_value.value),
56 tag_options.merge(:multiple => custom_field.multiple?))
56 tag_options.merge(:multiple => custom_field.multiple?))
57 if custom_field.multiple?
57 if custom_field.multiple?
58 s << hidden_field_tag(field_name, '')
58 s << hidden_field_tag(field_name, '')
59 end
59 end
60 s
60 s
61 else
61 else
62 text_field_tag(field_name, custom_value.value, tag_options)
62 text_field_tag(field_name, custom_value.value, tag_options)
63 end
63 end
64 end
64 end
65
65
66 # Return custom field label tag
66 # Return custom field label tag
67 def custom_field_label_tag(name, custom_value, options={})
67 def custom_field_label_tag(name, custom_value, options={})
68 required = options[:required] || custom_value.custom_field.is_required?
68 required = options[:required] || custom_value.custom_field.is_required?
69
69
70 content_tag "label", h(custom_value.custom_field.name) +
70 content_tag "label", h(custom_value.custom_field.name) +
71 (required ? " <span class=\"required\">*</span>".html_safe : ""),
71 (required ? " <span class=\"required\">*</span>".html_safe : ""),
72 :for => "#{name}_custom_field_values_#{custom_value.custom_field.id}"
72 :for => "#{name}_custom_field_values_#{custom_value.custom_field.id}"
73 end
73 end
74
74
75 # Return custom field tag with its label tag
75 # Return custom field tag with its label tag
76 def custom_field_tag_with_label(name, custom_value, options={})
76 def custom_field_tag_with_label(name, custom_value, options={})
77 custom_field_label_tag(name, custom_value, options) + custom_field_tag(name, custom_value)
77 custom_field_label_tag(name, custom_value, options) + custom_field_tag(name, custom_value)
78 end
78 end
79
79
80 def custom_field_tag_for_bulk_edit(name, custom_field, projects=nil)
80 def custom_field_tag_for_bulk_edit(name, custom_field, projects=nil, value=nil)
81 field_name = "#{name}[custom_field_values][#{custom_field.id}]"
81 field_name = "#{name}[custom_field_values][#{custom_field.id}]"
82 field_name << "[]" if custom_field.multiple?
82 field_name << "[]" if custom_field.multiple?
83 field_id = "#{name}_custom_field_values_#{custom_field.id}"
83 field_id = "#{name}_custom_field_values_#{custom_field.id}"
84
84
85 tag_options = {:id => field_id, :class => "#{custom_field.field_format}_cf"}
85 tag_options = {:id => field_id, :class => "#{custom_field.field_format}_cf"}
86
86
87 field_format = Redmine::CustomFieldFormat.find_by_name(custom_field.field_format)
87 field_format = Redmine::CustomFieldFormat.find_by_name(custom_field.field_format)
88 case field_format.try(:edit_as)
88 case field_format.try(:edit_as)
89 when "date"
89 when "date"
90 text_field_tag(field_name, '', tag_options.merge(:size => 10)) +
90 text_field_tag(field_name, value, tag_options.merge(:size => 10)) +
91 calendar_for(field_id)
91 calendar_for(field_id)
92 when "text"
92 when "text"
93 text_area_tag(field_name, '', tag_options.merge(:rows => 3))
93 text_area_tag(field_name, value, tag_options.merge(:rows => 3))
94 when "bool"
94 when "bool"
95 select_tag(field_name, options_for_select([[l(:label_no_change_option), ''],
95 select_tag(field_name, options_for_select([[l(:label_no_change_option), ''],
96 [l(:general_text_yes), '1'],
96 [l(:general_text_yes), '1'],
97 [l(:general_text_no), '0']]), tag_options)
97 [l(:general_text_no), '0']], value), tag_options)
98 when "list"
98 when "list"
99 options = []
99 options = []
100 options << [l(:label_no_change_option), ''] unless custom_field.multiple?
100 options << [l(:label_no_change_option), ''] unless custom_field.multiple?
101 options << [l(:label_none), '__none__'] unless custom_field.is_required?
101 options << [l(:label_none), '__none__'] unless custom_field.is_required?
102 options += custom_field.possible_values_options(projects)
102 options += custom_field.possible_values_options(projects)
103 select_tag(field_name, options_for_select(options), tag_options.merge(:multiple => custom_field.multiple?))
103 select_tag(field_name, options_for_select(options, value), tag_options.merge(:multiple => custom_field.multiple?))
104 else
104 else
105 text_field_tag(field_name, '', tag_options)
105 text_field_tag(field_name, value, tag_options)
106 end
106 end
107 end
107 end
108
108
109 # Return a string used to display a custom value
109 # Return a string used to display a custom value
110 def show_value(custom_value)
110 def show_value(custom_value)
111 return "" unless custom_value
111 return "" unless custom_value
112 format_value(custom_value.value, custom_value.custom_field.field_format)
112 format_value(custom_value.value, custom_value.custom_field.field_format)
113 end
113 end
114
114
115 # Return a string used to display a custom value
115 # Return a string used to display a custom value
116 def format_value(value, field_format)
116 def format_value(value, field_format)
117 if value.is_a?(Array)
117 if value.is_a?(Array)
118 value.collect {|v| format_value(v, field_format)}.compact.sort.join(', ')
118 value.collect {|v| format_value(v, field_format)}.compact.sort.join(', ')
119 else
119 else
120 Redmine::CustomFieldFormat.format_value(value, field_format)
120 Redmine::CustomFieldFormat.format_value(value, field_format)
121 end
121 end
122 end
122 end
123
123
124 # Return an array of custom field formats which can be used in select_tag
124 # Return an array of custom field formats which can be used in select_tag
125 def custom_field_formats_for_select(custom_field)
125 def custom_field_formats_for_select(custom_field)
126 Redmine::CustomFieldFormat.as_select(custom_field.class.customized_class.name)
126 Redmine::CustomFieldFormat.as_select(custom_field.class.customized_class.name)
127 end
127 end
128
128
129 # Renders the custom_values in api views
129 # Renders the custom_values in api views
130 def render_api_custom_values(custom_values, api)
130 def render_api_custom_values(custom_values, api)
131 api.array :custom_fields do
131 api.array :custom_fields do
132 custom_values.each do |custom_value|
132 custom_values.each do |custom_value|
133 attrs = {:id => custom_value.custom_field_id, :name => custom_value.custom_field.name}
133 attrs = {:id => custom_value.custom_field_id, :name => custom_value.custom_field.name}
134 attrs.merge!(:multiple => true) if custom_value.custom_field.multiple?
134 attrs.merge!(:multiple => true) if custom_value.custom_field.multiple?
135 api.custom_field attrs do
135 api.custom_field attrs do
136 if custom_value.value.is_a?(Array)
136 if custom_value.value.is_a?(Array)
137 api.array :value do
137 api.array :value do
138 custom_value.value.each do |value|
138 custom_value.value.each do |value|
139 api.value value unless value.blank?
139 api.value value unless value.blank?
140 end
140 end
141 end
141 end
142 else
142 else
143 api.value custom_value.value
143 api.value custom_value.value
144 end
144 end
145 end
145 end
146 end
146 end
147 end unless custom_values.empty?
147 end unless custom_values.empty?
148 end
148 end
149 end
149 end
@@ -1,83 +1,84
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 module ProjectsHelper
20 module ProjectsHelper
21 def link_to_version(version, options = {})
21 def link_to_version(version, options = {})
22 return '' unless version && version.is_a?(Version)
22 return '' unless version && version.is_a?(Version)
23 link_to_if version.visible?, format_version_name(version), { :controller => 'versions', :action => 'show', :id => version }, options
23 link_to_if version.visible?, format_version_name(version), { :controller => 'versions', :action => 'show', :id => version }, options
24 end
24 end
25
25
26 def project_settings_tabs
26 def project_settings_tabs
27 tabs = [{:name => 'info', :action => :edit_project, :partial => 'projects/edit', :label => :label_information_plural},
27 tabs = [{:name => 'info', :action => :edit_project, :partial => 'projects/edit', :label => :label_information_plural},
28 {:name => 'modules', :action => :select_project_modules, :partial => 'projects/settings/modules', :label => :label_module_plural},
28 {:name => 'modules', :action => :select_project_modules, :partial => 'projects/settings/modules', :label => :label_module_plural},
29 {:name => 'members', :action => :manage_members, :partial => 'projects/settings/members', :label => :label_member_plural},
29 {:name => 'members', :action => :manage_members, :partial => 'projects/settings/members', :label => :label_member_plural},
30 {:name => 'versions', :action => :manage_versions, :partial => 'projects/settings/versions', :label => :label_version_plural},
30 {:name => 'versions', :action => :manage_versions, :partial => 'projects/settings/versions', :label => :label_version_plural},
31 {:name => 'categories', :action => :manage_categories, :partial => 'projects/settings/issue_categories', :label => :label_issue_category_plural},
31 {:name => 'categories', :action => :manage_categories, :partial => 'projects/settings/issue_categories', :label => :label_issue_category_plural},
32 {:name => 'wiki', :action => :manage_wiki, :partial => 'projects/settings/wiki', :label => :label_wiki},
32 {:name => 'wiki', :action => :manage_wiki, :partial => 'projects/settings/wiki', :label => :label_wiki},
33 {:name => 'repositories', :action => :manage_repository, :partial => 'projects/settings/repositories', :label => :label_repository_plural},
33 {:name => 'repositories', :action => :manage_repository, :partial => 'projects/settings/repositories', :label => :label_repository_plural},
34 {:name => 'boards', :action => :manage_boards, :partial => 'projects/settings/boards', :label => :label_board_plural},
34 {:name => 'boards', :action => :manage_boards, :partial => 'projects/settings/boards', :label => :label_board_plural},
35 {:name => 'activities', :action => :manage_project_activities, :partial => 'projects/settings/activities', :label => :enumeration_activities}
35 {:name => 'activities', :action => :manage_project_activities, :partial => 'projects/settings/activities', :label => :enumeration_activities}
36 ]
36 ]
37 tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)}
37 tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)}
38 end
38 end
39
39
40 def parent_project_select_tag(project)
40 def parent_project_select_tag(project)
41 selected = project.parent
41 selected = project.parent
42 # retrieve the requested parent project
42 # retrieve the requested parent project
43 parent_id = (params[:project] && params[:project][:parent_id]) || params[:parent_id]
43 parent_id = (params[:project] && params[:project][:parent_id]) || params[:parent_id]
44 if parent_id
44 if parent_id
45 selected = (parent_id.blank? ? nil : Project.find(parent_id))
45 selected = (parent_id.blank? ? nil : Project.find(parent_id))
46 end
46 end
47
47
48 options = ''
48 options = ''
49 options << "<option value=''></option>" if project.allowed_parents.include?(nil)
49 options << "<option value=''></option>" if project.allowed_parents.include?(nil)
50 options << project_tree_options_for_select(project.allowed_parents.compact, :selected => selected)
50 options << project_tree_options_for_select(project.allowed_parents.compact, :selected => selected)
51 content_tag('select', options.html_safe, :name => 'project[parent_id]', :id => 'project_parent_id')
51 content_tag('select', options.html_safe, :name => 'project[parent_id]', :id => 'project_parent_id')
52 end
52 end
53
53
54 # Renders the projects index
54 # Renders the projects index
55 def render_project_hierarchy(projects)
55 def render_project_hierarchy(projects)
56 render_project_nested_lists(projects) do |project|
56 render_project_nested_lists(projects) do |project|
57 s = link_to_project(project, {}, :class => "#{project.css_classes} #{User.current.member_of?(project) ? 'my-project' : nil}")
57 s = link_to_project(project, {}, :class => "#{project.css_classes} #{User.current.member_of?(project) ? 'my-project' : nil}")
58 if project.description.present?
58 if project.description.present?
59 s << content_tag('div', textilizable(project.short_description, :project => project), :class => 'wiki description')
59 s << content_tag('div', textilizable(project.short_description, :project => project), :class => 'wiki description')
60 end
60 end
61 s
61 s
62 end
62 end
63 end
63 end
64
64
65 # Returns a set of options for a select field, grouped by project.
65 # Returns a set of options for a select field, grouped by project.
66 def version_options_for_select(versions, selected=nil)
66 def version_options_for_select(versions, selected=nil)
67 grouped = Hash.new {|h,k| h[k] = []}
67 grouped = Hash.new {|h,k| h[k] = []}
68 versions.each do |version|
68 versions.each do |version|
69 grouped[version.project.name] << [version.name, version.id]
69 grouped[version.project.name] << [version.name, version.id]
70 end
70 end
71
71
72 selected = selected.is_a?(Version) ? selected.id : selected
72 if grouped.keys.size > 1
73 if grouped.keys.size > 1
73 grouped_options_for_select(grouped, selected && selected.id)
74 grouped_options_for_select(grouped, selected)
74 else
75 else
75 options_for_select((grouped.values.first || []), selected && selected.id)
76 options_for_select((grouped.values.first || []), selected)
76 end
77 end
77 end
78 end
78
79
79 def format_version_sharing(sharing)
80 def format_version_sharing(sharing)
80 sharing = 'none' unless Version::VERSION_SHARINGS.include?(sharing)
81 sharing = 'none' unless Version::VERSION_SHARINGS.include?(sharing)
81 l("label_version_sharing_#{sharing}")
82 l("label_version_sharing_#{sharing}")
82 end
83 end
83 end
84 end
@@ -1,166 +1,180
1 <h2><%= @copy ? l(:button_copy) : l(:label_bulk_edit_selected_issues) %></h2>
1 <h2><%= @copy ? l(:button_copy) : l(:label_bulk_edit_selected_issues) %></h2>
2
2
3 <% if @saved_issues && @unsaved_issues.present? %>
3 <% if @saved_issues && @unsaved_issues.present? %>
4 <div id="errorExplanation">
4 <div id="errorExplanation">
5 <span>
5 <span>
6 <%= l(:notice_failed_to_save_issues,
6 <%= l(:notice_failed_to_save_issues,
7 :count => @unsaved_issues.size,
7 :count => @unsaved_issues.size,
8 :total => @saved_issues.size,
8 :total => @saved_issues.size,
9 :ids => @unsaved_issues.map {|i| "##{i.id}"}.join(', ')) %>
9 :ids => @unsaved_issues.map {|i| "##{i.id}"}.join(', ')) %>
10 </span>
10 </span>
11 <ul>
11 <ul>
12 <% bulk_edit_error_messages(@unsaved_issues).each do |message| %>
12 <% bulk_edit_error_messages(@unsaved_issues).each do |message| %>
13 <li><%= message %></li>
13 <li><%= message %></li>
14 <% end %>
14 <% end %>
15 </ul>
15 </ul>
16 </div>
16 </div>
17 <% end %>
17 <% end %>
18
18
19 <ul id="bulk-selection">
19 <ul id="bulk-selection">
20 <% @issues.each do |issue| %>
20 <% @issues.each do |issue| %>
21 <%= content_tag 'li', link_to_issue(issue) %>
21 <%= content_tag 'li', link_to_issue(issue) %>
22 <% end %>
22 <% end %>
23 </ul>
23 </ul>
24
24
25 <%= form_tag(bulk_update_issues_path, :id => 'bulk_edit_form') do %>
25 <%= form_tag(bulk_update_issues_path, :id => 'bulk_edit_form') do %>
26 <%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join("\n").html_safe %>
26 <%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join("\n").html_safe %>
27 <div class="box tabular">
27 <div class="box tabular">
28 <fieldset class="attributes">
28 <fieldset class="attributes">
29 <legend><%= l(:label_change_properties) %></legend>
29 <legend><%= l(:label_change_properties) %></legend>
30
30
31 <div class="splitcontentleft">
31 <div class="splitcontentleft">
32 <% if @allowed_projects.present? %>
32 <% if @allowed_projects.present? %>
33 <p>
33 <p>
34 <label for="issue_project_id"><%= l(:field_project) %></label>
34 <label for="issue_project_id"><%= l(:field_project) %></label>
35 <%= select_tag('issue[project_id]', content_tag('option', l(:label_no_change_option), :value => '') + project_tree_options_for_select(@allowed_projects, :selected => @target_project),
35 <%= select_tag('issue[project_id]',
36 content_tag('option', l(:label_no_change_option), :value => '') +
37 project_tree_options_for_select(@allowed_projects, :selected => @target_project),
36 :onchange => "updateBulkEditFrom('#{escape_javascript url_for(:action => 'bulk_edit', :format => 'js')}')") %>
38 :onchange => "updateBulkEditFrom('#{escape_javascript url_for(:action => 'bulk_edit', :format => 'js')}')") %>
37 </p>
39 </p>
38 <% end %>
40 <% end %>
39 <p>
41 <p>
40 <label for="issue_tracker_id"><%= l(:field_tracker) %></label>
42 <label for="issue_tracker_id"><%= l(:field_tracker) %></label>
41 <%= select_tag('issue[tracker_id]', content_tag('option', l(:label_no_change_option), :value => '') + options_from_collection_for_select(@trackers, :id, :name)) %>
43 <%= select_tag('issue[tracker_id]',
44 content_tag('option', l(:label_no_change_option), :value => '') +
45 options_from_collection_for_select(@trackers, :id, :name, @issue_params[:tracker_id])) %>
42 </p>
46 </p>
43 <% if @available_statuses.any? %>
47 <% if @available_statuses.any? %>
44 <p>
48 <p>
45 <label for='issue_status_id'><%= l(:field_status) %></label>
49 <label for='issue_status_id'><%= l(:field_status) %></label>
46 <%= select_tag('issue[status_id]',content_tag('option', l(:label_no_change_option), :value => '') + options_from_collection_for_select(@available_statuses, :id, :name)) %>
50 <%= select_tag('issue[status_id]',
51 content_tag('option', l(:label_no_change_option), :value => '') +
52 options_from_collection_for_select(@available_statuses, :id, :name, @issue_params[:status_id])) %>
47 </p>
53 </p>
48 <% end %>
54 <% end %>
49
55
50 <% if @safe_attributes.include?('priority_id') -%>
56 <% if @safe_attributes.include?('priority_id') -%>
51 <p>
57 <p>
52 <label for='issue_priority_id'><%= l(:field_priority) %></label>
58 <label for='issue_priority_id'><%= l(:field_priority) %></label>
53 <%= select_tag('issue[priority_id]', content_tag('option', l(:label_no_change_option), :value => '') + options_from_collection_for_select(IssuePriority.active, :id, :name)) %>
59 <%= select_tag('issue[priority_id]',
60 content_tag('option', l(:label_no_change_option), :value => '') +
61 options_from_collection_for_select(IssuePriority.active, :id, :name, @issue_params[:priority_id])) %>
54 </p>
62 </p>
55 <% end %>
63 <% end %>
56
64
57 <% if @safe_attributes.include?('assigned_to_id') -%>
65 <% if @safe_attributes.include?('assigned_to_id') -%>
58 <p>
66 <p>
59 <label for='issue_assigned_to_id'><%= l(:field_assigned_to) %></label>
67 <label for='issue_assigned_to_id'><%= l(:field_assigned_to) %></label>
60 <%= select_tag('issue[assigned_to_id]', content_tag('option', l(:label_no_change_option), :value => '') +
68 <%= select_tag('issue[assigned_to_id]',
61 content_tag('option', l(:label_nobody), :value => 'none') +
69 content_tag('option', l(:label_no_change_option), :value => '') +
62 principals_options_for_select(@assignables)) %>
70 content_tag('option', l(:label_nobody), :value => 'none', :selected => (@issue_params[:assigned_to_id] == 'none')) +
71 principals_options_for_select(@assignables, @issue_params[:assigned_to_id])) %>
63 </p>
72 </p>
64 <% end %>
73 <% end %>
65
74
66 <% if @safe_attributes.include?('category_id') -%>
75 <% if @safe_attributes.include?('category_id') -%>
67 <p>
76 <p>
68 <label for='issue_category_id'><%= l(:field_category) %></label>
77 <label for='issue_category_id'><%= l(:field_category) %></label>
69 <%= select_tag('issue[category_id]', content_tag('option', l(:label_no_change_option), :value => '') +
78 <%= select_tag('issue[category_id]', content_tag('option', l(:label_no_change_option), :value => '') +
70 content_tag('option', l(:label_none), :value => 'none') +
79 content_tag('option', l(:label_none), :value => 'none', :selected => (@issue_params[:category_id] == 'none')) +
71 options_from_collection_for_select(@categories, :id, :name)) %>
80 options_from_collection_for_select(@categories, :id, :name, @issue_params[:category_id])) %>
72 </p>
81 </p>
73 <% end %>
82 <% end %>
74
83
75 <% if @safe_attributes.include?('fixed_version_id') -%>
84 <% if @safe_attributes.include?('fixed_version_id') -%>
76 <p>
85 <p>
77 <label for='issue_fixed_version_id'><%= l(:field_fixed_version) %></label>
86 <label for='issue_fixed_version_id'><%= l(:field_fixed_version) %></label>
78 <%= select_tag('issue[fixed_version_id]', content_tag('option', l(:label_no_change_option), :value => '') +
87 <%= select_tag('issue[fixed_version_id]', content_tag('option', l(:label_no_change_option), :value => '') +
79 content_tag('option', l(:label_none), :value => 'none') +
88 content_tag('option', l(:label_none), :value => 'none', :selected => (@issue_params[:fixed_version_id] == 'none')) +
80 version_options_for_select(@versions.sort)) %>
89 version_options_for_select(@versions.sort, @issue_params[:fixed_version_id])) %>
81 </p>
90 </p>
82 <% end %>
91 <% end %>
83
92
84 <% @custom_fields.each do |custom_field| %>
93 <% @custom_fields.each do |custom_field| %>
85 <p><label><%= h(custom_field.name) %></label> <%= custom_field_tag_for_bulk_edit('issue', custom_field, @projects) %></p>
94 <p>
95 <label><%= h(custom_field.name) %></label>
96 <%= custom_field_tag_for_bulk_edit('issue', custom_field, @projects, @issue_params[:custom_field_values][custom_field.id.to_s]) %>
97 </p>
86 <% end %>
98 <% end %>
87
99
88 <% if @copy && @attachments_present %>
100 <% if @copy && @attachments_present %>
101 <%= hidden_field_tag 'copy_attachments', '0' %>
89 <p>
102 <p>
90 <label for='copy_attachments'><%= l(:label_copy_attachments) %></label>
103 <label for='copy_attachments'><%= l(:label_copy_attachments) %></label>
91 <%= check_box_tag 'copy_attachments', '1', true %>
104 <%= check_box_tag 'copy_attachments', '1', params[:copy_attachments] != '0' %>
92 </p>
105 </p>
93 <% end %>
106 <% end %>
94
107
95 <% if @copy && @subtasks_present %>
108 <% if @copy && @subtasks_present %>
109 <%= hidden_field_tag 'copy_subtasks', '0' %>
96 <p>
110 <p>
97 <label for='copy_subtasks'><%= l(:label_copy_subtasks) %></label>
111 <label for='copy_subtasks'><%= l(:label_copy_subtasks) %></label>
98 <%= check_box_tag 'copy_subtasks', '1', true %>
112 <%= check_box_tag 'copy_subtasks', '1', params[:copy_subtasks] != '0' %>
99 </p>
113 </p>
100 <% end %>
114 <% end %>
101
115
102 <%= call_hook(:view_issues_bulk_edit_details_bottom, { :issues => @issues }) %>
116 <%= call_hook(:view_issues_bulk_edit_details_bottom, { :issues => @issues }) %>
103 </div>
117 </div>
104
118
105 <div class="splitcontentright">
119 <div class="splitcontentright">
106 <% if @safe_attributes.include?('is_private') %>
120 <% if @safe_attributes.include?('is_private') %>
107 <p>
121 <p>
108 <label for='issue_is_private'><%= l(:field_is_private) %></label>
122 <label for='issue_is_private'><%= l(:field_is_private) %></label>
109 <%= select_tag('issue[is_private]', content_tag('option', l(:label_no_change_option), :value => '') +
123 <%= select_tag('issue[is_private]', content_tag('option', l(:label_no_change_option), :value => '') +
110 content_tag('option', l(:general_text_Yes), :value => '1') +
124 content_tag('option', l(:general_text_Yes), :value => '1', :selected => (@issue_params[:is_private] == '1')) +
111 content_tag('option', l(:general_text_No), :value => '0')) %>
125 content_tag('option', l(:general_text_No), :value => '0', :selected => (@issue_params[:is_private] == '0'))) %>
112 </p>
126 </p>
113 <% end %>
127 <% end %>
114
128
115 <% if @safe_attributes.include?('parent_issue_id') && @project %>
129 <% if @safe_attributes.include?('parent_issue_id') && @project %>
116 <p>
130 <p>
117 <label for='issue_parent_issue_id'><%= l(:field_parent_issue) %></label>
131 <label for='issue_parent_issue_id'><%= l(:field_parent_issue) %></label>
118 <%= text_field_tag 'issue[parent_issue_id]', '', :size => 10 %>
132 <%= text_field_tag 'issue[parent_issue_id]', '', :size => 10, :value => @issue_params[:parent_issue_id] %>
119 </p>
133 </p>
120 <%= javascript_tag "observeAutocompleteField('issue_parent_issue_id', '#{escape_javascript auto_complete_issues_path(:project_id => @project)}')" %>
134 <%= javascript_tag "observeAutocompleteField('issue_parent_issue_id', '#{escape_javascript auto_complete_issues_path(:project_id => @project)}')" %>
121 <% end %>
135 <% end %>
122
136
123 <% if @safe_attributes.include?('start_date') %>
137 <% if @safe_attributes.include?('start_date') %>
124 <p>
138 <p>
125 <label for='issue_start_date'><%= l(:field_start_date) %></label>
139 <label for='issue_start_date'><%= l(:field_start_date) %></label>
126 <%= text_field_tag 'issue[start_date]', '', :size => 10 %><%= calendar_for('issue_start_date') %>
140 <%= text_field_tag 'issue[start_date]', '', :value => @issue_params[:start_date], :size => 10 %><%= calendar_for('issue_start_date') %>
127 </p>
141 </p>
128 <% end %>
142 <% end %>
129
143
130 <% if @safe_attributes.include?('due_date') %>
144 <% if @safe_attributes.include?('due_date') %>
131 <p>
145 <p>
132 <label for='issue_due_date'><%= l(:field_due_date) %></label>
146 <label for='issue_due_date'><%= l(:field_due_date) %></label>
133 <%= text_field_tag 'issue[due_date]', '', :size => 10 %><%= calendar_for('issue_due_date') %>
147 <%= text_field_tag 'issue[due_date]', '', :value => @issue_params[:due_date], :size => 10 %><%= calendar_for('issue_due_date') %>
134 </p>
148 </p>
135 <% end %>
149 <% end %>
136
150
137 <% if @safe_attributes.include?('done_ratio') && Issue.use_field_for_done_ratio? %>
151 <% if @safe_attributes.include?('done_ratio') && Issue.use_field_for_done_ratio? %>
138 <p>
152 <p>
139 <label for='issue_done_ratio'><%= l(:field_done_ratio) %></label>
153 <label for='issue_done_ratio'><%= l(:field_done_ratio) %></label>
140 <%= select_tag 'issue[done_ratio]', options_for_select([[l(:label_no_change_option), '']] + (0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %>
154 <%= select_tag 'issue[done_ratio]', options_for_select([[l(:label_no_change_option), '']] + (0..10).to_a.collect {|r| ["#{r*10} %", r*10] }, @issue_params[:done_ratio]) %>
141 </p>
155 </p>
142 <% end %>
156 <% end %>
143 </div>
157 </div>
144 </fieldset>
158 </fieldset>
145
159
146 <fieldset>
160 <fieldset>
147 <legend><%= l(:field_notes) %></legend>
161 <legend><%= l(:field_notes) %></legend>
148 <%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
162 <%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
149 <%= wikitoolbar_for 'notes' %>
163 <%= wikitoolbar_for 'notes' %>
150 </fieldset>
164 </fieldset>
151 </div>
165 </div>
152
166
153 <p>
167 <p>
154 <% if @copy %>
168 <% if @copy %>
155 <%= hidden_field_tag 'copy', '1' %>
169 <%= hidden_field_tag 'copy', '1' %>
156 <%= submit_tag l(:button_copy) %>
170 <%= submit_tag l(:button_copy) %>
157 <%= submit_tag l(:button_copy_and_follow), :name => 'follow' %>
171 <%= submit_tag l(:button_copy_and_follow), :name => 'follow' %>
158 <% elsif @target_project %>
172 <% elsif @target_project %>
159 <%= submit_tag l(:button_move) %>
173 <%= submit_tag l(:button_move) %>
160 <%= submit_tag l(:button_move_and_follow), :name => 'follow' %>
174 <%= submit_tag l(:button_move_and_follow), :name => 'follow' %>
161 <% else %>
175 <% else %>
162 <%= submit_tag l(:button_submit) %>
176 <%= submit_tag l(:button_submit) %>
163 <% end %>
177 <% end %>
164 </p>
178 </p>
165
179
166 <% end %>
180 <% end %>
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now