##// END OF EJS Templates
Use HTML5 date input fields instead of text fields with jquery ui date pickers (#19468)....
Jean-Philippe Lang -
r14993:c418fab8a76b
parent child
Show More
@@ -1,1366 +1,1366
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 require 'forwardable'
21 21 require 'cgi'
22 22
23 23 module ApplicationHelper
24 24 include Redmine::WikiFormatting::Macros::Definitions
25 25 include Redmine::I18n
26 26 include GravatarHelper::PublicMethods
27 27 include Redmine::Pagination::Helper
28 28 include Redmine::SudoMode::Helper
29 29 include Redmine::Themes::Helper
30 30 include Redmine::Hook::Helper
31 31
32 32 extend Forwardable
33 33 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
34 34
35 35 # Return true if user is authorized for controller/action, otherwise false
36 36 def authorize_for(controller, action)
37 37 User.current.allowed_to?({:controller => controller, :action => action}, @project)
38 38 end
39 39
40 40 # Display a link if user is authorized
41 41 #
42 42 # @param [String] name Anchor text (passed to link_to)
43 43 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
44 44 # @param [optional, Hash] html_options Options passed to link_to
45 45 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
46 46 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
47 47 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
48 48 end
49 49
50 50 # Displays a link to user's account page if active
51 51 def link_to_user(user, options={})
52 52 if user.is_a?(User)
53 53 name = h(user.name(options[:format]))
54 54 if user.active? || (User.current.admin? && user.logged?)
55 55 link_to name, user_path(user), :class => user.css_classes
56 56 else
57 57 name
58 58 end
59 59 else
60 60 h(user.to_s)
61 61 end
62 62 end
63 63
64 64 # Displays a link to +issue+ with its subject.
65 65 # Examples:
66 66 #
67 67 # link_to_issue(issue) # => Defect #6: This is the subject
68 68 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
69 69 # link_to_issue(issue, :subject => false) # => Defect #6
70 70 # link_to_issue(issue, :project => true) # => Foo - Defect #6
71 71 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
72 72 #
73 73 def link_to_issue(issue, options={})
74 74 title = nil
75 75 subject = nil
76 76 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
77 77 if options[:subject] == false
78 78 title = issue.subject.truncate(60)
79 79 else
80 80 subject = issue.subject
81 81 if truncate_length = options[:truncate]
82 82 subject = subject.truncate(truncate_length)
83 83 end
84 84 end
85 85 only_path = options[:only_path].nil? ? true : options[:only_path]
86 86 s = link_to(text, issue_url(issue, :only_path => only_path),
87 87 :class => issue.css_classes, :title => title)
88 88 s << h(": #{subject}") if subject
89 89 s = h("#{issue.project} - ") + s if options[:project]
90 90 s
91 91 end
92 92
93 93 # Generates a link to an attachment.
94 94 # Options:
95 95 # * :text - Link text (default to attachment filename)
96 96 # * :download - Force download (default: false)
97 97 def link_to_attachment(attachment, options={})
98 98 text = options.delete(:text) || attachment.filename
99 99 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
100 100 html_options = options.slice!(:only_path)
101 101 options[:only_path] = true unless options.key?(:only_path)
102 102 url = send(route_method, attachment, attachment.filename, options)
103 103 link_to text, url, html_options
104 104 end
105 105
106 106 # Generates a link to a SCM revision
107 107 # Options:
108 108 # * :text - Link text (default to the formatted revision)
109 109 def link_to_revision(revision, repository, options={})
110 110 if repository.is_a?(Project)
111 111 repository = repository.repository
112 112 end
113 113 text = options.delete(:text) || format_revision(revision)
114 114 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
115 115 link_to(
116 116 h(text),
117 117 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
118 118 :title => l(:label_revision_id, format_revision(revision)),
119 119 :accesskey => options[:accesskey]
120 120 )
121 121 end
122 122
123 123 # Generates a link to a message
124 124 def link_to_message(message, options={}, html_options = nil)
125 125 link_to(
126 126 message.subject.truncate(60),
127 127 board_message_url(message.board_id, message.parent_id || message.id, {
128 128 :r => (message.parent_id && message.id),
129 129 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
130 130 :only_path => true
131 131 }.merge(options)),
132 132 html_options
133 133 )
134 134 end
135 135
136 136 # Generates a link to a project if active
137 137 # Examples:
138 138 #
139 139 # link_to_project(project) # => link to the specified project overview
140 140 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
141 141 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
142 142 #
143 143 def link_to_project(project, options={}, html_options = nil)
144 144 if project.archived?
145 145 h(project.name)
146 146 else
147 147 link_to project.name,
148 148 project_url(project, {:only_path => true}.merge(options)),
149 149 html_options
150 150 end
151 151 end
152 152
153 153 # Generates a link to a project settings if active
154 154 def link_to_project_settings(project, options={}, html_options=nil)
155 155 if project.active?
156 156 link_to project.name, settings_project_path(project, options), html_options
157 157 elsif project.archived?
158 158 h(project.name)
159 159 else
160 160 link_to project.name, project_path(project, options), html_options
161 161 end
162 162 end
163 163
164 164 # Generates a link to a version
165 165 def link_to_version(version, options = {})
166 166 return '' unless version && version.is_a?(Version)
167 167 options = {:title => format_date(version.effective_date)}.merge(options)
168 168 link_to_if version.visible?, format_version_name(version), version_path(version), options
169 169 end
170 170
171 171 # Helper that formats object for html or text rendering
172 172 def format_object(object, html=true, &block)
173 173 if block_given?
174 174 object = yield object
175 175 end
176 176 case object.class.name
177 177 when 'Array'
178 178 object.map {|o| format_object(o, html)}.join(', ').html_safe
179 179 when 'Time'
180 180 format_time(object)
181 181 when 'Date'
182 182 format_date(object)
183 183 when 'Fixnum'
184 184 object.to_s
185 185 when 'Float'
186 186 sprintf "%.2f", object
187 187 when 'User'
188 188 html ? link_to_user(object) : object.to_s
189 189 when 'Project'
190 190 html ? link_to_project(object) : object.to_s
191 191 when 'Version'
192 192 html ? link_to_version(object) : object.to_s
193 193 when 'TrueClass'
194 194 l(:general_text_Yes)
195 195 when 'FalseClass'
196 196 l(:general_text_No)
197 197 when 'Issue'
198 198 object.visible? && html ? link_to_issue(object) : "##{object.id}"
199 199 when 'CustomValue', 'CustomFieldValue'
200 200 if object.custom_field
201 201 f = object.custom_field.format.formatted_custom_value(self, object, html)
202 202 if f.nil? || f.is_a?(String)
203 203 f
204 204 else
205 205 format_object(f, html, &block)
206 206 end
207 207 else
208 208 object.value.to_s
209 209 end
210 210 else
211 211 html ? h(object) : object.to_s
212 212 end
213 213 end
214 214
215 215 def wiki_page_path(page, options={})
216 216 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
217 217 end
218 218
219 219 def thumbnail_tag(attachment)
220 220 link_to image_tag(thumbnail_path(attachment)),
221 221 named_attachment_path(attachment, attachment.filename),
222 222 :title => attachment.filename
223 223 end
224 224
225 225 def toggle_link(name, id, options={})
226 226 onclick = "$('##{id}').toggle(); "
227 227 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
228 228 onclick << "return false;"
229 229 link_to(name, "#", :onclick => onclick)
230 230 end
231 231
232 232 def format_activity_title(text)
233 233 h(truncate_single_line_raw(text, 100))
234 234 end
235 235
236 236 def format_activity_day(date)
237 237 date == User.current.today ? l(:label_today).titleize : format_date(date)
238 238 end
239 239
240 240 def format_activity_description(text)
241 241 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
242 242 ).gsub(/[\r\n]+/, "<br />").html_safe
243 243 end
244 244
245 245 def format_version_name(version)
246 246 if version.project == @project
247 247 h(version)
248 248 else
249 249 h("#{version.project} - #{version}")
250 250 end
251 251 end
252 252
253 253 def due_date_distance_in_words(date)
254 254 if date
255 255 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
256 256 end
257 257 end
258 258
259 259 # Renders a tree of projects as a nested set of unordered lists
260 260 # The given collection may be a subset of the whole project tree
261 261 # (eg. some intermediate nodes are private and can not be seen)
262 262 def render_project_nested_lists(projects, &block)
263 263 s = ''
264 264 if projects.any?
265 265 ancestors = []
266 266 original_project = @project
267 267 projects.sort_by(&:lft).each do |project|
268 268 # set the project environment to please macros.
269 269 @project = project
270 270 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
271 271 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
272 272 else
273 273 ancestors.pop
274 274 s << "</li>"
275 275 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
276 276 ancestors.pop
277 277 s << "</ul></li>\n"
278 278 end
279 279 end
280 280 classes = (ancestors.empty? ? 'root' : 'child')
281 281 s << "<li class='#{classes}'><div class='#{classes}'>"
282 282 s << h(block_given? ? capture(project, &block) : project.name)
283 283 s << "</div>\n"
284 284 ancestors << project
285 285 end
286 286 s << ("</li></ul>\n" * ancestors.size)
287 287 @project = original_project
288 288 end
289 289 s.html_safe
290 290 end
291 291
292 292 def render_page_hierarchy(pages, node=nil, options={})
293 293 content = ''
294 294 if pages[node]
295 295 content << "<ul class=\"pages-hierarchy\">\n"
296 296 pages[node].each do |page|
297 297 content << "<li>"
298 298 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
299 299 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
300 300 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
301 301 content << "</li>\n"
302 302 end
303 303 content << "</ul>\n"
304 304 end
305 305 content.html_safe
306 306 end
307 307
308 308 # Renders flash messages
309 309 def render_flash_messages
310 310 s = ''
311 311 flash.each do |k,v|
312 312 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
313 313 end
314 314 s.html_safe
315 315 end
316 316
317 317 # Renders tabs and their content
318 318 def render_tabs(tabs, selected=params[:tab])
319 319 if tabs.any?
320 320 unless tabs.detect {|tab| tab[:name] == selected}
321 321 selected = nil
322 322 end
323 323 selected ||= tabs.first[:name]
324 324 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
325 325 else
326 326 content_tag 'p', l(:label_no_data), :class => "nodata"
327 327 end
328 328 end
329 329
330 330 # Renders the project quick-jump box
331 331 def render_project_jump_box
332 332 return unless User.current.logged?
333 333 projects = User.current.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
334 334 if projects.any?
335 335 options =
336 336 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
337 337 '<option value="" disabled="disabled">---</option>').html_safe
338 338
339 339 options << project_tree_options_for_select(projects, :selected => @project) do |p|
340 340 { :value => project_path(:id => p, :jump => current_menu_item) }
341 341 end
342 342
343 343 content_tag( :span, nil, :class => 'jump-box-arrow') +
344 344 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
345 345 end
346 346 end
347 347
348 348 def project_tree_options_for_select(projects, options = {})
349 349 s = ''.html_safe
350 350 if blank_text = options[:include_blank]
351 351 if blank_text == true
352 352 blank_text = '&nbsp;'.html_safe
353 353 end
354 354 s << content_tag('option', blank_text, :value => '')
355 355 end
356 356 project_tree(projects) do |project, level|
357 357 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
358 358 tag_options = {:value => project.id}
359 359 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
360 360 tag_options[:selected] = 'selected'
361 361 else
362 362 tag_options[:selected] = nil
363 363 end
364 364 tag_options.merge!(yield(project)) if block_given?
365 365 s << content_tag('option', name_prefix + h(project), tag_options)
366 366 end
367 367 s.html_safe
368 368 end
369 369
370 370 # Yields the given block for each project with its level in the tree
371 371 #
372 372 # Wrapper for Project#project_tree
373 373 def project_tree(projects, &block)
374 374 Project.project_tree(projects, &block)
375 375 end
376 376
377 377 def principals_check_box_tags(name, principals)
378 378 s = ''
379 379 principals.each do |principal|
380 380 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
381 381 end
382 382 s.html_safe
383 383 end
384 384
385 385 # Returns a string for users/groups option tags
386 386 def principals_options_for_select(collection, selected=nil)
387 387 s = ''
388 388 if collection.include?(User.current)
389 389 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
390 390 end
391 391 groups = ''
392 392 collection.sort.each do |element|
393 393 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
394 394 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
395 395 end
396 396 unless groups.empty?
397 397 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
398 398 end
399 399 s.html_safe
400 400 end
401 401
402 402 def option_tag(name, text, value, selected=nil, options={})
403 403 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
404 404 end
405 405
406 406 def truncate_single_line_raw(string, length)
407 407 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
408 408 end
409 409
410 410 # Truncates at line break after 250 characters or options[:length]
411 411 def truncate_lines(string, options={})
412 412 length = options[:length] || 250
413 413 if string.to_s =~ /\A(.{#{length}}.*?)$/m
414 414 "#{$1}..."
415 415 else
416 416 string
417 417 end
418 418 end
419 419
420 420 def anchor(text)
421 421 text.to_s.gsub(' ', '_')
422 422 end
423 423
424 424 def html_hours(text)
425 425 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
426 426 end
427 427
428 428 def authoring(created, author, options={})
429 429 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
430 430 end
431 431
432 432 def time_tag(time)
433 433 text = distance_of_time_in_words(Time.now, time)
434 434 if @project
435 435 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
436 436 else
437 437 content_tag('abbr', text, :title => format_time(time))
438 438 end
439 439 end
440 440
441 441 def syntax_highlight_lines(name, content)
442 442 lines = []
443 443 syntax_highlight(name, content).each_line { |line| lines << line }
444 444 lines
445 445 end
446 446
447 447 def syntax_highlight(name, content)
448 448 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
449 449 end
450 450
451 451 def to_path_param(path)
452 452 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
453 453 str.blank? ? nil : str
454 454 end
455 455
456 456 def reorder_links(name, url, method = :post)
457 457 # TODO: remove associated styles from application.css too
458 458 ActiveSupport::Deprecation.warn "Application#reorder_links will be removed in Redmine 4."
459 459
460 460 link_to(l(:label_sort_highest),
461 461 url.merge({"#{name}[move_to]" => 'highest'}), :method => method,
462 462 :title => l(:label_sort_highest), :class => 'icon-only icon-move-top') +
463 463 link_to(l(:label_sort_higher),
464 464 url.merge({"#{name}[move_to]" => 'higher'}), :method => method,
465 465 :title => l(:label_sort_higher), :class => 'icon-only icon-move-up') +
466 466 link_to(l(:label_sort_lower),
467 467 url.merge({"#{name}[move_to]" => 'lower'}), :method => method,
468 468 :title => l(:label_sort_lower), :class => 'icon-only icon-move-down') +
469 469 link_to(l(:label_sort_lowest),
470 470 url.merge({"#{name}[move_to]" => 'lowest'}), :method => method,
471 471 :title => l(:label_sort_lowest), :class => 'icon-only icon-move-bottom')
472 472 end
473 473
474 474 def reorder_handle(object, options={})
475 475 data = {
476 476 :reorder_url => options[:url] || url_for(object),
477 477 :reorder_param => options[:param] || object.class.name.underscore
478 478 }
479 479 content_tag('span', '',
480 480 :class => "sort-handle",
481 481 :data => data,
482 482 :title => l(:button_sort))
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 << content_tag(:span, h(@project), class: 'current-project')
512 512 if b.size > 1
513 513 separator = content_tag(:span, ' &raquo; '.html_safe, class: 'separator')
514 514 path = safe_join(b[0..-2], separator) + separator
515 515 b = [content_tag(:span, path.html_safe, class: 'breadcrumbs'), b[-1]]
516 516 end
517 517 safe_join b
518 518 end
519 519 end
520 520
521 521 # Returns a h2 tag and sets the html title with the given arguments
522 522 def title(*args)
523 523 strings = args.map do |arg|
524 524 if arg.is_a?(Array) && arg.size >= 2
525 525 link_to(*arg)
526 526 else
527 527 h(arg.to_s)
528 528 end
529 529 end
530 530 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
531 531 content_tag('h2', strings.join(' &#187; ').html_safe)
532 532 end
533 533
534 534 # Sets the html title
535 535 # Returns the html title when called without arguments
536 536 # Current project name and app_title and automatically appended
537 537 # Exemples:
538 538 # html_title 'Foo', 'Bar'
539 539 # html_title # => 'Foo - Bar - My Project - Redmine'
540 540 def html_title(*args)
541 541 if args.empty?
542 542 title = @html_title || []
543 543 title << @project.name if @project
544 544 title << Setting.app_title unless Setting.app_title == title.last
545 545 title.reject(&:blank?).join(' - ')
546 546 else
547 547 @html_title ||= []
548 548 @html_title += args
549 549 end
550 550 end
551 551
552 552 # Returns the theme, controller name, and action as css classes for the
553 553 # HTML body.
554 554 def body_css_classes
555 555 css = []
556 556 if theme = Redmine::Themes.theme(Setting.ui_theme)
557 557 css << 'theme-' + theme.name
558 558 end
559 559
560 560 css << 'project-' + @project.identifier if @project && @project.identifier.present?
561 561 css << 'controller-' + controller_name
562 562 css << 'action-' + action_name
563 563 css.join(' ')
564 564 end
565 565
566 566 def accesskey(s)
567 567 @used_accesskeys ||= []
568 568 key = Redmine::AccessKeys.key_for(s)
569 569 return nil if @used_accesskeys.include?(key)
570 570 @used_accesskeys << key
571 571 key
572 572 end
573 573
574 574 # Formats text according to system settings.
575 575 # 2 ways to call this method:
576 576 # * with a String: textilizable(text, options)
577 577 # * with an object and one of its attribute: textilizable(issue, :description, options)
578 578 def textilizable(*args)
579 579 options = args.last.is_a?(Hash) ? args.pop : {}
580 580 case args.size
581 581 when 1
582 582 obj = options[:object]
583 583 text = args.shift
584 584 when 2
585 585 obj = args.shift
586 586 attr = args.shift
587 587 text = obj.send(attr).to_s
588 588 else
589 589 raise ArgumentError, 'invalid arguments to textilizable'
590 590 end
591 591 return '' if text.blank?
592 592 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
593 593 @only_path = only_path = options.delete(:only_path) == false ? false : true
594 594
595 595 text = text.dup
596 596 macros = catch_macros(text)
597 597 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
598 598
599 599 @parsed_headings = []
600 600 @heading_anchors = {}
601 601 @current_section = 0 if options[:edit_section_links]
602 602
603 603 parse_sections(text, project, obj, attr, only_path, options)
604 604 text = parse_non_pre_blocks(text, obj, macros) do |text|
605 605 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
606 606 send method_name, text, project, obj, attr, only_path, options
607 607 end
608 608 end
609 609 parse_headings(text, project, obj, attr, only_path, options)
610 610
611 611 if @parsed_headings.any?
612 612 replace_toc(text, @parsed_headings)
613 613 end
614 614
615 615 text.html_safe
616 616 end
617 617
618 618 def parse_non_pre_blocks(text, obj, macros)
619 619 s = StringScanner.new(text)
620 620 tags = []
621 621 parsed = ''
622 622 while !s.eos?
623 623 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
624 624 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
625 625 if tags.empty?
626 626 yield text
627 627 inject_macros(text, obj, macros) if macros.any?
628 628 else
629 629 inject_macros(text, obj, macros, false) if macros.any?
630 630 end
631 631 parsed << text
632 632 if tag
633 633 if closing
634 634 if tags.last && tags.last.casecmp(tag) == 0
635 635 tags.pop
636 636 end
637 637 else
638 638 tags << tag.downcase
639 639 end
640 640 parsed << full_tag
641 641 end
642 642 end
643 643 # Close any non closing tags
644 644 while tag = tags.pop
645 645 parsed << "</#{tag}>"
646 646 end
647 647 parsed
648 648 end
649 649
650 650 def parse_inline_attachments(text, project, obj, attr, only_path, options)
651 651 return if options[:inline_attachments] == false
652 652
653 653 # when using an image link, try to use an attachment, if possible
654 654 attachments = options[:attachments] || []
655 655 attachments += obj.attachments if obj.respond_to?(:attachments)
656 656 if attachments.present?
657 657 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
658 658 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
659 659 # search for the picture in attachments
660 660 if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
661 661 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
662 662 desc = found.description.to_s.gsub('"', '')
663 663 if !desc.blank? && alttext.blank?
664 664 alt = " title=\"#{desc}\" alt=\"#{desc}\""
665 665 end
666 666 "src=\"#{image_url}\"#{alt}"
667 667 else
668 668 m
669 669 end
670 670 end
671 671 end
672 672 end
673 673
674 674 # Wiki links
675 675 #
676 676 # Examples:
677 677 # [[mypage]]
678 678 # [[mypage|mytext]]
679 679 # wiki links can refer other project wikis, using project name or identifier:
680 680 # [[project:]] -> wiki starting page
681 681 # [[project:|mytext]]
682 682 # [[project:mypage]]
683 683 # [[project:mypage|mytext]]
684 684 def parse_wiki_links(text, project, obj, attr, only_path, options)
685 685 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
686 686 link_project = project
687 687 esc, all, page, title = $1, $2, $3, $5
688 688 if esc.nil?
689 689 if page =~ /^([^\:]+)\:(.*)$/
690 690 identifier, page = $1, $2
691 691 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
692 692 title ||= identifier if page.blank?
693 693 end
694 694
695 695 if link_project && link_project.wiki
696 696 # extract anchor
697 697 anchor = nil
698 698 if page =~ /^(.+?)\#(.+)$/
699 699 page, anchor = $1, $2
700 700 end
701 701 anchor = sanitize_anchor_name(anchor) if anchor.present?
702 702 # check if page exists
703 703 wiki_page = link_project.wiki.find_page(page)
704 704 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
705 705 "##{anchor}"
706 706 else
707 707 case options[:wiki_links]
708 708 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
709 709 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
710 710 else
711 711 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
712 712 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
713 713 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
714 714 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
715 715 end
716 716 end
717 717 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
718 718 else
719 719 # project or wiki doesn't exist
720 720 all
721 721 end
722 722 else
723 723 all
724 724 end
725 725 end
726 726 end
727 727
728 728 # Redmine links
729 729 #
730 730 # Examples:
731 731 # Issues:
732 732 # #52 -> Link to issue #52
733 733 # Changesets:
734 734 # r52 -> Link to revision 52
735 735 # commit:a85130f -> Link to scmid starting with a85130f
736 736 # Documents:
737 737 # document#17 -> Link to document with id 17
738 738 # document:Greetings -> Link to the document with title "Greetings"
739 739 # document:"Some document" -> Link to the document with title "Some document"
740 740 # Versions:
741 741 # version#3 -> Link to version with id 3
742 742 # version:1.0.0 -> Link to version named "1.0.0"
743 743 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
744 744 # Attachments:
745 745 # attachment:file.zip -> Link to the attachment of the current object named file.zip
746 746 # Source files:
747 747 # source:some/file -> Link to the file located at /some/file in the project's repository
748 748 # source:some/file@52 -> Link to the file's revision 52
749 749 # source:some/file#L120 -> Link to line 120 of the file
750 750 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
751 751 # export:some/file -> Force the download of the file
752 752 # Forum messages:
753 753 # message#1218 -> Link to message with id 1218
754 754 # Projects:
755 755 # project:someproject -> Link to project named "someproject"
756 756 # project#3 -> Link to project with id 3
757 757 #
758 758 # Links can refer other objects from other projects, using project identifier:
759 759 # identifier:r52
760 760 # identifier:document:"Some document"
761 761 # identifier:version:1.0.0
762 762 # identifier:source:some/file
763 763 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
764 764 text.gsub!(%r{<a( [^>]+?)?>(.*?)</a>|([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
765 765 tag_content, leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $2, $3, $4, $5, $6, $7, $12, $13, $10 || $14 || $20, $16 || $21, $17, $19
766 766 if tag_content
767 767 $&
768 768 else
769 769 link = nil
770 770 project = default_project
771 771 if project_identifier
772 772 project = Project.visible.find_by_identifier(project_identifier)
773 773 end
774 774 if esc.nil?
775 775 if prefix.nil? && sep == 'r'
776 776 if project
777 777 repository = nil
778 778 if repo_identifier
779 779 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
780 780 else
781 781 repository = project.repository
782 782 end
783 783 # project.changesets.visible raises an SQL error because of a double join on repositories
784 784 if repository &&
785 785 (changeset = Changeset.visible.
786 786 find_by_repository_id_and_revision(repository.id, identifier))
787 787 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
788 788 {:only_path => only_path, :controller => 'repositories',
789 789 :action => 'revision', :id => project,
790 790 :repository_id => repository.identifier_param,
791 791 :rev => changeset.revision},
792 792 :class => 'changeset',
793 793 :title => truncate_single_line_raw(changeset.comments, 100))
794 794 end
795 795 end
796 796 elsif sep == '#'
797 797 oid = identifier.to_i
798 798 case prefix
799 799 when nil
800 800 if oid.to_s == identifier &&
801 801 issue = Issue.visible.find_by_id(oid)
802 802 anchor = comment_id ? "note-#{comment_id}" : nil
803 803 link = link_to("##{oid}#{comment_suffix}",
804 804 issue_url(issue, :only_path => only_path, :anchor => anchor),
805 805 :class => issue.css_classes,
806 806 :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
807 807 end
808 808 when 'document'
809 809 if document = Document.visible.find_by_id(oid)
810 810 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
811 811 end
812 812 when 'version'
813 813 if version = Version.visible.find_by_id(oid)
814 814 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
815 815 end
816 816 when 'message'
817 817 if message = Message.visible.find_by_id(oid)
818 818 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
819 819 end
820 820 when 'forum'
821 821 if board = Board.visible.find_by_id(oid)
822 822 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
823 823 end
824 824 when 'news'
825 825 if news = News.visible.find_by_id(oid)
826 826 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
827 827 end
828 828 when 'project'
829 829 if p = Project.visible.find_by_id(oid)
830 830 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
831 831 end
832 832 end
833 833 elsif sep == ':'
834 834 # removes the double quotes if any
835 835 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
836 836 name = CGI.unescapeHTML(name)
837 837 case prefix
838 838 when 'document'
839 839 if project && document = project.documents.visible.find_by_title(name)
840 840 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
841 841 end
842 842 when 'version'
843 843 if project && version = project.versions.visible.find_by_name(name)
844 844 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
845 845 end
846 846 when 'forum'
847 847 if project && board = project.boards.visible.find_by_name(name)
848 848 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
849 849 end
850 850 when 'news'
851 851 if project && news = project.news.visible.find_by_title(name)
852 852 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
853 853 end
854 854 when 'commit', 'source', 'export'
855 855 if project
856 856 repository = nil
857 857 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
858 858 repo_prefix, repo_identifier, name = $1, $2, $3
859 859 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
860 860 else
861 861 repository = project.repository
862 862 end
863 863 if prefix == 'commit'
864 864 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
865 865 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},
866 866 :class => 'changeset',
867 867 :title => truncate_single_line_raw(changeset.comments, 100)
868 868 end
869 869 else
870 870 if repository && User.current.allowed_to?(:browse_repository, project)
871 871 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
872 872 path, rev, anchor = $1, $3, $5
873 873 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,
874 874 :path => to_path_param(path),
875 875 :rev => rev,
876 876 :anchor => anchor},
877 877 :class => (prefix == 'export' ? 'source download' : 'source')
878 878 end
879 879 end
880 880 repo_prefix = nil
881 881 end
882 882 when 'attachment'
883 883 attachments = options[:attachments] || []
884 884 attachments += obj.attachments if obj.respond_to?(:attachments)
885 885 if attachments && attachment = Attachment.latest_attach(attachments, name)
886 886 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
887 887 end
888 888 when 'project'
889 889 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
890 890 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
891 891 end
892 892 end
893 893 end
894 894 end
895 895 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
896 896 end
897 897 end
898 898 end
899 899
900 900 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
901 901
902 902 def parse_sections(text, project, obj, attr, only_path, options)
903 903 return unless options[:edit_section_links]
904 904 text.gsub!(HEADING_RE) do
905 905 heading, level = $1, $2
906 906 @current_section += 1
907 907 if @current_section > 1
908 908 content_tag('div',
909 909 link_to(l(:button_edit_section), options[:edit_section_links].merge(:section => @current_section),
910 910 :class => 'icon-only icon-edit'),
911 911 :class => "contextual heading-#{level}",
912 912 :title => l(:button_edit_section),
913 913 :id => "section-#{@current_section}") + heading.html_safe
914 914 else
915 915 heading
916 916 end
917 917 end
918 918 end
919 919
920 920 # Headings and TOC
921 921 # Adds ids and links to headings unless options[:headings] is set to false
922 922 def parse_headings(text, project, obj, attr, only_path, options)
923 923 return if options[:headings] == false
924 924
925 925 text.gsub!(HEADING_RE) do
926 926 level, attrs, content = $2.to_i, $3, $4
927 927 item = strip_tags(content).strip
928 928 anchor = sanitize_anchor_name(item)
929 929 # used for single-file wiki export
930 930 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
931 931 @heading_anchors[anchor] ||= 0
932 932 idx = (@heading_anchors[anchor] += 1)
933 933 if idx > 1
934 934 anchor = "#{anchor}-#{idx}"
935 935 end
936 936 @parsed_headings << [level, anchor, item]
937 937 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
938 938 end
939 939 end
940 940
941 941 MACROS_RE = /(
942 942 (!)? # escaping
943 943 (
944 944 \{\{ # opening tag
945 945 ([\w]+) # macro name
946 946 (\(([^\n\r]*?)\))? # optional arguments
947 947 ([\n\r].*?[\n\r])? # optional block of text
948 948 \}\} # closing tag
949 949 )
950 950 )/mx unless const_defined?(:MACROS_RE)
951 951
952 952 MACRO_SUB_RE = /(
953 953 \{\{
954 954 macro\((\d+)\)
955 955 \}\}
956 956 )/x unless const_defined?(:MACRO_SUB_RE)
957 957
958 958 # Extracts macros from text
959 959 def catch_macros(text)
960 960 macros = {}
961 961 text.gsub!(MACROS_RE) do
962 962 all, macro = $1, $4.downcase
963 963 if macro_exists?(macro) || all =~ MACRO_SUB_RE
964 964 index = macros.size
965 965 macros[index] = all
966 966 "{{macro(#{index})}}"
967 967 else
968 968 all
969 969 end
970 970 end
971 971 macros
972 972 end
973 973
974 974 # Executes and replaces macros in text
975 975 def inject_macros(text, obj, macros, execute=true)
976 976 text.gsub!(MACRO_SUB_RE) do
977 977 all, index = $1, $2.to_i
978 978 orig = macros.delete(index)
979 979 if execute && orig && orig =~ MACROS_RE
980 980 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
981 981 if esc.nil?
982 982 h(exec_macro(macro, obj, args, block) || all)
983 983 else
984 984 h(all)
985 985 end
986 986 elsif orig
987 987 h(orig)
988 988 else
989 989 h(all)
990 990 end
991 991 end
992 992 end
993 993
994 994 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
995 995
996 996 # Renders the TOC with given headings
997 997 def replace_toc(text, headings)
998 998 text.gsub!(TOC_RE) do
999 999 left_align, right_align = $2, $3
1000 1000 # Keep only the 4 first levels
1001 1001 headings = headings.select{|level, anchor, item| level <= 4}
1002 1002 if headings.empty?
1003 1003 ''
1004 1004 else
1005 1005 div_class = 'toc'
1006 1006 div_class << ' right' if right_align
1007 1007 div_class << ' left' if left_align
1008 1008 out = "<ul class=\"#{div_class}\"><li>"
1009 1009 root = headings.map(&:first).min
1010 1010 current = root
1011 1011 started = false
1012 1012 headings.each do |level, anchor, item|
1013 1013 if level > current
1014 1014 out << '<ul><li>' * (level - current)
1015 1015 elsif level < current
1016 1016 out << "</li></ul>\n" * (current - level) + "</li><li>"
1017 1017 elsif started
1018 1018 out << '</li><li>'
1019 1019 end
1020 1020 out << "<a href=\"##{anchor}\">#{item}</a>"
1021 1021 current = level
1022 1022 started = true
1023 1023 end
1024 1024 out << '</li></ul>' * (current - root)
1025 1025 out << '</li></ul>'
1026 1026 end
1027 1027 end
1028 1028 end
1029 1029
1030 1030 # Same as Rails' simple_format helper without using paragraphs
1031 1031 def simple_format_without_paragraph(text)
1032 1032 text.to_s.
1033 1033 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1034 1034 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1035 1035 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1036 1036 html_safe
1037 1037 end
1038 1038
1039 1039 def lang_options_for_select(blank=true)
1040 1040 (blank ? [["(auto)", ""]] : []) + languages_options
1041 1041 end
1042 1042
1043 1043 def labelled_form_for(*args, &proc)
1044 1044 args << {} unless args.last.is_a?(Hash)
1045 1045 options = args.last
1046 1046 if args.first.is_a?(Symbol)
1047 1047 options.merge!(:as => args.shift)
1048 1048 end
1049 1049 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1050 1050 form_for(*args, &proc)
1051 1051 end
1052 1052
1053 1053 def labelled_fields_for(*args, &proc)
1054 1054 args << {} unless args.last.is_a?(Hash)
1055 1055 options = args.last
1056 1056 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1057 1057 fields_for(*args, &proc)
1058 1058 end
1059 1059
1060 1060 # Render the error messages for the given objects
1061 1061 def error_messages_for(*objects)
1062 1062 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1063 1063 errors = objects.map {|o| o.errors.full_messages}.flatten
1064 1064 render_error_messages(errors)
1065 1065 end
1066 1066
1067 1067 # Renders a list of error messages
1068 1068 def render_error_messages(errors)
1069 1069 html = ""
1070 1070 if errors.present?
1071 1071 html << "<div id='errorExplanation'><ul>\n"
1072 1072 errors.each do |error|
1073 1073 html << "<li>#{h error}</li>\n"
1074 1074 end
1075 1075 html << "</ul></div>\n"
1076 1076 end
1077 1077 html.html_safe
1078 1078 end
1079 1079
1080 1080 def delete_link(url, options={})
1081 1081 options = {
1082 1082 :method => :delete,
1083 1083 :data => {:confirm => l(:text_are_you_sure)},
1084 1084 :class => 'icon icon-del'
1085 1085 }.merge(options)
1086 1086
1087 1087 link_to l(:button_delete), url, options
1088 1088 end
1089 1089
1090 1090 def preview_link(url, form, target='preview', options={})
1091 1091 content_tag 'a', l(:label_preview), {
1092 1092 :href => "#",
1093 1093 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1094 1094 :accesskey => accesskey(:preview)
1095 1095 }.merge(options)
1096 1096 end
1097 1097
1098 1098 def link_to_function(name, function, html_options={})
1099 1099 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1100 1100 end
1101 1101
1102 1102 # Helper to render JSON in views
1103 1103 def raw_json(arg)
1104 1104 arg.to_json.to_s.gsub('/', '\/').html_safe
1105 1105 end
1106 1106
1107 1107 def back_url
1108 1108 url = params[:back_url]
1109 1109 if url.nil? && referer = request.env['HTTP_REFERER']
1110 1110 url = CGI.unescape(referer.to_s)
1111 1111 end
1112 1112 url
1113 1113 end
1114 1114
1115 1115 def back_url_hidden_field_tag
1116 1116 url = back_url
1117 1117 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1118 1118 end
1119 1119
1120 1120 def check_all_links(form_name)
1121 1121 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1122 1122 " | ".html_safe +
1123 1123 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1124 1124 end
1125 1125
1126 1126 def toggle_checkboxes_link(selector)
1127 1127 link_to_function '',
1128 1128 "toggleCheckboxesBySelector('#{selector}')",
1129 1129 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}",
1130 1130 :class => 'toggle-checkboxes'
1131 1131 end
1132 1132
1133 1133 def progress_bar(pcts, options={})
1134 1134 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1135 1135 pcts = pcts.collect(&:round)
1136 1136 pcts[1] = pcts[1] - pcts[0]
1137 1137 pcts << (100 - pcts[1] - pcts[0])
1138 1138 titles = options[:titles].to_a
1139 1139 titles[0] = "#{pcts[0]}%" if titles[0].blank?
1140 1140 legend = options[:legend] || ''
1141 1141 content_tag('table',
1142 1142 content_tag('tr',
1143 1143 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed', :title => titles[0]) : ''.html_safe) +
1144 1144 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done', :title => titles[1]) : ''.html_safe) +
1145 1145 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo', :title => titles[2]) : ''.html_safe)
1146 1146 ), :class => "progress progress-#{pcts[0]}").html_safe +
1147 1147 content_tag('p', legend, :class => 'percent').html_safe
1148 1148 end
1149 1149
1150 1150 def checked_image(checked=true)
1151 1151 if checked
1152 1152 @checked_image_tag ||= content_tag(:span, nil, :class => 'icon-only icon-checked')
1153 1153 end
1154 1154 end
1155 1155
1156 1156 def context_menu(url)
1157 1157 unless @context_menu_included
1158 1158 content_for :header_tags do
1159 1159 javascript_include_tag('context_menu') +
1160 1160 stylesheet_link_tag('context_menu')
1161 1161 end
1162 1162 if l(:direction) == 'rtl'
1163 1163 content_for :header_tags do
1164 1164 stylesheet_link_tag('context_menu_rtl')
1165 1165 end
1166 1166 end
1167 1167 @context_menu_included = true
1168 1168 end
1169 1169 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1170 1170 end
1171 1171
1172 1172 def calendar_for(field_id)
1173 1173 include_calendar_headers_tags
1174 javascript_tag("$(function() { $('##{field_id}').addClass('date').datepicker(datepickerOptions); });")
1174 javascript_tag("$(function() { $('##{field_id}').addClass('date').datepickerFallback(datepickerOptions); });")
1175 1175 end
1176 1176
1177 1177 def include_calendar_headers_tags
1178 1178 unless @calendar_headers_tags_included
1179 1179 tags = ''.html_safe
1180 1180 @calendar_headers_tags_included = true
1181 1181 content_for :header_tags do
1182 1182 start_of_week = Setting.start_of_week
1183 1183 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1184 1184 # Redmine uses 1..7 (monday..sunday) in settings and locales
1185 1185 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1186 1186 start_of_week = start_of_week.to_i % 7
1187 1187 tags << javascript_tag(
1188 1188 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1189 1189 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1190 1190 path_to_image('/images/calendar.png') +
1191 1191 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1192 1192 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1193 1193 "beforeShow: beforeShowDatePicker};")
1194 1194 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1195 1195 unless jquery_locale == 'en'
1196 1196 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1197 1197 end
1198 1198 tags
1199 1199 end
1200 1200 end
1201 1201 end
1202 1202
1203 1203 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1204 1204 # Examples:
1205 1205 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1206 1206 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1207 1207 #
1208 1208 def stylesheet_link_tag(*sources)
1209 1209 options = sources.last.is_a?(Hash) ? sources.pop : {}
1210 1210 plugin = options.delete(:plugin)
1211 1211 sources = sources.map do |source|
1212 1212 if plugin
1213 1213 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1214 1214 elsif current_theme && current_theme.stylesheets.include?(source)
1215 1215 current_theme.stylesheet_path(source)
1216 1216 else
1217 1217 source
1218 1218 end
1219 1219 end
1220 1220 super *sources, options
1221 1221 end
1222 1222
1223 1223 # Overrides Rails' image_tag with themes and plugins support.
1224 1224 # Examples:
1225 1225 # image_tag('image.png') # => picks image.png from the current theme or defaults
1226 1226 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1227 1227 #
1228 1228 def image_tag(source, options={})
1229 1229 if plugin = options.delete(:plugin)
1230 1230 source = "/plugin_assets/#{plugin}/images/#{source}"
1231 1231 elsif current_theme && current_theme.images.include?(source)
1232 1232 source = current_theme.image_path(source)
1233 1233 end
1234 1234 super source, options
1235 1235 end
1236 1236
1237 1237 # Overrides Rails' javascript_include_tag with plugins support
1238 1238 # Examples:
1239 1239 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1240 1240 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1241 1241 #
1242 1242 def javascript_include_tag(*sources)
1243 1243 options = sources.last.is_a?(Hash) ? sources.pop : {}
1244 1244 if plugin = options.delete(:plugin)
1245 1245 sources = sources.map do |source|
1246 1246 if plugin
1247 1247 "/plugin_assets/#{plugin}/javascripts/#{source}"
1248 1248 else
1249 1249 source
1250 1250 end
1251 1251 end
1252 1252 end
1253 1253 super *sources, options
1254 1254 end
1255 1255
1256 1256 def sidebar_content?
1257 1257 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1258 1258 end
1259 1259
1260 1260 def view_layouts_base_sidebar_hook_response
1261 1261 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1262 1262 end
1263 1263
1264 1264 def email_delivery_enabled?
1265 1265 !!ActionMailer::Base.perform_deliveries
1266 1266 end
1267 1267
1268 1268 # Returns the avatar image tag for the given +user+ if avatars are enabled
1269 1269 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1270 1270 def avatar(user, options = { })
1271 1271 if Setting.gravatar_enabled?
1272 1272 options.merge!(:default => Setting.gravatar_default)
1273 1273 email = nil
1274 1274 if user.respond_to?(:mail)
1275 1275 email = user.mail
1276 1276 elsif user.to_s =~ %r{<(.+?)>}
1277 1277 email = $1
1278 1278 end
1279 1279 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1280 1280 else
1281 1281 ''
1282 1282 end
1283 1283 end
1284 1284
1285 1285 # Returns a link to edit user's avatar if avatars are enabled
1286 1286 def avatar_edit_link(user, options={})
1287 1287 if Setting.gravatar_enabled?
1288 1288 url = "https://gravatar.com"
1289 1289 link_to avatar(user, {:title => l(:button_edit)}.merge(options)), url, :target => '_blank'
1290 1290 end
1291 1291 end
1292 1292
1293 1293 def sanitize_anchor_name(anchor)
1294 1294 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1295 1295 end
1296 1296
1297 1297 # Returns the javascript tags that are included in the html layout head
1298 1298 def javascript_heads
1299 1299 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.4', 'application', 'responsive')
1300 1300 unless User.current.pref.warn_on_leaving_unsaved == '0'
1301 1301 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1302 1302 end
1303 1303 tags
1304 1304 end
1305 1305
1306 1306 def favicon
1307 1307 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1308 1308 end
1309 1309
1310 1310 # Returns the path to the favicon
1311 1311 def favicon_path
1312 1312 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1313 1313 image_path(icon)
1314 1314 end
1315 1315
1316 1316 # Returns the full URL to the favicon
1317 1317 def favicon_url
1318 1318 # TODO: use #image_url introduced in Rails4
1319 1319 path = favicon_path
1320 1320 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1321 1321 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1322 1322 end
1323 1323
1324 1324 def robot_exclusion_tag
1325 1325 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1326 1326 end
1327 1327
1328 1328 # Returns true if arg is expected in the API response
1329 1329 def include_in_api_response?(arg)
1330 1330 unless @included_in_api_response
1331 1331 param = params[:include]
1332 1332 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1333 1333 @included_in_api_response.collect!(&:strip)
1334 1334 end
1335 1335 @included_in_api_response.include?(arg.to_s)
1336 1336 end
1337 1337
1338 1338 # Returns options or nil if nometa param or X-Redmine-Nometa header
1339 1339 # was set in the request
1340 1340 def api_meta(options)
1341 1341 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1342 1342 # compatibility mode for activeresource clients that raise
1343 1343 # an error when deserializing an array with attributes
1344 1344 nil
1345 1345 else
1346 1346 options
1347 1347 end
1348 1348 end
1349 1349
1350 1350 def generate_csv(&block)
1351 1351 decimal_separator = l(:general_csv_decimal_separator)
1352 1352 encoding = l(:general_csv_encoding)
1353 1353 end
1354 1354
1355 1355 private
1356 1356
1357 1357 def wiki_helper
1358 1358 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1359 1359 extend helper
1360 1360 return self
1361 1361 end
1362 1362
1363 1363 def link_to_content_update(text, url_params = {}, html_options = {})
1364 1364 link_to(text, url_params, html_options)
1365 1365 end
1366 1366 end
@@ -1,3 +1,3
1 <p><%= f.text_field(:default_value, :size => 10) %></p>
1 <p><%= f.date_field(:default_value, :value => @custom_field.default_value, :size => 10) %></p>
2 2 <%= calendar_for('custom_field_default_value') %>
3 3 <p><%= f.text_field :url_pattern, :size => 50, :label => :label_link_values_to %></p>
@@ -1,83 +1,83
1 1 <%= labelled_fields_for :issue, @issue do |f| %>
2 2
3 3 <div class="splitcontent">
4 4 <div class="splitcontentleft">
5 5 <% if @issue.safe_attribute?('status_id') && @allowed_statuses.present? %>
6 6 <p><%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), {:required => true},
7 7 :onchange => "updateIssueFrom('#{escape_javascript update_issue_form_path(@project, @issue)}', this)" %></p>
8 8 <%= hidden_field_tag 'was_default_status', @issue.status_id, :id => nil if @issue.status == @issue.default_status %>
9 9 <% else %>
10 10 <p><label><%= l(:field_status) %></label> <%= @issue.status %></p>
11 11 <% end %>
12 12
13 13 <% if @issue.safe_attribute? 'priority_id' %>
14 14 <p><%= f.select :priority_id, (@priorities.collect {|p| [p.name, p.id]}), {:required => true} %></p>
15 15 <% end %>
16 16
17 17 <% if @issue.safe_attribute? 'assigned_to_id' %>
18 18 <p><%= f.select :assigned_to_id, principals_options_for_select(@issue.assignable_users, @issue.assigned_to), :include_blank => true, :required => @issue.required_attribute?('assigned_to_id') %></p>
19 19 <% end %>
20 20
21 21 <% if @issue.safe_attribute?('category_id') && @issue.project.issue_categories.any? %>
22 22 <p><%= f.select :category_id, (@issue.project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true, :required => @issue.required_attribute?('category_id') %>
23 23 <%= link_to(l(:label_issue_category_new),
24 24 new_project_issue_category_path(@issue.project),
25 25 :remote => true,
26 26 :method => 'get',
27 27 :title => l(:label_issue_category_new),
28 28 :tabindex => 200,
29 29 :class => 'icon-only icon-add'
30 30 ) if User.current.allowed_to?(:manage_categories, @issue.project) %></p>
31 31 <% end %>
32 32
33 33 <% if @issue.safe_attribute?('fixed_version_id') && @issue.assignable_versions.any? %>
34 34 <p><%= f.select :fixed_version_id, version_options_for_select(@issue.assignable_versions, @issue.fixed_version), :include_blank => true, :required => @issue.required_attribute?('fixed_version_id') %>
35 35 <%= link_to(l(:label_version_new),
36 36 new_project_version_path(@issue.project),
37 37 :remote => true,
38 38 :method => 'get',
39 39 :title => l(:label_version_new),
40 40 :tabindex => 200,
41 41 :class => 'icon-only icon-add'
42 42 ) if User.current.allowed_to?(:manage_versions, @issue.project) %>
43 43 </p>
44 44 <% end %>
45 45 </div>
46 46
47 47 <div class="splitcontentright">
48 48 <% if @issue.safe_attribute? 'parent_issue_id' %>
49 49 <p id="parent_issue"><%= f.text_field :parent_issue_id, :size => 10, :required => @issue.required_attribute?('parent_issue_id') %></p>
50 50 <%= javascript_tag "observeAutocompleteField('issue_parent_issue_id', '#{escape_javascript auto_complete_issues_path(:project_id => @issue.project, :scope => Setting.cross_project_subtasks)}')" %>
51 51 <% end %>
52 52
53 53 <% if @issue.safe_attribute? 'start_date' %>
54 54 <p id="start_date_area">
55 <%= f.text_field(:start_date, :size => 10, :required => @issue.required_attribute?('start_date')) %>
55 <%= f.date_field(:start_date, :size => 10, :required => @issue.required_attribute?('start_date')) %>
56 56 <%= calendar_for('issue_start_date') %>
57 57 </p>
58 58 <% end %>
59 59
60 60 <% if @issue.safe_attribute? 'due_date' %>
61 61 <p id="due_date_area">
62 <%= f.text_field(:due_date, :size => 10, :required => @issue.required_attribute?('due_date')) %>
62 <%= f.date_field(:due_date, :size => 10, :required => @issue.required_attribute?('due_date')) %>
63 63 <%= calendar_for('issue_due_date') %>
64 64 </p>
65 65 <% end %>
66 66
67 67 <% if @issue.safe_attribute? 'estimated_hours' %>
68 68 <p><%= f.text_field :estimated_hours, :size => 3, :required => @issue.required_attribute?('estimated_hours') %> <%= l(:field_hours) %></p>
69 69 <% end %>
70 70
71 71 <% if @issue.safe_attribute?('done_ratio') && Issue.use_field_for_done_ratio? %>
72 72 <p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }), :required => @issue.required_attribute?('done_ratio') %></p>
73 73 <% end %>
74 74 </div>
75 75 </div>
76 76
77 77 <% if @issue.safe_attribute? 'custom_field_values' %>
78 78 <%= render :partial => 'issues/form_custom_fields' %>
79 79 <% end %>
80 80
81 81 <% end %>
82 82
83 83 <% include_calendar_headers_tags %>
@@ -1,215 +1,215
1 1 <h2><%= @copy ? l(:button_copy) : l(:label_bulk_edit_selected_issues) %></h2>
2 2
3 3 <% if @saved_issues && @unsaved_issues.present? %>
4 4 <div id="errorExplanation">
5 5 <span>
6 6 <%= l(:notice_failed_to_save_issues,
7 7 :count => @unsaved_issues.size,
8 8 :total => @saved_issues.size,
9 9 :ids => @unsaved_issues.map {|i| "##{i.id}"}.join(', ')) %>
10 10 </span>
11 11 <ul>
12 12 <% bulk_edit_error_messages(@unsaved_issues).each do |message| %>
13 13 <li><%= message %></li>
14 14 <% end %>
15 15 </ul>
16 16 </div>
17 17 <% end %>
18 18
19 19 <ul id="bulk-selection">
20 20 <% @issues.each do |issue| %>
21 21 <%= content_tag 'li', link_to_issue(issue) %>
22 22 <% end %>
23 23 </ul>
24 24
25 25 <%= form_tag(bulk_update_issues_path, :id => 'bulk_edit_form') do %>
26 26 <%= @issues.collect {|i| hidden_field_tag('ids[]', i.id, :id => nil)}.join("\n").html_safe %>
27 27 <div class="box tabular">
28 28 <fieldset class="attributes">
29 29 <legend><%= l(:label_change_properties) %></legend>
30 30
31 31 <div class="splitcontentleft">
32 32 <% if @allowed_projects.present? %>
33 33 <p>
34 34 <label for="issue_project_id"><%= l(:field_project) %></label>
35 35 <%= select_tag('issue[project_id]',
36 36 project_tree_options_for_select(@allowed_projects,
37 37 :include_blank => ((!@copy || (@projects & @allowed_projects == @projects)) ? l(:label_no_change_option) : false),
38 38 :selected => @target_project),
39 39 :onchange => "updateBulkEditFrom('#{escape_javascript url_for(:action => 'bulk_edit', :format => 'js')}')") %>
40 40 </p>
41 41 <% end %>
42 42 <p>
43 43 <label for="issue_tracker_id"><%= l(:field_tracker) %></label>
44 44 <%= select_tag('issue[tracker_id]',
45 45 content_tag('option', l(:label_no_change_option), :value => '') +
46 46 options_from_collection_for_select(@trackers, :id, :name, @issue_params[:tracker_id])) %>
47 47 </p>
48 48 <% if @available_statuses.any? %>
49 49 <p>
50 50 <label for='issue_status_id'><%= l(:field_status) %></label>
51 51 <%= select_tag('issue[status_id]',
52 52 content_tag('option', l(:label_no_change_option), :value => '') +
53 53 options_from_collection_for_select(@available_statuses, :id, :name, @issue_params[:status_id])) %>
54 54 </p>
55 55 <% end %>
56 56
57 57 <% if @safe_attributes.include?('priority_id') -%>
58 58 <p>
59 59 <label for='issue_priority_id'><%= l(:field_priority) %></label>
60 60 <%= select_tag('issue[priority_id]',
61 61 content_tag('option', l(:label_no_change_option), :value => '') +
62 62 options_from_collection_for_select(IssuePriority.active, :id, :name, @issue_params[:priority_id])) %>
63 63 </p>
64 64 <% end %>
65 65
66 66 <% if @safe_attributes.include?('assigned_to_id') -%>
67 67 <p>
68 68 <label for='issue_assigned_to_id'><%= l(:field_assigned_to) %></label>
69 69 <%= select_tag('issue[assigned_to_id]',
70 70 content_tag('option', l(:label_no_change_option), :value => '') +
71 71 content_tag('option', l(:label_nobody), :value => 'none', :selected => (@issue_params[:assigned_to_id] == 'none')) +
72 72 principals_options_for_select(@assignables, @issue_params[:assigned_to_id])) %>
73 73 </p>
74 74 <% end %>
75 75
76 76 <% if @safe_attributes.include?('category_id') -%>
77 77 <p>
78 78 <label for='issue_category_id'><%= l(:field_category) %></label>
79 79 <%= select_tag('issue[category_id]', content_tag('option', l(:label_no_change_option), :value => '') +
80 80 content_tag('option', l(:label_none), :value => 'none', :selected => (@issue_params[:category_id] == 'none')) +
81 81 options_from_collection_for_select(@categories, :id, :name, @issue_params[:category_id])) %>
82 82 </p>
83 83 <% end %>
84 84
85 85 <% if @safe_attributes.include?('fixed_version_id') -%>
86 86 <p>
87 87 <label for='issue_fixed_version_id'><%= l(:field_fixed_version) %></label>
88 88 <%= select_tag('issue[fixed_version_id]', content_tag('option', l(:label_no_change_option), :value => '') +
89 89 content_tag('option', l(:label_none), :value => 'none', :selected => (@issue_params[:fixed_version_id] == 'none')) +
90 90 version_options_for_select(@versions.sort, @issue_params[:fixed_version_id])) %>
91 91 </p>
92 92 <% end %>
93 93
94 94 <% @custom_fields.each do |custom_field| %>
95 95 <p>
96 96 <label><%= custom_field.name %></label>
97 97 <%= custom_field_tag_for_bulk_edit('issue', custom_field, @issues, @issue_params[:custom_field_values][custom_field.id.to_s]) %>
98 98 </p>
99 99 <% end %>
100 100
101 101 <% if @copy && Setting.link_copied_issue == 'ask' %>
102 102 <p>
103 103 <label for='link_copy'><%= l(:label_link_copied_issue) %></label>
104 104 <%= hidden_field_tag 'link_copy', '0' %>
105 105 <%= check_box_tag 'link_copy', '1', params[:link_copy] != 0 %>
106 106 </p>
107 107 <% end %>
108 108
109 109 <% if @copy && @attachments_present %>
110 110 <%= hidden_field_tag 'copy_attachments', '0' %>
111 111 <p>
112 112 <label for='copy_attachments'><%= l(:label_copy_attachments) %></label>
113 113 <%= check_box_tag 'copy_attachments', '1', params[:copy_attachments] != '0' %>
114 114 </p>
115 115 <% end %>
116 116
117 117 <% if @copy && @subtasks_present %>
118 118 <%= hidden_field_tag 'copy_subtasks', '0' %>
119 119 <p>
120 120 <label for='copy_subtasks'><%= l(:label_copy_subtasks) %></label>
121 121 <%= check_box_tag 'copy_subtasks', '1', params[:copy_subtasks] != '0' %>
122 122 </p>
123 123 <% end %>
124 124
125 125 <%= call_hook(:view_issues_bulk_edit_details_bottom, { :issues => @issues }) %>
126 126 </div>
127 127
128 128 <div class="splitcontentright">
129 129 <% if @safe_attributes.include?('is_private') %>
130 130 <p>
131 131 <label for='issue_is_private'><%= l(:field_is_private) %></label>
132 132 <%= select_tag('issue[is_private]', content_tag('option', l(:label_no_change_option), :value => '') +
133 133 content_tag('option', l(:general_text_Yes), :value => '1', :selected => (@issue_params[:is_private] == '1')) +
134 134 content_tag('option', l(:general_text_No), :value => '0', :selected => (@issue_params[:is_private] == '0'))) %>
135 135 </p>
136 136 <% end %>
137 137
138 138 <% if @safe_attributes.include?('parent_issue_id') && @project %>
139 139 <p>
140 140 <label for='issue_parent_issue_id'><%= l(:field_parent_issue) %></label>
141 141 <%= text_field_tag 'issue[parent_issue_id]', '', :size => 10, :value => @issue_params[:parent_issue_id] %>
142 142 <label class="inline"><%= check_box_tag 'issue[parent_issue_id]', 'none', (@issue_params[:parent_issue_id] == 'none'), :id => nil, :data => {:disables => '#issue_parent_issue_id'} %><%= l(:button_clear) %></label>
143 143 </p>
144 144 <%= javascript_tag "observeAutocompleteField('issue_parent_issue_id', '#{escape_javascript auto_complete_issues_path(:project_id => @project, :scope => Setting.cross_project_subtasks)}')" %>
145 145 <% end %>
146 146
147 147 <% if @safe_attributes.include?('start_date') %>
148 148 <p>
149 149 <label for='issue_start_date'><%= l(:field_start_date) %></label>
150 <%= text_field_tag 'issue[start_date]', '', :value => @issue_params[:start_date], :size => 10 %><%= calendar_for('issue_start_date') %>
150 <%= date_field_tag 'issue[start_date]', '', :value => @issue_params[:start_date], :size => 10 %><%= calendar_for('issue_start_date') %>
151 151 <label class="inline"><%= check_box_tag 'issue[start_date]', 'none', (@issue_params[:start_date] == 'none'), :id => nil, :data => {:disables => '#issue_start_date'} %><%= l(:button_clear) %></label>
152 152 </p>
153 153 <% end %>
154 154
155 155 <% if @safe_attributes.include?('due_date') %>
156 156 <p>
157 157 <label for='issue_due_date'><%= l(:field_due_date) %></label>
158 <%= text_field_tag 'issue[due_date]', '', :value => @issue_params[:due_date], :size => 10 %><%= calendar_for('issue_due_date') %>
158 <%= date_field_tag 'issue[due_date]', '', :value => @issue_params[:due_date], :size => 10 %><%= calendar_for('issue_due_date') %>
159 159 <label class="inline"><%= check_box_tag 'issue[due_date]', 'none', (@issue_params[:due_date] == 'none'), :id => nil, :data => {:disables => '#issue_due_date'} %><%= l(:button_clear) %></label>
160 160 </p>
161 161 <% end %>
162 162
163 163 <% if @safe_attributes.include?('estimated_hours') %>
164 164 <p>
165 165 <label for='issue_estimated_hours'><%= l(:field_estimated_hours) %></label>
166 166 <%= text_field_tag 'issue[estimated_hours]', '', :value => @issue_params[:estimated_hours], :size => 10 %>
167 167 <label class="inline"><%= check_box_tag 'issue[estimated_hours]', 'none', (@issue_params[:estimated_hours] == 'none'), :id => nil, :data => {:disables => '#issue_estimated_hours'} %><%= l(:button_clear) %></label>
168 168 </p>
169 169 <% end %>
170 170
171 171 <% if @safe_attributes.include?('done_ratio') && Issue.use_field_for_done_ratio? %>
172 172 <p>
173 173 <label for='issue_done_ratio'><%= l(:field_done_ratio) %></label>
174 174 <%= select_tag 'issue[done_ratio]', options_for_select([[l(:label_no_change_option), '']] + (0..10).to_a.collect {|r| ["#{r*10} %", r*10] }, @issue_params[:done_ratio]) %>
175 175 </p>
176 176 <% end %>
177 177 </div>
178 178 </fieldset>
179 179
180 180 <fieldset>
181 181 <legend><%= l(:field_notes) %></legend>
182 182 <%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
183 183 <%= wikitoolbar_for 'notes' %>
184 184 </fieldset>
185 185 </div>
186 186
187 187 <p>
188 188 <% if @copy %>
189 189 <%= hidden_field_tag 'copy', '1' %>
190 190 <%= submit_tag l(:button_copy) %>
191 191 <%= submit_tag l(:button_copy_and_follow), :name => 'follow' %>
192 192 <% elsif @target_project %>
193 193 <%= submit_tag l(:button_move) %>
194 194 <%= submit_tag l(:button_move_and_follow), :name => 'follow' %>
195 195 <% else %>
196 196 <%= submit_tag l(:button_submit) %>
197 197 <% end %>
198 198 </p>
199 199
200 200 <% end %>
201 201
202 202 <%= javascript_tag do %>
203 203 $(window).load(function(){
204 204 $(document).on('change', 'input[data-disables]', function(){
205 205 if ($(this).prop('checked')){
206 206 $($(this).data('disables')).attr('disabled', true).val('');
207 207 } else {
208 208 $($(this).data('disables')).attr('disabled', false);
209 209 }
210 210 });
211 211 });
212 212 $(document).ready(function(){
213 213 $('input[data-disables]').trigger('change');
214 214 });
215 215 <% end %>
@@ -1,49 +1,49
1 1 <%= error_messages_for 'time_entry' %>
2 2 <%= back_url_hidden_field_tag %>
3 3
4 4 <div class="box tabular">
5 5 <% if @time_entry.new_record? %>
6 6 <% if params[:project_id] %>
7 7 <%= hidden_field_tag 'project_id', params[:project_id] %>
8 8 <% elsif params[:issue_id] %>
9 9 <%= hidden_field_tag 'issue_id', params[:issue_id] %>
10 10 <% else %>
11 11 <p><%= f.select :project_id, project_tree_options_for_select(Project.allowed_to(:log_time).to_a, :selected => @time_entry.project, :include_blank => true) %></p>
12 12 <% end %>
13 13 <% end %>
14 14 <p>
15 15 <%= f.text_field :issue_id, :size => 6 %>
16 16 <% if @time_entry.issue.try(:visible?) %>
17 17 <span id="time_entry_issue"><%= "#{@time_entry.issue.tracker.name} ##{@time_entry.issue.id}: #{@time_entry.issue.subject}" %></span>
18 18 <% end %>
19 19 </p>
20 <p><%= f.text_field :spent_on, :size => 10, :required => true %><%= calendar_for('time_entry_spent_on') %></p>
20 <p><%= f.date_field :spent_on, :size => 10, :required => true %><%= calendar_for('time_entry_spent_on') %></p>
21 21 <p><%= f.text_field :hours, :size => 6, :required => true %></p>
22 22 <p><%= f.text_field :comments, :size => 100, :maxlength => 1024 %></p>
23 23 <p><%= f.select :activity_id, activity_collection_for_select_options(@time_entry), :required => true %></p>
24 24 <% @time_entry.custom_field_values.each do |value| %>
25 25 <p><%= custom_field_tag_with_label :time_entry, value %></p>
26 26 <% end %>
27 27 <%= call_hook(:view_timelog_edit_form_bottom, { :time_entry => @time_entry, :form => f }) %>
28 28 </div>
29 29
30 30 <%= javascript_tag do %>
31 31 <% if @time_entry.new_record? %>
32 32 $(document).ready(function(){
33 33 $('#time_entry_project_id, #time_entry_issue_id').change(function(){
34 34 $.ajax({
35 35 url: '<%= escape_javascript new_time_entry_path(:format => 'js') %>',
36 36 type: 'post',
37 37 data: $('#new_time_entry').serialize()
38 38 });
39 39 });
40 40 });
41 41 <% end %>
42 42
43 43 observeAutocompleteField('time_entry_issue_id', '<%= escape_javascript auto_complete_issues_path(:project_id => @project, :scope => (@project ? nil : 'all'))%>', {
44 44 select: function(event, ui) {
45 45 $('#time_entry_issue').text(ui.item.label);
46 46 $('#time_entry_issue_id').blur();
47 47 }
48 48 });
49 49 <% end %>
@@ -1,50 +1,50
1 1 <h2><%= l(:label_bulk_edit_selected_time_entries) %></h2>
2 2
3 3 <ul id="bulk-selection">
4 4 <% @time_entries.each do |entry| %>
5 5 <%= content_tag 'li',
6 6 link_to("#{format_date(entry.spent_on)} - #{entry.project}: #{l(:label_f_hour_plural, :value => entry.hours)}", edit_time_entry_path(entry)) %>
7 7 <% end %>
8 8 </ul>
9 9
10 10 <%= form_tag(bulk_update_time_entries_path, :id => 'bulk_edit_form') do %>
11 11 <%= @time_entries.collect {|i| hidden_field_tag('ids[]', i.id, :id => nil)}.join.html_safe %>
12 12 <div class="box tabular">
13 13 <div>
14 14 <p>
15 15 <label><%= l(:field_issue) %></label>
16 16 <%= text_field :time_entry, :issue_id, :size => 6 %>
17 17 </p>
18 18
19 19 <p>
20 20 <label><%= l(:field_spent_on) %></label>
21 <%= text_field :time_entry, :spent_on, :size => 10 %><%= calendar_for('time_entry_spent_on') %>
21 <%= date_field :time_entry, :spent_on, :size => 10 %><%= calendar_for('time_entry_spent_on') %>
22 22 </p>
23 23
24 24 <p>
25 25 <label><%= l(:field_hours) %></label>
26 26 <%= text_field :time_entry, :hours, :size => 6 %>
27 27 </p>
28 28
29 29 <% if @available_activities.any? %>
30 30 <p>
31 31 <label><%= l(:field_activity) %></label>
32 32 <%= select_tag('time_entry[activity_id]', content_tag('option', l(:label_no_change_option), :value => '') + options_from_collection_for_select(@available_activities, :id, :name)) %>
33 33 </p>
34 34 <% end %>
35 35
36 36 <p>
37 37 <label><%= l(:field_comments) %></label>
38 38 <%= text_field(:time_entry, :comments, :size => 100) %>
39 39 </p>
40 40
41 41 <% @custom_fields.each do |custom_field| %>
42 42 <p><label><%= h(custom_field.name) %></label> <%= custom_field_tag_for_bulk_edit('time_entry', custom_field, @time_entries) %></p>
43 43 <% end %>
44 44
45 45 <%= call_hook(:view_time_entries_bulk_edit_details_bottom, { :time_entries => @time_entries }) %>
46 46 </div>
47 47 </div>
48 48
49 49 <p><%= submit_tag l(:button_submit) %></p>
50 50 <% end %>
@@ -1,16 +1,16
1 1 <%= back_url_hidden_field_tag %>
2 2 <%= error_messages_for 'version' %>
3 3
4 4 <div class="box tabular">
5 5 <p><%= f.text_field :name, :size => 60, :required => true %></p>
6 6 <p><%= f.text_field :description, :size => 60 %></p>
7 7 <p><%= f.select :status, Version::VERSION_STATUSES.collect {|s| [l("version_status_#{s}"), s]} %></p>
8 8 <p><%= f.text_field :wiki_page_title, :label => :label_wiki_page, :size => 60, :disabled => @project.wiki.nil? %></p>
9 <p><%= f.text_field :effective_date, :size => 10 %><%= calendar_for('version_effective_date') %></p>
9 <p><%= f.date_field :effective_date, :size => 10 %><%= calendar_for('version_effective_date') %></p>
10 10 <p><%= f.select :sharing, @version.allowed_sharings.collect {|v| [format_version_sharing(v), v]} %></p>
11 11
12 12 <% @version.custom_field_values.each do |value| %>
13 13 <p><%= custom_field_tag_with_label :version, value %></p>
14 14 <% end %>
15 15
16 16 </div>
@@ -1,805 +1,805
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 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 module Redmine
19 19 module FieldFormat
20 20 def self.add(name, klass)
21 21 all[name.to_s] = klass.instance
22 22 end
23 23
24 24 def self.delete(name)
25 25 all.delete(name.to_s)
26 26 end
27 27
28 28 def self.all
29 29 @formats ||= Hash.new(Base.instance)
30 30 end
31 31
32 32 def self.available_formats
33 33 all.keys
34 34 end
35 35
36 36 def self.find(name)
37 37 all[name.to_s]
38 38 end
39 39
40 40 # Return an array of custom field formats which can be used in select_tag
41 41 def self.as_select(class_name=nil)
42 42 formats = all.values.select do |format|
43 43 format.class.customized_class_names.nil? || format.class.customized_class_names.include?(class_name)
44 44 end
45 45 formats.map {|format| [::I18n.t(format.label), format.name] }.sort_by(&:first)
46 46 end
47 47
48 48 class Base
49 49 include Singleton
50 50 include Redmine::I18n
51 51 include ERB::Util
52 52
53 53 class_attribute :format_name
54 54 self.format_name = nil
55 55
56 56 # Set this to true if the format supports multiple values
57 57 class_attribute :multiple_supported
58 58 self.multiple_supported = false
59 59
60 60 # Set this to true if the format supports textual search on custom values
61 61 class_attribute :searchable_supported
62 62 self.searchable_supported = false
63 63
64 64 # Set this to true if field values can be summed up
65 65 class_attribute :totalable_supported
66 66 self.totalable_supported = false
67 67
68 68 # Restricts the classes that the custom field can be added to
69 69 # Set to nil for no restrictions
70 70 class_attribute :customized_class_names
71 71 self.customized_class_names = nil
72 72
73 73 # Name of the partial for editing the custom field
74 74 class_attribute :form_partial
75 75 self.form_partial = nil
76 76
77 77 class_attribute :change_as_diff
78 78 self.change_as_diff = false
79 79
80 80 def self.add(name)
81 81 self.format_name = name
82 82 Redmine::FieldFormat.add(name, self)
83 83 end
84 84 private_class_method :add
85 85
86 86 def self.field_attributes(*args)
87 87 CustomField.store_accessor :format_store, *args
88 88 end
89 89
90 90 field_attributes :url_pattern
91 91
92 92 def name
93 93 self.class.format_name
94 94 end
95 95
96 96 def label
97 97 "label_#{name}"
98 98 end
99 99
100 100 def cast_custom_value(custom_value)
101 101 cast_value(custom_value.custom_field, custom_value.value, custom_value.customized)
102 102 end
103 103
104 104 def cast_value(custom_field, value, customized=nil)
105 105 if value.blank?
106 106 nil
107 107 elsif value.is_a?(Array)
108 108 casted = value.map do |v|
109 109 cast_single_value(custom_field, v, customized)
110 110 end
111 111 casted.compact.sort
112 112 else
113 113 cast_single_value(custom_field, value, customized)
114 114 end
115 115 end
116 116
117 117 def cast_single_value(custom_field, value, customized=nil)
118 118 value.to_s
119 119 end
120 120
121 121 def target_class
122 122 nil
123 123 end
124 124
125 125 def possible_custom_value_options(custom_value)
126 126 possible_values_options(custom_value.custom_field, custom_value.customized)
127 127 end
128 128
129 129 def possible_values_options(custom_field, object=nil)
130 130 []
131 131 end
132 132
133 133 def value_from_keyword(custom_field, keyword, object)
134 134 possible_values_options = possible_values_options(custom_field, object)
135 135 if possible_values_options.present?
136 136 keyword = keyword.to_s
137 137 if v = possible_values_options.detect {|text, id| keyword.casecmp(text) == 0}
138 138 if v.is_a?(Array)
139 139 v.last
140 140 else
141 141 v
142 142 end
143 143 end
144 144 else
145 145 keyword
146 146 end
147 147 end
148 148
149 149 # Returns the validation errors for custom_field
150 150 # Should return an empty array if custom_field is valid
151 151 def validate_custom_field(custom_field)
152 152 []
153 153 end
154 154
155 155 # Returns the validation error messages for custom_value
156 156 # Should return an empty array if custom_value is valid
157 157 def validate_custom_value(custom_value)
158 158 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
159 159 errors = values.map do |value|
160 160 validate_single_value(custom_value.custom_field, value, custom_value.customized)
161 161 end
162 162 errors.flatten.uniq
163 163 end
164 164
165 165 def validate_single_value(custom_field, value, customized=nil)
166 166 []
167 167 end
168 168
169 169 def formatted_custom_value(view, custom_value, html=false)
170 170 formatted_value(view, custom_value.custom_field, custom_value.value, custom_value.customized, html)
171 171 end
172 172
173 173 def formatted_value(view, custom_field, value, customized=nil, html=false)
174 174 casted = cast_value(custom_field, value, customized)
175 175 if html && custom_field.url_pattern.present?
176 176 texts_and_urls = Array.wrap(casted).map do |single_value|
177 177 text = view.format_object(single_value, false).to_s
178 178 url = url_from_pattern(custom_field, single_value, customized)
179 179 [text, url]
180 180 end
181 181 links = texts_and_urls.sort_by(&:first).map {|text, url| view.link_to text, url}
182 182 links.join(', ').html_safe
183 183 else
184 184 casted
185 185 end
186 186 end
187 187
188 188 # Returns an URL generated with the custom field URL pattern
189 189 # and variables substitution:
190 190 # %value% => the custom field value
191 191 # %id% => id of the customized object
192 192 # %project_id% => id of the project of the customized object if defined
193 193 # %project_identifier% => identifier of the project of the customized object if defined
194 194 # %m1%, %m2%... => capture groups matches of the custom field regexp if defined
195 195 def url_from_pattern(custom_field, value, customized)
196 196 url = custom_field.url_pattern.to_s.dup
197 197 url.gsub!('%value%') {value.to_s}
198 198 url.gsub!('%id%') {customized.id.to_s}
199 199 url.gsub!('%project_id%') {(customized.respond_to?(:project) ? customized.project.try(:id) : nil).to_s}
200 200 url.gsub!('%project_identifier%') {(customized.respond_to?(:project) ? customized.project.try(:identifier) : nil).to_s}
201 201 if custom_field.regexp.present?
202 202 url.gsub!(%r{%m(\d+)%}) do
203 203 m = $1.to_i
204 204 if matches ||= value.to_s.match(Regexp.new(custom_field.regexp))
205 205 matches[m].to_s
206 206 end
207 207 end
208 208 end
209 209 url
210 210 end
211 211 protected :url_from_pattern
212 212
213 213 def edit_tag(view, tag_id, tag_name, custom_value, options={})
214 214 view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id))
215 215 end
216 216
217 217 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
218 218 view.text_field_tag(tag_name, value, options.merge(:id => tag_id)) +
219 219 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
220 220 end
221 221
222 222 def bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
223 223 if custom_field.is_required?
224 224 ''.html_safe
225 225 else
226 226 view.content_tag('label',
227 227 view.check_box_tag(tag_name, '__none__', (value == '__none__'), :id => nil, :data => {:disables => "##{tag_id}"}) + l(:button_clear),
228 228 :class => 'inline'
229 229 )
230 230 end
231 231 end
232 232 protected :bulk_clear_tag
233 233
234 234 def query_filter_options(custom_field, query)
235 235 {:type => :string}
236 236 end
237 237
238 238 def before_custom_field_save(custom_field)
239 239 end
240 240
241 241 # Returns a ORDER BY clause that can used to sort customized
242 242 # objects by their value of the custom field.
243 243 # Returns nil if the custom field can not be used for sorting.
244 244 def order_statement(custom_field)
245 245 # COALESCE is here to make sure that blank and NULL values are sorted equally
246 246 "COALESCE(#{join_alias custom_field}.value, '')"
247 247 end
248 248
249 249 # Returns a GROUP BY clause that can used to group by custom value
250 250 # Returns nil if the custom field can not be used for grouping.
251 251 def group_statement(custom_field)
252 252 nil
253 253 end
254 254
255 255 # Returns a JOIN clause that is added to the query when sorting by custom values
256 256 def join_for_order_statement(custom_field)
257 257 alias_name = join_alias(custom_field)
258 258
259 259 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
260 260 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
261 261 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
262 262 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
263 263 " AND (#{custom_field.visibility_by_project_condition})" +
264 264 " AND #{alias_name}.value <> ''" +
265 265 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
266 266 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
267 267 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
268 268 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)"
269 269 end
270 270
271 271 def join_alias(custom_field)
272 272 "cf_#{custom_field.id}"
273 273 end
274 274 protected :join_alias
275 275 end
276 276
277 277 class Unbounded < Base
278 278 def validate_single_value(custom_field, value, customized=nil)
279 279 errs = super
280 280 value = value.to_s
281 281 unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
282 282 errs << ::I18n.t('activerecord.errors.messages.invalid')
283 283 end
284 284 if custom_field.min_length && value.length < custom_field.min_length
285 285 errs << ::I18n.t('activerecord.errors.messages.too_short', :count => custom_field.min_length)
286 286 end
287 287 if custom_field.max_length && custom_field.max_length > 0 && value.length > custom_field.max_length
288 288 errs << ::I18n.t('activerecord.errors.messages.too_long', :count => custom_field.max_length)
289 289 end
290 290 errs
291 291 end
292 292 end
293 293
294 294 class StringFormat < Unbounded
295 295 add 'string'
296 296 self.searchable_supported = true
297 297 self.form_partial = 'custom_fields/formats/string'
298 298 field_attributes :text_formatting
299 299
300 300 def formatted_value(view, custom_field, value, customized=nil, html=false)
301 301 if html
302 302 if custom_field.url_pattern.present?
303 303 super
304 304 elsif custom_field.text_formatting == 'full'
305 305 view.textilizable(value, :object => customized)
306 306 else
307 307 value.to_s
308 308 end
309 309 else
310 310 value.to_s
311 311 end
312 312 end
313 313 end
314 314
315 315 class TextFormat < Unbounded
316 316 add 'text'
317 317 self.searchable_supported = true
318 318 self.form_partial = 'custom_fields/formats/text'
319 319 self.change_as_diff = true
320 320
321 321 def formatted_value(view, custom_field, value, customized=nil, html=false)
322 322 if html
323 323 if value.present?
324 324 if custom_field.text_formatting == 'full'
325 325 view.textilizable(value, :object => customized)
326 326 else
327 327 view.simple_format(html_escape(value))
328 328 end
329 329 else
330 330 ''
331 331 end
332 332 else
333 333 value.to_s
334 334 end
335 335 end
336 336
337 337 def edit_tag(view, tag_id, tag_name, custom_value, options={})
338 338 view.text_area_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :rows => 3))
339 339 end
340 340
341 341 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
342 342 view.text_area_tag(tag_name, value, options.merge(:id => tag_id, :rows => 3)) +
343 343 '<br />'.html_safe +
344 344 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
345 345 end
346 346
347 347 def query_filter_options(custom_field, query)
348 348 {:type => :text}
349 349 end
350 350 end
351 351
352 352 class LinkFormat < StringFormat
353 353 add 'link'
354 354 self.searchable_supported = false
355 355 self.form_partial = 'custom_fields/formats/link'
356 356
357 357 def formatted_value(view, custom_field, value, customized=nil, html=false)
358 358 if html && value.present?
359 359 if custom_field.url_pattern.present?
360 360 url = url_from_pattern(custom_field, value, customized)
361 361 else
362 362 url = value.to_s
363 363 unless url =~ %r{\A[a-z]+://}i
364 364 # no protocol found, use http by default
365 365 url = "http://" + url
366 366 end
367 367 end
368 368 view.link_to value.to_s.truncate(40), url
369 369 else
370 370 value.to_s
371 371 end
372 372 end
373 373 end
374 374
375 375 class Numeric < Unbounded
376 376 self.form_partial = 'custom_fields/formats/numeric'
377 377 self.totalable_supported = true
378 378
379 379 def order_statement(custom_field)
380 380 # Make the database cast values into numeric
381 381 # Postgresql will raise an error if a value can not be casted!
382 382 # CustomValue validations should ensure that it doesn't occur
383 383 "CAST(CASE #{join_alias custom_field}.value WHEN '' THEN '0' ELSE #{join_alias custom_field}.value END AS decimal(30,3))"
384 384 end
385 385
386 386 # Returns totals for the given scope
387 387 def total_for_scope(custom_field, scope)
388 388 scope.joins(:custom_values).
389 389 where(:custom_values => {:custom_field_id => custom_field.id}).
390 390 where.not(:custom_values => {:value => ''}).
391 391 sum("CAST(#{CustomValue.table_name}.value AS decimal(30,3))")
392 392 end
393 393
394 394 def cast_total_value(custom_field, value)
395 395 cast_single_value(custom_field, value)
396 396 end
397 397 end
398 398
399 399 class IntFormat < Numeric
400 400 add 'int'
401 401
402 402 def label
403 403 "label_integer"
404 404 end
405 405
406 406 def cast_single_value(custom_field, value, customized=nil)
407 407 value.to_i
408 408 end
409 409
410 410 def validate_single_value(custom_field, value, customized=nil)
411 411 errs = super
412 412 errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value.to_s =~ /^[+-]?\d+$/
413 413 errs
414 414 end
415 415
416 416 def query_filter_options(custom_field, query)
417 417 {:type => :integer}
418 418 end
419 419
420 420 def group_statement(custom_field)
421 421 order_statement(custom_field)
422 422 end
423 423 end
424 424
425 425 class FloatFormat < Numeric
426 426 add 'float'
427 427
428 428 def cast_single_value(custom_field, value, customized=nil)
429 429 value.to_f
430 430 end
431 431
432 432 def cast_total_value(custom_field, value)
433 433 value.to_f.round(2)
434 434 end
435 435
436 436 def validate_single_value(custom_field, value, customized=nil)
437 437 errs = super
438 438 errs << ::I18n.t('activerecord.errors.messages.invalid') unless (Kernel.Float(value) rescue nil)
439 439 errs
440 440 end
441 441
442 442 def query_filter_options(custom_field, query)
443 443 {:type => :float}
444 444 end
445 445 end
446 446
447 447 class DateFormat < Unbounded
448 448 add 'date'
449 449 self.form_partial = 'custom_fields/formats/date'
450 450
451 451 def cast_single_value(custom_field, value, customized=nil)
452 452 value.to_date rescue nil
453 453 end
454 454
455 455 def validate_single_value(custom_field, value, customized=nil)
456 456 if value =~ /^\d{4}-\d{2}-\d{2}$/ && (value.to_date rescue false)
457 457 []
458 458 else
459 459 [::I18n.t('activerecord.errors.messages.not_a_date')]
460 460 end
461 461 end
462 462
463 463 def edit_tag(view, tag_id, tag_name, custom_value, options={})
464 view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :size => 10)) +
464 view.date_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :size => 10)) +
465 465 view.calendar_for(tag_id)
466 466 end
467 467
468 468 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
469 view.text_field_tag(tag_name, value, options.merge(:id => tag_id, :size => 10)) +
469 view.date_field_tag(tag_name, value, options.merge(:id => tag_id, :size => 10)) +
470 470 view.calendar_for(tag_id) +
471 471 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
472 472 end
473 473
474 474 def query_filter_options(custom_field, query)
475 475 {:type => :date}
476 476 end
477 477
478 478 def group_statement(custom_field)
479 479 order_statement(custom_field)
480 480 end
481 481 end
482 482
483 483 class List < Base
484 484 self.multiple_supported = true
485 485 field_attributes :edit_tag_style
486 486
487 487 def edit_tag(view, tag_id, tag_name, custom_value, options={})
488 488 if custom_value.custom_field.edit_tag_style == 'check_box'
489 489 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
490 490 else
491 491 select_edit_tag(view, tag_id, tag_name, custom_value, options)
492 492 end
493 493 end
494 494
495 495 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
496 496 opts = []
497 497 opts << [l(:label_no_change_option), ''] unless custom_field.multiple?
498 498 opts << [l(:label_none), '__none__'] unless custom_field.is_required?
499 499 opts += possible_values_options(custom_field, objects)
500 500 view.select_tag(tag_name, view.options_for_select(opts, value), options.merge(:multiple => custom_field.multiple?))
501 501 end
502 502
503 503 def query_filter_options(custom_field, query)
504 504 {:type => :list_optional, :values => query_filter_values(custom_field, query)}
505 505 end
506 506
507 507 protected
508 508
509 509 # Returns the values that are available in the field filter
510 510 def query_filter_values(custom_field, query)
511 511 possible_values_options(custom_field, query.project)
512 512 end
513 513
514 514 # Renders the edit tag as a select tag
515 515 def select_edit_tag(view, tag_id, tag_name, custom_value, options={})
516 516 blank_option = ''.html_safe
517 517 unless custom_value.custom_field.multiple?
518 518 if custom_value.custom_field.is_required?
519 519 unless custom_value.custom_field.default_value.present?
520 520 blank_option = view.content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '')
521 521 end
522 522 else
523 523 blank_option = view.content_tag('option', '&nbsp;'.html_safe, :value => '')
524 524 end
525 525 end
526 526 options_tags = blank_option + view.options_for_select(possible_custom_value_options(custom_value), custom_value.value)
527 527 s = view.select_tag(tag_name, options_tags, options.merge(:id => tag_id, :multiple => custom_value.custom_field.multiple?))
528 528 if custom_value.custom_field.multiple?
529 529 s << view.hidden_field_tag(tag_name, '')
530 530 end
531 531 s
532 532 end
533 533
534 534 # Renders the edit tag as check box or radio tags
535 535 def check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
536 536 opts = []
537 537 unless custom_value.custom_field.multiple? || custom_value.custom_field.is_required?
538 538 opts << ["(#{l(:label_none)})", '']
539 539 end
540 540 opts += possible_custom_value_options(custom_value)
541 541 s = ''.html_safe
542 542 tag_method = custom_value.custom_field.multiple? ? :check_box_tag : :radio_button_tag
543 543 opts.each do |label, value|
544 544 value ||= label
545 545 checked = (custom_value.value.is_a?(Array) && custom_value.value.include?(value)) || custom_value.value.to_s == value
546 546 tag = view.send(tag_method, tag_name, value, checked, :id => tag_id)
547 547 # set the id on the first tag only
548 548 tag_id = nil
549 549 s << view.content_tag('label', tag + ' ' + label)
550 550 end
551 551 if custom_value.custom_field.multiple?
552 552 s << view.hidden_field_tag(tag_name, '')
553 553 end
554 554 css = "#{options[:class]} check_box_group"
555 555 view.content_tag('span', s, options.merge(:class => css))
556 556 end
557 557 end
558 558
559 559 class ListFormat < List
560 560 add 'list'
561 561 self.searchable_supported = true
562 562 self.form_partial = 'custom_fields/formats/list'
563 563
564 564 def possible_custom_value_options(custom_value)
565 565 options = possible_values_options(custom_value.custom_field)
566 566 missing = [custom_value.value].flatten.reject(&:blank?) - options
567 567 if missing.any?
568 568 options += missing
569 569 end
570 570 options
571 571 end
572 572
573 573 def possible_values_options(custom_field, object=nil)
574 574 custom_field.possible_values
575 575 end
576 576
577 577 def validate_custom_field(custom_field)
578 578 errors = []
579 579 errors << [:possible_values, :blank] if custom_field.possible_values.blank?
580 580 errors << [:possible_values, :invalid] unless custom_field.possible_values.is_a? Array
581 581 errors
582 582 end
583 583
584 584 def validate_custom_value(custom_value)
585 585 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
586 586 invalid_values = values - Array.wrap(custom_value.value_was) - custom_value.custom_field.possible_values
587 587 if invalid_values.any?
588 588 [::I18n.t('activerecord.errors.messages.inclusion')]
589 589 else
590 590 []
591 591 end
592 592 end
593 593
594 594 def group_statement(custom_field)
595 595 order_statement(custom_field)
596 596 end
597 597 end
598 598
599 599 class BoolFormat < List
600 600 add 'bool'
601 601 self.multiple_supported = false
602 602 self.form_partial = 'custom_fields/formats/bool'
603 603
604 604 def label
605 605 "label_boolean"
606 606 end
607 607
608 608 def cast_single_value(custom_field, value, customized=nil)
609 609 value == '1' ? true : false
610 610 end
611 611
612 612 def possible_values_options(custom_field, object=nil)
613 613 [[::I18n.t(:general_text_Yes), '1'], [::I18n.t(:general_text_No), '0']]
614 614 end
615 615
616 616 def group_statement(custom_field)
617 617 order_statement(custom_field)
618 618 end
619 619
620 620 def edit_tag(view, tag_id, tag_name, custom_value, options={})
621 621 case custom_value.custom_field.edit_tag_style
622 622 when 'check_box'
623 623 single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
624 624 when 'radio'
625 625 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
626 626 else
627 627 select_edit_tag(view, tag_id, tag_name, custom_value, options)
628 628 end
629 629 end
630 630
631 631 # Renders the edit tag as a simple check box
632 632 def single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
633 633 s = ''.html_safe
634 634 s << view.hidden_field_tag(tag_name, '0', :id => nil)
635 635 s << view.check_box_tag(tag_name, '1', custom_value.value.to_s == '1', :id => tag_id)
636 636 view.content_tag('span', s, options)
637 637 end
638 638 end
639 639
640 640 class RecordList < List
641 641 self.customized_class_names = %w(Issue TimeEntry Version Document Project)
642 642
643 643 def cast_single_value(custom_field, value, customized=nil)
644 644 target_class.find_by_id(value.to_i) if value.present?
645 645 end
646 646
647 647 def target_class
648 648 @target_class ||= self.class.name[/^(.*::)?(.+)Format$/, 2].constantize rescue nil
649 649 end
650 650
651 651 def reset_target_class
652 652 @target_class = nil
653 653 end
654 654
655 655 def possible_custom_value_options(custom_value)
656 656 options = possible_values_options(custom_value.custom_field, custom_value.customized)
657 657 missing = [custom_value.value_was].flatten.reject(&:blank?) - options.map(&:last)
658 658 if missing.any?
659 659 options += target_class.where(:id => missing.map(&:to_i)).map {|o| [o.to_s, o.id.to_s]}
660 660 end
661 661 options
662 662 end
663 663
664 664 def order_statement(custom_field)
665 665 if target_class.respond_to?(:fields_for_order_statement)
666 666 target_class.fields_for_order_statement(value_join_alias(custom_field))
667 667 end
668 668 end
669 669
670 670 def group_statement(custom_field)
671 671 "COALESCE(#{join_alias custom_field}.value, '')"
672 672 end
673 673
674 674 def join_for_order_statement(custom_field)
675 675 alias_name = join_alias(custom_field)
676 676
677 677 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
678 678 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
679 679 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
680 680 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
681 681 " AND (#{custom_field.visibility_by_project_condition})" +
682 682 " AND #{alias_name}.value <> ''" +
683 683 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
684 684 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
685 685 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
686 686 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)" +
687 687 " LEFT OUTER JOIN #{target_class.table_name} #{value_join_alias custom_field}" +
688 688 " ON CAST(CASE #{alias_name}.value WHEN '' THEN '0' ELSE #{alias_name}.value END AS decimal(30,0)) = #{value_join_alias custom_field}.id"
689 689 end
690 690
691 691 def value_join_alias(custom_field)
692 692 join_alias(custom_field) + "_" + custom_field.field_format
693 693 end
694 694 protected :value_join_alias
695 695 end
696 696
697 697 class EnumerationFormat < RecordList
698 698 add 'enumeration'
699 699 self.form_partial = 'custom_fields/formats/enumeration'
700 700
701 701 def label
702 702 "label_field_format_enumeration"
703 703 end
704 704
705 705 def target_class
706 706 @target_class ||= CustomFieldEnumeration
707 707 end
708 708
709 709 def possible_values_options(custom_field, object=nil)
710 710 possible_values_records(custom_field, object).map {|u| [u.name, u.id.to_s]}
711 711 end
712 712
713 713 def possible_values_records(custom_field, object=nil)
714 714 custom_field.enumerations.active
715 715 end
716 716
717 717 def value_from_keyword(custom_field, keyword, object)
718 718 value = custom_field.enumerations.where("LOWER(name) LIKE LOWER(?)", keyword)
719 719 value ? value.id : nil
720 720 end
721 721 end
722 722
723 723 class UserFormat < RecordList
724 724 add 'user'
725 725 self.form_partial = 'custom_fields/formats/user'
726 726 field_attributes :user_role
727 727
728 728 def possible_values_options(custom_field, object=nil)
729 729 possible_values_records(custom_field, object).map {|u| [u.name, u.id.to_s]}
730 730 end
731 731
732 732 def possible_values_records(custom_field, object=nil)
733 733 if object.is_a?(Array)
734 734 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
735 735 projects.map {|project| possible_values_records(custom_field, project)}.reduce(:&) || []
736 736 elsif object.respond_to?(:project) && object.project
737 737 scope = object.project.users
738 738 if custom_field.user_role.is_a?(Array)
739 739 role_ids = custom_field.user_role.map(&:to_s).reject(&:blank?).map(&:to_i)
740 740 if role_ids.any?
741 741 scope = scope.where("#{Member.table_name}.id IN (SELECT DISTINCT member_id FROM #{MemberRole.table_name} WHERE role_id IN (?))", role_ids)
742 742 end
743 743 end
744 744 scope.sorted
745 745 else
746 746 []
747 747 end
748 748 end
749 749
750 750 def value_from_keyword(custom_field, keyword, object)
751 751 users = possible_values_records(custom_field, object).to_a
752 752 user = Principal.detect_by_keyword(users, keyword)
753 753 user ? user.id : nil
754 754 end
755 755
756 756 def before_custom_field_save(custom_field)
757 757 super
758 758 if custom_field.user_role.is_a?(Array)
759 759 custom_field.user_role.map!(&:to_s).reject!(&:blank?)
760 760 end
761 761 end
762 762 end
763 763
764 764 class VersionFormat < RecordList
765 765 add 'version'
766 766 self.form_partial = 'custom_fields/formats/version'
767 767 field_attributes :version_status
768 768
769 769 def possible_values_options(custom_field, object=nil)
770 770 versions_options(custom_field, object)
771 771 end
772 772
773 773 def before_custom_field_save(custom_field)
774 774 super
775 775 if custom_field.version_status.is_a?(Array)
776 776 custom_field.version_status.map!(&:to_s).reject!(&:blank?)
777 777 end
778 778 end
779 779
780 780 protected
781 781
782 782 def query_filter_values(custom_field, query)
783 783 versions_options(custom_field, query.project, true)
784 784 end
785 785
786 786 def versions_options(custom_field, object, all_statuses=false)
787 787 if object.is_a?(Array)
788 788 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
789 789 projects.map {|project| possible_values_options(custom_field, project)}.reduce(:&) || []
790 790 elsif object.respond_to?(:project) && object.project
791 791 scope = object.project.shared_versions
792 792 if !all_statuses && custom_field.version_status.is_a?(Array)
793 793 statuses = custom_field.version_status.map(&:to_s).reject(&:blank?)
794 794 if statuses.any?
795 795 scope = scope.where(:status => statuses.map(&:to_s))
796 796 end
797 797 end
798 798 scope.sort.collect {|u| [u.to_s, u.id.to_s]}
799 799 else
800 800 []
801 801 end
802 802 end
803 803 end
804 804 end
805 805 end
@@ -1,746 +1,773
1 1 /* Redmine - project management software
2 2 Copyright (C) 2006-2016 Jean-Philippe Lang */
3 3
4 4 function checkAll(id, checked) {
5 5 $('#'+id).find('input[type=checkbox]:enabled').prop('checked', checked);
6 6 }
7 7
8 8 function toggleCheckboxesBySelector(selector) {
9 9 var all_checked = true;
10 10 $(selector).each(function(index) {
11 11 if (!$(this).is(':checked')) { all_checked = false; }
12 12 });
13 13 $(selector).prop('checked', !all_checked);
14 14 }
15 15
16 16 function showAndScrollTo(id, focus) {
17 17 $('#'+id).show();
18 18 if (focus !== null) {
19 19 $('#'+focus).focus();
20 20 }
21 21 $('html, body').animate({scrollTop: $('#'+id).offset().top}, 100);
22 22 }
23 23
24 24 function toggleRowGroup(el) {
25 25 var tr = $(el).parents('tr').first();
26 26 var n = tr.next();
27 27 tr.toggleClass('open');
28 28 while (n.length && !n.hasClass('group')) {
29 29 n.toggle();
30 30 n = n.next('tr');
31 31 }
32 32 }
33 33
34 34 function collapseAllRowGroups(el) {
35 35 var tbody = $(el).parents('tbody').first();
36 36 tbody.children('tr').each(function(index) {
37 37 if ($(this).hasClass('group')) {
38 38 $(this).removeClass('open');
39 39 } else {
40 40 $(this).hide();
41 41 }
42 42 });
43 43 }
44 44
45 45 function expandAllRowGroups(el) {
46 46 var tbody = $(el).parents('tbody').first();
47 47 tbody.children('tr').each(function(index) {
48 48 if ($(this).hasClass('group')) {
49 49 $(this).addClass('open');
50 50 } else {
51 51 $(this).show();
52 52 }
53 53 });
54 54 }
55 55
56 56 function toggleAllRowGroups(el) {
57 57 var tr = $(el).parents('tr').first();
58 58 if (tr.hasClass('open')) {
59 59 collapseAllRowGroups(el);
60 60 } else {
61 61 expandAllRowGroups(el);
62 62 }
63 63 }
64 64
65 65 function toggleFieldset(el) {
66 66 var fieldset = $(el).parents('fieldset').first();
67 67 fieldset.toggleClass('collapsed');
68 68 fieldset.children('div').toggle();
69 69 }
70 70
71 71 function hideFieldset(el) {
72 72 var fieldset = $(el).parents('fieldset').first();
73 73 fieldset.toggleClass('collapsed');
74 74 fieldset.children('div').hide();
75 75 }
76 76
77 77 // columns selection
78 78 function moveOptions(theSelFrom, theSelTo) {
79 79 $(theSelFrom).find('option:selected').detach().prop("selected", false).appendTo($(theSelTo));
80 80 }
81 81
82 82 function moveOptionUp(theSel) {
83 83 $(theSel).find('option:selected').each(function(){
84 84 $(this).prev(':not(:selected)').detach().insertAfter($(this));
85 85 });
86 86 }
87 87
88 88 function moveOptionTop(theSel) {
89 89 $(theSel).find('option:selected').detach().prependTo($(theSel));
90 90 }
91 91
92 92 function moveOptionDown(theSel) {
93 93 $($(theSel).find('option:selected').get().reverse()).each(function(){
94 94 $(this).next(':not(:selected)').detach().insertBefore($(this));
95 95 });
96 96 }
97 97
98 98 function moveOptionBottom(theSel) {
99 99 $(theSel).find('option:selected').detach().appendTo($(theSel));
100 100 }
101 101
102 102 function initFilters() {
103 103 $('#add_filter_select').change(function() {
104 104 addFilter($(this).val(), '', []);
105 105 });
106 106 $('#filters-table td.field input[type=checkbox]').each(function() {
107 107 toggleFilter($(this).val());
108 108 });
109 109 $('#filters-table').on('click', 'td.field input[type=checkbox]', function() {
110 110 toggleFilter($(this).val());
111 111 });
112 112 $('#filters-table').on('click', '.toggle-multiselect', function() {
113 113 toggleMultiSelect($(this).siblings('select'));
114 114 });
115 115 $('#filters-table').on('keypress', 'input[type=text]', function(e) {
116 116 if (e.keyCode == 13) $(this).closest('form').submit();
117 117 });
118 118 }
119 119
120 120 function addFilter(field, operator, values) {
121 121 var fieldId = field.replace('.', '_');
122 122 var tr = $('#tr_'+fieldId);
123 123 if (tr.length > 0) {
124 124 tr.show();
125 125 } else {
126 126 buildFilterRow(field, operator, values);
127 127 }
128 128 $('#cb_'+fieldId).prop('checked', true);
129 129 toggleFilter(field);
130 130 $('#add_filter_select').val('').find('option').each(function() {
131 131 if ($(this).attr('value') == field) {
132 132 $(this).attr('disabled', true);
133 133 }
134 134 });
135 135 }
136 136
137 137 function buildFilterRow(field, operator, values) {
138 138 var fieldId = field.replace('.', '_');
139 139 var filterTable = $("#filters-table");
140 140 var filterOptions = availableFilters[field];
141 141 if (!filterOptions) return;
142 142 var operators = operatorByType[filterOptions['type']];
143 143 var filterValues = filterOptions['values'];
144 144 var i, select;
145 145
146 146 var tr = $('<tr class="filter">').attr('id', 'tr_'+fieldId).html(
147 147 '<td class="field"><input checked="checked" id="cb_'+fieldId+'" name="f[]" value="'+field+'" type="checkbox"><label for="cb_'+fieldId+'"> '+filterOptions['name']+'</label></td>' +
148 148 '<td class="operator"><select id="operators_'+fieldId+'" name="op['+field+']"></td>' +
149 149 '<td class="values"></td>'
150 150 );
151 151 filterTable.append(tr);
152 152
153 153 select = tr.find('td.operator select');
154 154 for (i = 0; i < operators.length; i++) {
155 155 var option = $('<option>').val(operators[i]).text(operatorLabels[operators[i]]);
156 156 if (operators[i] == operator) { option.attr('selected', true); }
157 157 select.append(option);
158 158 }
159 159 select.change(function(){ toggleOperator(field); });
160 160
161 161 switch (filterOptions['type']) {
162 162 case "list":
163 163 case "list_optional":
164 164 case "list_status":
165 165 case "list_subprojects":
166 166 tr.find('td.values').append(
167 167 '<span style="display:none;"><select class="value" id="values_'+fieldId+'_1" name="v['+field+'][]"></select>' +
168 168 ' <span class="toggle-multiselect">&nbsp;</span></span>'
169 169 );
170 170 select = tr.find('td.values select');
171 171 if (values.length > 1) { select.attr('multiple', true); }
172 172 for (i = 0; i < filterValues.length; i++) {
173 173 var filterValue = filterValues[i];
174 174 var option = $('<option>');
175 175 if ($.isArray(filterValue)) {
176 176 option.val(filterValue[1]).text(filterValue[0]);
177 177 if ($.inArray(filterValue[1], values) > -1) {option.attr('selected', true);}
178 178 } else {
179 179 option.val(filterValue).text(filterValue);
180 180 if ($.inArray(filterValue, values) > -1) {option.attr('selected', true);}
181 181 }
182 182 select.append(option);
183 183 }
184 184 break;
185 185 case "date":
186 186 case "date_past":
187 187 tr.find('td.values').append(
188 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="10" class="value date_value" /></span>' +
189 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="10" class="value date_value" /></span>' +
188 '<span style="display:none;"><input type="date" name="v['+field+'][]" id="values_'+fieldId+'_1" size="10" class="value date_value" /></span>' +
189 ' <span style="display:none;"><input type="date" name="v['+field+'][]" id="values_'+fieldId+'_2" size="10" class="value date_value" /></span>' +
190 190 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="3" class="value" /> '+labelDayPlural+'</span>'
191 191 );
192 $('#values_'+fieldId+'_1').val(values[0]).datepicker(datepickerOptions);
193 $('#values_'+fieldId+'_2').val(values[1]).datepicker(datepickerOptions);
192 $('#values_'+fieldId+'_1').val(values[0]).datepickerFallback(datepickerOptions);
193 $('#values_'+fieldId+'_2').val(values[1]).datepickerFallback(datepickerOptions);
194 194 $('#values_'+fieldId).val(values[0]);
195 195 break;
196 196 case "string":
197 197 case "text":
198 198 tr.find('td.values').append(
199 199 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="30" class="value" /></span>'
200 200 );
201 201 $('#values_'+fieldId).val(values[0]);
202 202 break;
203 203 case "relation":
204 204 tr.find('td.values').append(
205 205 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="6" class="value" /></span>' +
206 206 '<span style="display:none;"><select class="value" name="v['+field+'][]" id="values_'+fieldId+'_1"></select></span>'
207 207 );
208 208 $('#values_'+fieldId).val(values[0]);
209 209 select = tr.find('td.values select');
210 210 for (i = 0; i < allProjects.length; i++) {
211 211 var filterValue = allProjects[i];
212 212 var option = $('<option>');
213 213 option.val(filterValue[1]).text(filterValue[0]);
214 214 if (values[0] == filterValue[1]) { option.attr('selected', true); }
215 215 select.append(option);
216 216 }
217 217 break;
218 218 case "integer":
219 219 case "float":
220 220 case "tree":
221 221 tr.find('td.values').append(
222 222 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="14" class="value" /></span>' +
223 223 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="14" class="value" /></span>'
224 224 );
225 225 $('#values_'+fieldId+'_1').val(values[0]);
226 226 $('#values_'+fieldId+'_2').val(values[1]);
227 227 break;
228 228 }
229 229 }
230 230
231 231 function toggleFilter(field) {
232 232 var fieldId = field.replace('.', '_');
233 233 if ($('#cb_' + fieldId).is(':checked')) {
234 234 $("#operators_" + fieldId).show().removeAttr('disabled');
235 235 toggleOperator(field);
236 236 } else {
237 237 $("#operators_" + fieldId).hide().attr('disabled', true);
238 238 enableValues(field, []);
239 239 }
240 240 }
241 241
242 242 function enableValues(field, indexes) {
243 243 var fieldId = field.replace('.', '_');
244 244 $('#tr_'+fieldId+' td.values .value').each(function(index) {
245 245 if ($.inArray(index, indexes) >= 0) {
246 246 $(this).removeAttr('disabled');
247 247 $(this).parents('span').first().show();
248 248 } else {
249 249 $(this).val('');
250 250 $(this).attr('disabled', true);
251 251 $(this).parents('span').first().hide();
252 252 }
253 253
254 254 if ($(this).hasClass('group')) {
255 255 $(this).addClass('open');
256 256 } else {
257 257 $(this).show();
258 258 }
259 259 });
260 260 }
261 261
262 262 function toggleOperator(field) {
263 263 var fieldId = field.replace('.', '_');
264 264 var operator = $("#operators_" + fieldId);
265 265 switch (operator.val()) {
266 266 case "!*":
267 267 case "*":
268 268 case "t":
269 269 case "ld":
270 270 case "w":
271 271 case "lw":
272 272 case "l2w":
273 273 case "m":
274 274 case "lm":
275 275 case "y":
276 276 case "o":
277 277 case "c":
278 278 case "*o":
279 279 case "!o":
280 280 enableValues(field, []);
281 281 break;
282 282 case "><":
283 283 enableValues(field, [0,1]);
284 284 break;
285 285 case "<t+":
286 286 case ">t+":
287 287 case "><t+":
288 288 case "t+":
289 289 case ">t-":
290 290 case "<t-":
291 291 case "><t-":
292 292 case "t-":
293 293 enableValues(field, [2]);
294 294 break;
295 295 case "=p":
296 296 case "=!p":
297 297 case "!p":
298 298 enableValues(field, [1]);
299 299 break;
300 300 default:
301 301 enableValues(field, [0]);
302 302 break;
303 303 }
304 304 }
305 305
306 306 function toggleMultiSelect(el) {
307 307 if (el.attr('multiple')) {
308 308 el.removeAttr('multiple');
309 309 el.attr('size', 1);
310 310 } else {
311 311 el.attr('multiple', true);
312 312 if (el.children().length > 10)
313 313 el.attr('size', 10);
314 314 else
315 315 el.attr('size', 4);
316 316 }
317 317 }
318 318
319 319 function showTab(name, url) {
320 320 $('#tab-content-' + name).parent().find('.tab-content').hide();
321 321 $('#tab-content-' + name).parent().find('div.tabs a').removeClass('selected');
322 322 $('#tab-content-' + name).show();
323 323 $('#tab-' + name).addClass('selected');
324 324 //replaces current URL with the "href" attribute of the current link
325 325 //(only triggered if supported by browser)
326 326 if ("replaceState" in window.history) {
327 327 window.history.replaceState(null, document.title, url);
328 328 }
329 329 return false;
330 330 }
331 331
332 332 function moveTabRight(el) {
333 333 var lis = $(el).parents('div.tabs').first().find('ul').children();
334 334 var bw = $(el).parents('div.tabs-buttons').outerWidth(true);
335 335 var tabsWidth = 0;
336 336 var i = 0;
337 337 lis.each(function() {
338 338 if ($(this).is(':visible')) {
339 339 tabsWidth += $(this).outerWidth(true);
340 340 }
341 341 });
342 342 if (tabsWidth < $(el).parents('div.tabs').first().width() - bw) { return; }
343 343 $(el).siblings('.tab-left').removeClass('disabled');
344 344 while (i<lis.length && !lis.eq(i).is(':visible')) { i++; }
345 345 var w = lis.eq(i).width();
346 346 lis.eq(i).hide();
347 347 if (tabsWidth - w < $(el).parents('div.tabs').first().width() - bw) {
348 348 $(el).addClass('disabled');
349 349 }
350 350 }
351 351
352 352 function moveTabLeft(el) {
353 353 var lis = $(el).parents('div.tabs').first().find('ul').children();
354 354 var i = 0;
355 355 while (i < lis.length && !lis.eq(i).is(':visible')) { i++; }
356 356 if (i > 0) {
357 357 lis.eq(i-1).show();
358 358 $(el).siblings('.tab-right').removeClass('disabled');
359 359 }
360 360 if (i <= 1) {
361 361 $(el).addClass('disabled');
362 362 }
363 363 }
364 364
365 365 function displayTabsButtons() {
366 366 var lis;
367 367 var tabsWidth;
368 368 var el;
369 369 var numHidden;
370 370 $('div.tabs').each(function() {
371 371 el = $(this);
372 372 lis = el.find('ul').children();
373 373 tabsWidth = 0;
374 374 numHidden = 0;
375 375 lis.each(function(){
376 376 if ($(this).is(':visible')) {
377 377 tabsWidth += $(this).outerWidth(true);
378 378 } else {
379 379 numHidden++;
380 380 }
381 381 });
382 382 var bw = $(el).parents('div.tabs-buttons').outerWidth(true);
383 383 if ((tabsWidth < el.width() - bw) && (lis.first().is(':visible'))) {
384 384 el.find('div.tabs-buttons').hide();
385 385 } else {
386 386 el.find('div.tabs-buttons').show().children('button.tab-left').toggleClass('disabled', numHidden == 0);
387 387 }
388 388 });
389 389 }
390 390
391 391 function setPredecessorFieldsVisibility() {
392 392 var relationType = $('#relation_relation_type');
393 393 if (relationType.val() == "precedes" || relationType.val() == "follows") {
394 394 $('#predecessor_fields').show();
395 395 } else {
396 396 $('#predecessor_fields').hide();
397 397 }
398 398 }
399 399
400 400 function showModal(id, width, title) {
401 401 var el = $('#'+id).first();
402 402 if (el.length === 0 || el.is(':visible')) {return;}
403 403 if (!title) title = el.find('h3.title').text();
404 404 // moves existing modals behind the transparent background
405 405 $(".modal").zIndex(99);
406 406 el.dialog({
407 407 width: width,
408 408 modal: true,
409 409 resizable: false,
410 410 dialogClass: 'modal',
411 411 title: title
412 412 }).on('dialogclose', function(){
413 413 $(".modal").zIndex(101);
414 414 });
415 415 el.find("input[type=text], input[type=submit]").first().focus();
416 416 }
417 417
418 418 function hideModal(el) {
419 419 var modal;
420 420 if (el) {
421 421 modal = $(el).parents('.ui-dialog-content');
422 422 } else {
423 423 modal = $('#ajax-modal');
424 424 }
425 425 modal.dialog("close");
426 426 }
427 427
428 428 function submitPreview(url, form, target) {
429 429 $.ajax({
430 430 url: url,
431 431 type: 'post',
432 432 data: $('#'+form).serialize(),
433 433 success: function(data){
434 434 $('#'+target).html(data);
435 435 }
436 436 });
437 437 }
438 438
439 439 function collapseScmEntry(id) {
440 440 $('.'+id).each(function() {
441 441 if ($(this).hasClass('open')) {
442 442 collapseScmEntry($(this).attr('id'));
443 443 }
444 444 $(this).hide();
445 445 });
446 446 $('#'+id).removeClass('open');
447 447 }
448 448
449 449 function expandScmEntry(id) {
450 450 $('.'+id).each(function() {
451 451 $(this).show();
452 452 if ($(this).hasClass('loaded') && !$(this).hasClass('collapsed')) {
453 453 expandScmEntry($(this).attr('id'));
454 454 }
455 455 });
456 456 $('#'+id).addClass('open');
457 457 }
458 458
459 459 function scmEntryClick(id, url) {
460 460 var el = $('#'+id);
461 461 if (el.hasClass('open')) {
462 462 collapseScmEntry(id);
463 463 el.addClass('collapsed');
464 464 return false;
465 465 } else if (el.hasClass('loaded')) {
466 466 expandScmEntry(id);
467 467 el.removeClass('collapsed');
468 468 return false;
469 469 }
470 470 if (el.hasClass('loading')) {
471 471 return false;
472 472 }
473 473 el.addClass('loading');
474 474 $.ajax({
475 475 url: url,
476 476 success: function(data) {
477 477 el.after(data);
478 478 el.addClass('open').addClass('loaded').removeClass('loading');
479 479 }
480 480 });
481 481 return true;
482 482 }
483 483
484 484 function randomKey(size) {
485 485 var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
486 486 var key = '';
487 487 for (var i = 0; i < size; i++) {
488 488 key += chars.charAt(Math.floor(Math.random() * chars.length));
489 489 }
490 490 return key;
491 491 }
492 492
493 493 function updateIssueFrom(url, el) {
494 494 $('#all_attributes input, #all_attributes textarea, #all_attributes select').each(function(){
495 495 $(this).data('valuebeforeupdate', $(this).val());
496 496 });
497 497 if (el) {
498 498 $("#form_update_triggered_by").val($(el).attr('id'));
499 499 }
500 500 return $.ajax({
501 501 url: url,
502 502 type: 'post',
503 503 data: $('#issue-form').serialize()
504 504 });
505 505 }
506 506
507 507 function replaceIssueFormWith(html){
508 508 var replacement = $(html);
509 509 $('#all_attributes input, #all_attributes textarea, #all_attributes select').each(function(){
510 510 var object_id = $(this).attr('id');
511 511 if (object_id && $(this).data('valuebeforeupdate')!=$(this).val()) {
512 512 replacement.find('#'+object_id).val($(this).val());
513 513 }
514 514 });
515 515 $('#all_attributes').empty();
516 516 $('#all_attributes').prepend(replacement);
517 517 }
518 518
519 519 function updateBulkEditFrom(url) {
520 520 $.ajax({
521 521 url: url,
522 522 type: 'post',
523 523 data: $('#bulk_edit_form').serialize()
524 524 });
525 525 }
526 526
527 527 function observeAutocompleteField(fieldId, url, options) {
528 528 $(document).ready(function() {
529 529 $('#'+fieldId).autocomplete($.extend({
530 530 source: url,
531 531 minLength: 2,
532 532 position: {collision: "flipfit"},
533 533 search: function(){$('#'+fieldId).addClass('ajax-loading');},
534 534 response: function(){$('#'+fieldId).removeClass('ajax-loading');}
535 535 }, options));
536 536 $('#'+fieldId).addClass('autocomplete');
537 537 });
538 538 }
539 539
540 540 function observeSearchfield(fieldId, targetId, url) {
541 541 $('#'+fieldId).each(function() {
542 542 var $this = $(this);
543 543 $this.addClass('autocomplete');
544 544 $this.attr('data-value-was', $this.val());
545 545 var check = function() {
546 546 var val = $this.val();
547 547 if ($this.attr('data-value-was') != val){
548 548 $this.attr('data-value-was', val);
549 549 $.ajax({
550 550 url: url,
551 551 type: 'get',
552 552 data: {q: $this.val()},
553 553 success: function(data){ if(targetId) $('#'+targetId).html(data); },
554 554 beforeSend: function(){ $this.addClass('ajax-loading'); },
555 555 complete: function(){ $this.removeClass('ajax-loading'); }
556 556 });
557 557 }
558 558 };
559 559 var reset = function() {
560 560 if (timer) {
561 561 clearInterval(timer);
562 562 timer = setInterval(check, 300);
563 563 }
564 564 };
565 565 var timer = setInterval(check, 300);
566 566 $this.bind('keyup click mousemove', reset);
567 567 });
568 568 }
569 569
570 570 function beforeShowDatePicker(input, inst) {
571 571 var default_date = null;
572 572 switch ($(input).attr("id")) {
573 573 case "issue_start_date" :
574 574 if ($("#issue_due_date").size() > 0) {
575 575 default_date = $("#issue_due_date").val();
576 576 }
577 577 break;
578 578 case "issue_due_date" :
579 579 if ($("#issue_start_date").size() > 0) {
580 580 var start_date = $("#issue_start_date").val();
581 581 if (start_date != "") {
582 582 start_date = new Date(Date.parse(start_date));
583 583 if (start_date > new Date()) {
584 584 default_date = $("#issue_start_date").val();
585 585 }
586 586 }
587 587 }
588 588 break;
589 589 }
590 $(input).datepicker("option", "defaultDate", default_date);
590 $(input).datepickerFallback("option", "defaultDate", default_date);
591 591 }
592 592
593 593 (function($){
594 594 $.fn.positionedItems = function(sortableOptions, options){
595 595 var settings = $.extend({
596 596 firstPosition: 1
597 597 }, options );
598 598
599 599 return this.sortable($.extend({
600 600 handle: ".sort-handle",
601 601 helper: function(event, ui){
602 602 ui.children('td').each(function(){
603 603 $(this).width($(this).width());
604 604 });
605 605 return ui;
606 606 },
607 607 update: function(event, ui) {
608 608 var sortable = $(this);
609 609 var handle = ui.item.find(".sort-handle").addClass("ajax-loading");
610 610 var url = handle.data("reorder-url");
611 611 var param = handle.data("reorder-param");
612 612 var data = {};
613 613 data[param] = {position: ui.item.index() + settings['firstPosition']};
614 614 $.ajax({
615 615 url: url,
616 616 type: 'put',
617 617 dataType: 'script',
618 618 data: data,
619 619 success: function(data){
620 620 sortable.children(":even").removeClass("even").addClass("odd");
621 621 sortable.children(":odd").removeClass("odd").addClass("even");
622 622 },
623 623 error: function(jqXHR, textStatus, errorThrown){
624 624 alert(jqXHR.status);
625 625 sortable.sortable("cancel");
626 626 },
627 627 complete: function(jqXHR, textStatus, errorThrown){
628 628 handle.removeClass("ajax-loading");
629 629 }
630 630 });
631 631 },
632 632 }, sortableOptions));
633 633 }
634 634 }( jQuery ));
635 635
636 636 function initMyPageSortable(list, url) {
637 637 $('#list-'+list).sortable({
638 638 connectWith: '.block-receiver',
639 639 tolerance: 'pointer',
640 640 update: function(){
641 641 $.ajax({
642 642 url: url,
643 643 type: 'post',
644 644 data: {'blocks': $.map($('#list-'+list).children(), function(el){return $(el).attr('id');})}
645 645 });
646 646 }
647 647 });
648 648 $("#list-top, #list-left, #list-right").disableSelection();
649 649 }
650 650
651 651 var warnLeavingUnsavedMessage;
652 652 function warnLeavingUnsaved(message) {
653 653 warnLeavingUnsavedMessage = message;
654 654 $(document).on('submit', 'form', function(){
655 655 $('textarea').removeData('changed');
656 656 });
657 657 $(document).on('change', 'textarea', function(){
658 658 $(this).data('changed', 'changed');
659 659 });
660 660 window.onbeforeunload = function(){
661 661 var warn = false;
662 662 $('textarea').blur().each(function(){
663 663 if ($(this).data('changed')) {
664 664 warn = true;
665 665 }
666 666 });
667 667 if (warn) {return warnLeavingUnsavedMessage;}
668 668 };
669 669 }
670 670
671 671 function setupAjaxIndicator() {
672 672 $(document).bind('ajaxSend', function(event, xhr, settings) {
673 673 if ($('.ajax-loading').length === 0 && settings.contentType != 'application/octet-stream') {
674 674 $('#ajax-indicator').show();
675 675 }
676 676 });
677 677 $(document).bind('ajaxStop', function() {
678 678 $('#ajax-indicator').hide();
679 679 });
680 680 }
681 681
682 682 function setupTabs() {
683 683 if($('.tabs').length > 0) {
684 684 displayTabsButtons();
685 685 $(window).resize(displayTabsButtons);
686 686 }
687 687 }
688 688
689 689 function hideOnLoad() {
690 690 $('.hol').hide();
691 691 }
692 692
693 693 function addFormObserversForDoubleSubmit() {
694 694 $('form[method=post]').each(function() {
695 695 if (!$(this).hasClass('multiple-submit')) {
696 696 $(this).submit(function(form_submission) {
697 697 if ($(form_submission.target).attr('data-submitted')) {
698 698 form_submission.preventDefault();
699 699 } else {
700 700 $(form_submission.target).attr('data-submitted', true);
701 701 }
702 702 });
703 703 }
704 704 });
705 705 }
706 706
707 707 function defaultFocus(){
708 708 if (($('#content :focus').length == 0) && (window.location.hash == '')) {
709 709 $('#content input[type=text], #content textarea').first().focus();
710 710 }
711 711 }
712 712
713 713 function blockEventPropagation(event) {
714 714 event.stopPropagation();
715 715 event.preventDefault();
716 716 }
717 717
718 718 function toggleDisabledOnChange() {
719 719 var checked = $(this).is(':checked');
720 720 $($(this).data('disables')).attr('disabled', checked);
721 721 $($(this).data('enables')).attr('disabled', !checked);
722 722 }
723 723 function toggleDisabledInit() {
724 724 $('input[data-disables], input[data-enables]').each(toggleDisabledOnChange);
725 725 }
726
727 (function ( $ ) {
728
729 // detect if native date input is supported
730 var nativeDateInputSupported = true;
731
732 var input = document.createElement('input');
733 input.setAttribute('type','date');
734 if (input.type === 'text') {
735 nativeDateInputSupported = false;
736 }
737
738 var notADateValue = 'not-a-date';
739 input.setAttribute('value', notADateValue);
740 if (input.value === notADateValue) {
741 nativeDateInputSupported = false;
742 }
743
744 $.fn.datepickerFallback = function( options ) {
745 if (nativeDateInputSupported) {
746 return this;
747 } else {
748 return this.datepicker( options );
749 }
750 };
751 }( jQuery ));
752
726 753 $(document).ready(function(){
727 754 $('#content').on('change', 'input[data-disables], input[data-enables]', toggleDisabledOnChange);
728 755 toggleDisabledInit();
729 756 });
730 757
731 758 function keepAnchorOnSignIn(form){
732 759 var hash = decodeURIComponent(self.document.location.hash);
733 760 if (hash) {
734 761 if (hash.indexOf("#") === -1) {
735 762 hash = "#" + hash;
736 763 }
737 764 form.action = form.action + hash;
738 765 }
739 766 return true;
740 767 }
741 768
742 769 $(document).ready(setupAjaxIndicator);
743 770 $(document).ready(hideOnLoad);
744 771 $(document).ready(addFormObserversForDoubleSubmit);
745 772 $(document).ready(defaultFocus);
746 773 $(document).ready(setupTabs);
General Comments 0
You need to be logged in to leave comments. Login now