##// END OF EJS Templates
Backport r13313 from rails-4.1 to trunk....
Toshi MARUYAMA -
r13045:cc688bb1b114
parent child
Show More

The requested changes are too big and content was truncated. Show full diff

1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,107 +1,107
1 1 source 'https://rubygems.org'
2 2
3 3 gem "rails", "3.2.19"
4 gem "jquery-rails", "~> 2.0.2"
4 gem "jquery-rails", "~> 3.1.1"
5 5 gem "coderay", "~> 1.1.0"
6 6 gem "fastercsv", "~> 1.5.0", :platforms => [:mri_18, :mingw_18, :jruby]
7 7 gem "builder", ">= 3.0.4"
8 8 gem "request_store", "1.0.5"
9 9 gem "mime-types"
10 10 gem "rbpdf", "~> 1.18.0"
11 11
12 12 # Optional gem for LDAP authentication
13 13 group :ldap do
14 14 gem "net-ldap", "~> 0.3.1"
15 15 end
16 16
17 17 # Optional gem for OpenID authentication
18 18 group :openid do
19 19 gem "ruby-openid", "~> 2.3.0", :require => "openid"
20 20 gem "rack-openid"
21 21 end
22 22
23 23 platforms :mri, :mingw do
24 24 # Optional gem for exporting the gantt to a PNG file, not supported with jruby
25 25 group :rmagick do
26 26 # RMagick 2 supports ruby 1.9
27 27 # RMagick 1 would be fine for ruby 1.8 but Bundler does not support
28 28 # different requirements for the same gem on different platforms
29 29 gem "rmagick", ">= 2.0.0"
30 30 end
31 31
32 32 # Optional Markdown support, not for JRuby
33 33 group :markdown do
34 34 # TODO: upgrade to redcarpet 3.x when ruby1.8 support is dropped
35 35 gem "redcarpet", "~> 2.3.0"
36 36 end
37 37 end
38 38
39 39 platforms :jruby do
40 40 # jruby-openssl is bundled with JRuby 1.7.0
41 41 gem "jruby-openssl" if Object.const_defined?(:JRUBY_VERSION) && JRUBY_VERSION < '1.7.0'
42 42 gem "activerecord-jdbc-adapter", "~> 1.3.2"
43 43 end
44 44
45 45 # Include database gems for the adapters found in the database
46 46 # configuration file
47 47 require 'erb'
48 48 require 'yaml'
49 49 database_file = File.join(File.dirname(__FILE__), "config/database.yml")
50 50 if File.exist?(database_file)
51 51 database_config = YAML::load(ERB.new(IO.read(database_file)).result)
52 52 adapters = database_config.values.map {|c| c['adapter']}.compact.uniq
53 53 if adapters.any?
54 54 adapters.each do |adapter|
55 55 case adapter
56 56 when 'mysql2'
57 57 gem "mysql2", "~> 0.3.11", :platforms => [:mri, :mingw]
58 58 gem "activerecord-jdbcmysql-adapter", :platforms => :jruby
59 59 when 'mysql'
60 60 gem "mysql", "~> 2.8.1", :platforms => [:mri, :mingw]
61 61 gem "activerecord-jdbcmysql-adapter", :platforms => :jruby
62 62 when /postgresql/
63 63 gem "pg", ">= 0.11.0", :platforms => [:mri, :mingw]
64 64 gem "activerecord-jdbcpostgresql-adapter", :platforms => :jruby
65 65 when /sqlite3/
66 66 gem "sqlite3", :platforms => [:mri, :mingw]
67 67 gem "activerecord-jdbcsqlite3-adapter", :platforms => :jruby
68 68 when /sqlserver/
69 69 gem "tiny_tds", "~> 0.6.2", :platforms => [:mri, :mingw]
70 70 gem "activerecord-sqlserver-adapter", :platforms => [:mri, :mingw]
71 71 else
72 72 warn("Unknown database adapter `#{adapter}` found in config/database.yml, use Gemfile.local to load your own database gems")
73 73 end
74 74 end
75 75 else
76 76 warn("No adapter found in config/database.yml, please configure it first")
77 77 end
78 78 else
79 79 warn("Please configure your config/database.yml first")
80 80 end
81 81
82 82 group :development do
83 83 gem "rdoc", ">= 2.4.2"
84 84 gem "yard"
85 85 end
86 86
87 87 group :test do
88 88 gem "shoulda", "~> 3.3.2"
89 89 gem "mocha", "~> 1.0.0", :require => 'mocha/api'
90 90 if RUBY_VERSION >= '1.9.3'
91 91 gem "capybara", "~> 2.1.0"
92 92 gem "selenium-webdriver"
93 93 end
94 94 end
95 95
96 96 local_gemfile = File.join(File.dirname(__FILE__), "Gemfile.local")
97 97 if File.exists?(local_gemfile)
98 98 puts "Loading Gemfile.local ..." if $DEBUG # `ruby -d` or `bundle -v`
99 99 instance_eval File.read(local_gemfile)
100 100 end
101 101
102 102 # Load plugins' Gemfiles
103 103 Dir.glob File.expand_path("../plugins/*/{Gemfile,PluginGemfile}", __FILE__) do |file|
104 104 puts "Loading #{file} ..." if $DEBUG # `ruby -d` or `bundle -v`
105 105 #TODO: switch to "eval_gemfile file" when bundler >= 1.2.0 will be required (rails 4)
106 106 instance_eval File.read(file), file
107 107 end
@@ -1,1360 +1,1360
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2014 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 require 'forwardable'
21 21 require 'cgi'
22 22
23 23 module ApplicationHelper
24 24 include Redmine::WikiFormatting::Macros::Definitions
25 25 include Redmine::I18n
26 26 include GravatarHelper::PublicMethods
27 27 include Redmine::Pagination::Helper
28 28
29 29 extend Forwardable
30 30 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
31 31
32 32 # Return true if user is authorized for controller/action, otherwise false
33 33 def authorize_for(controller, action)
34 34 User.current.allowed_to?({:controller => controller, :action => action}, @project)
35 35 end
36 36
37 37 # Display a link if user is authorized
38 38 #
39 39 # @param [String] name Anchor text (passed to link_to)
40 40 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
41 41 # @param [optional, Hash] html_options Options passed to link_to
42 42 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
43 43 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
44 44 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
45 45 end
46 46
47 47 # Displays a link to user's account page if active
48 48 def link_to_user(user, options={})
49 49 if user.is_a?(User)
50 50 name = h(user.name(options[:format]))
51 51 if user.active? || (User.current.admin? && user.logged?)
52 52 link_to name, user_path(user), :class => user.css_classes
53 53 else
54 54 name
55 55 end
56 56 else
57 57 h(user.to_s)
58 58 end
59 59 end
60 60
61 61 # Displays a link to +issue+ with its subject.
62 62 # Examples:
63 63 #
64 64 # link_to_issue(issue) # => Defect #6: This is the subject
65 65 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
66 66 # link_to_issue(issue, :subject => false) # => Defect #6
67 67 # link_to_issue(issue, :project => true) # => Foo - Defect #6
68 68 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
69 69 #
70 70 def link_to_issue(issue, options={})
71 71 title = nil
72 72 subject = nil
73 73 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
74 74 if options[:subject] == false
75 75 title = issue.subject.truncate(60)
76 76 else
77 77 subject = issue.subject
78 78 if truncate_length = options[:truncate]
79 79 subject = subject.truncate(truncate_length)
80 80 end
81 81 end
82 82 only_path = options[:only_path].nil? ? true : options[:only_path]
83 83 s = link_to(text, issue_path(issue, :only_path => only_path),
84 84 :class => issue.css_classes, :title => title)
85 85 s << h(": #{subject}") if subject
86 86 s = h("#{issue.project} - ") + s if options[:project]
87 87 s
88 88 end
89 89
90 90 # Generates a link to an attachment.
91 91 # Options:
92 92 # * :text - Link text (default to attachment filename)
93 93 # * :download - Force download (default: false)
94 94 def link_to_attachment(attachment, options={})
95 95 text = options.delete(:text) || attachment.filename
96 96 route_method = options.delete(:download) ? :download_named_attachment_path : :named_attachment_path
97 97 html_options = options.slice!(:only_path)
98 98 url = send(route_method, attachment, attachment.filename, options)
99 99 link_to text, url, html_options
100 100 end
101 101
102 102 # Generates a link to a SCM revision
103 103 # Options:
104 104 # * :text - Link text (default to the formatted revision)
105 105 def link_to_revision(revision, repository, options={})
106 106 if repository.is_a?(Project)
107 107 repository = repository.repository
108 108 end
109 109 text = options.delete(:text) || format_revision(revision)
110 110 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
111 111 link_to(
112 112 h(text),
113 113 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
114 114 :title => l(:label_revision_id, format_revision(revision))
115 115 )
116 116 end
117 117
118 118 # Generates a link to a message
119 119 def link_to_message(message, options={}, html_options = nil)
120 120 link_to(
121 121 message.subject.truncate(60),
122 122 board_message_path(message.board_id, message.parent_id || message.id, {
123 123 :r => (message.parent_id && message.id),
124 124 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
125 125 }.merge(options)),
126 126 html_options
127 127 )
128 128 end
129 129
130 130 # Generates a link to a project if active
131 131 # Examples:
132 132 #
133 133 # link_to_project(project) # => link to the specified project overview
134 134 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
135 135 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
136 136 #
137 137 def link_to_project(project, options={}, html_options = nil)
138 138 if project.archived?
139 139 h(project.name)
140 140 elsif options.key?(:action)
141 141 ActiveSupport::Deprecation.warn "#link_to_project with :action option is deprecated and will be removed in Redmine 3.0."
142 142 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
143 143 link_to project.name, url, html_options
144 144 else
145 145 link_to project.name, project_path(project, options), html_options
146 146 end
147 147 end
148 148
149 149 # Generates a link to a project settings if active
150 150 def link_to_project_settings(project, options={}, html_options=nil)
151 151 if project.active?
152 152 link_to project.name, settings_project_path(project, options), html_options
153 153 elsif project.archived?
154 154 h(project.name)
155 155 else
156 156 link_to project.name, project_path(project, options), html_options
157 157 end
158 158 end
159 159
160 160 # Generates a link to a version
161 161 def link_to_version(version, options = {})
162 162 return '' unless version && version.is_a?(Version)
163 163 options = {:title => format_date(version.effective_date)}.merge(options)
164 164 link_to_if version.visible?, format_version_name(version), version_path(version), options
165 165 end
166 166
167 167 # Helper that formats object for html or text rendering
168 168 def format_object(object, html=true, &block)
169 169 if block_given?
170 170 object = yield object
171 171 end
172 172 case object.class.name
173 173 when 'Array'
174 174 object.map {|o| format_object(o, html)}.join(', ').html_safe
175 175 when 'Time'
176 176 format_time(object)
177 177 when 'Date'
178 178 format_date(object)
179 179 when 'Fixnum'
180 180 object.to_s
181 181 when 'Float'
182 182 sprintf "%.2f", object
183 183 when 'User'
184 184 html ? link_to_user(object) : object.to_s
185 185 when 'Project'
186 186 html ? link_to_project(object) : object.to_s
187 187 when 'Version'
188 188 html ? link_to_version(object) : object.to_s
189 189 when 'TrueClass'
190 190 l(:general_text_Yes)
191 191 when 'FalseClass'
192 192 l(:general_text_No)
193 193 when 'Issue'
194 194 object.visible? && html ? link_to_issue(object) : "##{object.id}"
195 195 when 'CustomValue', 'CustomFieldValue'
196 196 if object.custom_field
197 197 f = object.custom_field.format.formatted_custom_value(self, object, html)
198 198 if f.nil? || f.is_a?(String)
199 199 f
200 200 else
201 201 format_object(f, html, &block)
202 202 end
203 203 else
204 204 object.value.to_s
205 205 end
206 206 else
207 207 html ? h(object) : object.to_s
208 208 end
209 209 end
210 210
211 211 def wiki_page_path(page, options={})
212 212 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
213 213 end
214 214
215 215 def thumbnail_tag(attachment)
216 216 link_to image_tag(thumbnail_path(attachment)),
217 217 named_attachment_path(attachment, attachment.filename),
218 218 :title => attachment.filename
219 219 end
220 220
221 221 def toggle_link(name, id, options={})
222 222 onclick = "$('##{id}').toggle(); "
223 223 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
224 224 onclick << "return false;"
225 225 link_to(name, "#", :onclick => onclick)
226 226 end
227 227
228 228 def image_to_function(name, function, html_options = {})
229 229 html_options.symbolize_keys!
230 230 tag(:input, html_options.merge({
231 231 :type => "image", :src => image_path(name),
232 232 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
233 233 }))
234 234 end
235 235
236 236 def format_activity_title(text)
237 237 h(truncate_single_line_raw(text, 100))
238 238 end
239 239
240 240 def format_activity_day(date)
241 241 date == User.current.today ? l(:label_today).titleize : format_date(date)
242 242 end
243 243
244 244 def format_activity_description(text)
245 245 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
246 246 ).gsub(/[\r\n]+/, "<br />").html_safe
247 247 end
248 248
249 249 def format_version_name(version)
250 250 if !version.shared? || version.project == @project
251 251 h(version)
252 252 else
253 253 h("#{version.project} - #{version}")
254 254 end
255 255 end
256 256
257 257 def due_date_distance_in_words(date)
258 258 if date
259 259 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
260 260 end
261 261 end
262 262
263 263 # Renders a tree of projects as a nested set of unordered lists
264 264 # The given collection may be a subset of the whole project tree
265 265 # (eg. some intermediate nodes are private and can not be seen)
266 266 def render_project_nested_lists(projects)
267 267 s = ''
268 268 if projects.any?
269 269 ancestors = []
270 270 original_project = @project
271 271 projects.sort_by(&:lft).each do |project|
272 272 # set the project environment to please macros.
273 273 @project = project
274 274 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
275 275 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
276 276 else
277 277 ancestors.pop
278 278 s << "</li>"
279 279 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
280 280 ancestors.pop
281 281 s << "</ul></li>\n"
282 282 end
283 283 end
284 284 classes = (ancestors.empty? ? 'root' : 'child')
285 285 s << "<li class='#{classes}'><div class='#{classes}'>"
286 286 s << h(block_given? ? yield(project) : project.name)
287 287 s << "</div>\n"
288 288 ancestors << project
289 289 end
290 290 s << ("</li></ul>\n" * ancestors.size)
291 291 @project = original_project
292 292 end
293 293 s.html_safe
294 294 end
295 295
296 296 def render_page_hierarchy(pages, node=nil, options={})
297 297 content = ''
298 298 if pages[node]
299 299 content << "<ul class=\"pages-hierarchy\">\n"
300 300 pages[node].each do |page|
301 301 content << "<li>"
302 302 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
303 303 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
304 304 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
305 305 content << "</li>\n"
306 306 end
307 307 content << "</ul>\n"
308 308 end
309 309 content.html_safe
310 310 end
311 311
312 312 # Renders flash messages
313 313 def render_flash_messages
314 314 s = ''
315 315 flash.each do |k,v|
316 316 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
317 317 end
318 318 s.html_safe
319 319 end
320 320
321 321 # Renders tabs and their content
322 322 def render_tabs(tabs, selected=params[:tab])
323 323 if tabs.any?
324 324 unless tabs.detect {|tab| tab[:name] == selected}
325 325 selected = nil
326 326 end
327 327 selected ||= tabs.first[:name]
328 328 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
329 329 else
330 330 content_tag 'p', l(:label_no_data), :class => "nodata"
331 331 end
332 332 end
333 333
334 334 # Renders the project quick-jump box
335 335 def render_project_jump_box
336 336 return unless User.current.logged?
337 337 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
338 338 if projects.any?
339 339 options =
340 340 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
341 341 '<option value="" disabled="disabled">---</option>').html_safe
342 342
343 343 options << project_tree_options_for_select(projects, :selected => @project) do |p|
344 344 { :value => project_path(:id => p, :jump => current_menu_item) }
345 345 end
346 346
347 347 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
348 348 end
349 349 end
350 350
351 351 def project_tree_options_for_select(projects, options = {})
352 352 s = ''
353 353 project_tree(projects) do |project, level|
354 354 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
355 355 tag_options = {:value => project.id}
356 356 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
357 357 tag_options[:selected] = 'selected'
358 358 else
359 359 tag_options[:selected] = nil
360 360 end
361 361 tag_options.merge!(yield(project)) if block_given?
362 362 s << content_tag('option', name_prefix + h(project), tag_options)
363 363 end
364 364 s.html_safe
365 365 end
366 366
367 367 # Yields the given block for each project with its level in the tree
368 368 #
369 369 # Wrapper for Project#project_tree
370 370 def project_tree(projects, &block)
371 371 Project.project_tree(projects, &block)
372 372 end
373 373
374 374 def principals_check_box_tags(name, principals)
375 375 s = ''
376 376 principals.each do |principal|
377 377 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
378 378 end
379 379 s.html_safe
380 380 end
381 381
382 382 # Returns a string for users/groups option tags
383 383 def principals_options_for_select(collection, selected=nil)
384 384 s = ''
385 385 if collection.include?(User.current)
386 386 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
387 387 end
388 388 groups = ''
389 389 collection.sort.each do |element|
390 390 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
391 391 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
392 392 end
393 393 unless groups.empty?
394 394 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
395 395 end
396 396 s.html_safe
397 397 end
398 398
399 399 # Options for the new membership projects combo-box
400 400 def options_for_membership_project_select(principal, projects)
401 401 options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
402 402 options << project_tree_options_for_select(projects) do |p|
403 403 {:disabled => principal.projects.to_a.include?(p)}
404 404 end
405 405 options
406 406 end
407 407
408 408 def option_tag(name, text, value, selected=nil, options={})
409 409 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
410 410 end
411 411
412 412 # Truncates and returns the string as a single line
413 413 def truncate_single_line(string, *args)
414 414 ActiveSupport::Deprecation.warn(
415 415 "ApplicationHelper#truncate_single_line is deprecated and will be removed in Rails 4 poring")
416 416 # Rails 4 ActionView::Helpers::TextHelper#truncate escapes.
417 417 # So, result is broken.
418 418 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
419 419 end
420 420
421 421 def truncate_single_line_raw(string, length)
422 422 string.truncate(length).gsub(%r{[\r\n]+}m, ' ')
423 423 end
424 424
425 425 # Truncates at line break after 250 characters or options[:length]
426 426 def truncate_lines(string, options={})
427 427 length = options[:length] || 250
428 428 if string.to_s =~ /\A(.{#{length}}.*?)$/m
429 429 "#{$1}..."
430 430 else
431 431 string
432 432 end
433 433 end
434 434
435 435 def anchor(text)
436 436 text.to_s.gsub(' ', '_')
437 437 end
438 438
439 439 def html_hours(text)
440 440 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
441 441 end
442 442
443 443 def authoring(created, author, options={})
444 444 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
445 445 end
446 446
447 447 def time_tag(time)
448 448 text = distance_of_time_in_words(Time.now, time)
449 449 if @project
450 450 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
451 451 else
452 452 content_tag('abbr', text, :title => format_time(time))
453 453 end
454 454 end
455 455
456 456 def syntax_highlight_lines(name, content)
457 457 lines = []
458 458 syntax_highlight(name, content).each_line { |line| lines << line }
459 459 lines
460 460 end
461 461
462 462 def syntax_highlight(name, content)
463 463 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
464 464 end
465 465
466 466 def to_path_param(path)
467 467 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
468 468 str.blank? ? nil : str
469 469 end
470 470
471 471 def reorder_links(name, url, method = :post)
472 472 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
473 473 url.merge({"#{name}[move_to]" => 'highest'}),
474 474 :method => method, :title => l(:label_sort_highest)) +
475 475 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
476 476 url.merge({"#{name}[move_to]" => 'higher'}),
477 477 :method => method, :title => l(:label_sort_higher)) +
478 478 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
479 479 url.merge({"#{name}[move_to]" => 'lower'}),
480 480 :method => method, :title => l(:label_sort_lower)) +
481 481 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
482 482 url.merge({"#{name}[move_to]" => 'lowest'}),
483 483 :method => method, :title => l(:label_sort_lowest))
484 484 end
485 485
486 486 def breadcrumb(*args)
487 487 elements = args.flatten
488 488 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
489 489 end
490 490
491 491 def other_formats_links(&block)
492 492 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
493 493 yield Redmine::Views::OtherFormatsBuilder.new(self)
494 494 concat('</p>'.html_safe)
495 495 end
496 496
497 497 def page_header_title
498 498 if @project.nil? || @project.new_record?
499 499 h(Setting.app_title)
500 500 else
501 501 b = []
502 502 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
503 503 if ancestors.any?
504 504 root = ancestors.shift
505 505 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
506 506 if ancestors.size > 2
507 507 b << "\xe2\x80\xa6"
508 508 ancestors = ancestors[-2, 2]
509 509 end
510 510 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
511 511 end
512 512 b << h(@project)
513 513 b.join(" \xc2\xbb ").html_safe
514 514 end
515 515 end
516 516
517 517 # Returns a h2 tag and sets the html title with the given arguments
518 518 def title(*args)
519 519 strings = args.map do |arg|
520 520 if arg.is_a?(Array) && arg.size >= 2
521 521 link_to(*arg)
522 522 else
523 523 h(arg.to_s)
524 524 end
525 525 end
526 526 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
527 527 content_tag('h2', strings.join(' &#187; ').html_safe)
528 528 end
529 529
530 530 # Sets the html title
531 531 # Returns the html title when called without arguments
532 532 # Current project name and app_title and automatically appended
533 533 # Exemples:
534 534 # html_title 'Foo', 'Bar'
535 535 # html_title # => 'Foo - Bar - My Project - Redmine'
536 536 def html_title(*args)
537 537 if args.empty?
538 538 title = @html_title || []
539 539 title << @project.name if @project
540 540 title << Setting.app_title unless Setting.app_title == title.last
541 541 title.reject(&:blank?).join(' - ')
542 542 else
543 543 @html_title ||= []
544 544 @html_title += args
545 545 end
546 546 end
547 547
548 548 # Returns the theme, controller name, and action as css classes for the
549 549 # HTML body.
550 550 def body_css_classes
551 551 css = []
552 552 if theme = Redmine::Themes.theme(Setting.ui_theme)
553 553 css << 'theme-' + theme.name
554 554 end
555 555
556 556 css << 'project-' + @project.identifier if @project && @project.identifier.present?
557 557 css << 'controller-' + controller_name
558 558 css << 'action-' + action_name
559 559 css.join(' ')
560 560 end
561 561
562 562 def accesskey(s)
563 563 @used_accesskeys ||= []
564 564 key = Redmine::AccessKeys.key_for(s)
565 565 return nil if @used_accesskeys.include?(key)
566 566 @used_accesskeys << key
567 567 key
568 568 end
569 569
570 570 # Formats text according to system settings.
571 571 # 2 ways to call this method:
572 572 # * with a String: textilizable(text, options)
573 573 # * with an object and one of its attribute: textilizable(issue, :description, options)
574 574 def textilizable(*args)
575 575 options = args.last.is_a?(Hash) ? args.pop : {}
576 576 case args.size
577 577 when 1
578 578 obj = options[:object]
579 579 text = args.shift
580 580 when 2
581 581 obj = args.shift
582 582 attr = args.shift
583 583 text = obj.send(attr).to_s
584 584 else
585 585 raise ArgumentError, 'invalid arguments to textilizable'
586 586 end
587 587 return '' if text.blank?
588 588 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
589 589 only_path = options.delete(:only_path) == false ? false : true
590 590
591 591 text = text.dup
592 592 macros = catch_macros(text)
593 593 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
594 594
595 595 @parsed_headings = []
596 596 @heading_anchors = {}
597 597 @current_section = 0 if options[:edit_section_links]
598 598
599 599 parse_sections(text, project, obj, attr, only_path, options)
600 600 text = parse_non_pre_blocks(text, obj, macros) do |text|
601 601 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
602 602 send method_name, text, project, obj, attr, only_path, options
603 603 end
604 604 end
605 605 parse_headings(text, project, obj, attr, only_path, options)
606 606
607 607 if @parsed_headings.any?
608 608 replace_toc(text, @parsed_headings)
609 609 end
610 610
611 611 text.html_safe
612 612 end
613 613
614 614 def parse_non_pre_blocks(text, obj, macros)
615 615 s = StringScanner.new(text)
616 616 tags = []
617 617 parsed = ''
618 618 while !s.eos?
619 619 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
620 620 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
621 621 if tags.empty?
622 622 yield text
623 623 inject_macros(text, obj, macros) if macros.any?
624 624 else
625 625 inject_macros(text, obj, macros, false) if macros.any?
626 626 end
627 627 parsed << text
628 628 if tag
629 629 if closing
630 630 if tags.last == tag.downcase
631 631 tags.pop
632 632 end
633 633 else
634 634 tags << tag.downcase
635 635 end
636 636 parsed << full_tag
637 637 end
638 638 end
639 639 # Close any non closing tags
640 640 while tag = tags.pop
641 641 parsed << "</#{tag}>"
642 642 end
643 643 parsed
644 644 end
645 645
646 646 def parse_inline_attachments(text, project, obj, attr, only_path, options)
647 647 # when using an image link, try to use an attachment, if possible
648 648 attachments = options[:attachments] || []
649 649 attachments += obj.attachments if obj.respond_to?(:attachments)
650 650 if attachments.present?
651 651 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
652 652 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
653 653 # search for the picture in attachments
654 654 if found = Attachment.latest_attach(attachments, filename)
655 655 image_url = download_named_attachment_path(found, found.filename, :only_path => only_path)
656 656 desc = found.description.to_s.gsub('"', '')
657 657 if !desc.blank? && alttext.blank?
658 658 alt = " title=\"#{desc}\" alt=\"#{desc}\""
659 659 end
660 660 "src=\"#{image_url}\"#{alt}"
661 661 else
662 662 m
663 663 end
664 664 end
665 665 end
666 666 end
667 667
668 668 # Wiki links
669 669 #
670 670 # Examples:
671 671 # [[mypage]]
672 672 # [[mypage|mytext]]
673 673 # wiki links can refer other project wikis, using project name or identifier:
674 674 # [[project:]] -> wiki starting page
675 675 # [[project:|mytext]]
676 676 # [[project:mypage]]
677 677 # [[project:mypage|mytext]]
678 678 def parse_wiki_links(text, project, obj, attr, only_path, options)
679 679 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
680 680 link_project = project
681 681 esc, all, page, title = $1, $2, $3, $5
682 682 if esc.nil?
683 683 if page =~ /^([^\:]+)\:(.*)$/
684 684 identifier, page = $1, $2
685 685 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
686 686 title ||= identifier if page.blank?
687 687 end
688 688
689 689 if link_project && link_project.wiki
690 690 # extract anchor
691 691 anchor = nil
692 692 if page =~ /^(.+?)\#(.+)$/
693 693 page, anchor = $1, $2
694 694 end
695 695 anchor = sanitize_anchor_name(anchor) if anchor.present?
696 696 # check if page exists
697 697 wiki_page = link_project.wiki.find_page(page)
698 698 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
699 699 "##{anchor}"
700 700 else
701 701 case options[:wiki_links]
702 702 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
703 703 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
704 704 else
705 705 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
706 706 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
707 707 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
708 708 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
709 709 end
710 710 end
711 711 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
712 712 else
713 713 # project or wiki doesn't exist
714 714 all
715 715 end
716 716 else
717 717 all
718 718 end
719 719 end
720 720 end
721 721
722 722 # Redmine links
723 723 #
724 724 # Examples:
725 725 # Issues:
726 726 # #52 -> Link to issue #52
727 727 # Changesets:
728 728 # r52 -> Link to revision 52
729 729 # commit:a85130f -> Link to scmid starting with a85130f
730 730 # Documents:
731 731 # document#17 -> Link to document with id 17
732 732 # document:Greetings -> Link to the document with title "Greetings"
733 733 # document:"Some document" -> Link to the document with title "Some document"
734 734 # Versions:
735 735 # version#3 -> Link to version with id 3
736 736 # version:1.0.0 -> Link to version named "1.0.0"
737 737 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
738 738 # Attachments:
739 739 # attachment:file.zip -> Link to the attachment of the current object named file.zip
740 740 # Source files:
741 741 # source:some/file -> Link to the file located at /some/file in the project's repository
742 742 # source:some/file@52 -> Link to the file's revision 52
743 743 # source:some/file#L120 -> Link to line 120 of the file
744 744 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
745 745 # export:some/file -> Force the download of the file
746 746 # Forum messages:
747 747 # message#1218 -> Link to message with id 1218
748 748 # Projects:
749 749 # project:someproject -> Link to project named "someproject"
750 750 # project#3 -> Link to project with id 3
751 751 #
752 752 # Links can refer other objects from other projects, using project identifier:
753 753 # identifier:r52
754 754 # identifier:document:"Some document"
755 755 # identifier:version:1.0.0
756 756 # identifier:source:some/file
757 757 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
758 758 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
759 759 leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17
760 760 link = nil
761 761 project = default_project
762 762 if project_identifier
763 763 project = Project.visible.find_by_identifier(project_identifier)
764 764 end
765 765 if esc.nil?
766 766 if prefix.nil? && sep == 'r'
767 767 if project
768 768 repository = nil
769 769 if repo_identifier
770 770 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
771 771 else
772 772 repository = project.repository
773 773 end
774 774 # project.changesets.visible raises an SQL error because of a double join on repositories
775 775 if repository &&
776 776 (changeset = Changeset.visible.
777 777 find_by_repository_id_and_revision(repository.id, identifier))
778 778 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
779 779 {:only_path => only_path, :controller => 'repositories',
780 780 :action => 'revision', :id => project,
781 781 :repository_id => repository.identifier_param,
782 782 :rev => changeset.revision},
783 783 :class => 'changeset',
784 784 :title => truncate_single_line_raw(changeset.comments, 100))
785 785 end
786 786 end
787 787 elsif sep == '#'
788 788 oid = identifier.to_i
789 789 case prefix
790 790 when nil
791 791 if oid.to_s == identifier &&
792 792 issue = Issue.visible.includes(:status).find_by_id(oid)
793 793 anchor = comment_id ? "note-#{comment_id}" : nil
794 794 link = link_to(h("##{oid}#{comment_suffix}"),
795 795 {:only_path => only_path, :controller => 'issues',
796 796 :action => 'show', :id => oid, :anchor => anchor},
797 797 :class => issue.css_classes,
798 798 :title => "#{issue.subject.truncate(100)} (#{issue.status.name})")
799 799 end
800 800 when 'document'
801 801 if document = Document.visible.find_by_id(oid)
802 802 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
803 803 :class => 'document'
804 804 end
805 805 when 'version'
806 806 if version = Version.visible.find_by_id(oid)
807 807 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
808 808 :class => 'version'
809 809 end
810 810 when 'message'
811 811 if message = Message.visible.includes(:parent).find_by_id(oid)
812 812 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
813 813 end
814 814 when 'forum'
815 815 if board = Board.visible.find_by_id(oid)
816 816 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
817 817 :class => 'board'
818 818 end
819 819 when 'news'
820 820 if news = News.visible.find_by_id(oid)
821 821 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
822 822 :class => 'news'
823 823 end
824 824 when 'project'
825 825 if p = Project.visible.find_by_id(oid)
826 826 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
827 827 end
828 828 end
829 829 elsif sep == ':'
830 830 # removes the double quotes if any
831 831 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
832 832 name = CGI.unescapeHTML(name)
833 833 case prefix
834 834 when 'document'
835 835 if project && document = project.documents.visible.find_by_title(name)
836 836 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
837 837 :class => 'document'
838 838 end
839 839 when 'version'
840 840 if project && version = project.versions.visible.find_by_name(name)
841 841 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
842 842 :class => 'version'
843 843 end
844 844 when 'forum'
845 845 if project && board = project.boards.visible.find_by_name(name)
846 846 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
847 847 :class => 'board'
848 848 end
849 849 when 'news'
850 850 if project && news = project.news.visible.find_by_title(name)
851 851 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
852 852 :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
899 899 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
900 900
901 901 def parse_sections(text, project, obj, attr, only_path, options)
902 902 return unless options[:edit_section_links]
903 903 text.gsub!(HEADING_RE) do
904 904 heading = $1
905 905 @current_section += 1
906 906 if @current_section > 1
907 907 content_tag('div',
908 908 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
909 909 :class => 'contextual',
910 910 :title => l(:button_edit_section),
911 911 :id => "section-#{@current_section}") + heading.html_safe
912 912 else
913 913 heading
914 914 end
915 915 end
916 916 end
917 917
918 918 # Headings and TOC
919 919 # Adds ids and links to headings unless options[:headings] is set to false
920 920 def parse_headings(text, project, obj, attr, only_path, options)
921 921 return if options[:headings] == false
922 922
923 923 text.gsub!(HEADING_RE) do
924 924 level, attrs, content = $2.to_i, $3, $4
925 925 item = strip_tags(content).strip
926 926 anchor = sanitize_anchor_name(item)
927 927 # used for single-file wiki export
928 928 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
929 929 @heading_anchors[anchor] ||= 0
930 930 idx = (@heading_anchors[anchor] += 1)
931 931 if idx > 1
932 932 anchor = "#{anchor}-#{idx}"
933 933 end
934 934 @parsed_headings << [level, anchor, item]
935 935 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
936 936 end
937 937 end
938 938
939 939 MACROS_RE = /(
940 940 (!)? # escaping
941 941 (
942 942 \{\{ # opening tag
943 943 ([\w]+) # macro name
944 944 (\(([^\n\r]*?)\))? # optional arguments
945 945 ([\n\r].*?[\n\r])? # optional block of text
946 946 \}\} # closing tag
947 947 )
948 948 )/mx unless const_defined?(:MACROS_RE)
949 949
950 950 MACRO_SUB_RE = /(
951 951 \{\{
952 952 macro\((\d+)\)
953 953 \}\}
954 954 )/x unless const_defined?(:MACRO_SUB_RE)
955 955
956 956 # Extracts macros from text
957 957 def catch_macros(text)
958 958 macros = {}
959 959 text.gsub!(MACROS_RE) do
960 960 all, macro = $1, $4.downcase
961 961 if macro_exists?(macro) || all =~ MACRO_SUB_RE
962 962 index = macros.size
963 963 macros[index] = all
964 964 "{{macro(#{index})}}"
965 965 else
966 966 all
967 967 end
968 968 end
969 969 macros
970 970 end
971 971
972 972 # Executes and replaces macros in text
973 973 def inject_macros(text, obj, macros, execute=true)
974 974 text.gsub!(MACRO_SUB_RE) do
975 975 all, index = $1, $2.to_i
976 976 orig = macros.delete(index)
977 977 if execute && orig && orig =~ MACROS_RE
978 978 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
979 979 if esc.nil?
980 980 h(exec_macro(macro, obj, args, block) || all)
981 981 else
982 982 h(all)
983 983 end
984 984 elsif orig
985 985 h(orig)
986 986 else
987 987 h(all)
988 988 end
989 989 end
990 990 end
991 991
992 992 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
993 993
994 994 # Renders the TOC with given headings
995 995 def replace_toc(text, headings)
996 996 text.gsub!(TOC_RE) do
997 997 left_align, right_align = $2, $3
998 998 # Keep only the 4 first levels
999 999 headings = headings.select{|level, anchor, item| level <= 4}
1000 1000 if headings.empty?
1001 1001 ''
1002 1002 else
1003 1003 div_class = 'toc'
1004 1004 div_class << ' right' if right_align
1005 1005 div_class << ' left' if left_align
1006 1006 out = "<ul class=\"#{div_class}\"><li>"
1007 1007 root = headings.map(&:first).min
1008 1008 current = root
1009 1009 started = false
1010 1010 headings.each do |level, anchor, item|
1011 1011 if level > current
1012 1012 out << '<ul><li>' * (level - current)
1013 1013 elsif level < current
1014 1014 out << "</li></ul>\n" * (current - level) + "</li><li>"
1015 1015 elsif started
1016 1016 out << '</li><li>'
1017 1017 end
1018 1018 out << "<a href=\"##{anchor}\">#{item}</a>"
1019 1019 current = level
1020 1020 started = true
1021 1021 end
1022 1022 out << '</li></ul>' * (current - root)
1023 1023 out << '</li></ul>'
1024 1024 end
1025 1025 end
1026 1026 end
1027 1027
1028 1028 # Same as Rails' simple_format helper without using paragraphs
1029 1029 def simple_format_without_paragraph(text)
1030 1030 text.to_s.
1031 1031 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1032 1032 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1033 1033 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1034 1034 html_safe
1035 1035 end
1036 1036
1037 1037 def lang_options_for_select(blank=true)
1038 1038 (blank ? [["(auto)", ""]] : []) + languages_options
1039 1039 end
1040 1040
1041 1041 def label_tag_for(name, option_tags = nil, options = {})
1042 1042 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
1043 1043 content_tag("label", label_text)
1044 1044 end
1045 1045
1046 1046 def labelled_form_for(*args, &proc)
1047 1047 args << {} unless args.last.is_a?(Hash)
1048 1048 options = args.last
1049 1049 if args.first.is_a?(Symbol)
1050 1050 options.merge!(:as => args.shift)
1051 1051 end
1052 1052 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1053 1053 form_for(*args, &proc)
1054 1054 end
1055 1055
1056 1056 def labelled_fields_for(*args, &proc)
1057 1057 args << {} unless args.last.is_a?(Hash)
1058 1058 options = args.last
1059 1059 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1060 1060 fields_for(*args, &proc)
1061 1061 end
1062 1062
1063 1063 def labelled_remote_form_for(*args, &proc)
1064 1064 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
1065 1065 args << {} unless args.last.is_a?(Hash)
1066 1066 options = args.last
1067 1067 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
1068 1068 form_for(*args, &proc)
1069 1069 end
1070 1070
1071 1071 def error_messages_for(*objects)
1072 1072 html = ""
1073 1073 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1074 1074 errors = objects.map {|o| o.errors.full_messages}.flatten
1075 1075 if errors.any?
1076 1076 html << "<div id='errorExplanation'><ul>\n"
1077 1077 errors.each do |error|
1078 1078 html << "<li>#{h error}</li>\n"
1079 1079 end
1080 1080 html << "</ul></div>\n"
1081 1081 end
1082 1082 html.html_safe
1083 1083 end
1084 1084
1085 1085 def delete_link(url, options={})
1086 1086 options = {
1087 1087 :method => :delete,
1088 1088 :data => {:confirm => l(:text_are_you_sure)},
1089 1089 :class => 'icon icon-del'
1090 1090 }.merge(options)
1091 1091
1092 1092 link_to l(:button_delete), url, options
1093 1093 end
1094 1094
1095 1095 def preview_link(url, form, target='preview', options={})
1096 1096 content_tag 'a', l(:label_preview), {
1097 1097 :href => "#",
1098 1098 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1099 1099 :accesskey => accesskey(:preview)
1100 1100 }.merge(options)
1101 1101 end
1102 1102
1103 1103 def link_to_function(name, function, html_options={})
1104 1104 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1105 1105 end
1106 1106
1107 1107 # Helper to render JSON in views
1108 1108 def raw_json(arg)
1109 1109 arg.to_json.to_s.gsub('/', '\/').html_safe
1110 1110 end
1111 1111
1112 1112 def back_url
1113 1113 url = params[:back_url]
1114 1114 if url.nil? && referer = request.env['HTTP_REFERER']
1115 1115 url = CGI.unescape(referer.to_s)
1116 1116 end
1117 1117 url
1118 1118 end
1119 1119
1120 1120 def back_url_hidden_field_tag
1121 1121 url = back_url
1122 1122 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1123 1123 end
1124 1124
1125 1125 def check_all_links(form_name)
1126 1126 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1127 1127 " | ".html_safe +
1128 1128 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1129 1129 end
1130 1130
1131 1131 def progress_bar(pcts, options={})
1132 1132 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1133 1133 pcts = pcts.collect(&:round)
1134 1134 pcts[1] = pcts[1] - pcts[0]
1135 1135 pcts << (100 - pcts[1] - pcts[0])
1136 1136 width = options[:width] || '100px;'
1137 1137 legend = options[:legend] || ''
1138 1138 content_tag('table',
1139 1139 content_tag('tr',
1140 1140 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1141 1141 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1142 1142 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1143 1143 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1144 1144 content_tag('p', legend, :class => 'percent').html_safe
1145 1145 end
1146 1146
1147 1147 def checked_image(checked=true)
1148 1148 if checked
1149 1149 image_tag 'toggle_check.png'
1150 1150 end
1151 1151 end
1152 1152
1153 1153 def context_menu(url)
1154 1154 unless @context_menu_included
1155 1155 content_for :header_tags do
1156 1156 javascript_include_tag('context_menu') +
1157 1157 stylesheet_link_tag('context_menu')
1158 1158 end
1159 1159 if l(:direction) == 'rtl'
1160 1160 content_for :header_tags do
1161 1161 stylesheet_link_tag('context_menu_rtl')
1162 1162 end
1163 1163 end
1164 1164 @context_menu_included = true
1165 1165 end
1166 1166 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1167 1167 end
1168 1168
1169 1169 def calendar_for(field_id)
1170 1170 include_calendar_headers_tags
1171 1171 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1172 1172 end
1173 1173
1174 1174 def include_calendar_headers_tags
1175 1175 unless @calendar_headers_tags_included
1176 1176 tags = javascript_include_tag("datepicker")
1177 1177 @calendar_headers_tags_included = true
1178 1178 content_for :header_tags do
1179 1179 start_of_week = Setting.start_of_week
1180 1180 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1181 1181 # Redmine uses 1..7 (monday..sunday) in settings and locales
1182 1182 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1183 1183 start_of_week = start_of_week.to_i % 7
1184 1184 tags << javascript_tag(
1185 1185 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1186 1186 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1187 1187 path_to_image('/images/calendar.png') +
1188 1188 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1189 1189 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1190 1190 "beforeShow: beforeShowDatePicker};")
1191 1191 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1192 1192 unless jquery_locale == 'en'
1193 1193 tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1194 1194 end
1195 1195 tags
1196 1196 end
1197 1197 end
1198 1198 end
1199 1199
1200 1200 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1201 1201 # Examples:
1202 1202 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1203 1203 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1204 1204 #
1205 1205 def stylesheet_link_tag(*sources)
1206 1206 options = sources.last.is_a?(Hash) ? sources.pop : {}
1207 1207 plugin = options.delete(:plugin)
1208 1208 sources = sources.map do |source|
1209 1209 if plugin
1210 1210 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1211 1211 elsif current_theme && current_theme.stylesheets.include?(source)
1212 1212 current_theme.stylesheet_path(source)
1213 1213 else
1214 1214 source
1215 1215 end
1216 1216 end
1217 1217 super sources, options
1218 1218 end
1219 1219
1220 1220 # Overrides Rails' image_tag with themes and plugins support.
1221 1221 # Examples:
1222 1222 # image_tag('image.png') # => picks image.png from the current theme or defaults
1223 1223 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1224 1224 #
1225 1225 def image_tag(source, options={})
1226 1226 if plugin = options.delete(:plugin)
1227 1227 source = "/plugin_assets/#{plugin}/images/#{source}"
1228 1228 elsif current_theme && current_theme.images.include?(source)
1229 1229 source = current_theme.image_path(source)
1230 1230 end
1231 1231 super source, options
1232 1232 end
1233 1233
1234 1234 # Overrides Rails' javascript_include_tag with plugins support
1235 1235 # Examples:
1236 1236 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1237 1237 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1238 1238 #
1239 1239 def javascript_include_tag(*sources)
1240 1240 options = sources.last.is_a?(Hash) ? sources.pop : {}
1241 1241 if plugin = options.delete(:plugin)
1242 1242 sources = sources.map do |source|
1243 1243 if plugin
1244 1244 "/plugin_assets/#{plugin}/javascripts/#{source}"
1245 1245 else
1246 1246 source
1247 1247 end
1248 1248 end
1249 1249 end
1250 1250 super sources, options
1251 1251 end
1252 1252
1253 1253 # TODO: remove this in 2.5.0
1254 1254 def has_content?(name)
1255 1255 content_for?(name)
1256 1256 end
1257 1257
1258 1258 def sidebar_content?
1259 1259 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1260 1260 end
1261 1261
1262 1262 def view_layouts_base_sidebar_hook_response
1263 1263 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1264 1264 end
1265 1265
1266 1266 def email_delivery_enabled?
1267 1267 !!ActionMailer::Base.perform_deliveries
1268 1268 end
1269 1269
1270 1270 # Returns the avatar image tag for the given +user+ if avatars are enabled
1271 1271 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1272 1272 def avatar(user, options = { })
1273 1273 if Setting.gravatar_enabled?
1274 1274 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1275 1275 email = nil
1276 1276 if user.respond_to?(:mail)
1277 1277 email = user.mail
1278 1278 elsif user.to_s =~ %r{<(.+?)>}
1279 1279 email = $1
1280 1280 end
1281 1281 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1282 1282 else
1283 1283 ''
1284 1284 end
1285 1285 end
1286 1286
1287 1287 def sanitize_anchor_name(anchor)
1288 1288 if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1289 1289 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1290 1290 else
1291 1291 # TODO: remove when ruby1.8 is no longer supported
1292 1292 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1293 1293 end
1294 1294 end
1295 1295
1296 1296 # Returns the javascript tags that are included in the html layout head
1297 1297 def javascript_heads
1298 tags = javascript_include_tag('jquery-1.8.3-ui-1.9.2-ujs-2.0.3', 'application')
1298 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.1', 'application')
1299 1299 unless User.current.pref.warn_on_leaving_unsaved == '0'
1300 1300 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1301 1301 end
1302 1302 tags
1303 1303 end
1304 1304
1305 1305 def favicon
1306 1306 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1307 1307 end
1308 1308
1309 1309 # Returns the path to the favicon
1310 1310 def favicon_path
1311 1311 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1312 1312 image_path(icon)
1313 1313 end
1314 1314
1315 1315 # Returns the full URL to the favicon
1316 1316 def favicon_url
1317 1317 # TODO: use #image_url introduced in Rails4
1318 1318 path = favicon_path
1319 1319 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1320 1320 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1321 1321 end
1322 1322
1323 1323 def robot_exclusion_tag
1324 1324 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1325 1325 end
1326 1326
1327 1327 # Returns true if arg is expected in the API response
1328 1328 def include_in_api_response?(arg)
1329 1329 unless @included_in_api_response
1330 1330 param = params[:include]
1331 1331 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1332 1332 @included_in_api_response.collect!(&:strip)
1333 1333 end
1334 1334 @included_in_api_response.include?(arg.to_s)
1335 1335 end
1336 1336
1337 1337 # Returns options or nil if nometa param or X-Redmine-Nometa header
1338 1338 # was set in the request
1339 1339 def api_meta(options)
1340 1340 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1341 1341 # compatibility mode for activeresource clients that raise
1342 1342 # an error when deserializing an array with attributes
1343 1343 nil
1344 1344 else
1345 1345 options
1346 1346 end
1347 1347 end
1348 1348
1349 1349 private
1350 1350
1351 1351 def wiki_helper
1352 1352 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1353 1353 extend helper
1354 1354 return self
1355 1355 end
1356 1356
1357 1357 def link_to_content_update(text, url_params = {}, html_options = {})
1358 1358 link_to(text, url_params, html_options)
1359 1359 end
1360 1360 end
@@ -1,198 +1,198
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)}.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 content_tag('option', l(:label_no_change_option), :value => '') +
37 37 project_tree_options_for_select(@allowed_projects, :selected => @target_project),
38 38 :onchange => "updateBulkEditFrom('#{escape_javascript url_for(:action => 'bulk_edit', :format => 'js')}')") %>
39 39 </p>
40 40 <% end %>
41 41 <p>
42 42 <label for="issue_tracker_id"><%= l(:field_tracker) %></label>
43 43 <%= select_tag('issue[tracker_id]',
44 44 content_tag('option', l(:label_no_change_option), :value => '') +
45 45 options_from_collection_for_select(@trackers, :id, :name, @issue_params[:tracker_id])) %>
46 46 </p>
47 47 <% if @available_statuses.any? %>
48 48 <p>
49 49 <label for='issue_status_id'><%= l(:field_status) %></label>
50 50 <%= select_tag('issue[status_id]',
51 51 content_tag('option', l(:label_no_change_option), :value => '') +
52 52 options_from_collection_for_select(@available_statuses, :id, :name, @issue_params[:status_id])) %>
53 53 </p>
54 54 <% end %>
55 55
56 56 <% if @safe_attributes.include?('priority_id') -%>
57 57 <p>
58 58 <label for='issue_priority_id'><%= l(:field_priority) %></label>
59 59 <%= select_tag('issue[priority_id]',
60 60 content_tag('option', l(:label_no_change_option), :value => '') +
61 61 options_from_collection_for_select(IssuePriority.active, :id, :name, @issue_params[:priority_id])) %>
62 62 </p>
63 63 <% end %>
64 64
65 65 <% if @safe_attributes.include?('assigned_to_id') -%>
66 66 <p>
67 67 <label for='issue_assigned_to_id'><%= l(:field_assigned_to) %></label>
68 68 <%= select_tag('issue[assigned_to_id]',
69 69 content_tag('option', l(:label_no_change_option), :value => '') +
70 70 content_tag('option', l(:label_nobody), :value => 'none', :selected => (@issue_params[:assigned_to_id] == 'none')) +
71 71 principals_options_for_select(@assignables, @issue_params[:assigned_to_id])) %>
72 72 </p>
73 73 <% end %>
74 74
75 75 <% if @safe_attributes.include?('category_id') -%>
76 76 <p>
77 77 <label for='issue_category_id'><%= l(:field_category) %></label>
78 78 <%= select_tag('issue[category_id]', content_tag('option', l(:label_no_change_option), :value => '') +
79 79 content_tag('option', l(:label_none), :value => 'none', :selected => (@issue_params[:category_id] == 'none')) +
80 80 options_from_collection_for_select(@categories, :id, :name, @issue_params[:category_id])) %>
81 81 </p>
82 82 <% end %>
83 83
84 84 <% if @safe_attributes.include?('fixed_version_id') -%>
85 85 <p>
86 86 <label for='issue_fixed_version_id'><%= l(:field_fixed_version) %></label>
87 87 <%= select_tag('issue[fixed_version_id]', content_tag('option', l(:label_no_change_option), :value => '') +
88 88 content_tag('option', l(:label_none), :value => 'none', :selected => (@issue_params[:fixed_version_id] == 'none')) +
89 89 version_options_for_select(@versions.sort, @issue_params[:fixed_version_id])) %>
90 90 </p>
91 91 <% end %>
92 92
93 93 <% @custom_fields.each do |custom_field| %>
94 94 <p>
95 95 <label><%= h(custom_field.name) %></label>
96 96 <%= custom_field_tag_for_bulk_edit('issue', custom_field, @issues, @issue_params[:custom_field_values][custom_field.id.to_s]) %>
97 97 </p>
98 98 <% end %>
99 99
100 100 <% if @copy && @attachments_present %>
101 101 <%= hidden_field_tag 'copy_attachments', '0' %>
102 102 <p>
103 103 <label for='copy_attachments'><%= l(:label_copy_attachments) %></label>
104 104 <%= check_box_tag 'copy_attachments', '1', params[:copy_attachments] != '0' %>
105 105 </p>
106 106 <% end %>
107 107
108 108 <% if @copy && @subtasks_present %>
109 109 <%= hidden_field_tag 'copy_subtasks', '0' %>
110 110 <p>
111 111 <label for='copy_subtasks'><%= l(:label_copy_subtasks) %></label>
112 112 <%= check_box_tag 'copy_subtasks', '1', params[:copy_subtasks] != '0' %>
113 113 </p>
114 114 <% end %>
115 115
116 116 <%= call_hook(:view_issues_bulk_edit_details_bottom, { :issues => @issues }) %>
117 117 </div>
118 118
119 119 <div class="splitcontentright">
120 120 <% if @safe_attributes.include?('is_private') %>
121 121 <p>
122 122 <label for='issue_is_private'><%= l(:field_is_private) %></label>
123 123 <%= select_tag('issue[is_private]', content_tag('option', l(:label_no_change_option), :value => '') +
124 124 content_tag('option', l(:general_text_Yes), :value => '1', :selected => (@issue_params[:is_private] == '1')) +
125 125 content_tag('option', l(:general_text_No), :value => '0', :selected => (@issue_params[:is_private] == '0'))) %>
126 126 </p>
127 127 <% end %>
128 128
129 129 <% if @safe_attributes.include?('parent_issue_id') && @project %>
130 130 <p>
131 131 <label for='issue_parent_issue_id'><%= l(:field_parent_issue) %></label>
132 132 <%= text_field_tag 'issue[parent_issue_id]', '', :size => 10, :value => @issue_params[:parent_issue_id] %>
133 133 <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>
134 134 </p>
135 135 <%= javascript_tag "observeAutocompleteField('issue_parent_issue_id', '#{escape_javascript auto_complete_issues_path(:project_id => @project)}')" %>
136 136 <% end %>
137 137
138 138 <% if @safe_attributes.include?('start_date') %>
139 139 <p>
140 140 <label for='issue_start_date'><%= l(:field_start_date) %></label>
141 141 <%= text_field_tag 'issue[start_date]', '', :value => @issue_params[:start_date], :size => 10 %><%= calendar_for('issue_start_date') %>
142 142 <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>
143 143 </p>
144 144 <% end %>
145 145
146 146 <% if @safe_attributes.include?('due_date') %>
147 147 <p>
148 148 <label for='issue_due_date'><%= l(:field_due_date) %></label>
149 149 <%= text_field_tag 'issue[due_date]', '', :value => @issue_params[:due_date], :size => 10 %><%= calendar_for('issue_due_date') %>
150 150 <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>
151 151 </p>
152 152 <% end %>
153 153
154 154 <% if @safe_attributes.include?('done_ratio') && Issue.use_field_for_done_ratio? %>
155 155 <p>
156 156 <label for='issue_done_ratio'><%= l(:field_done_ratio) %></label>
157 157 <%= 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]) %>
158 158 </p>
159 159 <% end %>
160 160 </div>
161 161 </fieldset>
162 162
163 163 <fieldset>
164 164 <legend><%= l(:field_notes) %></legend>
165 165 <%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
166 166 <%= wikitoolbar_for 'notes' %>
167 167 </fieldset>
168 168 </div>
169 169
170 170 <p>
171 171 <% if @copy %>
172 172 <%= hidden_field_tag 'copy', '1' %>
173 173 <%= submit_tag l(:button_copy) %>
174 174 <%= submit_tag l(:button_copy_and_follow), :name => 'follow' %>
175 175 <% elsif @target_project %>
176 176 <%= submit_tag l(:button_move) %>
177 177 <%= submit_tag l(:button_move_and_follow), :name => 'follow' %>
178 178 <% else %>
179 179 <%= submit_tag l(:button_submit) %>
180 180 <% end %>
181 181 </p>
182 182
183 183 <% end %>
184 184
185 185 <%= javascript_tag do %>
186 186 $(window).load(function(){
187 187 $(document).on('change', 'input[data-disables]', function(){
188 if ($(this).attr('checked')){
188 if ($(this).prop('checked')){
189 189 $($(this).data('disables')).attr('disabled', true).val('');
190 190 } else {
191 191 $($(this).data('disables')).attr('disabled', false);
192 192 }
193 193 });
194 194 });
195 195 $(document).ready(function(){
196 196 $('input[data-disables]').trigger('change');
197 197 });
198 198 <% end %>
@@ -1,10 +1,10
1 1 $('#issue_notes').val("<%= raw escape_javascript(@content) %>");
2 2 <%
3 3 # when quoting a private journal, check the private checkbox
4 4 if @journal && @journal.private_notes?
5 5 %>
6 $('#issue_private_notes').attr('checked', true);
6 $('#issue_private_notes').prop('checked', true);
7 7 <% end %>
8 8
9 9 showAndScrollTo("update", "notes");
10 10 $('#notes').scrollTop = $('#notes').scrollHeight - $('#notes').clientHeight;
@@ -1,80 +1,80
1 1 <!DOCTYPE html>
2 2 <html lang="<%= current_language %>">
3 3 <head>
4 4 <meta charset="utf-8" />
5 5 <title><%=h html_title %></title>
6 6 <meta name="description" content="<%= Redmine::Info.app_name %>" />
7 7 <meta name="keywords" content="issue,bug,tracker" />
8 8 <%= csrf_meta_tag %>
9 9 <%= favicon %>
10 <%= stylesheet_link_tag 'jquery/jquery-ui-1.9.2', 'application', :media => 'all' %>
10 <%= stylesheet_link_tag 'jquery/jquery-ui-1.11.0', 'application', :media => 'all' %>
11 11 <%= stylesheet_link_tag 'rtl', :media => 'all' if l(:direction) == 'rtl' %>
12 12 <%= javascript_heads %>
13 13 <%= heads_for_theme %>
14 14 <%= call_hook :view_layouts_base_html_head %>
15 15 <!-- page specific tags -->
16 16 <%= yield :header_tags -%>
17 17 </head>
18 18 <body class="<%=h body_css_classes %>">
19 19 <div id="wrapper">
20 20 <div id="wrapper2">
21 21 <div id="wrapper3">
22 22 <div id="top-menu">
23 23 <div id="account">
24 24 <%= render_menu :account_menu -%>
25 25 </div>
26 26 <%= content_tag('div', "#{l(:label_logged_as)} #{link_to_user(User.current, :format => :username)}".html_safe, :id => 'loggedas') if User.current.logged? %>
27 27 <%= render_menu :top_menu if User.current.logged? || !Setting.login_required? -%>
28 28 </div>
29 29
30 30 <div id="header">
31 31 <% if User.current.logged? || !Setting.login_required? %>
32 32 <div id="quick-search">
33 33 <%= form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get ) do %>
34 34 <%= hidden_field_tag(controller.default_search_scope, 1, :id => nil) if controller.default_search_scope %>
35 35 <label for='q'>
36 36 <%= link_to l(:label_search), {:controller => 'search', :action => 'index', :id => @project}, :accesskey => accesskey(:search) %>:
37 37 </label>
38 38 <%= text_field_tag 'q', @question, :size => 20, :class => 'small', :accesskey => accesskey(:quick_search) %>
39 39 <% end %>
40 40 <%= render_project_jump_box %>
41 41 </div>
42 42 <% end %>
43 43
44 44 <h1><%= page_header_title %></h1>
45 45
46 46 <% if display_main_menu?(@project) %>
47 47 <div id="main-menu">
48 48 <%= render_main_menu(@project) %>
49 49 </div>
50 50 <% end %>
51 51 </div>
52 52
53 53 <div id="main" class="<%= sidebar_content? ? '' : 'nosidebar' %>">
54 54 <div id="sidebar">
55 55 <%= yield :sidebar %>
56 56 <%= view_layouts_base_sidebar_hook_response %>
57 57 </div>
58 58
59 59 <div id="content">
60 60 <%= render_flash_messages %>
61 61 <%= yield %>
62 62 <%= call_hook :view_layouts_base_content %>
63 63 <div style="clear:both;"></div>
64 64 </div>
65 65 </div>
66 66 </div>
67 67
68 68 <div id="ajax-indicator" style="display:none;"><span><%= l(:label_loading) %></span></div>
69 69 <div id="ajax-modal" style="display:none;"></div>
70 70
71 71 <div id="footer">
72 72 <div class="bgl"><div class="bgr">
73 73 Powered by <%= link_to Redmine::Info.app_name, Redmine::Info.url %> &copy; 2006-2014 Jean-Philippe Lang
74 74 </div></div>
75 75 </div>
76 76 </div>
77 77 </div>
78 78 <%= call_hook :view_layouts_base_body_bottom %>
79 79 </body>
80 80 </html>
@@ -1,102 +1,102
1 1 <%= error_messages_for 'project' %>
2 2
3 3 <div class="box tabular">
4 4 <!--[form:project]-->
5 5 <p><%= f.text_field :name, :required => true, :size => 60 %></p>
6 6
7 7 <p><%= f.text_area :description, :rows => 8, :class => 'wiki-edit' %></p>
8 8 <p><%= f.text_field :identifier, :required => true, :size => 60, :disabled => @project.identifier_frozen?, :maxlength => Project::IDENTIFIER_MAX_LENGTH %>
9 9 <% unless @project.identifier_frozen? %>
10 10 <em class="info"><%= l(:text_length_between, :min => 1, :max => Project::IDENTIFIER_MAX_LENGTH) %> <%= l(:text_project_identifier_info).html_safe %></em>
11 11 <% end %></p>
12 12 <p><%= f.text_field :homepage, :size => 60 %></p>
13 13 <p><%= f.check_box :is_public %></p>
14 14
15 15 <% unless @project.allowed_parents.compact.empty? %>
16 16 <p><%= label(:project, :parent_id, l(:field_parent)) %><%= parent_project_select_tag(@project) %></p>
17 17 <% end %>
18 18
19 19 <% if @project.safe_attribute? 'inherit_members' %>
20 20 <p><%= f.check_box :inherit_members %></p>
21 21 <% end %>
22 22
23 23 <%= wikitoolbar_for 'project_description' %>
24 24
25 25 <% @project.custom_field_values.each do |value| %>
26 26 <p><%= custom_field_tag_with_label :project, value %></p>
27 27 <% end %>
28 28 <%= call_hook(:view_projects_form, :project => @project, :form => f) %>
29 29 </div>
30 30
31 31 <% if @project.new_record? %>
32 32 <fieldset class="box tabular"><legend><%= l(:label_module_plural) %></legend>
33 33 <% Redmine::AccessControl.available_project_modules.each do |m| %>
34 34 <label class="floating">
35 35 <%= check_box_tag 'project[enabled_module_names][]', m, @project.module_enabled?(m), :id => "project_enabled_module_names_#{m}" %>
36 36 <%= l_or_humanize(m, :prefix => "project_module_") %>
37 37 </label>
38 38 <% end %>
39 39 <%= hidden_field_tag 'project[enabled_module_names][]', '' %>
40 40 </fieldset>
41 41 <% end %>
42 42
43 43 <% if @project.new_record? || @project.module_enabled?('issue_tracking') %>
44 44 <% unless @trackers.empty? %>
45 45 <fieldset class="box tabular" id="project_trackers"><legend><%=l(:label_tracker_plural)%></legend>
46 46 <% @trackers.each do |tracker| %>
47 47 <label class="floating">
48 48 <%= check_box_tag 'project[tracker_ids][]', tracker.id, @project.trackers.include?(tracker), :id => nil %>
49 49 <%=h tracker %>
50 50 </label>
51 51 <% end %>
52 52 <%= hidden_field_tag 'project[tracker_ids][]', '' %>
53 53 </fieldset>
54 54 <% end %>
55 55
56 56 <% unless @issue_custom_fields.empty? %>
57 57 <fieldset class="box tabular" id="project_issue_custom_fields"><legend><%=l(:label_custom_field_plural)%></legend>
58 58 <% @issue_custom_fields.each do |custom_field| %>
59 59 <label class="floating">
60 60 <%= check_box_tag 'project[issue_custom_field_ids][]', custom_field.id, (@project.all_issue_custom_fields.include? custom_field),
61 61 :disabled => (custom_field.is_for_all? ? "disabled" : nil),
62 62 :id => nil %>
63 63 <%=h custom_field.name %>
64 64 </label>
65 65 <% end %>
66 66 <%= hidden_field_tag 'project[issue_custom_field_ids][]', '' %>
67 67 </fieldset>
68 68 <% end %>
69 69 <% end %>
70 70 <!--[eoform:project]-->
71 71
72 72 <% unless @project.identifier_frozen? %>
73 73 <% content_for :header_tags do %>
74 74 <%= javascript_include_tag 'project_identifier' %>
75 75 <% end %>
76 76 <% end %>
77 77
78 78 <% if !User.current.admin? && @project.inherit_members? && @project.parent && User.current.member_of?(@project.parent) %>
79 79 <%= javascript_tag do %>
80 80 $(document).ready(function() {
81 81 $("#project_inherit_members").change(function(){
82 82 if (!$(this).is(':checked')) {
83 83 if (!confirm("<%= escape_javascript(l(:text_own_membership_delete_confirmation)) %>")) {
84 84 $("#project_inherit_members").attr("checked", true);
85 85 }
86 86 }
87 87 });
88 88 });
89 89 <% end %>
90 90 <% end %>
91 91
92 92 <%= javascript_tag do %>
93 93 $(document).ready(function() {
94 94 $('#project_enabled_module_names_issue_tracking').on('change', function(){
95 if ($(this).attr('checked')){
95 if ($(this).prop('checked')){
96 96 $('#project_trackers, #project_issue_custom_fields').show();
97 97 } else {
98 98 $('#project_trackers, #project_issue_custom_fields').hide();
99 99 }
100 100 }).trigger('change');
101 101 });
102 102 <% end %>
@@ -1,51 +1,51
1 1 <% show_revision_graph = ( @repository.supports_revision_graph? && path.blank? ) %>
2 2 <%= if show_revision_graph && revisions && revisions.any?
3 3 indexed_commits, graph_space = index_commits(revisions, @repository.branches) do |scmid|
4 4 url_for(
5 5 :controller => 'repositories',
6 6 :action => 'revision',
7 7 :id => project,
8 8 :repository_id => @repository.identifier_param,
9 9 :rev => scmid)
10 10 end
11 11 render :partial => 'revision_graph',
12 12 :locals => {
13 13 :commits => indexed_commits,
14 14 :space => graph_space
15 15 }
16 16 end %>
17 17 <%= form_tag(
18 18 {:controller => 'repositories', :action => 'diff', :id => project,
19 19 :repository_id => @repository.identifier_param, :path => to_path_param(path)},
20 20 :method => :get
21 21 ) do %>
22 22 <table class="list changesets">
23 23 <thead><tr>
24 24 <th>#</th>
25 25 <th></th>
26 26 <th></th>
27 27 <th><%= l(:label_date) %></th>
28 28 <th><%= l(:field_author) %></th>
29 29 <th><%= l(:field_comments) %></th>
30 30 </tr></thead>
31 31 <tbody>
32 32 <% show_diff = revisions.size > 1 %>
33 33 <% line_num = 1 %>
34 34 <% revisions.each do |changeset| %>
35 35 <tr class="changeset <%= cycle 'odd', 'even' %>">
36 36 <% id_style = (show_revision_graph ? "padding-left:#{(graph_space + 1) * 20}px" : nil) %>
37 37 <%= content_tag(:td, :class => 'id', :style => id_style) do %>
38 38 <%= link_to_revision(changeset, @repository) %>
39 39 <% end %>
40 <td class="checkbox"><%= radio_button_tag('rev', changeset.identifier, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('#cbto-#{line_num+1}').attr('checked',true);") if show_diff && (line_num < revisions.size) %></td>
41 <td class="checkbox"><%= radio_button_tag('rev_to', changeset.identifier, (line_num==2), :id => "cbto-#{line_num}", :onclick => "if ($('#cb-#{line_num}').attr('checked')) {$('#cb-#{line_num-1}').attr('checked',true);}") if show_diff && (line_num > 1) %></td>
40 <td class="checkbox"><%= radio_button_tag('rev', changeset.identifier, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('#cbto-#{line_num+1}').prop('checked',true);") if show_diff && (line_num < revisions.size) %></td>
41 <td class="checkbox"><%= radio_button_tag('rev_to', changeset.identifier, (line_num==2), :id => "cbto-#{line_num}", :onclick => "if ($('#cb-#{line_num}').prop('checked')) {$('#cb-#{line_num-1}').prop('checked',true);}") if show_diff && (line_num > 1) %></td>
42 42 <td class="committed_on"><%= format_time(changeset.committed_on) %></td>
43 43 <td class="author"><%= changeset.author.to_s.truncate(30) %></td>
44 44 <td class="comments"><%= textilizable(truncate_at_line_break(changeset.comments)) %></td>
45 45 </tr>
46 46 <% line_num += 1 %>
47 47 <% end %>
48 48 </tbody>
49 49 </table>
50 50 <%= submit_tag(l(:label_view_diff), :name => nil) if show_diff %>
51 51 <% end %>
@@ -1,65 +1,65
1 1 <h2><%= l(:label_search) %></h2>
2 2
3 3 <div class="box">
4 4 <%= form_tag({}, :method => :get, :id => 'search-form') do %>
5 5 <%= label_tag "search-input", l(:description_search), :class => "hidden-for-sighted" %>
6 6 <p><%= text_field_tag 'q', @question, :size => 60, :id => 'search-input' %>
7 7 <%= project_select_tag %>
8 8 <%= hidden_field_tag 'all_words', '', :id => nil %>
9 9 <label><%= check_box_tag 'all_words', 1, @all_words %> <%= l(:label_all_words) %></label>
10 10 <%= hidden_field_tag 'titles_only', '', :id => nil %>
11 11 <label><%= check_box_tag 'titles_only', 1, @titles_only %> <%= l(:label_search_titles_only) %></label>
12 12 </p>
13 13
14 14 <p id="search-types">
15 15 <% @object_types.each do |t| %>
16 16 <label><%= check_box_tag t, 1, @scope.include?(t) %> <%= link_to type_label(t), "#" %></label>
17 17 <% end %>
18 18 </p>
19 19
20 20 <p><%= submit_tag l(:button_submit) %></p>
21 21 <% end %>
22 22 </div>
23 23
24 24 <% if @results %>
25 25 <div id="search-results-counts">
26 26 <%= render_results_by_type(@results_by_type) unless @scope.size == 1 %>
27 27 </div>
28 28 <h3><%= l(:label_result_plural) %> (<%= @results_by_type.values.sum %>)</h3>
29 29 <dl id="search-results">
30 30 <% @results.each do |e| %>
31 31 <dt class="<%= e.event_type %>">
32 32 <%= content_tag('span', h(e.project), :class => 'project') unless @project == e.project %>
33 33 <%= link_to(highlight_tokens(e.event_title.truncate(255), @tokens), e.event_url) %>
34 34 </dt>
35 35 <dd><span class="description"><%= highlight_tokens(e.event_description, @tokens) %></span>
36 36 <span class="author"><%= format_time(e.event_datetime) %></span></dd>
37 37 <% end %>
38 38 </dl>
39 39 <% end %>
40 40
41 41 <p class="pagination">
42 42 <% if @pagination_previous_date %>
43 43 <%= link_to_content_update("\xc2\xab " + l(:label_previous),
44 44 params.merge(:previous => 1,
45 45 :offset => @pagination_previous_date.strftime("%Y%m%d%H%M%S"))) %>&nbsp;
46 46 <% end %>
47 47 <% if @pagination_next_date %>
48 48 <%= link_to_content_update(l(:label_next) + " \xc2\xbb",
49 49 params.merge(:previous => nil,
50 50 :offset => @pagination_next_date.strftime("%Y%m%d%H%M%S"))) %>
51 51 <% end %>
52 52 </p>
53 53
54 54 <% html_title(l(:label_search)) -%>
55 55
56 56 <%= javascript_tag do %>
57 57 $("#search-types a").click(function(e){
58 58 e.preventDefault();
59 $("#search-types input[type=checkbox]").attr('checked', false);
60 $(this).siblings("input[type=checkbox]").attr('checked', true);
59 $("#search-types input[type=checkbox]").prop('checked', false);
60 $(this).siblings("input[type=checkbox]").prop('checked', true);
61 61 if ($("#search-input").val() != "") {
62 62 $("#search-form").submit();
63 63 }
64 64 });
65 65 <% end %>
@@ -1,22 +1,22
1 1 <%= wiki_page_breadcrumb(@page) %>
2 2
3 3 <h2><%=h @page.pretty_title %></h2>
4 4
5 5 <%= form_tag({}, :method => :delete) do %>
6 6 <div class="box">
7 7 <p><strong><%= l(:text_wiki_page_destroy_question, :descendants => @descendants_count) %></strong></p>
8 8 <p><label><%= radio_button_tag 'todo', 'nullify', true %> <%= l(:text_wiki_page_nullify_children) %></label><br />
9 9 <label><%= radio_button_tag 'todo', 'destroy', false %> <%= l(:text_wiki_page_destroy_children) %></label>
10 10 <% if @reassignable_to.any? %>
11 11 <br />
12 12 <label><%= radio_button_tag 'todo', 'reassign', false %> <%= l(:text_wiki_page_reassign_children) %></label>:
13 13 <%= label_tag "reassign_to_id", l(:description_wiki_subpages_reassign), :class => "hidden-for-sighted" %>
14 14 <%= select_tag 'reassign_to_id', wiki_page_options_for_select(@reassignable_to),
15 :onclick => "$('#todo_reassign').attr('checked', true);" %>
15 :onclick => "$('#todo_reassign').prop('checked', true);" %>
16 16 <% end %>
17 17 </p>
18 18 </div>
19 19
20 20 <%= submit_tag l(:button_apply) %>
21 21 <%= link_to l(:button_cancel), :controller => 'wiki', :action => 'show', :project_id => @project, :id => @page.title %>
22 22 <% end %>
@@ -1,40 +1,40
1 1 <%= wiki_page_breadcrumb(@page) %>
2 2
3 3 <%= title [@page.pretty_title, project_wiki_page_path(@page.project, @page.title, :version => nil)], l(:label_history) %>
4 4
5 5 <%= form_tag({:controller => 'wiki', :action => 'diff',
6 6 :project_id => @page.project, :id => @page.title},
7 7 :method => :get) do %>
8 8 <table class="list wiki-page-versions">
9 9 <thead><tr>
10 10 <th>#</th>
11 11 <th></th>
12 12 <th></th>
13 13 <th><%= l(:field_updated_on) %></th>
14 14 <th><%= l(:field_author) %></th>
15 15 <th><%= l(:field_comments) %></th>
16 16 <th></th>
17 17 </tr></thead>
18 18 <tbody>
19 19 <% show_diff = @versions.size > 1 %>
20 20 <% line_num = 1 %>
21 21 <% @versions.each do |ver| %>
22 22 <tr class="wiki-page-version <%= cycle("odd", "even") %>">
23 23 <td class="id"><%= link_to h(ver.version), :action => 'show', :id => @page.title, :project_id => @page.project, :version => ver.version %></td>
24 <td class="checkbox"><%= radio_button_tag('version', ver.version, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('#cbto-#{line_num+1}').attr('checked', true);") if show_diff && (line_num < @versions.size) %></td>
24 <td class="checkbox"><%= radio_button_tag('version', ver.version, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('#cbto-#{line_num+1}').prop('checked', true);") if show_diff && (line_num < @versions.size) %></td>
25 25 <td class="checkbox"><%= radio_button_tag('version_from', ver.version, (line_num==2), :id => "cbto-#{line_num}") if show_diff && (line_num > 1) %></td>
26 26 <td class="updated_on"><%= format_time(ver.updated_on) %></td>
27 27 <td class="author"><%= link_to_user ver.author %></td>
28 28 <td class="comments"><%=h ver.comments %></td>
29 29 <td class="buttons">
30 30 <%= link_to l(:button_annotate), :action => 'annotate', :id => @page.title, :version => ver.version %>
31 31 <%= delete_link wiki_page_path(@page, :version => ver.version) if User.current.allowed_to?(:delete_wiki_pages, @page.project) && @version_count > 1 %>
32 32 </td>
33 33 </tr>
34 34 <% line_num += 1 %>
35 35 <% end %>
36 36 </tbody>
37 37 </table>
38 38 <%= submit_tag l(:label_view_diff), :class => 'small' if show_diff %>
39 39 <span class="pagination"><%= pagination_links_full @version_pages, @version_count %></span>
40 40 <% end %>
@@ -1,614 +1,614
1 1 /* Redmine - project management software
2 2 Copyright (C) 2006-2014 Jean-Philippe Lang */
3 3
4 4 function checkAll(id, checked) {
5 $('#'+id).find('input[type=checkbox]:enabled').attr('checked', checked);
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 $(selector).attr('checked', !all_checked);
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 function initFilters() {
78 78 $('#add_filter_select').change(function() {
79 79 addFilter($(this).val(), '', []);
80 80 });
81 81 $('#filters-table td.field input[type=checkbox]').each(function() {
82 82 toggleFilter($(this).val());
83 83 });
84 84 $('#filters-table').on('click', 'td.field input[type=checkbox]', function() {
85 85 toggleFilter($(this).val());
86 86 });
87 87 $('#filters-table').on('click', '.toggle-multiselect', function() {
88 88 toggleMultiSelect($(this).siblings('select'));
89 89 });
90 90 $('#filters-table').on('keypress', 'input[type=text]', function(e) {
91 91 if (e.keyCode == 13) submit_query_form("query_form");
92 92 });
93 93 }
94 94
95 95 function addFilter(field, operator, values) {
96 96 var fieldId = field.replace('.', '_');
97 97 var tr = $('#tr_'+fieldId);
98 98 if (tr.length > 0) {
99 99 tr.show();
100 100 } else {
101 101 buildFilterRow(field, operator, values);
102 102 }
103 $('#cb_'+fieldId).attr('checked', true);
103 $('#cb_'+fieldId).prop('checked', true);
104 104 toggleFilter(field);
105 105 $('#add_filter_select').val('').children('option').each(function() {
106 106 if ($(this).attr('value') == field) {
107 107 $(this).attr('disabled', true);
108 108 }
109 109 });
110 110 }
111 111
112 112 function buildFilterRow(field, operator, values) {
113 113 var fieldId = field.replace('.', '_');
114 114 var filterTable = $("#filters-table");
115 115 var filterOptions = availableFilters[field];
116 116 if (!filterOptions) return;
117 117 var operators = operatorByType[filterOptions['type']];
118 118 var filterValues = filterOptions['values'];
119 119 var i, select;
120 120
121 121 var tr = $('<tr class="filter">').attr('id', 'tr_'+fieldId).html(
122 122 '<td class="field"><input checked="checked" id="cb_'+fieldId+'" name="f[]" value="'+field+'" type="checkbox"><label for="cb_'+fieldId+'"> '+filterOptions['name']+'</label></td>' +
123 123 '<td class="operator"><select id="operators_'+fieldId+'" name="op['+field+']"></td>' +
124 124 '<td class="values"></td>'
125 125 );
126 126 filterTable.append(tr);
127 127
128 128 select = tr.find('td.operator select');
129 129 for (i = 0; i < operators.length; i++) {
130 130 var option = $('<option>').val(operators[i]).text(operatorLabels[operators[i]]);
131 131 if (operators[i] == operator) { option.attr('selected', true); }
132 132 select.append(option);
133 133 }
134 134 select.change(function(){ toggleOperator(field); });
135 135
136 136 switch (filterOptions['type']) {
137 137 case "list":
138 138 case "list_optional":
139 139 case "list_status":
140 140 case "list_subprojects":
141 141 tr.find('td.values').append(
142 142 '<span style="display:none;"><select class="value" id="values_'+fieldId+'_1" name="v['+field+'][]"></select>' +
143 143 ' <span class="toggle-multiselect">&nbsp;</span></span>'
144 144 );
145 145 select = tr.find('td.values select');
146 146 if (values.length > 1) { select.attr('multiple', true); }
147 147 for (i = 0; i < filterValues.length; i++) {
148 148 var filterValue = filterValues[i];
149 149 var option = $('<option>');
150 150 if ($.isArray(filterValue)) {
151 151 option.val(filterValue[1]).text(filterValue[0]);
152 152 if ($.inArray(filterValue[1], values) > -1) {option.attr('selected', true);}
153 153 } else {
154 154 option.val(filterValue).text(filterValue);
155 155 if ($.inArray(filterValue, values) > -1) {option.attr('selected', true);}
156 156 }
157 157 select.append(option);
158 158 }
159 159 break;
160 160 case "date":
161 161 case "date_past":
162 162 tr.find('td.values').append(
163 163 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="10" class="value date_value" /></span>' +
164 164 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="10" class="value date_value" /></span>' +
165 165 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="3" class="value" /> '+labelDayPlural+'</span>'
166 166 );
167 167 $('#values_'+fieldId+'_1').val(values[0]).datepicker(datepickerOptions);
168 168 $('#values_'+fieldId+'_2').val(values[1]).datepicker(datepickerOptions);
169 169 $('#values_'+fieldId).val(values[0]);
170 170 break;
171 171 case "string":
172 172 case "text":
173 173 tr.find('td.values').append(
174 174 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="30" class="value" /></span>'
175 175 );
176 176 $('#values_'+fieldId).val(values[0]);
177 177 break;
178 178 case "relation":
179 179 tr.find('td.values').append(
180 180 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="6" class="value" /></span>' +
181 181 '<span style="display:none;"><select class="value" name="v['+field+'][]" id="values_'+fieldId+'_1"></select></span>'
182 182 );
183 183 $('#values_'+fieldId).val(values[0]);
184 184 select = tr.find('td.values select');
185 185 for (i = 0; i < allProjects.length; i++) {
186 186 var filterValue = allProjects[i];
187 187 var option = $('<option>');
188 188 option.val(filterValue[1]).text(filterValue[0]);
189 189 if (values[0] == filterValue[1]) { option.attr('selected', true); }
190 190 select.append(option);
191 191 }
192 192 case "integer":
193 193 case "float":
194 194 tr.find('td.values').append(
195 195 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="6" class="value" /></span>' +
196 196 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="6" class="value" /></span>'
197 197 );
198 198 $('#values_'+fieldId+'_1').val(values[0]);
199 199 $('#values_'+fieldId+'_2').val(values[1]);
200 200 break;
201 201 }
202 202 }
203 203
204 204 function toggleFilter(field) {
205 205 var fieldId = field.replace('.', '_');
206 206 if ($('#cb_' + fieldId).is(':checked')) {
207 207 $("#operators_" + fieldId).show().removeAttr('disabled');
208 208 toggleOperator(field);
209 209 } else {
210 210 $("#operators_" + fieldId).hide().attr('disabled', true);
211 211 enableValues(field, []);
212 212 }
213 213 }
214 214
215 215 function enableValues(field, indexes) {
216 216 var fieldId = field.replace('.', '_');
217 217 $('#tr_'+fieldId+' td.values .value').each(function(index) {
218 218 if ($.inArray(index, indexes) >= 0) {
219 219 $(this).removeAttr('disabled');
220 220 $(this).parents('span').first().show();
221 221 } else {
222 222 $(this).val('');
223 223 $(this).attr('disabled', true);
224 224 $(this).parents('span').first().hide();
225 225 }
226 226
227 227 if ($(this).hasClass('group')) {
228 228 $(this).addClass('open');
229 229 } else {
230 230 $(this).show();
231 231 }
232 232 });
233 233 }
234 234
235 235 function toggleOperator(field) {
236 236 var fieldId = field.replace('.', '_');
237 237 var operator = $("#operators_" + fieldId);
238 238 switch (operator.val()) {
239 239 case "!*":
240 240 case "*":
241 241 case "t":
242 242 case "ld":
243 243 case "w":
244 244 case "lw":
245 245 case "l2w":
246 246 case "m":
247 247 case "lm":
248 248 case "y":
249 249 case "o":
250 250 case "c":
251 251 enableValues(field, []);
252 252 break;
253 253 case "><":
254 254 enableValues(field, [0,1]);
255 255 break;
256 256 case "<t+":
257 257 case ">t+":
258 258 case "><t+":
259 259 case "t+":
260 260 case ">t-":
261 261 case "<t-":
262 262 case "><t-":
263 263 case "t-":
264 264 enableValues(field, [2]);
265 265 break;
266 266 case "=p":
267 267 case "=!p":
268 268 case "!p":
269 269 enableValues(field, [1]);
270 270 break;
271 271 default:
272 272 enableValues(field, [0]);
273 273 break;
274 274 }
275 275 }
276 276
277 277 function toggleMultiSelect(el) {
278 278 if (el.attr('multiple')) {
279 279 el.removeAttr('multiple');
280 280 el.attr('size', 1);
281 281 } else {
282 282 el.attr('multiple', true);
283 283 if (el.children().length > 10)
284 284 el.attr('size', 10);
285 285 else
286 286 el.attr('size', 4);
287 287 }
288 288 }
289 289
290 290 function submit_query_form(id) {
291 291 selectAllOptions("selected_columns");
292 292 $('#'+id).submit();
293 293 }
294 294
295 295 function showTab(name, url) {
296 296 $('div#content .tab-content').hide();
297 297 $('div.tabs a').removeClass('selected');
298 298 $('#tab-content-' + name).show();
299 299 $('#tab-' + name).addClass('selected');
300 300 //replaces current URL with the "href" attribute of the current link
301 301 //(only triggered if supported by browser)
302 302 if ("replaceState" in window.history) {
303 303 window.history.replaceState(null, document.title, url);
304 304 }
305 305 return false;
306 306 }
307 307
308 308 function moveTabRight(el) {
309 309 var lis = $(el).parents('div.tabs').first().find('ul').children();
310 310 var tabsWidth = 0;
311 311 var i = 0;
312 312 lis.each(function() {
313 313 if ($(this).is(':visible')) {
314 314 tabsWidth += $(this).width() + 6;
315 315 }
316 316 });
317 317 if (tabsWidth < $(el).parents('div.tabs').first().width() - 60) { return; }
318 318 while (i<lis.length && !lis.eq(i).is(':visible')) { i++; }
319 319 lis.eq(i).hide();
320 320 }
321 321
322 322 function moveTabLeft(el) {
323 323 var lis = $(el).parents('div.tabs').first().find('ul').children();
324 324 var i = 0;
325 325 while (i < lis.length && !lis.eq(i).is(':visible')) { i++; }
326 326 if (i > 0) {
327 327 lis.eq(i-1).show();
328 328 }
329 329 }
330 330
331 331 function displayTabsButtons() {
332 332 var lis;
333 333 var tabsWidth = 0;
334 334 var el;
335 335 $('div.tabs').each(function() {
336 336 el = $(this);
337 337 lis = el.find('ul').children();
338 338 lis.each(function(){
339 339 if ($(this).is(':visible')) {
340 340 tabsWidth += $(this).width() + 6;
341 341 }
342 342 });
343 343 if ((tabsWidth < el.width() - 60) && (lis.first().is(':visible'))) {
344 344 el.find('div.tabs-buttons').hide();
345 345 } else {
346 346 el.find('div.tabs-buttons').show();
347 347 }
348 348 });
349 349 }
350 350
351 351 function setPredecessorFieldsVisibility() {
352 352 var relationType = $('#relation_relation_type');
353 353 if (relationType.val() == "precedes" || relationType.val() == "follows") {
354 354 $('#predecessor_fields').show();
355 355 } else {
356 356 $('#predecessor_fields').hide();
357 357 }
358 358 }
359 359
360 360 function showModal(id, width) {
361 361 var el = $('#'+id).first();
362 362 if (el.length === 0 || el.is(':visible')) {return;}
363 363 var title = el.find('h3.title').text();
364 364 el.dialog({
365 365 width: width,
366 366 modal: true,
367 367 resizable: false,
368 368 dialogClass: 'modal',
369 369 title: title
370 370 });
371 371 el.find("input[type=text], input[type=submit]").first().focus();
372 372 }
373 373
374 374 function hideModal(el) {
375 375 var modal;
376 376 if (el) {
377 377 modal = $(el).parents('.ui-dialog-content');
378 378 } else {
379 379 modal = $('#ajax-modal');
380 380 }
381 381 modal.dialog("close");
382 382 }
383 383
384 384 function submitPreview(url, form, target) {
385 385 $.ajax({
386 386 url: url,
387 387 type: 'post',
388 388 data: $('#'+form).serialize(),
389 389 success: function(data){
390 390 $('#'+target).html(data);
391 391 }
392 392 });
393 393 }
394 394
395 395 function collapseScmEntry(id) {
396 396 $('.'+id).each(function() {
397 397 if ($(this).hasClass('open')) {
398 398 collapseScmEntry($(this).attr('id'));
399 399 }
400 400 $(this).hide();
401 401 });
402 402 $('#'+id).removeClass('open');
403 403 }
404 404
405 405 function expandScmEntry(id) {
406 406 $('.'+id).each(function() {
407 407 $(this).show();
408 408 if ($(this).hasClass('loaded') && !$(this).hasClass('collapsed')) {
409 409 expandScmEntry($(this).attr('id'));
410 410 }
411 411 });
412 412 $('#'+id).addClass('open');
413 413 }
414 414
415 415 function scmEntryClick(id, url) {
416 416 var el = $('#'+id);
417 417 if (el.hasClass('open')) {
418 418 collapseScmEntry(id);
419 419 el.addClass('collapsed');
420 420 return false;
421 421 } else if (el.hasClass('loaded')) {
422 422 expandScmEntry(id);
423 423 el.removeClass('collapsed');
424 424 return false;
425 425 }
426 426 if (el.hasClass('loading')) {
427 427 return false;
428 428 }
429 429 el.addClass('loading');
430 430 $.ajax({
431 431 url: url,
432 432 success: function(data) {
433 433 el.after(data);
434 434 el.addClass('open').addClass('loaded').removeClass('loading');
435 435 }
436 436 });
437 437 return true;
438 438 }
439 439
440 440 function randomKey(size) {
441 441 var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
442 442 var key = '';
443 443 for (var i = 0; i < size; i++) {
444 444 key += chars.charAt(Math.floor(Math.random() * chars.length));
445 445 }
446 446 return key;
447 447 }
448 448
449 449 function updateIssueFrom(url) {
450 450 $('#all_attributes input, #all_attributes textarea, #all_attributes select').each(function(){
451 451 $(this).data('valuebeforeupdate', $(this).val());
452 452 });
453 453 $.ajax({
454 454 url: url,
455 455 type: 'post',
456 456 data: $('#issue-form').serialize()
457 457 });
458 458 }
459 459
460 460 function replaceIssueFormWith(html){
461 461 var replacement = $(html);
462 462 $('#all_attributes input, #all_attributes textarea, #all_attributes select').each(function(){
463 463 var object_id = $(this).attr('id');
464 464 if (object_id && $(this).data('valuebeforeupdate')!=$(this).val()) {
465 465 replacement.find('#'+object_id).val($(this).val());
466 466 }
467 467 });
468 468 $('#all_attributes').empty();
469 469 $('#all_attributes').prepend(replacement);
470 470 }
471 471
472 472 function updateBulkEditFrom(url) {
473 473 $.ajax({
474 474 url: url,
475 475 type: 'post',
476 476 data: $('#bulk_edit_form').serialize()
477 477 });
478 478 }
479 479
480 480 function observeAutocompleteField(fieldId, url, options) {
481 481 $(document).ready(function() {
482 482 $('#'+fieldId).autocomplete($.extend({
483 483 source: url,
484 484 minLength: 2,
485 485 search: function(){$('#'+fieldId).addClass('ajax-loading');},
486 486 response: function(){$('#'+fieldId).removeClass('ajax-loading');}
487 487 }, options));
488 488 $('#'+fieldId).addClass('autocomplete');
489 489 });
490 490 }
491 491
492 492 function observeSearchfield(fieldId, targetId, url) {
493 493 $('#'+fieldId).each(function() {
494 494 var $this = $(this);
495 495 $this.addClass('autocomplete');
496 496 $this.attr('data-value-was', $this.val());
497 497 var check = function() {
498 498 var val = $this.val();
499 499 if ($this.attr('data-value-was') != val){
500 500 $this.attr('data-value-was', val);
501 501 $.ajax({
502 502 url: url,
503 503 type: 'get',
504 504 data: {q: $this.val()},
505 505 success: function(data){ if(targetId) $('#'+targetId).html(data); },
506 506 beforeSend: function(){ $this.addClass('ajax-loading'); },
507 507 complete: function(){ $this.removeClass('ajax-loading'); }
508 508 });
509 509 }
510 510 };
511 511 var reset = function() {
512 512 if (timer) {
513 513 clearInterval(timer);
514 514 timer = setInterval(check, 300);
515 515 }
516 516 };
517 517 var timer = setInterval(check, 300);
518 518 $this.bind('keyup click mousemove', reset);
519 519 });
520 520 }
521 521
522 522 function initMyPageSortable(list, url) {
523 523 $('#list-'+list).sortable({
524 524 connectWith: '.block-receiver',
525 525 tolerance: 'pointer',
526 526 update: function(){
527 527 $.ajax({
528 528 url: url,
529 529 type: 'post',
530 530 data: {'blocks': $.map($('#list-'+list).children(), function(el){return $(el).attr('id');})}
531 531 });
532 532 }
533 533 });
534 534 $("#list-top, #list-left, #list-right").disableSelection();
535 535 }
536 536
537 537 var warnLeavingUnsavedMessage;
538 538 function warnLeavingUnsaved(message) {
539 539 warnLeavingUnsavedMessage = message;
540 540 $(document).on('submit', 'form', function(){
541 541 $('textarea').removeData('changed');
542 542 });
543 543 $(document).on('change', 'textarea', function(){
544 544 $(this).data('changed', 'changed');
545 545 });
546 546 window.onbeforeunload = function(){
547 547 var warn = false;
548 548 $('textarea').blur().each(function(){
549 549 if ($(this).data('changed')) {
550 550 warn = true;
551 551 }
552 552 });
553 553 if (warn) {return warnLeavingUnsavedMessage;}
554 554 };
555 555 }
556 556
557 557 function setupAjaxIndicator() {
558 $('#ajax-indicator').bind('ajaxSend', function(event, xhr, settings) {
558 $(document).bind('ajaxSend', function(event, xhr, settings) {
559 559 if ($('.ajax-loading').length === 0 && settings.contentType != 'application/octet-stream') {
560 560 $('#ajax-indicator').show();
561 561 }
562 562 });
563 $('#ajax-indicator').bind('ajaxStop', function() {
563 $(document).bind('ajaxStop', function() {
564 564 $('#ajax-indicator').hide();
565 565 });
566 566 }
567 567
568 568 function hideOnLoad() {
569 569 $('.hol').hide();
570 570 }
571 571
572 572 function addFormObserversForDoubleSubmit() {
573 573 $('form[method=post]').each(function() {
574 574 if (!$(this).hasClass('multiple-submit')) {
575 575 $(this).submit(function(form_submission) {
576 576 if ($(form_submission.target).attr('data-submitted')) {
577 577 form_submission.preventDefault();
578 578 } else {
579 579 $(form_submission.target).attr('data-submitted', true);
580 580 }
581 581 });
582 582 }
583 583 });
584 584 }
585 585
586 586 function defaultFocus(){
587 587 if ($('#content :focus').length == 0) {
588 588 $('#content input[type=text], #content textarea').first().focus();
589 589 }
590 590 }
591 591
592 592 function blockEventPropagation(event) {
593 593 event.stopPropagation();
594 594 event.preventDefault();
595 595 }
596 596
597 597 function toggleDisabledOnChange() {
598 598 var checked = $(this).is(':checked');
599 599 $($(this).data('disables')).attr('disabled', checked);
600 600 $($(this).data('enables')).attr('disabled', !checked);
601 601 }
602 602 function toggleDisabledInit() {
603 603 $('input[data-disables], input[data-enables]').each(toggleDisabledOnChange);
604 604 }
605 605 $(document).ready(function(){
606 606 $('#content').on('change', 'input[data-disables], input[data-enables]', toggleDisabledOnChange);
607 607 toggleDisabledInit();
608 608 });
609 609
610 610 $(document).ready(setupAjaxIndicator);
611 611 $(document).ready(hideOnLoad);
612 612 $(document).ready(addFormObserversForDoubleSubmit);
613 613 $(document).ready(defaultFocus);
614 614
@@ -1,236 +1,236
1 1 var contextMenuObserving;
2 2 var contextMenuUrl;
3 3
4 4 function contextMenuRightClick(event) {
5 5 var target = $(event.target);
6 6 if (target.is('a')) {return;}
7 7 var tr = target.parents('tr').first();
8 8 if (!tr.hasClass('hascontextmenu')) {return;}
9 9 event.preventDefault();
10 10 if (!contextMenuIsSelected(tr)) {
11 11 contextMenuUnselectAll();
12 12 contextMenuAddSelection(tr);
13 13 contextMenuSetLastSelected(tr);
14 14 }
15 15 contextMenuShow(event);
16 16 }
17 17
18 18 function contextMenuClick(event) {
19 19 var target = $(event.target);
20 20 var lastSelected;
21 21
22 22 if (target.is('a') && target.hasClass('submenu')) {
23 23 event.preventDefault();
24 24 return;
25 25 }
26 26 contextMenuHide();
27 27 if (target.is('a') || target.is('img')) { return; }
28 28 if (event.which == 1 || (navigator.appVersion.match(/\bMSIE\b/))) {
29 29 var tr = target.parents('tr').first();
30 30 if (tr.length && tr.hasClass('hascontextmenu')) {
31 31 // a row was clicked, check if the click was on checkbox
32 32 if (target.is('input')) {
33 33 // a checkbox may be clicked
34 if (target.attr('checked')) {
34 if (target.prop('checked')) {
35 35 tr.addClass('context-menu-selection');
36 36 } else {
37 37 tr.removeClass('context-menu-selection');
38 38 }
39 39 } else {
40 40 if (event.ctrlKey || event.metaKey) {
41 41 contextMenuToggleSelection(tr);
42 42 } else if (event.shiftKey) {
43 43 lastSelected = contextMenuLastSelected();
44 44 if (lastSelected.length) {
45 45 var toggling = false;
46 46 $('.hascontextmenu').each(function(){
47 47 if (toggling || $(this).is(tr)) {
48 48 contextMenuAddSelection($(this));
49 49 }
50 50 if ($(this).is(tr) || $(this).is(lastSelected)) {
51 51 toggling = !toggling;
52 52 }
53 53 });
54 54 } else {
55 55 contextMenuAddSelection(tr);
56 56 }
57 57 } else {
58 58 contextMenuUnselectAll();
59 59 contextMenuAddSelection(tr);
60 60 }
61 61 contextMenuSetLastSelected(tr);
62 62 }
63 63 } else {
64 64 // click is outside the rows
65 65 if (target.is('a') && (target.hasClass('disabled') || target.hasClass('submenu'))) {
66 66 event.preventDefault();
67 67 } else {
68 68 contextMenuUnselectAll();
69 69 }
70 70 }
71 71 }
72 72 }
73 73
74 74 function contextMenuCreate() {
75 75 if ($('#context-menu').length < 1) {
76 76 var menu = document.createElement("div");
77 77 menu.setAttribute("id", "context-menu");
78 78 menu.setAttribute("style", "display:none;");
79 79 document.getElementById("content").appendChild(menu);
80 80 }
81 81 }
82 82
83 83 function contextMenuShow(event) {
84 84 var mouse_x = event.pageX;
85 85 var mouse_y = event.pageY;
86 86 var render_x = mouse_x;
87 87 var render_y = mouse_y;
88 88 var dims;
89 89 var menu_width;
90 90 var menu_height;
91 91 var window_width;
92 92 var window_height;
93 93 var max_width;
94 94 var max_height;
95 95
96 96 $('#context-menu').css('left', (render_x + 'px'));
97 97 $('#context-menu').css('top', (render_y + 'px'));
98 98 $('#context-menu').html('');
99 99
100 100 $.ajax({
101 101 url: contextMenuUrl,
102 102 data: $(event.target).parents('form').first().serialize(),
103 103 success: function(data, textStatus, jqXHR) {
104 104 $('#context-menu').html(data);
105 105 menu_width = $('#context-menu').width();
106 106 menu_height = $('#context-menu').height();
107 107 max_width = mouse_x + 2*menu_width;
108 108 max_height = mouse_y + menu_height;
109 109
110 110 var ws = window_size();
111 111 window_width = ws.width;
112 112 window_height = ws.height;
113 113
114 114 /* display the menu above and/or to the left of the click if needed */
115 115 if (max_width > window_width) {
116 116 render_x -= menu_width;
117 117 $('#context-menu').addClass('reverse-x');
118 118 } else {
119 119 $('#context-menu').removeClass('reverse-x');
120 120 }
121 121 if (max_height > window_height) {
122 122 render_y -= menu_height;
123 123 $('#context-menu').addClass('reverse-y');
124 124 } else {
125 125 $('#context-menu').removeClass('reverse-y');
126 126 }
127 127 if (render_x <= 0) render_x = 1;
128 128 if (render_y <= 0) render_y = 1;
129 129 $('#context-menu').css('left', (render_x + 'px'));
130 130 $('#context-menu').css('top', (render_y + 'px'));
131 131 $('#context-menu').show();
132 132
133 133 //if (window.parseStylesheets) { window.parseStylesheets(); } // IE
134 134
135 135 }
136 136 });
137 137 }
138 138
139 139 function contextMenuSetLastSelected(tr) {
140 140 $('.cm-last').removeClass('cm-last');
141 141 tr.addClass('cm-last');
142 142 }
143 143
144 144 function contextMenuLastSelected() {
145 145 return $('.cm-last').first();
146 146 }
147 147
148 148 function contextMenuUnselectAll() {
149 149 $('.hascontextmenu').each(function(){
150 150 contextMenuRemoveSelection($(this));
151 151 });
152 152 $('.cm-last').removeClass('cm-last');
153 153 }
154 154
155 155 function contextMenuHide() {
156 156 $('#context-menu').hide();
157 157 }
158 158
159 159 function contextMenuToggleSelection(tr) {
160 160 if (contextMenuIsSelected(tr)) {
161 161 contextMenuRemoveSelection(tr);
162 162 } else {
163 163 contextMenuAddSelection(tr);
164 164 }
165 165 }
166 166
167 167 function contextMenuAddSelection(tr) {
168 168 tr.addClass('context-menu-selection');
169 169 contextMenuCheckSelectionBox(tr, true);
170 170 contextMenuClearDocumentSelection();
171 171 }
172 172
173 173 function contextMenuRemoveSelection(tr) {
174 174 tr.removeClass('context-menu-selection');
175 175 contextMenuCheckSelectionBox(tr, false);
176 176 }
177 177
178 178 function contextMenuIsSelected(tr) {
179 179 return tr.hasClass('context-menu-selection');
180 180 }
181 181
182 182 function contextMenuCheckSelectionBox(tr, checked) {
183 tr.find('input[type=checkbox]').attr('checked', checked);
183 tr.find('input[type=checkbox]').prop('checked', checked);
184 184 }
185 185
186 186 function contextMenuClearDocumentSelection() {
187 187 // TODO
188 188 if (document.selection) {
189 189 document.selection.empty(); // IE
190 190 } else {
191 191 window.getSelection().removeAllRanges();
192 192 }
193 193 }
194 194
195 195 function contextMenuInit(url) {
196 196 contextMenuUrl = url;
197 197 contextMenuCreate();
198 198 contextMenuUnselectAll();
199 199
200 200 if (!contextMenuObserving) {
201 201 $(document).click(contextMenuClick);
202 202 $(document).contextmenu(contextMenuRightClick);
203 203 contextMenuObserving = true;
204 204 }
205 205 }
206 206
207 207 function toggleIssuesSelection(el) {
208 208 var boxes = $(el).parents('form').find('input[type=checkbox]');
209 209 var all_checked = true;
210 boxes.each(function(){ if (!$(this).attr('checked')) { all_checked = false; } });
210 boxes.each(function(){ if (!$(this).prop('checked')) { all_checked = false; } });
211 211 boxes.each(function(){
212 212 if (all_checked) {
213 213 $(this).removeAttr('checked');
214 214 $(this).parents('tr').removeClass('context-menu-selection');
215 } else if (!$(this).attr('checked')) {
216 $(this).attr('checked', true);
215 } else if (!$(this).prop('checked')) {
216 $(this).prop('checked', true);
217 217 $(this).parents('tr').addClass('context-menu-selection');
218 218 }
219 219 });
220 220 }
221 221
222 222 function window_size() {
223 223 var w;
224 224 var h;
225 225 if (window.innerWidth) {
226 226 w = window.innerWidth;
227 227 h = window.innerHeight;
228 228 } else if (document.documentElement) {
229 229 w = document.documentElement.clientWidth;
230 230 h = document.documentElement.clientHeight;
231 231 } else {
232 232 w = document.body.clientWidth;
233 233 h = document.body.clientHeight;
234 234 }
235 235 return {width: w, height: h};
236 236 }
@@ -1,172 +1,172
1 1 var draw_gantt = null;
2 2 var draw_top;
3 3 var draw_right;
4 4 var draw_left;
5 5
6 6 var rels_stroke_width = 2;
7 7
8 8 function setDrawArea() {
9 9 draw_top = $("#gantt_draw_area").position().top;
10 10 draw_right = $("#gantt_draw_area").width();
11 11 draw_left = $("#gantt_area").scrollLeft();
12 12 }
13 13
14 14 function getRelationsArray() {
15 15 var arr = new Array();
16 16 $.each($('div.task_todo[data-rels]'), function(index_div, element) {
17 17 var element_id = $(element).attr("id");
18 18 if (element_id != null) {
19 19 var issue_id = element_id.replace("task-todo-issue-", "");
20 20 var data_rels = $(element).data("rels");
21 21 for (rel_type_key in data_rels) {
22 22 $.each(data_rels[rel_type_key], function(index_issue, element_issue) {
23 23 arr.push({issue_from: issue_id, issue_to: element_issue,
24 24 rel_type: rel_type_key});
25 25 });
26 26 }
27 27 }
28 28 });
29 29 return arr;
30 30 }
31 31
32 32 function drawRelations() {
33 33 var arr = getRelationsArray();
34 34 $.each(arr, function(index_issue, element_issue) {
35 35 var issue_from = $("#task-todo-issue-" + element_issue["issue_from"]);
36 36 var issue_to = $("#task-todo-issue-" + element_issue["issue_to"]);
37 37 if (issue_from.size() == 0 || issue_to.size() == 0) {
38 38 return;
39 39 }
40 40 var issue_height = issue_from.height();
41 41 var issue_from_top = issue_from.position().top + (issue_height / 2) - draw_top;
42 42 var issue_from_right = issue_from.position().left + issue_from.width();
43 43 var issue_to_top = issue_to.position().top + (issue_height / 2) - draw_top;
44 44 var issue_to_left = issue_to.position().left;
45 45 var color = issue_relation_type[element_issue["rel_type"]]["color"];
46 46 var landscape_margin = issue_relation_type[element_issue["rel_type"]]["landscape_margin"];
47 47 var issue_from_right_rel = issue_from_right + landscape_margin;
48 48 var issue_to_left_rel = issue_to_left - landscape_margin;
49 49 draw_gantt.path(["M", issue_from_right + draw_left, issue_from_top,
50 50 "L", issue_from_right_rel + draw_left, issue_from_top])
51 51 .attr({stroke: color,
52 52 "stroke-width": rels_stroke_width
53 53 });
54 54 if (issue_from_right_rel < issue_to_left_rel) {
55 55 draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_from_top,
56 56 "L", issue_from_right_rel + draw_left, issue_to_top])
57 57 .attr({stroke: color,
58 58 "stroke-width": rels_stroke_width
59 59 });
60 60 draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_to_top,
61 61 "L", issue_to_left + draw_left, issue_to_top])
62 62 .attr({stroke: color,
63 63 "stroke-width": rels_stroke_width
64 64 });
65 65 } else {
66 66 var issue_middle_top = issue_to_top +
67 67 (issue_height *
68 68 ((issue_from_top > issue_to_top) ? 1 : -1));
69 69 draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_from_top,
70 70 "L", issue_from_right_rel + draw_left, issue_middle_top])
71 71 .attr({stroke: color,
72 72 "stroke-width": rels_stroke_width
73 73 });
74 74 draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_middle_top,
75 75 "L", issue_to_left_rel + draw_left, issue_middle_top])
76 76 .attr({stroke: color,
77 77 "stroke-width": rels_stroke_width
78 78 });
79 79 draw_gantt.path(["M", issue_to_left_rel + draw_left, issue_middle_top,
80 80 "L", issue_to_left_rel + draw_left, issue_to_top])
81 81 .attr({stroke: color,
82 82 "stroke-width": rels_stroke_width
83 83 });
84 84 draw_gantt.path(["M", issue_to_left_rel + draw_left, issue_to_top,
85 85 "L", issue_to_left + draw_left, issue_to_top])
86 86 .attr({stroke: color,
87 87 "stroke-width": rels_stroke_width
88 88 });
89 89 }
90 90 draw_gantt.path(["M", issue_to_left + draw_left, issue_to_top,
91 91 "l", -4 * rels_stroke_width, -2 * rels_stroke_width,
92 92 "l", 0, 4 * rels_stroke_width, "z"])
93 93 .attr({stroke: "none",
94 94 fill: color,
95 95 "stroke-linecap": "butt",
96 96 "stroke-linejoin": "miter"
97 97 });
98 98 });
99 99 }
100 100
101 101 function getProgressLinesArray() {
102 102 var arr = new Array();
103 103 var today_left = $('#today_line').position().left;
104 104 arr.push({left: today_left, top: 0});
105 105 $.each($('div.issue-subject, div.version-name'), function(index, element) {
106 106 var t = $(element).position().top - draw_top ;
107 107 var h = ($(element).height() / 9);
108 108 var element_top_upper = t - h;
109 109 var element_top_center = t + (h * 3);
110 110 var element_top_lower = t + (h * 8);
111 111 var issue_closed = $(element).children('span').hasClass('issue-closed');
112 112 var version_closed = $(element).children('span').hasClass('version-closed');
113 113 if (issue_closed || version_closed) {
114 114 arr.push({left: today_left, top: element_top_center});
115 115 } else {
116 116 var issue_done = $("#task-done-" + $(element).attr("id"));
117 117 var is_behind_start = $(element).children('span').hasClass('behind-start-date');
118 118 var is_over_end = $(element).children('span').hasClass('over-end-date');
119 119 if (is_over_end) {
120 120 arr.push({left: draw_right, top: element_top_upper, is_right_edge: true});
121 121 arr.push({left: draw_right, top: element_top_lower, is_right_edge: true, none_stroke: true});
122 122 } else if (issue_done.size() > 0) {
123 123 var done_left = issue_done.first().position().left +
124 124 issue_done.first().width();
125 125 arr.push({left: done_left, top: element_top_center});
126 126 } else if (is_behind_start) {
127 127 arr.push({left: 0 , top: element_top_upper, is_left_edge: true});
128 128 arr.push({left: 0 , top: element_top_lower, is_left_edge: true, none_stroke: true});
129 129 } else {
130 130 var todo_left = today_left;
131 131 var issue_todo = $("#task-todo-" + $(element).attr("id"));
132 132 if (issue_todo.size() > 0){
133 133 todo_left = issue_todo.first().position().left;
134 134 }
135 135 arr.push({left: Math.min(today_left, todo_left), top: element_top_center});
136 136 }
137 137 }
138 138 });
139 139 return arr;
140 140 }
141 141
142 142 function drawGanttProgressLines() {
143 143 var arr = getProgressLinesArray();
144 144 var color = $("#today_line")
145 145 .css("border-left-color");
146 146 var i;
147 147 for(i = 1 ; i < arr.length ; i++) {
148 148 if (!("none_stroke" in arr[i]) &&
149 149 (!("is_right_edge" in arr[i - 1] && "is_right_edge" in arr[i]) &&
150 150 !("is_left_edge" in arr[i - 1] && "is_left_edge" in arr[i]))
151 151 ) {
152 152 var x1 = (arr[i - 1].left == 0) ? 0 : arr[i - 1].left + draw_left;
153 153 var x2 = (arr[i].left == 0) ? 0 : arr[i].left + draw_left;
154 154 draw_gantt.path(["M", x1, arr[i - 1].top,
155 155 "L", x2, arr[i].top])
156 156 .attr({stroke: color, "stroke-width": 2});
157 157 }
158 158 }
159 159 }
160 160
161 161 function drawGanttHandler() {
162 162 var folder = document.getElementById('gantt_draw_area');
163 163 if(draw_gantt != null)
164 164 draw_gantt.clear();
165 165 else
166 166 draw_gantt = Raphael(folder);
167 167 setDrawArea();
168 if ($("#draw_progress_line").attr('checked'))
168 if ($("#draw_progress_line").prop('checked'))
169 169 drawGanttProgressLines();
170 if ($("#draw_relations").attr('checked'))
170 if ($("#draw_relations").prop('checked'))
171 171 drawRelations();
172 172 }
1 NO CONTENT: modified file, binary diff hidden
1 NO CONTENT: modified file, binary diff hidden
1 NO CONTENT: modified file, binary diff hidden
1 NO CONTENT: modified file, binary diff hidden
1 NO CONTENT: modified file, binary diff hidden
1 NO CONTENT: modified file, binary diff hidden
1 NO CONTENT: modified file, binary diff hidden
1 NO CONTENT: modified file, binary diff hidden
1 NO CONTENT: modified file, binary diff hidden
1 NO CONTENT: modified file, binary diff hidden
1 NO CONTENT: modified file, binary diff hidden
1 NO CONTENT: modified file, binary diff hidden
1 NO CONTENT: modified file, binary diff hidden
1 NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now