##// END OF EJS Templates
More readable regexp for parse_redmine_links (#24382)....
Jean-Philippe Lang -
r15770:79c2e4288991
parent child
Show More
@@ -1,1403 +1,1448
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2016 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 include Redmine::SudoMode::Helper
29 29 include Redmine::Themes::Helper
30 30 include Redmine::Hook::Helper
31 31 include Redmine::Helpers::URL
32 32
33 33 extend Forwardable
34 34 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
35 35
36 36 # Return true if user is authorized for controller/action, otherwise false
37 37 def authorize_for(controller, action)
38 38 User.current.allowed_to?({:controller => controller, :action => action}, @project)
39 39 end
40 40
41 41 # Display a link if user is authorized
42 42 #
43 43 # @param [String] name Anchor text (passed to link_to)
44 44 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
45 45 # @param [optional, Hash] html_options Options passed to link_to
46 46 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
47 47 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
48 48 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
49 49 end
50 50
51 51 # Displays a link to user's account page if active
52 52 def link_to_user(user, options={})
53 53 if user.is_a?(User)
54 54 name = h(user.name(options[:format]))
55 55 if user.active? || (User.current.admin? && user.logged?)
56 56 link_to name, user_path(user), :class => user.css_classes
57 57 else
58 58 name
59 59 end
60 60 else
61 61 h(user.to_s)
62 62 end
63 63 end
64 64
65 65 # Displays a link to +issue+ with its subject.
66 66 # Examples:
67 67 #
68 68 # link_to_issue(issue) # => Defect #6: This is the subject
69 69 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
70 70 # link_to_issue(issue, :subject => false) # => Defect #6
71 71 # link_to_issue(issue, :project => true) # => Foo - Defect #6
72 72 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
73 73 #
74 74 def link_to_issue(issue, options={})
75 75 title = nil
76 76 subject = nil
77 77 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
78 78 if options[:subject] == false
79 79 title = issue.subject.truncate(60)
80 80 else
81 81 subject = issue.subject
82 82 if truncate_length = options[:truncate]
83 83 subject = subject.truncate(truncate_length)
84 84 end
85 85 end
86 86 only_path = options[:only_path].nil? ? true : options[:only_path]
87 87 s = link_to(text, issue_url(issue, :only_path => only_path),
88 88 :class => issue.css_classes, :title => title)
89 89 s << h(": #{subject}") if subject
90 90 s = h("#{issue.project} - ") + s if options[:project]
91 91 s
92 92 end
93 93
94 94 # Generates a link to an attachment.
95 95 # Options:
96 96 # * :text - Link text (default to attachment filename)
97 97 # * :download - Force download (default: false)
98 98 def link_to_attachment(attachment, options={})
99 99 text = options.delete(:text) || attachment.filename
100 100 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
101 101 html_options = options.slice!(:only_path)
102 102 options[:only_path] = true unless options.key?(:only_path)
103 103 url = send(route_method, attachment, attachment.filename, options)
104 104 link_to text, url, html_options
105 105 end
106 106
107 107 # Generates a link to a SCM revision
108 108 # Options:
109 109 # * :text - Link text (default to the formatted revision)
110 110 def link_to_revision(revision, repository, options={})
111 111 if repository.is_a?(Project)
112 112 repository = repository.repository
113 113 end
114 114 text = options.delete(:text) || format_revision(revision)
115 115 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
116 116 link_to(
117 117 h(text),
118 118 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
119 119 :title => l(:label_revision_id, format_revision(revision)),
120 120 :accesskey => options[:accesskey]
121 121 )
122 122 end
123 123
124 124 # Generates a link to a message
125 125 def link_to_message(message, options={}, html_options = nil)
126 126 link_to(
127 127 message.subject.truncate(60),
128 128 board_message_url(message.board_id, message.parent_id || message.id, {
129 129 :r => (message.parent_id && message.id),
130 130 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
131 131 :only_path => true
132 132 }.merge(options)),
133 133 html_options
134 134 )
135 135 end
136 136
137 137 # Generates a link to a project if active
138 138 # Examples:
139 139 #
140 140 # link_to_project(project) # => link to the specified project overview
141 141 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
142 142 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
143 143 #
144 144 def link_to_project(project, options={}, html_options = nil)
145 145 if project.archived?
146 146 h(project.name)
147 147 else
148 148 link_to project.name,
149 149 project_url(project, {:only_path => true}.merge(options)),
150 150 html_options
151 151 end
152 152 end
153 153
154 154 # Generates a link to a project settings if active
155 155 def link_to_project_settings(project, options={}, html_options=nil)
156 156 if project.active?
157 157 link_to project.name, settings_project_path(project, options), html_options
158 158 elsif project.archived?
159 159 h(project.name)
160 160 else
161 161 link_to project.name, project_path(project, options), html_options
162 162 end
163 163 end
164 164
165 165 # Generates a link to a version
166 166 def link_to_version(version, options = {})
167 167 return '' unless version && version.is_a?(Version)
168 168 options = {:title => format_date(version.effective_date)}.merge(options)
169 169 link_to_if version.visible?, format_version_name(version), version_path(version), options
170 170 end
171 171
172 172 # Helper that formats object for html or text rendering
173 173 def format_object(object, html=true, &block)
174 174 if block_given?
175 175 object = yield object
176 176 end
177 177 case object.class.name
178 178 when 'Array'
179 179 object.map {|o| format_object(o, html)}.join(', ').html_safe
180 180 when 'Time'
181 181 format_time(object)
182 182 when 'Date'
183 183 format_date(object)
184 184 when 'Fixnum'
185 185 object.to_s
186 186 when 'Float'
187 187 sprintf "%.2f", object
188 188 when 'User'
189 189 html ? link_to_user(object) : object.to_s
190 190 when 'Project'
191 191 html ? link_to_project(object) : object.to_s
192 192 when 'Version'
193 193 html ? link_to_version(object) : object.to_s
194 194 when 'TrueClass'
195 195 l(:general_text_Yes)
196 196 when 'FalseClass'
197 197 l(:general_text_No)
198 198 when 'Issue'
199 199 object.visible? && html ? link_to_issue(object) : "##{object.id}"
200 200 when 'Attachment'
201 201 html ? link_to_attachment(object, :download => true) : object.filename
202 202 when 'CustomValue', 'CustomFieldValue'
203 203 if object.custom_field
204 204 f = object.custom_field.format.formatted_custom_value(self, object, html)
205 205 if f.nil? || f.is_a?(String)
206 206 f
207 207 else
208 208 format_object(f, html, &block)
209 209 end
210 210 else
211 211 object.value.to_s
212 212 end
213 213 else
214 214 html ? h(object) : object.to_s
215 215 end
216 216 end
217 217
218 218 def wiki_page_path(page, options={})
219 219 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
220 220 end
221 221
222 222 def thumbnail_tag(attachment)
223 223 link_to image_tag(thumbnail_path(attachment)),
224 224 named_attachment_path(attachment, attachment.filename),
225 225 :title => attachment.filename
226 226 end
227 227
228 228 def toggle_link(name, id, options={})
229 229 onclick = "$('##{id}').toggle(); "
230 230 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
231 231 onclick << "return false;"
232 232 link_to(name, "#", :onclick => onclick)
233 233 end
234 234
235 235 # Used to format item titles on the activity view
236 236 def format_activity_title(text)
237 237 text
238 238 end
239 239
240 240 def format_activity_day(date)
241 241 date == User.current.today ? l(:label_today).titleize : format_date(date)
242 242 end
243 243
244 244 def format_activity_description(text)
245 245 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
246 246 ).gsub(/[\r\n]+/, "<br />").html_safe
247 247 end
248 248
249 249 def format_version_name(version)
250 250 if version.project == @project
251 251 h(version)
252 252 else
253 253 h("#{version.project} - #{version}")
254 254 end
255 255 end
256 256
257 257 def format_changeset_comments(changeset, options={})
258 258 method = options[:short] ? :short_comments : :comments
259 259 textilizable changeset, method, :formatting => Setting.commit_logs_formatting?
260 260 end
261 261
262 262 def due_date_distance_in_words(date)
263 263 if date
264 264 l((date < User.current.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(User.current.today, date))
265 265 end
266 266 end
267 267
268 268 # Renders a tree of projects as a nested set of unordered lists
269 269 # The given collection may be a subset of the whole project tree
270 270 # (eg. some intermediate nodes are private and can not be seen)
271 271 def render_project_nested_lists(projects, &block)
272 272 s = ''
273 273 if projects.any?
274 274 ancestors = []
275 275 original_project = @project
276 276 projects.sort_by(&:lft).each do |project|
277 277 # set the project environment to please macros.
278 278 @project = project
279 279 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
280 280 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
281 281 else
282 282 ancestors.pop
283 283 s << "</li>"
284 284 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
285 285 ancestors.pop
286 286 s << "</ul></li>\n"
287 287 end
288 288 end
289 289 classes = (ancestors.empty? ? 'root' : 'child')
290 290 s << "<li class='#{classes}'><div class='#{classes}'>"
291 291 s << h(block_given? ? capture(project, &block) : project.name)
292 292 s << "</div>\n"
293 293 ancestors << project
294 294 end
295 295 s << ("</li></ul>\n" * ancestors.size)
296 296 @project = original_project
297 297 end
298 298 s.html_safe
299 299 end
300 300
301 301 def render_page_hierarchy(pages, node=nil, options={})
302 302 content = ''
303 303 if pages[node]
304 304 content << "<ul class=\"pages-hierarchy\">\n"
305 305 pages[node].each do |page|
306 306 content << "<li>"
307 307 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
308 308 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
309 309 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
310 310 content << "</li>\n"
311 311 end
312 312 content << "</ul>\n"
313 313 end
314 314 content.html_safe
315 315 end
316 316
317 317 # Renders flash messages
318 318 def render_flash_messages
319 319 s = ''
320 320 flash.each do |k,v|
321 321 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
322 322 end
323 323 s.html_safe
324 324 end
325 325
326 326 # Renders tabs and their content
327 327 def render_tabs(tabs, selected=params[:tab])
328 328 if tabs.any?
329 329 unless tabs.detect {|tab| tab[:name] == selected}
330 330 selected = nil
331 331 end
332 332 selected ||= tabs.first[:name]
333 333 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
334 334 else
335 335 content_tag 'p', l(:label_no_data), :class => "nodata"
336 336 end
337 337 end
338 338
339 339 # Returns an array of projects that are displayed in the quick-jump box
340 340 def projects_for_jump_box(user=User.current)
341 341 if user.logged?
342 342 user.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
343 343 else
344 344 []
345 345 end
346 346 end
347 347
348 348 def render_projects_for_jump_box(projects, selected=nil)
349 349 s = ''.html_safe
350 350 project_tree(projects) do |project, level|
351 351 padding = level * 16
352 352 text = content_tag('span', project.name, :style => "padding-left:#{padding}px;")
353 353 s << link_to(text, project_path(project, :jump => current_menu_item), :title => project.name, :class => (project == selected ? 'selected' : nil))
354 354 end
355 355 s
356 356 end
357 357
358 358 # Renders the project quick-jump box
359 359 def render_project_jump_box
360 360 projects = projects_for_jump_box(User.current)
361 361 if projects.any?
362 362 text = @project.try(:name) || l(:label_jump_to_a_project)
363 363 trigger = content_tag('span', text, :class => 'drdn-trigger')
364 364 q = text_field_tag('q', '', :id => 'projects-quick-search', :class => 'autocomplete', :data => {:automcomplete_url => projects_path(:format => 'js')})
365 365 content = content_tag('div',
366 366 content_tag('div', q, :class => 'quick-search') +
367 367 content_tag('div', render_projects_for_jump_box(projects, @project), :class => 'drdn-items selection'),
368 368 :class => 'drdn-content'
369 369 )
370 370
371 371 content_tag('span', trigger + content, :id => "project-jump", :class => "drdn")
372 372 end
373 373 end
374 374
375 375 def project_tree_options_for_select(projects, options = {})
376 376 s = ''.html_safe
377 377 if blank_text = options[:include_blank]
378 378 if blank_text == true
379 379 blank_text = '&nbsp;'.html_safe
380 380 end
381 381 s << content_tag('option', blank_text, :value => '')
382 382 end
383 383 project_tree(projects) do |project, level|
384 384 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
385 385 tag_options = {:value => project.id}
386 386 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
387 387 tag_options[:selected] = 'selected'
388 388 else
389 389 tag_options[:selected] = nil
390 390 end
391 391 tag_options.merge!(yield(project)) if block_given?
392 392 s << content_tag('option', name_prefix + h(project), tag_options)
393 393 end
394 394 s.html_safe
395 395 end
396 396
397 397 # Yields the given block for each project with its level in the tree
398 398 #
399 399 # Wrapper for Project#project_tree
400 400 def project_tree(projects, options={}, &block)
401 401 Project.project_tree(projects, options, &block)
402 402 end
403 403
404 404 def principals_check_box_tags(name, principals)
405 405 s = ''
406 406 principals.each do |principal|
407 407 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
408 408 end
409 409 s.html_safe
410 410 end
411 411
412 412 # Returns a string for users/groups option tags
413 413 def principals_options_for_select(collection, selected=nil)
414 414 s = ''
415 415 if collection.include?(User.current)
416 416 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
417 417 end
418 418 groups = ''
419 419 collection.sort.each do |element|
420 420 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
421 421 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
422 422 end
423 423 unless groups.empty?
424 424 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
425 425 end
426 426 s.html_safe
427 427 end
428 428
429 429 def option_tag(name, text, value, selected=nil, options={})
430 430 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
431 431 end
432 432
433 433 def truncate_single_line_raw(string, length)
434 434 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
435 435 end
436 436
437 437 # Truncates at line break after 250 characters or options[:length]
438 438 def truncate_lines(string, options={})
439 439 length = options[:length] || 250
440 440 if string.to_s =~ /\A(.{#{length}}.*?)$/m
441 441 "#{$1}..."
442 442 else
443 443 string
444 444 end
445 445 end
446 446
447 447 def anchor(text)
448 448 text.to_s.gsub(' ', '_')
449 449 end
450 450
451 451 def html_hours(text)
452 452 text.gsub(%r{(\d+)([\.:])(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">\2\3</span>').html_safe
453 453 end
454 454
455 455 def authoring(created, author, options={})
456 456 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
457 457 end
458 458
459 459 def time_tag(time)
460 460 text = distance_of_time_in_words(Time.now, time)
461 461 if @project
462 462 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
463 463 else
464 464 content_tag('abbr', text, :title => format_time(time))
465 465 end
466 466 end
467 467
468 468 def syntax_highlight_lines(name, content)
469 469 lines = []
470 470 syntax_highlight(name, content).each_line { |line| lines << line }
471 471 lines
472 472 end
473 473
474 474 def syntax_highlight(name, content)
475 475 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
476 476 end
477 477
478 478 def to_path_param(path)
479 479 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
480 480 str.blank? ? nil : str
481 481 end
482 482
483 483 def reorder_links(name, url, method = :post)
484 484 # TODO: remove associated styles from application.css too
485 485 ActiveSupport::Deprecation.warn "Application#reorder_links will be removed in Redmine 4."
486 486
487 487 link_to(l(:label_sort_highest),
488 488 url.merge({"#{name}[move_to]" => 'highest'}), :method => method,
489 489 :title => l(:label_sort_highest), :class => 'icon-only icon-move-top') +
490 490 link_to(l(:label_sort_higher),
491 491 url.merge({"#{name}[move_to]" => 'higher'}), :method => method,
492 492 :title => l(:label_sort_higher), :class => 'icon-only icon-move-up') +
493 493 link_to(l(:label_sort_lower),
494 494 url.merge({"#{name}[move_to]" => 'lower'}), :method => method,
495 495 :title => l(:label_sort_lower), :class => 'icon-only icon-move-down') +
496 496 link_to(l(:label_sort_lowest),
497 497 url.merge({"#{name}[move_to]" => 'lowest'}), :method => method,
498 498 :title => l(:label_sort_lowest), :class => 'icon-only icon-move-bottom')
499 499 end
500 500
501 501 def reorder_handle(object, options={})
502 502 data = {
503 503 :reorder_url => options[:url] || url_for(object),
504 504 :reorder_param => options[:param] || object.class.name.underscore
505 505 }
506 506 content_tag('span', '',
507 507 :class => "sort-handle",
508 508 :data => data,
509 509 :title => l(:button_sort))
510 510 end
511 511
512 512 def breadcrumb(*args)
513 513 elements = args.flatten
514 514 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
515 515 end
516 516
517 517 def other_formats_links(&block)
518 518 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
519 519 yield Redmine::Views::OtherFormatsBuilder.new(self)
520 520 concat('</p>'.html_safe)
521 521 end
522 522
523 523 def page_header_title
524 524 if @project.nil? || @project.new_record?
525 525 h(Setting.app_title)
526 526 else
527 527 b = []
528 528 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
529 529 if ancestors.any?
530 530 root = ancestors.shift
531 531 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
532 532 if ancestors.size > 2
533 533 b << "\xe2\x80\xa6"
534 534 ancestors = ancestors[-2, 2]
535 535 end
536 536 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
537 537 end
538 538 b << content_tag(:span, h(@project), class: 'current-project')
539 539 if b.size > 1
540 540 separator = content_tag(:span, ' &raquo; '.html_safe, class: 'separator')
541 541 path = safe_join(b[0..-2], separator) + separator
542 542 b = [content_tag(:span, path.html_safe, class: 'breadcrumbs'), b[-1]]
543 543 end
544 544 safe_join b
545 545 end
546 546 end
547 547
548 548 # Returns a h2 tag and sets the html title with the given arguments
549 549 def title(*args)
550 550 strings = args.map do |arg|
551 551 if arg.is_a?(Array) && arg.size >= 2
552 552 link_to(*arg)
553 553 else
554 554 h(arg.to_s)
555 555 end
556 556 end
557 557 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
558 558 content_tag('h2', strings.join(' &#187; ').html_safe)
559 559 end
560 560
561 561 # Sets the html title
562 562 # Returns the html title when called without arguments
563 563 # Current project name and app_title and automatically appended
564 564 # Exemples:
565 565 # html_title 'Foo', 'Bar'
566 566 # html_title # => 'Foo - Bar - My Project - Redmine'
567 567 def html_title(*args)
568 568 if args.empty?
569 569 title = @html_title || []
570 570 title << @project.name if @project
571 571 title << Setting.app_title unless Setting.app_title == title.last
572 572 title.reject(&:blank?).join(' - ')
573 573 else
574 574 @html_title ||= []
575 575 @html_title += args
576 576 end
577 577 end
578 578
579 579 # Returns the theme, controller name, and action as css classes for the
580 580 # HTML body.
581 581 def body_css_classes
582 582 css = []
583 583 if theme = Redmine::Themes.theme(Setting.ui_theme)
584 584 css << 'theme-' + theme.name
585 585 end
586 586
587 587 css << 'project-' + @project.identifier if @project && @project.identifier.present?
588 588 css << 'controller-' + controller_name
589 589 css << 'action-' + action_name
590 590 if UserPreference::TEXTAREA_FONT_OPTIONS.include?(User.current.pref.textarea_font)
591 591 css << "textarea-#{User.current.pref.textarea_font}"
592 592 end
593 593 css.join(' ')
594 594 end
595 595
596 596 def accesskey(s)
597 597 @used_accesskeys ||= []
598 598 key = Redmine::AccessKeys.key_for(s)
599 599 return nil if @used_accesskeys.include?(key)
600 600 @used_accesskeys << key
601 601 key
602 602 end
603 603
604 604 # Formats text according to system settings.
605 605 # 2 ways to call this method:
606 606 # * with a String: textilizable(text, options)
607 607 # * with an object and one of its attribute: textilizable(issue, :description, options)
608 608 def textilizable(*args)
609 609 options = args.last.is_a?(Hash) ? args.pop : {}
610 610 case args.size
611 611 when 1
612 612 obj = options[:object]
613 613 text = args.shift
614 614 when 2
615 615 obj = args.shift
616 616 attr = args.shift
617 617 text = obj.send(attr).to_s
618 618 else
619 619 raise ArgumentError, 'invalid arguments to textilizable'
620 620 end
621 621 return '' if text.blank?
622 622 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
623 623 @only_path = only_path = options.delete(:only_path) == false ? false : true
624 624
625 625 text = text.dup
626 626 macros = catch_macros(text)
627 627
628 628 if options[:formatting] == false
629 629 text = h(text)
630 630 else
631 631 formatting = options[:formatting] || Setting.text_formatting
632 632 text = Redmine::WikiFormatting.to_html(formatting, text, :object => obj, :attribute => attr)
633 633 end
634 634
635 635 @parsed_headings = []
636 636 @heading_anchors = {}
637 637 @current_section = 0 if options[:edit_section_links]
638 638
639 639 parse_sections(text, project, obj, attr, only_path, options)
640 640 text = parse_non_pre_blocks(text, obj, macros) do |text|
641 641 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
642 642 send method_name, text, project, obj, attr, only_path, options
643 643 end
644 644 end
645 645 parse_headings(text, project, obj, attr, only_path, options)
646 646
647 647 if @parsed_headings.any?
648 648 replace_toc(text, @parsed_headings)
649 649 end
650 650
651 651 text.html_safe
652 652 end
653 653
654 654 def parse_non_pre_blocks(text, obj, macros)
655 655 s = StringScanner.new(text)
656 656 tags = []
657 657 parsed = ''
658 658 while !s.eos?
659 659 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
660 660 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
661 661 if tags.empty?
662 662 yield text
663 663 inject_macros(text, obj, macros) if macros.any?
664 664 else
665 665 inject_macros(text, obj, macros, false) if macros.any?
666 666 end
667 667 parsed << text
668 668 if tag
669 669 if closing
670 670 if tags.last && tags.last.casecmp(tag) == 0
671 671 tags.pop
672 672 end
673 673 else
674 674 tags << tag.downcase
675 675 end
676 676 parsed << full_tag
677 677 end
678 678 end
679 679 # Close any non closing tags
680 680 while tag = tags.pop
681 681 parsed << "</#{tag}>"
682 682 end
683 683 parsed
684 684 end
685 685
686 686 def parse_inline_attachments(text, project, obj, attr, only_path, options)
687 687 return if options[:inline_attachments] == false
688 688
689 689 # when using an image link, try to use an attachment, if possible
690 690 attachments = options[:attachments] || []
691 691 attachments += obj.attachments if obj.respond_to?(:attachments)
692 692 if attachments.present?
693 693 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
694 694 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
695 695 # search for the picture in attachments
696 696 if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
697 697 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
698 698 desc = found.description.to_s.gsub('"', '')
699 699 if !desc.blank? && alttext.blank?
700 700 alt = " title=\"#{desc}\" alt=\"#{desc}\""
701 701 end
702 702 "src=\"#{image_url}\"#{alt}"
703 703 else
704 704 m
705 705 end
706 706 end
707 707 end
708 708 end
709 709
710 710 # Wiki links
711 711 #
712 712 # Examples:
713 713 # [[mypage]]
714 714 # [[mypage|mytext]]
715 715 # wiki links can refer other project wikis, using project name or identifier:
716 716 # [[project:]] -> wiki starting page
717 717 # [[project:|mytext]]
718 718 # [[project:mypage]]
719 719 # [[project:mypage|mytext]]
720 720 def parse_wiki_links(text, project, obj, attr, only_path, options)
721 721 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
722 722 link_project = project
723 723 esc, all, page, title = $1, $2, $3, $5
724 724 if esc.nil?
725 725 if page =~ /^([^\:]+)\:(.*)$/
726 726 identifier, page = $1, $2
727 727 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
728 728 title ||= identifier if page.blank?
729 729 end
730 730
731 731 if link_project && link_project.wiki
732 732 # extract anchor
733 733 anchor = nil
734 734 if page =~ /^(.+?)\#(.+)$/
735 735 page, anchor = $1, $2
736 736 end
737 737 anchor = sanitize_anchor_name(anchor) if anchor.present?
738 738 # check if page exists
739 739 wiki_page = link_project.wiki.find_page(page)
740 740 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
741 741 "##{anchor}"
742 742 else
743 743 case options[:wiki_links]
744 744 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
745 745 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
746 746 else
747 747 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
748 748 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
749 749 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
750 750 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
751 751 end
752 752 end
753 753 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
754 754 else
755 755 # project or wiki doesn't exist
756 756 all
757 757 end
758 758 else
759 759 all
760 760 end
761 761 end
762 762 end
763 763
764 764 # Redmine links
765 765 #
766 766 # Examples:
767 767 # Issues:
768 768 # #52 -> Link to issue #52
769 769 # Changesets:
770 770 # r52 -> Link to revision 52
771 771 # commit:a85130f -> Link to scmid starting with a85130f
772 772 # Documents:
773 773 # document#17 -> Link to document with id 17
774 774 # document:Greetings -> Link to the document with title "Greetings"
775 775 # document:"Some document" -> Link to the document with title "Some document"
776 776 # Versions:
777 777 # version#3 -> Link to version with id 3
778 778 # version:1.0.0 -> Link to version named "1.0.0"
779 779 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
780 780 # Attachments:
781 781 # attachment:file.zip -> Link to the attachment of the current object named file.zip
782 782 # Source files:
783 783 # source:some/file -> Link to the file located at /some/file in the project's repository
784 784 # source:some/file@52 -> Link to the file's revision 52
785 785 # source:some/file#L120 -> Link to line 120 of the file
786 786 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
787 787 # export:some/file -> Force the download of the file
788 788 # Forum messages:
789 789 # message#1218 -> Link to message with id 1218
790 790 # Projects:
791 791 # project:someproject -> Link to project named "someproject"
792 792 # project#3 -> Link to project with id 3
793 793 #
794 794 # Links can refer other objects from other projects, using project identifier:
795 795 # identifier:r52
796 796 # identifier:document:"Some document"
797 797 # identifier:version:1.0.0
798 798 # identifier:source:some/file
799 799 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
800 text.gsub!(%r{<a( [^>]+?)?>(.*?)</a>|([\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|
801 tag_content, leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $2, $3, $4, $5, $6, $7, $12, $13, $10 || $14 || $20, $16 || $21, $17, $19
800 text.gsub!(LINKS_RE) do |_|
801 tag_content = $~[:tag_content]
802 leading = $~[:leading]
803 esc = $~[:esc]
804 project_prefix = $~[:project_prefix]
805 project_identifier = $~[:project_identifier]
806 prefix = $~[:prefix]
807 repo_prefix = $~[:repo_prefix]
808 repo_identifier = $~[:repo_identifier]
809 sep = $~[:sep1] || $~[:sep2] || $~[:sep3]
810 identifier = $~[:identifier1] || $~[:identifier2]
811 comment_suffix = $~[:comment_suffix]
812 comment_id = $~[:comment_id]
813
802 814 if tag_content
803 815 $&
804 816 else
805 817 link = nil
806 818 project = default_project
807 819 if project_identifier
808 820 project = Project.visible.find_by_identifier(project_identifier)
809 821 end
810 822 if esc.nil?
811 823 if prefix.nil? && sep == 'r'
812 824 if project
813 825 repository = nil
814 826 if repo_identifier
815 827 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
816 828 else
817 829 repository = project.repository
818 830 end
819 831 # project.changesets.visible raises an SQL error because of a double join on repositories
820 832 if repository &&
821 833 (changeset = Changeset.visible.
822 834 find_by_repository_id_and_revision(repository.id, identifier))
823 835 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
824 836 {:only_path => only_path, :controller => 'repositories',
825 837 :action => 'revision', :id => project,
826 838 :repository_id => repository.identifier_param,
827 839 :rev => changeset.revision},
828 840 :class => 'changeset',
829 841 :title => truncate_single_line_raw(changeset.comments, 100))
830 842 end
831 843 end
832 844 elsif sep == '#'
833 845 oid = identifier.to_i
834 846 case prefix
835 847 when nil
836 848 if oid.to_s == identifier &&
837 849 issue = Issue.visible.find_by_id(oid)
838 850 anchor = comment_id ? "note-#{comment_id}" : nil
839 851 link = link_to("##{oid}#{comment_suffix}",
840 852 issue_url(issue, :only_path => only_path, :anchor => anchor),
841 853 :class => issue.css_classes,
842 854 :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
843 855 end
844 856 when 'document'
845 857 if document = Document.visible.find_by_id(oid)
846 858 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
847 859 end
848 860 when 'version'
849 861 if version = Version.visible.find_by_id(oid)
850 862 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
851 863 end
852 864 when 'message'
853 865 if message = Message.visible.find_by_id(oid)
854 866 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
855 867 end
856 868 when 'forum'
857 869 if board = Board.visible.find_by_id(oid)
858 870 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
859 871 end
860 872 when 'news'
861 873 if news = News.visible.find_by_id(oid)
862 874 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
863 875 end
864 876 when 'project'
865 877 if p = Project.visible.find_by_id(oid)
866 878 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
867 879 end
868 880 end
869 881 elsif sep == ':'
870 882 # removes the double quotes if any
871 883 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
872 884 name = CGI.unescapeHTML(name)
873 885 case prefix
874 886 when 'document'
875 887 if project && document = project.documents.visible.find_by_title(name)
876 888 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
877 889 end
878 890 when 'version'
879 891 if project && version = project.versions.visible.find_by_name(name)
880 892 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
881 893 end
882 894 when 'forum'
883 895 if project && board = project.boards.visible.find_by_name(name)
884 896 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
885 897 end
886 898 when 'news'
887 899 if project && news = project.news.visible.find_by_title(name)
888 900 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
889 901 end
890 902 when 'commit', 'source', 'export'
891 903 if project
892 904 repository = nil
893 905 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
894 906 repo_prefix, repo_identifier, name = $1, $2, $3
895 907 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
896 908 else
897 909 repository = project.repository
898 910 end
899 911 if prefix == 'commit'
900 912 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
901 913 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},
902 914 :class => 'changeset',
903 915 :title => truncate_single_line_raw(changeset.comments, 100)
904 916 end
905 917 else
906 918 if repository && User.current.allowed_to?(:browse_repository, project)
907 919 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
908 920 path, rev, anchor = $1, $3, $5
909 921 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
910 922 :path => to_path_param(path),
911 923 :rev => rev,
912 924 :anchor => anchor},
913 925 :class => (prefix == 'export' ? 'source download' : 'source')
914 926 end
915 927 end
916 928 repo_prefix = nil
917 929 end
918 930 when 'attachment'
919 931 attachments = options[:attachments] || []
920 932 attachments += obj.attachments if obj.respond_to?(:attachments)
921 933 if attachments && attachment = Attachment.latest_attach(attachments, name)
922 934 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
923 935 end
924 936 when 'project'
925 937 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
926 938 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
927 939 end
928 940 end
929 941 end
930 942 end
931 943 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
932 944 end
933 945 end
934 946 end
935 947
948 LINKS_RE =
949 %r{
950 <a( [^>]+?)?>(?<tag_content>.*?)</a>|
951 (?<leading>[\s\(,\-\[\>]|^)
952 (?<esc>!)?
953 (?<project_prefix>(?<project_identifier>[a-z0-9\-_]+):)?
954 (?<prefix>attachment|document|version|forum|news|message|project|commit|source|export)?
955 (
956 (
957 (?<sep1>\#)|
958 (
959 (?<repo_prefix>(?<repo_identifier>[a-z0-9\-_]+)\|)?
960 (?<sep2>r)
961 )
962 )
963 (
964 (?<identifier1>\d+)
965 (?<comment_suffix>
966 (\#note)?
967 -(?<comment_id>\d+)
968 )?
969 )|
970 (?<sep3>:)
971 (?<identifier2>[^"\s<>][^\s<>]*?|"[^"]+?")
972 )
973 (?=
974 (?=[[:punct:]][^A-Za-z0-9_/])|
975 ,|
976 \s|
977 \]|
978 <|
979 $)
980 }x
936 981 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
937 982
938 983 def parse_sections(text, project, obj, attr, only_path, options)
939 984 return unless options[:edit_section_links]
940 985 text.gsub!(HEADING_RE) do
941 986 heading, level = $1, $2
942 987 @current_section += 1
943 988 if @current_section > 1
944 989 content_tag('div',
945 990 link_to(l(:button_edit_section), options[:edit_section_links].merge(:section => @current_section),
946 991 :class => 'icon-only icon-edit'),
947 992 :class => "contextual heading-#{level}",
948 993 :title => l(:button_edit_section),
949 994 :id => "section-#{@current_section}") + heading.html_safe
950 995 else
951 996 heading
952 997 end
953 998 end
954 999 end
955 1000
956 1001 # Headings and TOC
957 1002 # Adds ids and links to headings unless options[:headings] is set to false
958 1003 def parse_headings(text, project, obj, attr, only_path, options)
959 1004 return if options[:headings] == false
960 1005
961 1006 text.gsub!(HEADING_RE) do
962 1007 level, attrs, content = $2.to_i, $3, $4
963 1008 item = strip_tags(content).strip
964 1009 anchor = sanitize_anchor_name(item)
965 1010 # used for single-file wiki export
966 1011 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
967 1012 @heading_anchors[anchor] ||= 0
968 1013 idx = (@heading_anchors[anchor] += 1)
969 1014 if idx > 1
970 1015 anchor = "#{anchor}-#{idx}"
971 1016 end
972 1017 @parsed_headings << [level, anchor, item]
973 1018 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
974 1019 end
975 1020 end
976 1021
977 1022 MACROS_RE = /(
978 1023 (!)? # escaping
979 1024 (
980 1025 \{\{ # opening tag
981 1026 ([\w]+) # macro name
982 1027 (\(([^\n\r]*?)\))? # optional arguments
983 1028 ([\n\r].*?[\n\r])? # optional block of text
984 1029 \}\} # closing tag
985 1030 )
986 1031 )/mx unless const_defined?(:MACROS_RE)
987 1032
988 1033 MACRO_SUB_RE = /(
989 1034 \{\{
990 1035 macro\((\d+)\)
991 1036 \}\}
992 1037 )/x unless const_defined?(:MACRO_SUB_RE)
993 1038
994 1039 # Extracts macros from text
995 1040 def catch_macros(text)
996 1041 macros = {}
997 1042 text.gsub!(MACROS_RE) do
998 1043 all, macro = $1, $4.downcase
999 1044 if macro_exists?(macro) || all =~ MACRO_SUB_RE
1000 1045 index = macros.size
1001 1046 macros[index] = all
1002 1047 "{{macro(#{index})}}"
1003 1048 else
1004 1049 all
1005 1050 end
1006 1051 end
1007 1052 macros
1008 1053 end
1009 1054
1010 1055 # Executes and replaces macros in text
1011 1056 def inject_macros(text, obj, macros, execute=true)
1012 1057 text.gsub!(MACRO_SUB_RE) do
1013 1058 all, index = $1, $2.to_i
1014 1059 orig = macros.delete(index)
1015 1060 if execute && orig && orig =~ MACROS_RE
1016 1061 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
1017 1062 if esc.nil?
1018 1063 h(exec_macro(macro, obj, args, block) || all)
1019 1064 else
1020 1065 h(all)
1021 1066 end
1022 1067 elsif orig
1023 1068 h(orig)
1024 1069 else
1025 1070 h(all)
1026 1071 end
1027 1072 end
1028 1073 end
1029 1074
1030 1075 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
1031 1076
1032 1077 # Renders the TOC with given headings
1033 1078 def replace_toc(text, headings)
1034 1079 text.gsub!(TOC_RE) do
1035 1080 left_align, right_align = $2, $3
1036 1081 # Keep only the 4 first levels
1037 1082 headings = headings.select{|level, anchor, item| level <= 4}
1038 1083 if headings.empty?
1039 1084 ''
1040 1085 else
1041 1086 div_class = 'toc'
1042 1087 div_class << ' right' if right_align
1043 1088 div_class << ' left' if left_align
1044 1089 out = "<ul class=\"#{div_class}\"><li><strong>#{l :label_table_of_contents}</strong></li><li>"
1045 1090 root = headings.map(&:first).min
1046 1091 current = root
1047 1092 started = false
1048 1093 headings.each do |level, anchor, item|
1049 1094 if level > current
1050 1095 out << '<ul><li>' * (level - current)
1051 1096 elsif level < current
1052 1097 out << "</li></ul>\n" * (current - level) + "</li><li>"
1053 1098 elsif started
1054 1099 out << '</li><li>'
1055 1100 end
1056 1101 out << "<a href=\"##{anchor}\">#{item}</a>"
1057 1102 current = level
1058 1103 started = true
1059 1104 end
1060 1105 out << '</li></ul>' * (current - root)
1061 1106 out << '</li></ul>'
1062 1107 end
1063 1108 end
1064 1109 end
1065 1110
1066 1111 # Same as Rails' simple_format helper without using paragraphs
1067 1112 def simple_format_without_paragraph(text)
1068 1113 text.to_s.
1069 1114 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1070 1115 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1071 1116 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1072 1117 html_safe
1073 1118 end
1074 1119
1075 1120 def lang_options_for_select(blank=true)
1076 1121 (blank ? [["(auto)", ""]] : []) + languages_options
1077 1122 end
1078 1123
1079 1124 def labelled_form_for(*args, &proc)
1080 1125 args << {} unless args.last.is_a?(Hash)
1081 1126 options = args.last
1082 1127 if args.first.is_a?(Symbol)
1083 1128 options.merge!(:as => args.shift)
1084 1129 end
1085 1130 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1086 1131 form_for(*args, &proc)
1087 1132 end
1088 1133
1089 1134 def labelled_fields_for(*args, &proc)
1090 1135 args << {} unless args.last.is_a?(Hash)
1091 1136 options = args.last
1092 1137 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1093 1138 fields_for(*args, &proc)
1094 1139 end
1095 1140
1096 1141 # Render the error messages for the given objects
1097 1142 def error_messages_for(*objects)
1098 1143 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1099 1144 errors = objects.map {|o| o.errors.full_messages}.flatten
1100 1145 render_error_messages(errors)
1101 1146 end
1102 1147
1103 1148 # Renders a list of error messages
1104 1149 def render_error_messages(errors)
1105 1150 html = ""
1106 1151 if errors.present?
1107 1152 html << "<div id='errorExplanation'><ul>\n"
1108 1153 errors.each do |error|
1109 1154 html << "<li>#{h error}</li>\n"
1110 1155 end
1111 1156 html << "</ul></div>\n"
1112 1157 end
1113 1158 html.html_safe
1114 1159 end
1115 1160
1116 1161 def delete_link(url, options={})
1117 1162 options = {
1118 1163 :method => :delete,
1119 1164 :data => {:confirm => l(:text_are_you_sure)},
1120 1165 :class => 'icon icon-del'
1121 1166 }.merge(options)
1122 1167
1123 1168 link_to l(:button_delete), url, options
1124 1169 end
1125 1170
1126 1171 def preview_link(url, form, target='preview', options={})
1127 1172 content_tag 'a', l(:label_preview), {
1128 1173 :href => "#",
1129 1174 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1130 1175 :accesskey => accesskey(:preview)
1131 1176 }.merge(options)
1132 1177 end
1133 1178
1134 1179 def link_to_function(name, function, html_options={})
1135 1180 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1136 1181 end
1137 1182
1138 1183 # Helper to render JSON in views
1139 1184 def raw_json(arg)
1140 1185 arg.to_json.to_s.gsub('/', '\/').html_safe
1141 1186 end
1142 1187
1143 1188 def back_url
1144 1189 url = params[:back_url]
1145 1190 if url.nil? && referer = request.env['HTTP_REFERER']
1146 1191 url = CGI.unescape(referer.to_s)
1147 1192 # URLs that contains the utf8=[checkmark] parameter added by Rails are
1148 1193 # parsed as invalid by URI.parse so the redirect to the back URL would
1149 1194 # not be accepted (ApplicationController#validate_back_url would return
1150 1195 # false)
1151 1196 url.gsub!(/(\?|&)utf8=\u2713&?/, '\1')
1152 1197 end
1153 1198 url
1154 1199 end
1155 1200
1156 1201 def back_url_hidden_field_tag
1157 1202 url = back_url
1158 1203 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1159 1204 end
1160 1205
1161 1206 def check_all_links(form_name)
1162 1207 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1163 1208 " | ".html_safe +
1164 1209 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1165 1210 end
1166 1211
1167 1212 def toggle_checkboxes_link(selector)
1168 1213 link_to_function '',
1169 1214 "toggleCheckboxesBySelector('#{selector}')",
1170 1215 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}",
1171 1216 :class => 'toggle-checkboxes'
1172 1217 end
1173 1218
1174 1219 def progress_bar(pcts, options={})
1175 1220 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1176 1221 pcts = pcts.collect(&:round)
1177 1222 pcts[1] = pcts[1] - pcts[0]
1178 1223 pcts << (100 - pcts[1] - pcts[0])
1179 1224 titles = options[:titles].to_a
1180 1225 titles[0] = "#{pcts[0]}%" if titles[0].blank?
1181 1226 legend = options[:legend] || ''
1182 1227 content_tag('table',
1183 1228 content_tag('tr',
1184 1229 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed', :title => titles[0]) : ''.html_safe) +
1185 1230 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done', :title => titles[1]) : ''.html_safe) +
1186 1231 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo', :title => titles[2]) : ''.html_safe)
1187 1232 ), :class => "progress progress-#{pcts[0]}").html_safe +
1188 1233 content_tag('p', legend, :class => 'percent').html_safe
1189 1234 end
1190 1235
1191 1236 def checked_image(checked=true)
1192 1237 if checked
1193 1238 @checked_image_tag ||= content_tag(:span, nil, :class => 'icon-only icon-checked')
1194 1239 end
1195 1240 end
1196 1241
1197 1242 def context_menu
1198 1243 unless @context_menu_included
1199 1244 content_for :header_tags do
1200 1245 javascript_include_tag('context_menu') +
1201 1246 stylesheet_link_tag('context_menu')
1202 1247 end
1203 1248 if l(:direction) == 'rtl'
1204 1249 content_for :header_tags do
1205 1250 stylesheet_link_tag('context_menu_rtl')
1206 1251 end
1207 1252 end
1208 1253 @context_menu_included = true
1209 1254 end
1210 1255 nil
1211 1256 end
1212 1257
1213 1258 def calendar_for(field_id)
1214 1259 include_calendar_headers_tags
1215 1260 javascript_tag("$(function() { $('##{field_id}').addClass('date').datepickerFallback(datepickerOptions); });")
1216 1261 end
1217 1262
1218 1263 def include_calendar_headers_tags
1219 1264 unless @calendar_headers_tags_included
1220 1265 tags = ''.html_safe
1221 1266 @calendar_headers_tags_included = true
1222 1267 content_for :header_tags do
1223 1268 start_of_week = Setting.start_of_week
1224 1269 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1225 1270 # Redmine uses 1..7 (monday..sunday) in settings and locales
1226 1271 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1227 1272 start_of_week = start_of_week.to_i % 7
1228 1273 tags << javascript_tag(
1229 1274 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1230 1275 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1231 1276 path_to_image('/images/calendar.png') +
1232 1277 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1233 1278 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1234 1279 "beforeShow: beforeShowDatePicker};")
1235 1280 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1236 1281 unless jquery_locale == 'en'
1237 1282 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1238 1283 end
1239 1284 tags
1240 1285 end
1241 1286 end
1242 1287 end
1243 1288
1244 1289 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1245 1290 # Examples:
1246 1291 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1247 1292 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1248 1293 #
1249 1294 def stylesheet_link_tag(*sources)
1250 1295 options = sources.last.is_a?(Hash) ? sources.pop : {}
1251 1296 plugin = options.delete(:plugin)
1252 1297 sources = sources.map do |source|
1253 1298 if plugin
1254 1299 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1255 1300 elsif current_theme && current_theme.stylesheets.include?(source)
1256 1301 current_theme.stylesheet_path(source)
1257 1302 else
1258 1303 source
1259 1304 end
1260 1305 end
1261 1306 super *sources, options
1262 1307 end
1263 1308
1264 1309 # Overrides Rails' image_tag with themes and plugins support.
1265 1310 # Examples:
1266 1311 # image_tag('image.png') # => picks image.png from the current theme or defaults
1267 1312 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1268 1313 #
1269 1314 def image_tag(source, options={})
1270 1315 if plugin = options.delete(:plugin)
1271 1316 source = "/plugin_assets/#{plugin}/images/#{source}"
1272 1317 elsif current_theme && current_theme.images.include?(source)
1273 1318 source = current_theme.image_path(source)
1274 1319 end
1275 1320 super source, options
1276 1321 end
1277 1322
1278 1323 # Overrides Rails' javascript_include_tag with plugins support
1279 1324 # Examples:
1280 1325 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1281 1326 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1282 1327 #
1283 1328 def javascript_include_tag(*sources)
1284 1329 options = sources.last.is_a?(Hash) ? sources.pop : {}
1285 1330 if plugin = options.delete(:plugin)
1286 1331 sources = sources.map do |source|
1287 1332 if plugin
1288 1333 "/plugin_assets/#{plugin}/javascripts/#{source}"
1289 1334 else
1290 1335 source
1291 1336 end
1292 1337 end
1293 1338 end
1294 1339 super *sources, options
1295 1340 end
1296 1341
1297 1342 def sidebar_content?
1298 1343 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1299 1344 end
1300 1345
1301 1346 def view_layouts_base_sidebar_hook_response
1302 1347 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1303 1348 end
1304 1349
1305 1350 def email_delivery_enabled?
1306 1351 !!ActionMailer::Base.perform_deliveries
1307 1352 end
1308 1353
1309 1354 # Returns the avatar image tag for the given +user+ if avatars are enabled
1310 1355 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1311 1356 def avatar(user, options = { })
1312 1357 if Setting.gravatar_enabled?
1313 1358 options.merge!(:default => Setting.gravatar_default)
1314 1359 email = nil
1315 1360 if user.respond_to?(:mail)
1316 1361 email = user.mail
1317 1362 elsif user.to_s =~ %r{<(.+?)>}
1318 1363 email = $1
1319 1364 end
1320 1365 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1321 1366 else
1322 1367 ''
1323 1368 end
1324 1369 end
1325 1370
1326 1371 # Returns a link to edit user's avatar if avatars are enabled
1327 1372 def avatar_edit_link(user, options={})
1328 1373 if Setting.gravatar_enabled?
1329 1374 url = "https://gravatar.com"
1330 1375 link_to avatar(user, {:title => l(:button_edit)}.merge(options)), url, :target => '_blank'
1331 1376 end
1332 1377 end
1333 1378
1334 1379 def sanitize_anchor_name(anchor)
1335 1380 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1336 1381 end
1337 1382
1338 1383 # Returns the javascript tags that are included in the html layout head
1339 1384 def javascript_heads
1340 1385 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.4', 'application', 'responsive')
1341 1386 unless User.current.pref.warn_on_leaving_unsaved == '0'
1342 1387 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1343 1388 end
1344 1389 tags
1345 1390 end
1346 1391
1347 1392 def favicon
1348 1393 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1349 1394 end
1350 1395
1351 1396 # Returns the path to the favicon
1352 1397 def favicon_path
1353 1398 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1354 1399 image_path(icon)
1355 1400 end
1356 1401
1357 1402 # Returns the full URL to the favicon
1358 1403 def favicon_url
1359 1404 # TODO: use #image_url introduced in Rails4
1360 1405 path = favicon_path
1361 1406 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1362 1407 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1363 1408 end
1364 1409
1365 1410 def robot_exclusion_tag
1366 1411 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1367 1412 end
1368 1413
1369 1414 # Returns true if arg is expected in the API response
1370 1415 def include_in_api_response?(arg)
1371 1416 unless @included_in_api_response
1372 1417 param = params[:include]
1373 1418 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1374 1419 @included_in_api_response.collect!(&:strip)
1375 1420 end
1376 1421 @included_in_api_response.include?(arg.to_s)
1377 1422 end
1378 1423
1379 1424 # Returns options or nil if nometa param or X-Redmine-Nometa header
1380 1425 # was set in the request
1381 1426 def api_meta(options)
1382 1427 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1383 1428 # compatibility mode for activeresource clients that raise
1384 1429 # an error when deserializing an array with attributes
1385 1430 nil
1386 1431 else
1387 1432 options
1388 1433 end
1389 1434 end
1390 1435
1391 1436 def generate_csv(&block)
1392 1437 decimal_separator = l(:general_csv_decimal_separator)
1393 1438 encoding = l(:general_csv_encoding)
1394 1439 end
1395 1440
1396 1441 private
1397 1442
1398 1443 def wiki_helper
1399 1444 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1400 1445 extend helper
1401 1446 return self
1402 1447 end
1403 1448 end
General Comments 0
You need to be logged in to leave comments. Login now