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