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