##// END OF EJS Templates
Add additional markup to page_header_title to enable better styling (#21947)....
Jean-Philippe Lang -
r14874:93eda89d7fbe
parent child
Show More
@@ -1,1341 +1,1346
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 link_to('',
458 458 url.merge({"#{name}[move_to]" => 'highest'}), :method => method,
459 459 :title => l(:label_sort_highest), :class => 'icon-only icon-move-top') +
460 460 link_to('',
461 461 url.merge({"#{name}[move_to]" => 'higher'}), :method => method,
462 462 :title => l(:label_sort_higher), :class => 'icon-only icon-move-up') +
463 463 link_to('',
464 464 url.merge({"#{name}[move_to]" => 'lower'}), :method => method,
465 465 :title => l(:label_sort_lower), :class => 'icon-only icon-move-down') +
466 466 link_to('',
467 467 url.merge({"#{name}[move_to]" => 'lowest'}), :method => method,
468 468 :title => l(:label_sort_lowest), :class => 'icon-only icon-move-bottom')
469 469 end
470 470
471 471 def breadcrumb(*args)
472 472 elements = args.flatten
473 473 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
474 474 end
475 475
476 476 def other_formats_links(&block)
477 477 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
478 478 yield Redmine::Views::OtherFormatsBuilder.new(self)
479 479 concat('</p>'.html_safe)
480 480 end
481 481
482 482 def page_header_title
483 483 if @project.nil? || @project.new_record?
484 484 h(Setting.app_title)
485 485 else
486 486 b = []
487 487 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
488 488 if ancestors.any?
489 489 root = ancestors.shift
490 490 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
491 491 if ancestors.size > 2
492 492 b << "\xe2\x80\xa6"
493 493 ancestors = ancestors[-2, 2]
494 494 end
495 495 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
496 496 end
497 b << h(@project)
498 b.join(" \xc2\xbb ").html_safe
497 b << content_tag(:span, h(@project), class: 'current-project')
498 if b.size > 1
499 separator = content_tag(:span, ' &raquo; '.html_safe, class: 'separator')
500 path = safe_join(b[0..-2], separator) + separator
501 b = [content_tag(:span, path.html_safe, class: 'breadcrumbs'), b[-1]]
502 end
503 safe_join b
499 504 end
500 505 end
501 506
502 507 # Returns a h2 tag and sets the html title with the given arguments
503 508 def title(*args)
504 509 strings = args.map do |arg|
505 510 if arg.is_a?(Array) && arg.size >= 2
506 511 link_to(*arg)
507 512 else
508 513 h(arg.to_s)
509 514 end
510 515 end
511 516 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
512 517 content_tag('h2', strings.join(' &#187; ').html_safe)
513 518 end
514 519
515 520 # Sets the html title
516 521 # Returns the html title when called without arguments
517 522 # Current project name and app_title and automatically appended
518 523 # Exemples:
519 524 # html_title 'Foo', 'Bar'
520 525 # html_title # => 'Foo - Bar - My Project - Redmine'
521 526 def html_title(*args)
522 527 if args.empty?
523 528 title = @html_title || []
524 529 title << @project.name if @project
525 530 title << Setting.app_title unless Setting.app_title == title.last
526 531 title.reject(&:blank?).join(' - ')
527 532 else
528 533 @html_title ||= []
529 534 @html_title += args
530 535 end
531 536 end
532 537
533 538 # Returns the theme, controller name, and action as css classes for the
534 539 # HTML body.
535 540 def body_css_classes
536 541 css = []
537 542 if theme = Redmine::Themes.theme(Setting.ui_theme)
538 543 css << 'theme-' + theme.name
539 544 end
540 545
541 546 css << 'project-' + @project.identifier if @project && @project.identifier.present?
542 547 css << 'controller-' + controller_name
543 548 css << 'action-' + action_name
544 549 css.join(' ')
545 550 end
546 551
547 552 def accesskey(s)
548 553 @used_accesskeys ||= []
549 554 key = Redmine::AccessKeys.key_for(s)
550 555 return nil if @used_accesskeys.include?(key)
551 556 @used_accesskeys << key
552 557 key
553 558 end
554 559
555 560 # Formats text according to system settings.
556 561 # 2 ways to call this method:
557 562 # * with a String: textilizable(text, options)
558 563 # * with an object and one of its attribute: textilizable(issue, :description, options)
559 564 def textilizable(*args)
560 565 options = args.last.is_a?(Hash) ? args.pop : {}
561 566 case args.size
562 567 when 1
563 568 obj = options[:object]
564 569 text = args.shift
565 570 when 2
566 571 obj = args.shift
567 572 attr = args.shift
568 573 text = obj.send(attr).to_s
569 574 else
570 575 raise ArgumentError, 'invalid arguments to textilizable'
571 576 end
572 577 return '' if text.blank?
573 578 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
574 579 @only_path = only_path = options.delete(:only_path) == false ? false : true
575 580
576 581 text = text.dup
577 582 macros = catch_macros(text)
578 583 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
579 584
580 585 @parsed_headings = []
581 586 @heading_anchors = {}
582 587 @current_section = 0 if options[:edit_section_links]
583 588
584 589 parse_sections(text, project, obj, attr, only_path, options)
585 590 text = parse_non_pre_blocks(text, obj, macros) do |text|
586 591 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
587 592 send method_name, text, project, obj, attr, only_path, options
588 593 end
589 594 end
590 595 parse_headings(text, project, obj, attr, only_path, options)
591 596
592 597 if @parsed_headings.any?
593 598 replace_toc(text, @parsed_headings)
594 599 end
595 600
596 601 text.html_safe
597 602 end
598 603
599 604 def parse_non_pre_blocks(text, obj, macros)
600 605 s = StringScanner.new(text)
601 606 tags = []
602 607 parsed = ''
603 608 while !s.eos?
604 609 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
605 610 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
606 611 if tags.empty?
607 612 yield text
608 613 inject_macros(text, obj, macros) if macros.any?
609 614 else
610 615 inject_macros(text, obj, macros, false) if macros.any?
611 616 end
612 617 parsed << text
613 618 if tag
614 619 if closing
615 620 if tags.last && tags.last.casecmp(tag) == 0
616 621 tags.pop
617 622 end
618 623 else
619 624 tags << tag.downcase
620 625 end
621 626 parsed << full_tag
622 627 end
623 628 end
624 629 # Close any non closing tags
625 630 while tag = tags.pop
626 631 parsed << "</#{tag}>"
627 632 end
628 633 parsed
629 634 end
630 635
631 636 def parse_inline_attachments(text, project, obj, attr, only_path, options)
632 637 return if options[:inline_attachments] == false
633 638
634 639 # when using an image link, try to use an attachment, if possible
635 640 attachments = options[:attachments] || []
636 641 attachments += obj.attachments if obj.respond_to?(:attachments)
637 642 if attachments.present?
638 643 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
639 644 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
640 645 # search for the picture in attachments
641 646 if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
642 647 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
643 648 desc = found.description.to_s.gsub('"', '')
644 649 if !desc.blank? && alttext.blank?
645 650 alt = " title=\"#{desc}\" alt=\"#{desc}\""
646 651 end
647 652 "src=\"#{image_url}\"#{alt}"
648 653 else
649 654 m
650 655 end
651 656 end
652 657 end
653 658 end
654 659
655 660 # Wiki links
656 661 #
657 662 # Examples:
658 663 # [[mypage]]
659 664 # [[mypage|mytext]]
660 665 # wiki links can refer other project wikis, using project name or identifier:
661 666 # [[project:]] -> wiki starting page
662 667 # [[project:|mytext]]
663 668 # [[project:mypage]]
664 669 # [[project:mypage|mytext]]
665 670 def parse_wiki_links(text, project, obj, attr, only_path, options)
666 671 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
667 672 link_project = project
668 673 esc, all, page, title = $1, $2, $3, $5
669 674 if esc.nil?
670 675 if page =~ /^([^\:]+)\:(.*)$/
671 676 identifier, page = $1, $2
672 677 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
673 678 title ||= identifier if page.blank?
674 679 end
675 680
676 681 if link_project && link_project.wiki
677 682 # extract anchor
678 683 anchor = nil
679 684 if page =~ /^(.+?)\#(.+)$/
680 685 page, anchor = $1, $2
681 686 end
682 687 anchor = sanitize_anchor_name(anchor) if anchor.present?
683 688 # check if page exists
684 689 wiki_page = link_project.wiki.find_page(page)
685 690 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
686 691 "##{anchor}"
687 692 else
688 693 case options[:wiki_links]
689 694 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
690 695 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
691 696 else
692 697 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
693 698 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
694 699 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
695 700 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
696 701 end
697 702 end
698 703 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
699 704 else
700 705 # project or wiki doesn't exist
701 706 all
702 707 end
703 708 else
704 709 all
705 710 end
706 711 end
707 712 end
708 713
709 714 # Redmine links
710 715 #
711 716 # Examples:
712 717 # Issues:
713 718 # #52 -> Link to issue #52
714 719 # Changesets:
715 720 # r52 -> Link to revision 52
716 721 # commit:a85130f -> Link to scmid starting with a85130f
717 722 # Documents:
718 723 # document#17 -> Link to document with id 17
719 724 # document:Greetings -> Link to the document with title "Greetings"
720 725 # document:"Some document" -> Link to the document with title "Some document"
721 726 # Versions:
722 727 # version#3 -> Link to version with id 3
723 728 # version:1.0.0 -> Link to version named "1.0.0"
724 729 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
725 730 # Attachments:
726 731 # attachment:file.zip -> Link to the attachment of the current object named file.zip
727 732 # Source files:
728 733 # source:some/file -> Link to the file located at /some/file in the project's repository
729 734 # source:some/file@52 -> Link to the file's revision 52
730 735 # source:some/file#L120 -> Link to line 120 of the file
731 736 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
732 737 # export:some/file -> Force the download of the file
733 738 # Forum messages:
734 739 # message#1218 -> Link to message with id 1218
735 740 # Projects:
736 741 # project:someproject -> Link to project named "someproject"
737 742 # project#3 -> Link to project with id 3
738 743 #
739 744 # Links can refer other objects from other projects, using project identifier:
740 745 # identifier:r52
741 746 # identifier:document:"Some document"
742 747 # identifier:version:1.0.0
743 748 # identifier:source:some/file
744 749 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
745 750 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|
746 751 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
747 752 if tag_content
748 753 $&
749 754 else
750 755 link = nil
751 756 project = default_project
752 757 if project_identifier
753 758 project = Project.visible.find_by_identifier(project_identifier)
754 759 end
755 760 if esc.nil?
756 761 if prefix.nil? && sep == 'r'
757 762 if project
758 763 repository = nil
759 764 if repo_identifier
760 765 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
761 766 else
762 767 repository = project.repository
763 768 end
764 769 # project.changesets.visible raises an SQL error because of a double join on repositories
765 770 if repository &&
766 771 (changeset = Changeset.visible.
767 772 find_by_repository_id_and_revision(repository.id, identifier))
768 773 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
769 774 {:only_path => only_path, :controller => 'repositories',
770 775 :action => 'revision', :id => project,
771 776 :repository_id => repository.identifier_param,
772 777 :rev => changeset.revision},
773 778 :class => 'changeset',
774 779 :title => truncate_single_line_raw(changeset.comments, 100))
775 780 end
776 781 end
777 782 elsif sep == '#'
778 783 oid = identifier.to_i
779 784 case prefix
780 785 when nil
781 786 if oid.to_s == identifier &&
782 787 issue = Issue.visible.find_by_id(oid)
783 788 anchor = comment_id ? "note-#{comment_id}" : nil
784 789 link = link_to("##{oid}#{comment_suffix}",
785 790 issue_url(issue, :only_path => only_path, :anchor => anchor),
786 791 :class => issue.css_classes,
787 792 :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
788 793 end
789 794 when 'document'
790 795 if document = Document.visible.find_by_id(oid)
791 796 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
792 797 end
793 798 when 'version'
794 799 if version = Version.visible.find_by_id(oid)
795 800 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
796 801 end
797 802 when 'message'
798 803 if message = Message.visible.find_by_id(oid)
799 804 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
800 805 end
801 806 when 'forum'
802 807 if board = Board.visible.find_by_id(oid)
803 808 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
804 809 end
805 810 when 'news'
806 811 if news = News.visible.find_by_id(oid)
807 812 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
808 813 end
809 814 when 'project'
810 815 if p = Project.visible.find_by_id(oid)
811 816 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
812 817 end
813 818 end
814 819 elsif sep == ':'
815 820 # removes the double quotes if any
816 821 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
817 822 name = CGI.unescapeHTML(name)
818 823 case prefix
819 824 when 'document'
820 825 if project && document = project.documents.visible.find_by_title(name)
821 826 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
822 827 end
823 828 when 'version'
824 829 if project && version = project.versions.visible.find_by_name(name)
825 830 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
826 831 end
827 832 when 'forum'
828 833 if project && board = project.boards.visible.find_by_name(name)
829 834 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
830 835 end
831 836 when 'news'
832 837 if project && news = project.news.visible.find_by_title(name)
833 838 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
834 839 end
835 840 when 'commit', 'source', 'export'
836 841 if project
837 842 repository = nil
838 843 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
839 844 repo_prefix, repo_identifier, name = $1, $2, $3
840 845 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
841 846 else
842 847 repository = project.repository
843 848 end
844 849 if prefix == 'commit'
845 850 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
846 851 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},
847 852 :class => 'changeset',
848 853 :title => truncate_single_line_raw(changeset.comments, 100)
849 854 end
850 855 else
851 856 if repository && User.current.allowed_to?(:browse_repository, project)
852 857 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
853 858 path, rev, anchor = $1, $3, $5
854 859 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,
855 860 :path => to_path_param(path),
856 861 :rev => rev,
857 862 :anchor => anchor},
858 863 :class => (prefix == 'export' ? 'source download' : 'source')
859 864 end
860 865 end
861 866 repo_prefix = nil
862 867 end
863 868 when 'attachment'
864 869 attachments = options[:attachments] || []
865 870 attachments += obj.attachments if obj.respond_to?(:attachments)
866 871 if attachments && attachment = Attachment.latest_attach(attachments, name)
867 872 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
868 873 end
869 874 when 'project'
870 875 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
871 876 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
872 877 end
873 878 end
874 879 end
875 880 end
876 881 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
877 882 end
878 883 end
879 884 end
880 885
881 886 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
882 887
883 888 def parse_sections(text, project, obj, attr, only_path, options)
884 889 return unless options[:edit_section_links]
885 890 text.gsub!(HEADING_RE) do
886 891 heading, level = $1, $2
887 892 @current_section += 1
888 893 if @current_section > 1
889 894 content_tag('div',
890 895 link_to('', options[:edit_section_links].merge(:section => @current_section),
891 896 :class => 'icon-only icon-edit'),
892 897 :class => "contextual heading-#{level}",
893 898 :title => l(:button_edit_section),
894 899 :id => "section-#{@current_section}") + heading.html_safe
895 900 else
896 901 heading
897 902 end
898 903 end
899 904 end
900 905
901 906 # Headings and TOC
902 907 # Adds ids and links to headings unless options[:headings] is set to false
903 908 def parse_headings(text, project, obj, attr, only_path, options)
904 909 return if options[:headings] == false
905 910
906 911 text.gsub!(HEADING_RE) do
907 912 level, attrs, content = $2.to_i, $3, $4
908 913 item = strip_tags(content).strip
909 914 anchor = sanitize_anchor_name(item)
910 915 # used for single-file wiki export
911 916 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
912 917 @heading_anchors[anchor] ||= 0
913 918 idx = (@heading_anchors[anchor] += 1)
914 919 if idx > 1
915 920 anchor = "#{anchor}-#{idx}"
916 921 end
917 922 @parsed_headings << [level, anchor, item]
918 923 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
919 924 end
920 925 end
921 926
922 927 MACROS_RE = /(
923 928 (!)? # escaping
924 929 (
925 930 \{\{ # opening tag
926 931 ([\w]+) # macro name
927 932 (\(([^\n\r]*?)\))? # optional arguments
928 933 ([\n\r].*?[\n\r])? # optional block of text
929 934 \}\} # closing tag
930 935 )
931 936 )/mx unless const_defined?(:MACROS_RE)
932 937
933 938 MACRO_SUB_RE = /(
934 939 \{\{
935 940 macro\((\d+)\)
936 941 \}\}
937 942 )/x unless const_defined?(:MACRO_SUB_RE)
938 943
939 944 # Extracts macros from text
940 945 def catch_macros(text)
941 946 macros = {}
942 947 text.gsub!(MACROS_RE) do
943 948 all, macro = $1, $4.downcase
944 949 if macro_exists?(macro) || all =~ MACRO_SUB_RE
945 950 index = macros.size
946 951 macros[index] = all
947 952 "{{macro(#{index})}}"
948 953 else
949 954 all
950 955 end
951 956 end
952 957 macros
953 958 end
954 959
955 960 # Executes and replaces macros in text
956 961 def inject_macros(text, obj, macros, execute=true)
957 962 text.gsub!(MACRO_SUB_RE) do
958 963 all, index = $1, $2.to_i
959 964 orig = macros.delete(index)
960 965 if execute && orig && orig =~ MACROS_RE
961 966 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
962 967 if esc.nil?
963 968 h(exec_macro(macro, obj, args, block) || all)
964 969 else
965 970 h(all)
966 971 end
967 972 elsif orig
968 973 h(orig)
969 974 else
970 975 h(all)
971 976 end
972 977 end
973 978 end
974 979
975 980 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
976 981
977 982 # Renders the TOC with given headings
978 983 def replace_toc(text, headings)
979 984 text.gsub!(TOC_RE) do
980 985 left_align, right_align = $2, $3
981 986 # Keep only the 4 first levels
982 987 headings = headings.select{|level, anchor, item| level <= 4}
983 988 if headings.empty?
984 989 ''
985 990 else
986 991 div_class = 'toc'
987 992 div_class << ' right' if right_align
988 993 div_class << ' left' if left_align
989 994 out = "<ul class=\"#{div_class}\"><li>"
990 995 root = headings.map(&:first).min
991 996 current = root
992 997 started = false
993 998 headings.each do |level, anchor, item|
994 999 if level > current
995 1000 out << '<ul><li>' * (level - current)
996 1001 elsif level < current
997 1002 out << "</li></ul>\n" * (current - level) + "</li><li>"
998 1003 elsif started
999 1004 out << '</li><li>'
1000 1005 end
1001 1006 out << "<a href=\"##{anchor}\">#{item}</a>"
1002 1007 current = level
1003 1008 started = true
1004 1009 end
1005 1010 out << '</li></ul>' * (current - root)
1006 1011 out << '</li></ul>'
1007 1012 end
1008 1013 end
1009 1014 end
1010 1015
1011 1016 # Same as Rails' simple_format helper without using paragraphs
1012 1017 def simple_format_without_paragraph(text)
1013 1018 text.to_s.
1014 1019 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1015 1020 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1016 1021 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1017 1022 html_safe
1018 1023 end
1019 1024
1020 1025 def lang_options_for_select(blank=true)
1021 1026 (blank ? [["(auto)", ""]] : []) + languages_options
1022 1027 end
1023 1028
1024 1029 def labelled_form_for(*args, &proc)
1025 1030 args << {} unless args.last.is_a?(Hash)
1026 1031 options = args.last
1027 1032 if args.first.is_a?(Symbol)
1028 1033 options.merge!(:as => args.shift)
1029 1034 end
1030 1035 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1031 1036 form_for(*args, &proc)
1032 1037 end
1033 1038
1034 1039 def labelled_fields_for(*args, &proc)
1035 1040 args << {} unless args.last.is_a?(Hash)
1036 1041 options = args.last
1037 1042 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1038 1043 fields_for(*args, &proc)
1039 1044 end
1040 1045
1041 1046 def error_messages_for(*objects)
1042 1047 html = ""
1043 1048 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1044 1049 errors = objects.map {|o| o.errors.full_messages}.flatten
1045 1050 if errors.any?
1046 1051 html << "<div id='errorExplanation'><ul>\n"
1047 1052 errors.each do |error|
1048 1053 html << "<li>#{h error}</li>\n"
1049 1054 end
1050 1055 html << "</ul></div>\n"
1051 1056 end
1052 1057 html.html_safe
1053 1058 end
1054 1059
1055 1060 def delete_link(url, options={})
1056 1061 options = {
1057 1062 :method => :delete,
1058 1063 :data => {:confirm => l(:text_are_you_sure)},
1059 1064 :class => 'icon icon-del'
1060 1065 }.merge(options)
1061 1066
1062 1067 link_to l(:button_delete), url, options
1063 1068 end
1064 1069
1065 1070 def preview_link(url, form, target='preview', options={})
1066 1071 content_tag 'a', l(:label_preview), {
1067 1072 :href => "#",
1068 1073 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1069 1074 :accesskey => accesskey(:preview)
1070 1075 }.merge(options)
1071 1076 end
1072 1077
1073 1078 def link_to_function(name, function, html_options={})
1074 1079 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1075 1080 end
1076 1081
1077 1082 # Helper to render JSON in views
1078 1083 def raw_json(arg)
1079 1084 arg.to_json.to_s.gsub('/', '\/').html_safe
1080 1085 end
1081 1086
1082 1087 def back_url
1083 1088 url = params[:back_url]
1084 1089 if url.nil? && referer = request.env['HTTP_REFERER']
1085 1090 url = CGI.unescape(referer.to_s)
1086 1091 end
1087 1092 url
1088 1093 end
1089 1094
1090 1095 def back_url_hidden_field_tag
1091 1096 url = back_url
1092 1097 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1093 1098 end
1094 1099
1095 1100 def check_all_links(form_name)
1096 1101 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1097 1102 " | ".html_safe +
1098 1103 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1099 1104 end
1100 1105
1101 1106 def toggle_checkboxes_link(selector)
1102 1107 link_to_function '',
1103 1108 "toggleCheckboxesBySelector('#{selector}')",
1104 1109 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}",
1105 1110 :class => 'toggle-checkboxes'
1106 1111 end
1107 1112
1108 1113 def progress_bar(pcts, options={})
1109 1114 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1110 1115 pcts = pcts.collect(&:round)
1111 1116 pcts[1] = pcts[1] - pcts[0]
1112 1117 pcts << (100 - pcts[1] - pcts[0])
1113 1118 titles = options[:titles].to_a
1114 1119 titles[0] = "#{pcts[0]}%" if titles[0].blank?
1115 1120 legend = options[:legend] || ''
1116 1121 content_tag('table',
1117 1122 content_tag('tr',
1118 1123 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed', :title => titles[0]) : ''.html_safe) +
1119 1124 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done', :title => titles[1]) : ''.html_safe) +
1120 1125 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo', :title => titles[2]) : ''.html_safe)
1121 1126 ), :class => "progress progress-#{pcts[0]}").html_safe +
1122 1127 content_tag('p', legend, :class => 'percent').html_safe
1123 1128 end
1124 1129
1125 1130 def checked_image(checked=true)
1126 1131 if checked
1127 1132 @checked_image_tag ||= content_tag(:span, nil, :class => 'icon-only icon-checked')
1128 1133 end
1129 1134 end
1130 1135
1131 1136 def context_menu(url)
1132 1137 unless @context_menu_included
1133 1138 content_for :header_tags do
1134 1139 javascript_include_tag('context_menu') +
1135 1140 stylesheet_link_tag('context_menu')
1136 1141 end
1137 1142 if l(:direction) == 'rtl'
1138 1143 content_for :header_tags do
1139 1144 stylesheet_link_tag('context_menu_rtl')
1140 1145 end
1141 1146 end
1142 1147 @context_menu_included = true
1143 1148 end
1144 1149 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1145 1150 end
1146 1151
1147 1152 def calendar_for(field_id)
1148 1153 include_calendar_headers_tags
1149 1154 javascript_tag("$(function() { $('##{field_id}').addClass('date').datepicker(datepickerOptions); });")
1150 1155 end
1151 1156
1152 1157 def include_calendar_headers_tags
1153 1158 unless @calendar_headers_tags_included
1154 1159 tags = ''.html_safe
1155 1160 @calendar_headers_tags_included = true
1156 1161 content_for :header_tags do
1157 1162 start_of_week = Setting.start_of_week
1158 1163 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1159 1164 # Redmine uses 1..7 (monday..sunday) in settings and locales
1160 1165 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1161 1166 start_of_week = start_of_week.to_i % 7
1162 1167 tags << javascript_tag(
1163 1168 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1164 1169 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1165 1170 path_to_image('/images/calendar.png') +
1166 1171 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1167 1172 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1168 1173 "beforeShow: beforeShowDatePicker};")
1169 1174 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1170 1175 unless jquery_locale == 'en'
1171 1176 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1172 1177 end
1173 1178 tags
1174 1179 end
1175 1180 end
1176 1181 end
1177 1182
1178 1183 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1179 1184 # Examples:
1180 1185 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1181 1186 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1182 1187 #
1183 1188 def stylesheet_link_tag(*sources)
1184 1189 options = sources.last.is_a?(Hash) ? sources.pop : {}
1185 1190 plugin = options.delete(:plugin)
1186 1191 sources = sources.map do |source|
1187 1192 if plugin
1188 1193 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1189 1194 elsif current_theme && current_theme.stylesheets.include?(source)
1190 1195 current_theme.stylesheet_path(source)
1191 1196 else
1192 1197 source
1193 1198 end
1194 1199 end
1195 1200 super *sources, options
1196 1201 end
1197 1202
1198 1203 # Overrides Rails' image_tag with themes and plugins support.
1199 1204 # Examples:
1200 1205 # image_tag('image.png') # => picks image.png from the current theme or defaults
1201 1206 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1202 1207 #
1203 1208 def image_tag(source, options={})
1204 1209 if plugin = options.delete(:plugin)
1205 1210 source = "/plugin_assets/#{plugin}/images/#{source}"
1206 1211 elsif current_theme && current_theme.images.include?(source)
1207 1212 source = current_theme.image_path(source)
1208 1213 end
1209 1214 super source, options
1210 1215 end
1211 1216
1212 1217 # Overrides Rails' javascript_include_tag with plugins support
1213 1218 # Examples:
1214 1219 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1215 1220 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1216 1221 #
1217 1222 def javascript_include_tag(*sources)
1218 1223 options = sources.last.is_a?(Hash) ? sources.pop : {}
1219 1224 if plugin = options.delete(:plugin)
1220 1225 sources = sources.map do |source|
1221 1226 if plugin
1222 1227 "/plugin_assets/#{plugin}/javascripts/#{source}"
1223 1228 else
1224 1229 source
1225 1230 end
1226 1231 end
1227 1232 end
1228 1233 super *sources, options
1229 1234 end
1230 1235
1231 1236 def sidebar_content?
1232 1237 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1233 1238 end
1234 1239
1235 1240 def view_layouts_base_sidebar_hook_response
1236 1241 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1237 1242 end
1238 1243
1239 1244 def email_delivery_enabled?
1240 1245 !!ActionMailer::Base.perform_deliveries
1241 1246 end
1242 1247
1243 1248 # Returns the avatar image tag for the given +user+ if avatars are enabled
1244 1249 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1245 1250 def avatar(user, options = { })
1246 1251 if Setting.gravatar_enabled?
1247 1252 options.merge!(:default => Setting.gravatar_default)
1248 1253 email = nil
1249 1254 if user.respond_to?(:mail)
1250 1255 email = user.mail
1251 1256 elsif user.to_s =~ %r{<(.+?)>}
1252 1257 email = $1
1253 1258 end
1254 1259 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1255 1260 else
1256 1261 ''
1257 1262 end
1258 1263 end
1259 1264
1260 1265 # Returns a link to edit user's avatar if avatars are enabled
1261 1266 def avatar_edit_link(user, options={})
1262 1267 if Setting.gravatar_enabled?
1263 1268 url = "https://gravatar.com"
1264 1269 link_to avatar(user, {:title => l(:button_edit)}.merge(options)), url, :target => '_blank'
1265 1270 end
1266 1271 end
1267 1272
1268 1273 def sanitize_anchor_name(anchor)
1269 1274 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1270 1275 end
1271 1276
1272 1277 # Returns the javascript tags that are included in the html layout head
1273 1278 def javascript_heads
1274 1279 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.4', 'application', 'responsive')
1275 1280 unless User.current.pref.warn_on_leaving_unsaved == '0'
1276 1281 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1277 1282 end
1278 1283 tags
1279 1284 end
1280 1285
1281 1286 def favicon
1282 1287 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1283 1288 end
1284 1289
1285 1290 # Returns the path to the favicon
1286 1291 def favicon_path
1287 1292 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1288 1293 image_path(icon)
1289 1294 end
1290 1295
1291 1296 # Returns the full URL to the favicon
1292 1297 def favicon_url
1293 1298 # TODO: use #image_url introduced in Rails4
1294 1299 path = favicon_path
1295 1300 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1296 1301 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1297 1302 end
1298 1303
1299 1304 def robot_exclusion_tag
1300 1305 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1301 1306 end
1302 1307
1303 1308 # Returns true if arg is expected in the API response
1304 1309 def include_in_api_response?(arg)
1305 1310 unless @included_in_api_response
1306 1311 param = params[:include]
1307 1312 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1308 1313 @included_in_api_response.collect!(&:strip)
1309 1314 end
1310 1315 @included_in_api_response.include?(arg.to_s)
1311 1316 end
1312 1317
1313 1318 # Returns options or nil if nometa param or X-Redmine-Nometa header
1314 1319 # was set in the request
1315 1320 def api_meta(options)
1316 1321 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1317 1322 # compatibility mode for activeresource clients that raise
1318 1323 # an error when deserializing an array with attributes
1319 1324 nil
1320 1325 else
1321 1326 options
1322 1327 end
1323 1328 end
1324 1329
1325 1330 def generate_csv(&block)
1326 1331 decimal_separator = l(:general_csv_decimal_separator)
1327 1332 encoding = l(:general_csv_encoding)
1328 1333 end
1329 1334
1330 1335 private
1331 1336
1332 1337 def wiki_helper
1333 1338 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1334 1339 extend helper
1335 1340 return self
1336 1341 end
1337 1342
1338 1343 def link_to_content_update(text, url_params = {}, html_options = {})
1339 1344 link_to(text, url_params, html_options)
1340 1345 end
1341 1346 end
General Comments 0
You need to be logged in to leave comments. Login now