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