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