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