##// END OF EJS Templates
Deprecates unused stuff (#12909)....
Jean-Philippe Lang -
r14956:db64340419ec
parent child
Show More
@@ -1,1356 +1,1359
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 # TODO: remove associated styles from application.css too
458 ActiveSupport::Deprecation.warn "Application#reorder_links will be removed in Redmine 4."
459
457 460 link_to(l(:label_sort_highest),
458 461 url.merge({"#{name}[move_to]" => 'highest'}), :method => method,
459 462 :title => l(:label_sort_highest), :class => 'icon-only icon-move-top') +
460 463 link_to(l(:label_sort_higher),
461 464 url.merge({"#{name}[move_to]" => 'higher'}), :method => method,
462 465 :title => l(:label_sort_higher), :class => 'icon-only icon-move-up') +
463 466 link_to(l(:label_sort_lower),
464 467 url.merge({"#{name}[move_to]" => 'lower'}), :method => method,
465 468 :title => l(:label_sort_lower), :class => 'icon-only icon-move-down') +
466 469 link_to(l(:label_sort_lowest),
467 470 url.merge({"#{name}[move_to]" => 'lowest'}), :method => method,
468 471 :title => l(:label_sort_lowest), :class => 'icon-only icon-move-bottom')
469 472 end
470 473
471 474 def reorder_handle(object, options={})
472 475 data = {
473 476 :reorder_url => options[:url] || url_for(object),
474 477 :reorder_param => options[:param] || object.class.name.underscore
475 478 }
476 479 content_tag('span', '',
477 480 :class => "sort-handle ui-icon ui-icon-arrowthick-2-n-s",
478 481 :data => data)
479 482 end
480 483
481 484 def breadcrumb(*args)
482 485 elements = args.flatten
483 486 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
484 487 end
485 488
486 489 def other_formats_links(&block)
487 490 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
488 491 yield Redmine::Views::OtherFormatsBuilder.new(self)
489 492 concat('</p>'.html_safe)
490 493 end
491 494
492 495 def page_header_title
493 496 if @project.nil? || @project.new_record?
494 497 h(Setting.app_title)
495 498 else
496 499 b = []
497 500 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
498 501 if ancestors.any?
499 502 root = ancestors.shift
500 503 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
501 504 if ancestors.size > 2
502 505 b << "\xe2\x80\xa6"
503 506 ancestors = ancestors[-2, 2]
504 507 end
505 508 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
506 509 end
507 510 b << content_tag(:span, h(@project), class: 'current-project')
508 511 if b.size > 1
509 512 separator = content_tag(:span, ' &raquo; '.html_safe, class: 'separator')
510 513 path = safe_join(b[0..-2], separator) + separator
511 514 b = [content_tag(:span, path.html_safe, class: 'breadcrumbs'), b[-1]]
512 515 end
513 516 safe_join b
514 517 end
515 518 end
516 519
517 520 # Returns a h2 tag and sets the html title with the given arguments
518 521 def title(*args)
519 522 strings = args.map do |arg|
520 523 if arg.is_a?(Array) && arg.size >= 2
521 524 link_to(*arg)
522 525 else
523 526 h(arg.to_s)
524 527 end
525 528 end
526 529 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
527 530 content_tag('h2', strings.join(' &#187; ').html_safe)
528 531 end
529 532
530 533 # Sets the html title
531 534 # Returns the html title when called without arguments
532 535 # Current project name and app_title and automatically appended
533 536 # Exemples:
534 537 # html_title 'Foo', 'Bar'
535 538 # html_title # => 'Foo - Bar - My Project - Redmine'
536 539 def html_title(*args)
537 540 if args.empty?
538 541 title = @html_title || []
539 542 title << @project.name if @project
540 543 title << Setting.app_title unless Setting.app_title == title.last
541 544 title.reject(&:blank?).join(' - ')
542 545 else
543 546 @html_title ||= []
544 547 @html_title += args
545 548 end
546 549 end
547 550
548 551 # Returns the theme, controller name, and action as css classes for the
549 552 # HTML body.
550 553 def body_css_classes
551 554 css = []
552 555 if theme = Redmine::Themes.theme(Setting.ui_theme)
553 556 css << 'theme-' + theme.name
554 557 end
555 558
556 559 css << 'project-' + @project.identifier if @project && @project.identifier.present?
557 560 css << 'controller-' + controller_name
558 561 css << 'action-' + action_name
559 562 css.join(' ')
560 563 end
561 564
562 565 def accesskey(s)
563 566 @used_accesskeys ||= []
564 567 key = Redmine::AccessKeys.key_for(s)
565 568 return nil if @used_accesskeys.include?(key)
566 569 @used_accesskeys << key
567 570 key
568 571 end
569 572
570 573 # Formats text according to system settings.
571 574 # 2 ways to call this method:
572 575 # * with a String: textilizable(text, options)
573 576 # * with an object and one of its attribute: textilizable(issue, :description, options)
574 577 def textilizable(*args)
575 578 options = args.last.is_a?(Hash) ? args.pop : {}
576 579 case args.size
577 580 when 1
578 581 obj = options[:object]
579 582 text = args.shift
580 583 when 2
581 584 obj = args.shift
582 585 attr = args.shift
583 586 text = obj.send(attr).to_s
584 587 else
585 588 raise ArgumentError, 'invalid arguments to textilizable'
586 589 end
587 590 return '' if text.blank?
588 591 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
589 592 @only_path = only_path = options.delete(:only_path) == false ? false : true
590 593
591 594 text = text.dup
592 595 macros = catch_macros(text)
593 596 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
594 597
595 598 @parsed_headings = []
596 599 @heading_anchors = {}
597 600 @current_section = 0 if options[:edit_section_links]
598 601
599 602 parse_sections(text, project, obj, attr, only_path, options)
600 603 text = parse_non_pre_blocks(text, obj, macros) do |text|
601 604 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
602 605 send method_name, text, project, obj, attr, only_path, options
603 606 end
604 607 end
605 608 parse_headings(text, project, obj, attr, only_path, options)
606 609
607 610 if @parsed_headings.any?
608 611 replace_toc(text, @parsed_headings)
609 612 end
610 613
611 614 text.html_safe
612 615 end
613 616
614 617 def parse_non_pre_blocks(text, obj, macros)
615 618 s = StringScanner.new(text)
616 619 tags = []
617 620 parsed = ''
618 621 while !s.eos?
619 622 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
620 623 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
621 624 if tags.empty?
622 625 yield text
623 626 inject_macros(text, obj, macros) if macros.any?
624 627 else
625 628 inject_macros(text, obj, macros, false) if macros.any?
626 629 end
627 630 parsed << text
628 631 if tag
629 632 if closing
630 633 if tags.last && tags.last.casecmp(tag) == 0
631 634 tags.pop
632 635 end
633 636 else
634 637 tags << tag.downcase
635 638 end
636 639 parsed << full_tag
637 640 end
638 641 end
639 642 # Close any non closing tags
640 643 while tag = tags.pop
641 644 parsed << "</#{tag}>"
642 645 end
643 646 parsed
644 647 end
645 648
646 649 def parse_inline_attachments(text, project, obj, attr, only_path, options)
647 650 return if options[:inline_attachments] == false
648 651
649 652 # when using an image link, try to use an attachment, if possible
650 653 attachments = options[:attachments] || []
651 654 attachments += obj.attachments if obj.respond_to?(:attachments)
652 655 if attachments.present?
653 656 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
654 657 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
655 658 # search for the picture in attachments
656 659 if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
657 660 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
658 661 desc = found.description.to_s.gsub('"', '')
659 662 if !desc.blank? && alttext.blank?
660 663 alt = " title=\"#{desc}\" alt=\"#{desc}\""
661 664 end
662 665 "src=\"#{image_url}\"#{alt}"
663 666 else
664 667 m
665 668 end
666 669 end
667 670 end
668 671 end
669 672
670 673 # Wiki links
671 674 #
672 675 # Examples:
673 676 # [[mypage]]
674 677 # [[mypage|mytext]]
675 678 # wiki links can refer other project wikis, using project name or identifier:
676 679 # [[project:]] -> wiki starting page
677 680 # [[project:|mytext]]
678 681 # [[project:mypage]]
679 682 # [[project:mypage|mytext]]
680 683 def parse_wiki_links(text, project, obj, attr, only_path, options)
681 684 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
682 685 link_project = project
683 686 esc, all, page, title = $1, $2, $3, $5
684 687 if esc.nil?
685 688 if page =~ /^([^\:]+)\:(.*)$/
686 689 identifier, page = $1, $2
687 690 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
688 691 title ||= identifier if page.blank?
689 692 end
690 693
691 694 if link_project && link_project.wiki
692 695 # extract anchor
693 696 anchor = nil
694 697 if page =~ /^(.+?)\#(.+)$/
695 698 page, anchor = $1, $2
696 699 end
697 700 anchor = sanitize_anchor_name(anchor) if anchor.present?
698 701 # check if page exists
699 702 wiki_page = link_project.wiki.find_page(page)
700 703 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
701 704 "##{anchor}"
702 705 else
703 706 case options[:wiki_links]
704 707 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
705 708 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
706 709 else
707 710 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
708 711 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
709 712 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
710 713 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
711 714 end
712 715 end
713 716 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
714 717 else
715 718 # project or wiki doesn't exist
716 719 all
717 720 end
718 721 else
719 722 all
720 723 end
721 724 end
722 725 end
723 726
724 727 # Redmine links
725 728 #
726 729 # Examples:
727 730 # Issues:
728 731 # #52 -> Link to issue #52
729 732 # Changesets:
730 733 # r52 -> Link to revision 52
731 734 # commit:a85130f -> Link to scmid starting with a85130f
732 735 # Documents:
733 736 # document#17 -> Link to document with id 17
734 737 # document:Greetings -> Link to the document with title "Greetings"
735 738 # document:"Some document" -> Link to the document with title "Some document"
736 739 # Versions:
737 740 # version#3 -> Link to version with id 3
738 741 # version:1.0.0 -> Link to version named "1.0.0"
739 742 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
740 743 # Attachments:
741 744 # attachment:file.zip -> Link to the attachment of the current object named file.zip
742 745 # Source files:
743 746 # source:some/file -> Link to the file located at /some/file in the project's repository
744 747 # source:some/file@52 -> Link to the file's revision 52
745 748 # source:some/file#L120 -> Link to line 120 of the file
746 749 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
747 750 # export:some/file -> Force the download of the file
748 751 # Forum messages:
749 752 # message#1218 -> Link to message with id 1218
750 753 # Projects:
751 754 # project:someproject -> Link to project named "someproject"
752 755 # project#3 -> Link to project with id 3
753 756 #
754 757 # Links can refer other objects from other projects, using project identifier:
755 758 # identifier:r52
756 759 # identifier:document:"Some document"
757 760 # identifier:version:1.0.0
758 761 # identifier:source:some/file
759 762 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
760 763 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|
761 764 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
762 765 if tag_content
763 766 $&
764 767 else
765 768 link = nil
766 769 project = default_project
767 770 if project_identifier
768 771 project = Project.visible.find_by_identifier(project_identifier)
769 772 end
770 773 if esc.nil?
771 774 if prefix.nil? && sep == 'r'
772 775 if project
773 776 repository = nil
774 777 if repo_identifier
775 778 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
776 779 else
777 780 repository = project.repository
778 781 end
779 782 # project.changesets.visible raises an SQL error because of a double join on repositories
780 783 if repository &&
781 784 (changeset = Changeset.visible.
782 785 find_by_repository_id_and_revision(repository.id, identifier))
783 786 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
784 787 {:only_path => only_path, :controller => 'repositories',
785 788 :action => 'revision', :id => project,
786 789 :repository_id => repository.identifier_param,
787 790 :rev => changeset.revision},
788 791 :class => 'changeset',
789 792 :title => truncate_single_line_raw(changeset.comments, 100))
790 793 end
791 794 end
792 795 elsif sep == '#'
793 796 oid = identifier.to_i
794 797 case prefix
795 798 when nil
796 799 if oid.to_s == identifier &&
797 800 issue = Issue.visible.find_by_id(oid)
798 801 anchor = comment_id ? "note-#{comment_id}" : nil
799 802 link = link_to("##{oid}#{comment_suffix}",
800 803 issue_url(issue, :only_path => only_path, :anchor => anchor),
801 804 :class => issue.css_classes,
802 805 :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
803 806 end
804 807 when 'document'
805 808 if document = Document.visible.find_by_id(oid)
806 809 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
807 810 end
808 811 when 'version'
809 812 if version = Version.visible.find_by_id(oid)
810 813 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
811 814 end
812 815 when 'message'
813 816 if message = Message.visible.find_by_id(oid)
814 817 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
815 818 end
816 819 when 'forum'
817 820 if board = Board.visible.find_by_id(oid)
818 821 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
819 822 end
820 823 when 'news'
821 824 if news = News.visible.find_by_id(oid)
822 825 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
823 826 end
824 827 when 'project'
825 828 if p = Project.visible.find_by_id(oid)
826 829 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
827 830 end
828 831 end
829 832 elsif sep == ':'
830 833 # removes the double quotes if any
831 834 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
832 835 name = CGI.unescapeHTML(name)
833 836 case prefix
834 837 when 'document'
835 838 if project && document = project.documents.visible.find_by_title(name)
836 839 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
837 840 end
838 841 when 'version'
839 842 if project && version = project.versions.visible.find_by_name(name)
840 843 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
841 844 end
842 845 when 'forum'
843 846 if project && board = project.boards.visible.find_by_name(name)
844 847 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
845 848 end
846 849 when 'news'
847 850 if project && news = project.news.visible.find_by_title(name)
848 851 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
849 852 end
850 853 when 'commit', 'source', 'export'
851 854 if project
852 855 repository = nil
853 856 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
854 857 repo_prefix, repo_identifier, name = $1, $2, $3
855 858 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
856 859 else
857 860 repository = project.repository
858 861 end
859 862 if prefix == 'commit'
860 863 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
861 864 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},
862 865 :class => 'changeset',
863 866 :title => truncate_single_line_raw(changeset.comments, 100)
864 867 end
865 868 else
866 869 if repository && User.current.allowed_to?(:browse_repository, project)
867 870 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
868 871 path, rev, anchor = $1, $3, $5
869 872 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,
870 873 :path => to_path_param(path),
871 874 :rev => rev,
872 875 :anchor => anchor},
873 876 :class => (prefix == 'export' ? 'source download' : 'source')
874 877 end
875 878 end
876 879 repo_prefix = nil
877 880 end
878 881 when 'attachment'
879 882 attachments = options[:attachments] || []
880 883 attachments += obj.attachments if obj.respond_to?(:attachments)
881 884 if attachments && attachment = Attachment.latest_attach(attachments, name)
882 885 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
883 886 end
884 887 when 'project'
885 888 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
886 889 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
887 890 end
888 891 end
889 892 end
890 893 end
891 894 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
892 895 end
893 896 end
894 897 end
895 898
896 899 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
897 900
898 901 def parse_sections(text, project, obj, attr, only_path, options)
899 902 return unless options[:edit_section_links]
900 903 text.gsub!(HEADING_RE) do
901 904 heading, level = $1, $2
902 905 @current_section += 1
903 906 if @current_section > 1
904 907 content_tag('div',
905 908 link_to(l(:button_edit_section), options[:edit_section_links].merge(:section => @current_section),
906 909 :class => 'icon-only icon-edit'),
907 910 :class => "contextual heading-#{level}",
908 911 :title => l(:button_edit_section),
909 912 :id => "section-#{@current_section}") + heading.html_safe
910 913 else
911 914 heading
912 915 end
913 916 end
914 917 end
915 918
916 919 # Headings and TOC
917 920 # Adds ids and links to headings unless options[:headings] is set to false
918 921 def parse_headings(text, project, obj, attr, only_path, options)
919 922 return if options[:headings] == false
920 923
921 924 text.gsub!(HEADING_RE) do
922 925 level, attrs, content = $2.to_i, $3, $4
923 926 item = strip_tags(content).strip
924 927 anchor = sanitize_anchor_name(item)
925 928 # used for single-file wiki export
926 929 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
927 930 @heading_anchors[anchor] ||= 0
928 931 idx = (@heading_anchors[anchor] += 1)
929 932 if idx > 1
930 933 anchor = "#{anchor}-#{idx}"
931 934 end
932 935 @parsed_headings << [level, anchor, item]
933 936 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
934 937 end
935 938 end
936 939
937 940 MACROS_RE = /(
938 941 (!)? # escaping
939 942 (
940 943 \{\{ # opening tag
941 944 ([\w]+) # macro name
942 945 (\(([^\n\r]*?)\))? # optional arguments
943 946 ([\n\r].*?[\n\r])? # optional block of text
944 947 \}\} # closing tag
945 948 )
946 949 )/mx unless const_defined?(:MACROS_RE)
947 950
948 951 MACRO_SUB_RE = /(
949 952 \{\{
950 953 macro\((\d+)\)
951 954 \}\}
952 955 )/x unless const_defined?(:MACRO_SUB_RE)
953 956
954 957 # Extracts macros from text
955 958 def catch_macros(text)
956 959 macros = {}
957 960 text.gsub!(MACROS_RE) do
958 961 all, macro = $1, $4.downcase
959 962 if macro_exists?(macro) || all =~ MACRO_SUB_RE
960 963 index = macros.size
961 964 macros[index] = all
962 965 "{{macro(#{index})}}"
963 966 else
964 967 all
965 968 end
966 969 end
967 970 macros
968 971 end
969 972
970 973 # Executes and replaces macros in text
971 974 def inject_macros(text, obj, macros, execute=true)
972 975 text.gsub!(MACRO_SUB_RE) do
973 976 all, index = $1, $2.to_i
974 977 orig = macros.delete(index)
975 978 if execute && orig && orig =~ MACROS_RE
976 979 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
977 980 if esc.nil?
978 981 h(exec_macro(macro, obj, args, block) || all)
979 982 else
980 983 h(all)
981 984 end
982 985 elsif orig
983 986 h(orig)
984 987 else
985 988 h(all)
986 989 end
987 990 end
988 991 end
989 992
990 993 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
991 994
992 995 # Renders the TOC with given headings
993 996 def replace_toc(text, headings)
994 997 text.gsub!(TOC_RE) do
995 998 left_align, right_align = $2, $3
996 999 # Keep only the 4 first levels
997 1000 headings = headings.select{|level, anchor, item| level <= 4}
998 1001 if headings.empty?
999 1002 ''
1000 1003 else
1001 1004 div_class = 'toc'
1002 1005 div_class << ' right' if right_align
1003 1006 div_class << ' left' if left_align
1004 1007 out = "<ul class=\"#{div_class}\"><li>"
1005 1008 root = headings.map(&:first).min
1006 1009 current = root
1007 1010 started = false
1008 1011 headings.each do |level, anchor, item|
1009 1012 if level > current
1010 1013 out << '<ul><li>' * (level - current)
1011 1014 elsif level < current
1012 1015 out << "</li></ul>\n" * (current - level) + "</li><li>"
1013 1016 elsif started
1014 1017 out << '</li><li>'
1015 1018 end
1016 1019 out << "<a href=\"##{anchor}\">#{item}</a>"
1017 1020 current = level
1018 1021 started = true
1019 1022 end
1020 1023 out << '</li></ul>' * (current - root)
1021 1024 out << '</li></ul>'
1022 1025 end
1023 1026 end
1024 1027 end
1025 1028
1026 1029 # Same as Rails' simple_format helper without using paragraphs
1027 1030 def simple_format_without_paragraph(text)
1028 1031 text.to_s.
1029 1032 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1030 1033 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1031 1034 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1032 1035 html_safe
1033 1036 end
1034 1037
1035 1038 def lang_options_for_select(blank=true)
1036 1039 (blank ? [["(auto)", ""]] : []) + languages_options
1037 1040 end
1038 1041
1039 1042 def labelled_form_for(*args, &proc)
1040 1043 args << {} unless args.last.is_a?(Hash)
1041 1044 options = args.last
1042 1045 if args.first.is_a?(Symbol)
1043 1046 options.merge!(:as => args.shift)
1044 1047 end
1045 1048 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1046 1049 form_for(*args, &proc)
1047 1050 end
1048 1051
1049 1052 def labelled_fields_for(*args, &proc)
1050 1053 args << {} unless args.last.is_a?(Hash)
1051 1054 options = args.last
1052 1055 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1053 1056 fields_for(*args, &proc)
1054 1057 end
1055 1058
1056 1059 def error_messages_for(*objects)
1057 1060 html = ""
1058 1061 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1059 1062 errors = objects.map {|o| o.errors.full_messages}.flatten
1060 1063 if errors.any?
1061 1064 html << "<div id='errorExplanation'><ul>\n"
1062 1065 errors.each do |error|
1063 1066 html << "<li>#{h error}</li>\n"
1064 1067 end
1065 1068 html << "</ul></div>\n"
1066 1069 end
1067 1070 html.html_safe
1068 1071 end
1069 1072
1070 1073 def delete_link(url, options={})
1071 1074 options = {
1072 1075 :method => :delete,
1073 1076 :data => {:confirm => l(:text_are_you_sure)},
1074 1077 :class => 'icon icon-del'
1075 1078 }.merge(options)
1076 1079
1077 1080 link_to l(:button_delete), url, options
1078 1081 end
1079 1082
1080 1083 def preview_link(url, form, target='preview', options={})
1081 1084 content_tag 'a', l(:label_preview), {
1082 1085 :href => "#",
1083 1086 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1084 1087 :accesskey => accesskey(:preview)
1085 1088 }.merge(options)
1086 1089 end
1087 1090
1088 1091 def link_to_function(name, function, html_options={})
1089 1092 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1090 1093 end
1091 1094
1092 1095 # Helper to render JSON in views
1093 1096 def raw_json(arg)
1094 1097 arg.to_json.to_s.gsub('/', '\/').html_safe
1095 1098 end
1096 1099
1097 1100 def back_url
1098 1101 url = params[:back_url]
1099 1102 if url.nil? && referer = request.env['HTTP_REFERER']
1100 1103 url = CGI.unescape(referer.to_s)
1101 1104 end
1102 1105 url
1103 1106 end
1104 1107
1105 1108 def back_url_hidden_field_tag
1106 1109 url = back_url
1107 1110 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1108 1111 end
1109 1112
1110 1113 def check_all_links(form_name)
1111 1114 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1112 1115 " | ".html_safe +
1113 1116 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1114 1117 end
1115 1118
1116 1119 def toggle_checkboxes_link(selector)
1117 1120 link_to_function '',
1118 1121 "toggleCheckboxesBySelector('#{selector}')",
1119 1122 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}",
1120 1123 :class => 'toggle-checkboxes'
1121 1124 end
1122 1125
1123 1126 def progress_bar(pcts, options={})
1124 1127 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1125 1128 pcts = pcts.collect(&:round)
1126 1129 pcts[1] = pcts[1] - pcts[0]
1127 1130 pcts << (100 - pcts[1] - pcts[0])
1128 1131 titles = options[:titles].to_a
1129 1132 titles[0] = "#{pcts[0]}%" if titles[0].blank?
1130 1133 legend = options[:legend] || ''
1131 1134 content_tag('table',
1132 1135 content_tag('tr',
1133 1136 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed', :title => titles[0]) : ''.html_safe) +
1134 1137 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done', :title => titles[1]) : ''.html_safe) +
1135 1138 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo', :title => titles[2]) : ''.html_safe)
1136 1139 ), :class => "progress progress-#{pcts[0]}").html_safe +
1137 1140 content_tag('p', legend, :class => 'percent').html_safe
1138 1141 end
1139 1142
1140 1143 def checked_image(checked=true)
1141 1144 if checked
1142 1145 @checked_image_tag ||= content_tag(:span, nil, :class => 'icon-only icon-checked')
1143 1146 end
1144 1147 end
1145 1148
1146 1149 def context_menu(url)
1147 1150 unless @context_menu_included
1148 1151 content_for :header_tags do
1149 1152 javascript_include_tag('context_menu') +
1150 1153 stylesheet_link_tag('context_menu')
1151 1154 end
1152 1155 if l(:direction) == 'rtl'
1153 1156 content_for :header_tags do
1154 1157 stylesheet_link_tag('context_menu_rtl')
1155 1158 end
1156 1159 end
1157 1160 @context_menu_included = true
1158 1161 end
1159 1162 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1160 1163 end
1161 1164
1162 1165 def calendar_for(field_id)
1163 1166 include_calendar_headers_tags
1164 1167 javascript_tag("$(function() { $('##{field_id}').addClass('date').datepicker(datepickerOptions); });")
1165 1168 end
1166 1169
1167 1170 def include_calendar_headers_tags
1168 1171 unless @calendar_headers_tags_included
1169 1172 tags = ''.html_safe
1170 1173 @calendar_headers_tags_included = true
1171 1174 content_for :header_tags do
1172 1175 start_of_week = Setting.start_of_week
1173 1176 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1174 1177 # Redmine uses 1..7 (monday..sunday) in settings and locales
1175 1178 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1176 1179 start_of_week = start_of_week.to_i % 7
1177 1180 tags << javascript_tag(
1178 1181 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1179 1182 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1180 1183 path_to_image('/images/calendar.png') +
1181 1184 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1182 1185 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1183 1186 "beforeShow: beforeShowDatePicker};")
1184 1187 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1185 1188 unless jquery_locale == 'en'
1186 1189 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1187 1190 end
1188 1191 tags
1189 1192 end
1190 1193 end
1191 1194 end
1192 1195
1193 1196 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1194 1197 # Examples:
1195 1198 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1196 1199 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1197 1200 #
1198 1201 def stylesheet_link_tag(*sources)
1199 1202 options = sources.last.is_a?(Hash) ? sources.pop : {}
1200 1203 plugin = options.delete(:plugin)
1201 1204 sources = sources.map do |source|
1202 1205 if plugin
1203 1206 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1204 1207 elsif current_theme && current_theme.stylesheets.include?(source)
1205 1208 current_theme.stylesheet_path(source)
1206 1209 else
1207 1210 source
1208 1211 end
1209 1212 end
1210 1213 super *sources, options
1211 1214 end
1212 1215
1213 1216 # Overrides Rails' image_tag with themes and plugins support.
1214 1217 # Examples:
1215 1218 # image_tag('image.png') # => picks image.png from the current theme or defaults
1216 1219 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1217 1220 #
1218 1221 def image_tag(source, options={})
1219 1222 if plugin = options.delete(:plugin)
1220 1223 source = "/plugin_assets/#{plugin}/images/#{source}"
1221 1224 elsif current_theme && current_theme.images.include?(source)
1222 1225 source = current_theme.image_path(source)
1223 1226 end
1224 1227 super source, options
1225 1228 end
1226 1229
1227 1230 # Overrides Rails' javascript_include_tag with plugins support
1228 1231 # Examples:
1229 1232 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1230 1233 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1231 1234 #
1232 1235 def javascript_include_tag(*sources)
1233 1236 options = sources.last.is_a?(Hash) ? sources.pop : {}
1234 1237 if plugin = options.delete(:plugin)
1235 1238 sources = sources.map do |source|
1236 1239 if plugin
1237 1240 "/plugin_assets/#{plugin}/javascripts/#{source}"
1238 1241 else
1239 1242 source
1240 1243 end
1241 1244 end
1242 1245 end
1243 1246 super *sources, options
1244 1247 end
1245 1248
1246 1249 def sidebar_content?
1247 1250 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1248 1251 end
1249 1252
1250 1253 def view_layouts_base_sidebar_hook_response
1251 1254 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1252 1255 end
1253 1256
1254 1257 def email_delivery_enabled?
1255 1258 !!ActionMailer::Base.perform_deliveries
1256 1259 end
1257 1260
1258 1261 # Returns the avatar image tag for the given +user+ if avatars are enabled
1259 1262 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1260 1263 def avatar(user, options = { })
1261 1264 if Setting.gravatar_enabled?
1262 1265 options.merge!(:default => Setting.gravatar_default)
1263 1266 email = nil
1264 1267 if user.respond_to?(:mail)
1265 1268 email = user.mail
1266 1269 elsif user.to_s =~ %r{<(.+?)>}
1267 1270 email = $1
1268 1271 end
1269 1272 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1270 1273 else
1271 1274 ''
1272 1275 end
1273 1276 end
1274 1277
1275 1278 # Returns a link to edit user's avatar if avatars are enabled
1276 1279 def avatar_edit_link(user, options={})
1277 1280 if Setting.gravatar_enabled?
1278 1281 url = "https://gravatar.com"
1279 1282 link_to avatar(user, {:title => l(:button_edit)}.merge(options)), url, :target => '_blank'
1280 1283 end
1281 1284 end
1282 1285
1283 1286 def sanitize_anchor_name(anchor)
1284 1287 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1285 1288 end
1286 1289
1287 1290 # Returns the javascript tags that are included in the html layout head
1288 1291 def javascript_heads
1289 1292 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.4', 'application', 'responsive')
1290 1293 unless User.current.pref.warn_on_leaving_unsaved == '0'
1291 1294 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1292 1295 end
1293 1296 tags
1294 1297 end
1295 1298
1296 1299 def favicon
1297 1300 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1298 1301 end
1299 1302
1300 1303 # Returns the path to the favicon
1301 1304 def favicon_path
1302 1305 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1303 1306 image_path(icon)
1304 1307 end
1305 1308
1306 1309 # Returns the full URL to the favicon
1307 1310 def favicon_url
1308 1311 # TODO: use #image_url introduced in Rails4
1309 1312 path = favicon_path
1310 1313 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1311 1314 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1312 1315 end
1313 1316
1314 1317 def robot_exclusion_tag
1315 1318 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1316 1319 end
1317 1320
1318 1321 # Returns true if arg is expected in the API response
1319 1322 def include_in_api_response?(arg)
1320 1323 unless @included_in_api_response
1321 1324 param = params[:include]
1322 1325 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1323 1326 @included_in_api_response.collect!(&:strip)
1324 1327 end
1325 1328 @included_in_api_response.include?(arg.to_s)
1326 1329 end
1327 1330
1328 1331 # Returns options or nil if nometa param or X-Redmine-Nometa header
1329 1332 # was set in the request
1330 1333 def api_meta(options)
1331 1334 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1332 1335 # compatibility mode for activeresource clients that raise
1333 1336 # an error when deserializing an array with attributes
1334 1337 nil
1335 1338 else
1336 1339 options
1337 1340 end
1338 1341 end
1339 1342
1340 1343 def generate_csv(&block)
1341 1344 decimal_separator = l(:general_csv_decimal_separator)
1342 1345 encoding = l(:general_csv_encoding)
1343 1346 end
1344 1347
1345 1348 private
1346 1349
1347 1350 def wiki_helper
1348 1351 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1349 1352 extend helper
1350 1353 return self
1351 1354 end
1352 1355
1353 1356 def link_to_content_update(text, url_params = {}, html_options = {})
1354 1357 link_to(text, url_params, html_options)
1355 1358 end
1356 1359 end
@@ -1,280 +1,281
1 1 module ActiveRecord
2 2 module Acts #:nodoc:
3 3 module List #:nodoc:
4 4 def self.included(base)
5 5 base.extend(ClassMethods)
6 6 end
7 7
8 8 # This +acts_as+ extension provides the capabilities for sorting and reordering a number of objects in a list.
9 9 # The class that has this specified needs to have a +position+ column defined as an integer on
10 10 # the mapped database table.
11 11 #
12 12 # Todo list example:
13 13 #
14 14 # class TodoList < ActiveRecord::Base
15 15 # has_many :todo_items, :order => "position"
16 16 # end
17 17 #
18 18 # class TodoItem < ActiveRecord::Base
19 19 # belongs_to :todo_list
20 20 # acts_as_list :scope => :todo_list
21 21 # end
22 22 #
23 23 # todo_list.first.move_to_bottom
24 24 # todo_list.last.move_higher
25 25 module ClassMethods
26 26 # Configuration options are:
27 27 #
28 28 # * +column+ - specifies the column name to use for keeping the position integer (default: +position+)
29 29 # * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach <tt>_id</tt>
30 30 # (if it hasn't already been added) and use that as the foreign key restriction. It's also possible
31 31 # to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
32 32 # Example: <tt>acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
33 33 def acts_as_list(options = {})
34 ActiveSupport::Deprecation.warn "The acts_as_list plugin will be removed from Redmine 4 core, use the acts_as_list gem or similar implementation instead."
34 35 configuration = { :column => "position", :scope => "1 = 1" }
35 36 configuration.update(options) if options.is_a?(Hash)
36 37
37 38 configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
38 39
39 40 if configuration[:scope].is_a?(Symbol)
40 41 scope_condition_method = %(
41 42 def scope_condition
42 43 if #{configuration[:scope].to_s}.nil?
43 44 "#{configuration[:scope].to_s} IS NULL"
44 45 else
45 46 "#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}"
46 47 end
47 48 end
48 49 )
49 50 else
50 51 scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end"
51 52 end
52 53
53 54 class_eval <<-EOV
54 55 include ActiveRecord::Acts::List::InstanceMethods
55 56
56 57 def acts_as_list_class
57 58 ::#{self.name}
58 59 end
59 60
60 61 def position_column
61 62 '#{configuration[:column]}'
62 63 end
63 64
64 65 #{scope_condition_method}
65 66
66 67 before_destroy :remove_from_list
67 68 before_create :add_to_list_bottom
68 69 EOV
69 70 end
70 71 end
71 72
72 73 # All the methods available to a record that has had <tt>acts_as_list</tt> specified. Each method works
73 74 # by assuming the object to be the item in the list, so <tt>chapter.move_lower</tt> would move that chapter
74 75 # lower in the list of all chapters. Likewise, <tt>chapter.first?</tt> would return +true+ if that chapter is
75 76 # the first in the list of all chapters.
76 77 module InstanceMethods
77 78 # Insert the item at the given position (defaults to the top position of 1).
78 79 def insert_at(position = 1)
79 80 insert_at_position(position)
80 81 end
81 82
82 83 # Swap positions with the next lower item, if one exists.
83 84 def move_lower
84 85 return unless lower_item
85 86
86 87 acts_as_list_class.transaction do
87 88 lower_item.decrement_position
88 89 increment_position
89 90 end
90 91 end
91 92
92 93 # Swap positions with the next higher item, if one exists.
93 94 def move_higher
94 95 return unless higher_item
95 96
96 97 acts_as_list_class.transaction do
97 98 higher_item.increment_position
98 99 decrement_position
99 100 end
100 101 end
101 102
102 103 # Move to the bottom of the list. If the item is already in the list, the items below it have their
103 104 # position adjusted accordingly.
104 105 def move_to_bottom
105 106 return unless in_list?
106 107 acts_as_list_class.transaction do
107 108 decrement_positions_on_lower_items
108 109 assume_bottom_position
109 110 end
110 111 end
111 112
112 113 # Move to the top of the list. If the item is already in the list, the items above it have their
113 114 # position adjusted accordingly.
114 115 def move_to_top
115 116 return unless in_list?
116 117 acts_as_list_class.transaction do
117 118 increment_positions_on_higher_items
118 119 assume_top_position
119 120 end
120 121 end
121 122
122 123 # Move to the given position
123 124 def move_to=(pos)
124 125 case pos.to_s
125 126 when 'highest'
126 127 move_to_top
127 128 when 'higher'
128 129 move_higher
129 130 when 'lower'
130 131 move_lower
131 132 when 'lowest'
132 133 move_to_bottom
133 134 end
134 135 reset_positions_in_list
135 136 end
136 137
137 138 def reset_positions_in_list
138 139 acts_as_list_class.where(scope_condition).reorder("#{position_column} ASC, id ASC").each_with_index do |item, i|
139 140 unless item.send(position_column) == (i + 1)
140 141 acts_as_list_class.where({:id => item.id}).
141 142 update_all({position_column => (i + 1)})
142 143 end
143 144 end
144 145 end
145 146
146 147 # Removes the item from the list.
147 148 def remove_from_list
148 149 if in_list?
149 150 decrement_positions_on_lower_items
150 151 update_attribute position_column, nil
151 152 end
152 153 end
153 154
154 155 # Increase the position of this item without adjusting the rest of the list.
155 156 def increment_position
156 157 return unless in_list?
157 158 update_attribute position_column, self.send(position_column).to_i + 1
158 159 end
159 160
160 161 # Decrease the position of this item without adjusting the rest of the list.
161 162 def decrement_position
162 163 return unless in_list?
163 164 update_attribute position_column, self.send(position_column).to_i - 1
164 165 end
165 166
166 167 # Return +true+ if this object is the first in the list.
167 168 def first?
168 169 return false unless in_list?
169 170 self.send(position_column) == 1
170 171 end
171 172
172 173 # Return +true+ if this object is the last in the list.
173 174 def last?
174 175 return false unless in_list?
175 176 self.send(position_column) == bottom_position_in_list
176 177 end
177 178
178 179 # Return the next higher item in the list.
179 180 def higher_item
180 181 return nil unless in_list?
181 182 acts_as_list_class.where(
182 183 "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}"
183 184 ).first
184 185 end
185 186
186 187 # Return the next lower item in the list.
187 188 def lower_item
188 189 return nil unless in_list?
189 190 acts_as_list_class.where(
190 191 "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}"
191 192 ).first
192 193 end
193 194
194 195 # Test if this record is in a list
195 196 def in_list?
196 197 !send(position_column).nil?
197 198 end
198 199
199 200 private
200 201 def add_to_list_top
201 202 increment_positions_on_all_items
202 203 end
203 204
204 205 def add_to_list_bottom
205 206 self[position_column] = bottom_position_in_list.to_i + 1
206 207 end
207 208
208 209 # Overwrite this method to define the scope of the list changes
209 210 def scope_condition() "1" end
210 211
211 212 # Returns the bottom position number in the list.
212 213 # bottom_position_in_list # => 2
213 214 def bottom_position_in_list(except = nil)
214 215 item = bottom_item(except)
215 216 item ? item.send(position_column) : 0
216 217 end
217 218
218 219 # Returns the bottom item
219 220 def bottom_item(except = nil)
220 221 conditions = scope_condition
221 222 conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
222 223 acts_as_list_class.where(conditions).reorder("#{position_column} DESC").first
223 224 end
224 225
225 226 # Forces item to assume the bottom position in the list.
226 227 def assume_bottom_position
227 228 update_attribute(position_column, bottom_position_in_list(self).to_i + 1)
228 229 end
229 230
230 231 # Forces item to assume the top position in the list.
231 232 def assume_top_position
232 233 update_attribute(position_column, 1)
233 234 end
234 235
235 236 # This has the effect of moving all the higher items up one.
236 237 def decrement_positions_on_higher_items(position)
237 238 acts_as_list_class.
238 239 where("#{scope_condition} AND #{position_column} <= #{position}").
239 240 update_all("#{position_column} = (#{position_column} - 1)")
240 241 end
241 242
242 243 # This has the effect of moving all the lower items up one.
243 244 def decrement_positions_on_lower_items
244 245 return unless in_list?
245 246 acts_as_list_class.
246 247 where("#{scope_condition} AND #{position_column} > #{send(position_column).to_i}").
247 248 update_all("#{position_column} = (#{position_column} - 1)")
248 249 end
249 250
250 251 # This has the effect of moving all the higher items down one.
251 252 def increment_positions_on_higher_items
252 253 return unless in_list?
253 254 acts_as_list_class.
254 255 where("#{scope_condition} AND #{position_column} < #{send(position_column).to_i}").
255 256 update_all("#{position_column} = (#{position_column} + 1)")
256 257 end
257 258
258 259 # This has the effect of moving all the lower items down one.
259 260 def increment_positions_on_lower_items(position)
260 261 acts_as_list_class.
261 262 where("#{scope_condition} AND #{position_column} >= #{position}").
262 263 update_all("#{position_column} = (#{position_column} + 1)")
263 264 end
264 265
265 266 # Increments position (<tt>position_column</tt>) of all items in the list.
266 267 def increment_positions_on_all_items
267 268 acts_as_list_class.
268 269 where("#{scope_condition}").
269 270 update_all("#{position_column} = (#{position_column} + 1)")
270 271 end
271 272
272 273 def insert_at_position(position)
273 274 remove_from_list
274 275 increment_positions_on_lower_items(position)
275 276 self.update_attribute(position_column, position)
276 277 end
277 278 end
278 279 end
279 280 end
280 281 end
General Comments 0
You need to be logged in to leave comments. Login now