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