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