##// END OF EJS Templates
Adds a class for handling CSV generation (#7037)....
Jean-Philippe Lang -
r13920:49914f010dfb
parent child
Show More
@@ -0,0 +1,59
1 # encoding: utf-8
2 #
3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 #
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
20 require 'csv'
21
22 module Redmine
23 module Export
24 module CSV
25 def self.generate(*args, &block)
26 Base.generate(*args, &block)
27 end
28
29 class Base < ::CSV
30 include Redmine::I18n
31
32 class << self
33
34 def generate(&block)
35 col_sep = l(:general_csv_separator)
36 encoding = l(:general_csv_encoding)
37
38 super(:col_sep => col_sep, :encoding => encoding, &block)
39 end
40 end
41
42 def <<(row)
43 row = row.map do |field|
44 case field
45 when String
46 Redmine::CodesetUtil.from_utf8(field, self.encoding.name)
47 when Float
48 @decimal_separator ||= l(:general_csv_decimal_separator)
49 ("%.2f" % field).gsub('.', @decimal_separator)
50 else
51 field
52 end
53 end
54 super row
55 end
56 end
57 end
58 end
59 end
@@ -1,1321 +1,1326
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2015 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_url(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_url : :named_attachment_url
97 97 html_options = options.slice!(:only_path)
98 98 options[:only_path] = true unless options.key?(:only_path)
99 99 url = send(route_method, attachment, attachment.filename, options)
100 100 link_to text, url, html_options
101 101 end
102 102
103 103 # Generates a link to a SCM revision
104 104 # Options:
105 105 # * :text - Link text (default to the formatted revision)
106 106 def link_to_revision(revision, repository, options={})
107 107 if repository.is_a?(Project)
108 108 repository = repository.repository
109 109 end
110 110 text = options.delete(:text) || format_revision(revision)
111 111 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
112 112 link_to(
113 113 h(text),
114 114 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
115 115 :title => l(:label_revision_id, format_revision(revision)),
116 116 :accesskey => options[:accesskey]
117 117 )
118 118 end
119 119
120 120 # Generates a link to a message
121 121 def link_to_message(message, options={}, html_options = nil)
122 122 link_to(
123 123 message.subject.truncate(60),
124 124 board_message_url(message.board_id, message.parent_id || message.id, {
125 125 :r => (message.parent_id && message.id),
126 126 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
127 127 :only_path => true
128 128 }.merge(options)),
129 129 html_options
130 130 )
131 131 end
132 132
133 133 # Generates a link to a project if active
134 134 # Examples:
135 135 #
136 136 # link_to_project(project) # => link to the specified project overview
137 137 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
138 138 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
139 139 #
140 140 def link_to_project(project, options={}, html_options = nil)
141 141 if project.archived?
142 142 h(project.name)
143 143 else
144 144 link_to project.name,
145 145 project_url(project, {:only_path => true}.merge(options)),
146 146 html_options
147 147 end
148 148 end
149 149
150 150 # Generates a link to a project settings if active
151 151 def link_to_project_settings(project, options={}, html_options=nil)
152 152 if project.active?
153 153 link_to project.name, settings_project_path(project, options), html_options
154 154 elsif project.archived?
155 155 h(project.name)
156 156 else
157 157 link_to project.name, project_path(project, options), html_options
158 158 end
159 159 end
160 160
161 161 # Generates a link to a version
162 162 def link_to_version(version, options = {})
163 163 return '' unless version && version.is_a?(Version)
164 164 options = {:title => format_date(version.effective_date)}.merge(options)
165 165 link_to_if version.visible?, format_version_name(version), version_path(version), options
166 166 end
167 167
168 168 # Helper that formats object for html or text rendering
169 169 def format_object(object, html=true, &block)
170 170 if block_given?
171 171 object = yield object
172 172 end
173 173 case object.class.name
174 174 when 'Array'
175 175 object.map {|o| format_object(o, html)}.join(', ').html_safe
176 176 when 'Time'
177 177 format_time(object)
178 178 when 'Date'
179 179 format_date(object)
180 180 when 'Fixnum'
181 181 object.to_s
182 182 when 'Float'
183 183 sprintf "%.2f", object
184 184 when 'User'
185 185 html ? link_to_user(object) : object.to_s
186 186 when 'Project'
187 187 html ? link_to_project(object) : object.to_s
188 188 when 'Version'
189 189 html ? link_to_version(object) : object.to_s
190 190 when 'TrueClass'
191 191 l(:general_text_Yes)
192 192 when 'FalseClass'
193 193 l(:general_text_No)
194 194 when 'Issue'
195 195 object.visible? && html ? link_to_issue(object) : "##{object.id}"
196 196 when 'CustomValue', 'CustomFieldValue'
197 197 if object.custom_field
198 198 f = object.custom_field.format.formatted_custom_value(self, object, html)
199 199 if f.nil? || f.is_a?(String)
200 200 f
201 201 else
202 202 format_object(f, html, &block)
203 203 end
204 204 else
205 205 object.value.to_s
206 206 end
207 207 else
208 208 html ? h(object) : object.to_s
209 209 end
210 210 end
211 211
212 212 def wiki_page_path(page, options={})
213 213 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
214 214 end
215 215
216 216 def thumbnail_tag(attachment)
217 217 link_to image_tag(thumbnail_path(attachment)),
218 218 named_attachment_path(attachment, attachment.filename),
219 219 :title => attachment.filename
220 220 end
221 221
222 222 def toggle_link(name, id, options={})
223 223 onclick = "$('##{id}').toggle(); "
224 224 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
225 225 onclick << "return false;"
226 226 link_to(name, "#", :onclick => onclick)
227 227 end
228 228
229 229 def format_activity_title(text)
230 230 h(truncate_single_line_raw(text, 100))
231 231 end
232 232
233 233 def format_activity_day(date)
234 234 date == User.current.today ? l(:label_today).titleize : format_date(date)
235 235 end
236 236
237 237 def format_activity_description(text)
238 238 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
239 239 ).gsub(/[\r\n]+/, "<br />").html_safe
240 240 end
241 241
242 242 def format_version_name(version)
243 243 if version.project == @project
244 244 h(version)
245 245 else
246 246 h("#{version.project} - #{version}")
247 247 end
248 248 end
249 249
250 250 def due_date_distance_in_words(date)
251 251 if date
252 252 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
253 253 end
254 254 end
255 255
256 256 # Renders a tree of projects as a nested set of unordered lists
257 257 # The given collection may be a subset of the whole project tree
258 258 # (eg. some intermediate nodes are private and can not be seen)
259 259 def render_project_nested_lists(projects, &block)
260 260 s = ''
261 261 if projects.any?
262 262 ancestors = []
263 263 original_project = @project
264 264 projects.sort_by(&:lft).each do |project|
265 265 # set the project environment to please macros.
266 266 @project = project
267 267 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
268 268 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
269 269 else
270 270 ancestors.pop
271 271 s << "</li>"
272 272 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
273 273 ancestors.pop
274 274 s << "</ul></li>\n"
275 275 end
276 276 end
277 277 classes = (ancestors.empty? ? 'root' : 'child')
278 278 s << "<li class='#{classes}'><div class='#{classes}'>"
279 279 s << h(block_given? ? capture(project, &block) : project.name)
280 280 s << "</div>\n"
281 281 ancestors << project
282 282 end
283 283 s << ("</li></ul>\n" * ancestors.size)
284 284 @project = original_project
285 285 end
286 286 s.html_safe
287 287 end
288 288
289 289 def render_page_hierarchy(pages, node=nil, options={})
290 290 content = ''
291 291 if pages[node]
292 292 content << "<ul class=\"pages-hierarchy\">\n"
293 293 pages[node].each do |page|
294 294 content << "<li>"
295 295 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
296 296 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
297 297 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
298 298 content << "</li>\n"
299 299 end
300 300 content << "</ul>\n"
301 301 end
302 302 content.html_safe
303 303 end
304 304
305 305 # Renders flash messages
306 306 def render_flash_messages
307 307 s = ''
308 308 flash.each do |k,v|
309 309 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
310 310 end
311 311 s.html_safe
312 312 end
313 313
314 314 # Renders tabs and their content
315 315 def render_tabs(tabs, selected=params[:tab])
316 316 if tabs.any?
317 317 unless tabs.detect {|tab| tab[:name] == selected}
318 318 selected = nil
319 319 end
320 320 selected ||= tabs.first[:name]
321 321 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
322 322 else
323 323 content_tag 'p', l(:label_no_data), :class => "nodata"
324 324 end
325 325 end
326 326
327 327 # Renders the project quick-jump box
328 328 def render_project_jump_box
329 329 return unless User.current.logged?
330 330 projects = User.current.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
331 331 if projects.any?
332 332 options =
333 333 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
334 334 '<option value="" disabled="disabled">---</option>').html_safe
335 335
336 336 options << project_tree_options_for_select(projects, :selected => @project) do |p|
337 337 { :value => project_path(:id => p, :jump => current_menu_item) }
338 338 end
339 339
340 340 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
341 341 end
342 342 end
343 343
344 344 def project_tree_options_for_select(projects, options = {})
345 345 s = ''.html_safe
346 346 if blank_text = options[:include_blank]
347 347 if blank_text == true
348 348 blank_text = '&nbsp;'.html_safe
349 349 end
350 350 s << content_tag('option', blank_text, :value => '')
351 351 end
352 352 project_tree(projects) do |project, level|
353 353 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
354 354 tag_options = {:value => project.id}
355 355 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
356 356 tag_options[:selected] = 'selected'
357 357 else
358 358 tag_options[:selected] = nil
359 359 end
360 360 tag_options.merge!(yield(project)) if block_given?
361 361 s << content_tag('option', name_prefix + h(project), tag_options)
362 362 end
363 363 s.html_safe
364 364 end
365 365
366 366 # Yields the given block for each project with its level in the tree
367 367 #
368 368 # Wrapper for Project#project_tree
369 369 def project_tree(projects, &block)
370 370 Project.project_tree(projects, &block)
371 371 end
372 372
373 373 def principals_check_box_tags(name, principals)
374 374 s = ''
375 375 principals.each do |principal|
376 376 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
377 377 end
378 378 s.html_safe
379 379 end
380 380
381 381 # Returns a string for users/groups option tags
382 382 def principals_options_for_select(collection, selected=nil)
383 383 s = ''
384 384 if collection.include?(User.current)
385 385 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
386 386 end
387 387 groups = ''
388 388 collection.sort.each do |element|
389 389 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
390 390 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
391 391 end
392 392 unless groups.empty?
393 393 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
394 394 end
395 395 s.html_safe
396 396 end
397 397
398 398 def option_tag(name, text, value, selected=nil, options={})
399 399 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
400 400 end
401 401
402 402 def truncate_single_line_raw(string, length)
403 403 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
404 404 end
405 405
406 406 # Truncates at line break after 250 characters or options[:length]
407 407 def truncate_lines(string, options={})
408 408 length = options[:length] || 250
409 409 if string.to_s =~ /\A(.{#{length}}.*?)$/m
410 410 "#{$1}..."
411 411 else
412 412 string
413 413 end
414 414 end
415 415
416 416 def anchor(text)
417 417 text.to_s.gsub(' ', '_')
418 418 end
419 419
420 420 def html_hours(text)
421 421 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
422 422 end
423 423
424 424 def authoring(created, author, options={})
425 425 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
426 426 end
427 427
428 428 def time_tag(time)
429 429 text = distance_of_time_in_words(Time.now, time)
430 430 if @project
431 431 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
432 432 else
433 433 content_tag('abbr', text, :title => format_time(time))
434 434 end
435 435 end
436 436
437 437 def syntax_highlight_lines(name, content)
438 438 lines = []
439 439 syntax_highlight(name, content).each_line { |line| lines << line }
440 440 lines
441 441 end
442 442
443 443 def syntax_highlight(name, content)
444 444 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
445 445 end
446 446
447 447 def to_path_param(path)
448 448 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
449 449 str.blank? ? nil : str
450 450 end
451 451
452 452 def reorder_links(name, url, method = :post)
453 453 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
454 454 url.merge({"#{name}[move_to]" => 'highest'}),
455 455 :method => method, :title => l(:label_sort_highest)) +
456 456 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
457 457 url.merge({"#{name}[move_to]" => 'higher'}),
458 458 :method => method, :title => l(:label_sort_higher)) +
459 459 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
460 460 url.merge({"#{name}[move_to]" => 'lower'}),
461 461 :method => method, :title => l(:label_sort_lower)) +
462 462 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
463 463 url.merge({"#{name}[move_to]" => 'lowest'}),
464 464 :method => method, :title => l(:label_sort_lowest))
465 465 end
466 466
467 467 def breadcrumb(*args)
468 468 elements = args.flatten
469 469 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
470 470 end
471 471
472 472 def other_formats_links(&block)
473 473 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
474 474 yield Redmine::Views::OtherFormatsBuilder.new(self)
475 475 concat('</p>'.html_safe)
476 476 end
477 477
478 478 def page_header_title
479 479 if @project.nil? || @project.new_record?
480 480 h(Setting.app_title)
481 481 else
482 482 b = []
483 483 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
484 484 if ancestors.any?
485 485 root = ancestors.shift
486 486 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
487 487 if ancestors.size > 2
488 488 b << "\xe2\x80\xa6"
489 489 ancestors = ancestors[-2, 2]
490 490 end
491 491 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
492 492 end
493 493 b << h(@project)
494 494 b.join(" \xc2\xbb ").html_safe
495 495 end
496 496 end
497 497
498 498 # Returns a h2 tag and sets the html title with the given arguments
499 499 def title(*args)
500 500 strings = args.map do |arg|
501 501 if arg.is_a?(Array) && arg.size >= 2
502 502 link_to(*arg)
503 503 else
504 504 h(arg.to_s)
505 505 end
506 506 end
507 507 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
508 508 content_tag('h2', strings.join(' &#187; ').html_safe)
509 509 end
510 510
511 511 # Sets the html title
512 512 # Returns the html title when called without arguments
513 513 # Current project name and app_title and automatically appended
514 514 # Exemples:
515 515 # html_title 'Foo', 'Bar'
516 516 # html_title # => 'Foo - Bar - My Project - Redmine'
517 517 def html_title(*args)
518 518 if args.empty?
519 519 title = @html_title || []
520 520 title << @project.name if @project
521 521 title << Setting.app_title unless Setting.app_title == title.last
522 522 title.reject(&:blank?).join(' - ')
523 523 else
524 524 @html_title ||= []
525 525 @html_title += args
526 526 end
527 527 end
528 528
529 529 # Returns the theme, controller name, and action as css classes for the
530 530 # HTML body.
531 531 def body_css_classes
532 532 css = []
533 533 if theme = Redmine::Themes.theme(Setting.ui_theme)
534 534 css << 'theme-' + theme.name
535 535 end
536 536
537 537 css << 'project-' + @project.identifier if @project && @project.identifier.present?
538 538 css << 'controller-' + controller_name
539 539 css << 'action-' + action_name
540 540 css.join(' ')
541 541 end
542 542
543 543 def accesskey(s)
544 544 @used_accesskeys ||= []
545 545 key = Redmine::AccessKeys.key_for(s)
546 546 return nil if @used_accesskeys.include?(key)
547 547 @used_accesskeys << key
548 548 key
549 549 end
550 550
551 551 # Formats text according to system settings.
552 552 # 2 ways to call this method:
553 553 # * with a String: textilizable(text, options)
554 554 # * with an object and one of its attribute: textilizable(issue, :description, options)
555 555 def textilizable(*args)
556 556 options = args.last.is_a?(Hash) ? args.pop : {}
557 557 case args.size
558 558 when 1
559 559 obj = options[:object]
560 560 text = args.shift
561 561 when 2
562 562 obj = args.shift
563 563 attr = args.shift
564 564 text = obj.send(attr).to_s
565 565 else
566 566 raise ArgumentError, 'invalid arguments to textilizable'
567 567 end
568 568 return '' if text.blank?
569 569 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
570 570 @only_path = only_path = options.delete(:only_path) == false ? false : true
571 571
572 572 text = text.dup
573 573 macros = catch_macros(text)
574 574 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
575 575
576 576 @parsed_headings = []
577 577 @heading_anchors = {}
578 578 @current_section = 0 if options[:edit_section_links]
579 579
580 580 parse_sections(text, project, obj, attr, only_path, options)
581 581 text = parse_non_pre_blocks(text, obj, macros) do |text|
582 582 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
583 583 send method_name, text, project, obj, attr, only_path, options
584 584 end
585 585 end
586 586 parse_headings(text, project, obj, attr, only_path, options)
587 587
588 588 if @parsed_headings.any?
589 589 replace_toc(text, @parsed_headings)
590 590 end
591 591
592 592 text.html_safe
593 593 end
594 594
595 595 def parse_non_pre_blocks(text, obj, macros)
596 596 s = StringScanner.new(text)
597 597 tags = []
598 598 parsed = ''
599 599 while !s.eos?
600 600 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
601 601 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
602 602 if tags.empty?
603 603 yield text
604 604 inject_macros(text, obj, macros) if macros.any?
605 605 else
606 606 inject_macros(text, obj, macros, false) if macros.any?
607 607 end
608 608 parsed << text
609 609 if tag
610 610 if closing
611 611 if tags.last == tag.downcase
612 612 tags.pop
613 613 end
614 614 else
615 615 tags << tag.downcase
616 616 end
617 617 parsed << full_tag
618 618 end
619 619 end
620 620 # Close any non closing tags
621 621 while tag = tags.pop
622 622 parsed << "</#{tag}>"
623 623 end
624 624 parsed
625 625 end
626 626
627 627 def parse_inline_attachments(text, project, obj, attr, only_path, options)
628 628 return if options[:inline_attachments] == false
629 629
630 630 # when using an image link, try to use an attachment, if possible
631 631 attachments = options[:attachments] || []
632 632 attachments += obj.attachments if obj.respond_to?(:attachments)
633 633 if attachments.present?
634 634 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
635 635 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
636 636 # search for the picture in attachments
637 637 if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
638 638 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
639 639 desc = found.description.to_s.gsub('"', '')
640 640 if !desc.blank? && alttext.blank?
641 641 alt = " title=\"#{desc}\" alt=\"#{desc}\""
642 642 end
643 643 "src=\"#{image_url}\"#{alt}"
644 644 else
645 645 m
646 646 end
647 647 end
648 648 end
649 649 end
650 650
651 651 # Wiki links
652 652 #
653 653 # Examples:
654 654 # [[mypage]]
655 655 # [[mypage|mytext]]
656 656 # wiki links can refer other project wikis, using project name or identifier:
657 657 # [[project:]] -> wiki starting page
658 658 # [[project:|mytext]]
659 659 # [[project:mypage]]
660 660 # [[project:mypage|mytext]]
661 661 def parse_wiki_links(text, project, obj, attr, only_path, options)
662 662 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
663 663 link_project = project
664 664 esc, all, page, title = $1, $2, $3, $5
665 665 if esc.nil?
666 666 if page =~ /^([^\:]+)\:(.*)$/
667 667 identifier, page = $1, $2
668 668 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
669 669 title ||= identifier if page.blank?
670 670 end
671 671
672 672 if link_project && link_project.wiki
673 673 # extract anchor
674 674 anchor = nil
675 675 if page =~ /^(.+?)\#(.+)$/
676 676 page, anchor = $1, $2
677 677 end
678 678 anchor = sanitize_anchor_name(anchor) if anchor.present?
679 679 # check if page exists
680 680 wiki_page = link_project.wiki.find_page(page)
681 681 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
682 682 "##{anchor}"
683 683 else
684 684 case options[:wiki_links]
685 685 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
686 686 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
687 687 else
688 688 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
689 689 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
690 690 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
691 691 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
692 692 end
693 693 end
694 694 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
695 695 else
696 696 # project or wiki doesn't exist
697 697 all
698 698 end
699 699 else
700 700 all
701 701 end
702 702 end
703 703 end
704 704
705 705 # Redmine links
706 706 #
707 707 # Examples:
708 708 # Issues:
709 709 # #52 -> Link to issue #52
710 710 # Changesets:
711 711 # r52 -> Link to revision 52
712 712 # commit:a85130f -> Link to scmid starting with a85130f
713 713 # Documents:
714 714 # document#17 -> Link to document with id 17
715 715 # document:Greetings -> Link to the document with title "Greetings"
716 716 # document:"Some document" -> Link to the document with title "Some document"
717 717 # Versions:
718 718 # version#3 -> Link to version with id 3
719 719 # version:1.0.0 -> Link to version named "1.0.0"
720 720 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
721 721 # Attachments:
722 722 # attachment:file.zip -> Link to the attachment of the current object named file.zip
723 723 # Source files:
724 724 # source:some/file -> Link to the file located at /some/file in the project's repository
725 725 # source:some/file@52 -> Link to the file's revision 52
726 726 # source:some/file#L120 -> Link to line 120 of the file
727 727 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
728 728 # export:some/file -> Force the download of the file
729 729 # Forum messages:
730 730 # message#1218 -> Link to message with id 1218
731 731 # Projects:
732 732 # project:someproject -> Link to project named "someproject"
733 733 # project#3 -> Link to project with id 3
734 734 #
735 735 # Links can refer other objects from other projects, using project identifier:
736 736 # identifier:r52
737 737 # identifier:document:"Some document"
738 738 # identifier:version:1.0.0
739 739 # identifier:source:some/file
740 740 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
741 741 text.gsub!(%r{<a( [^>]+?)?>(.*?)</a>|([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
742 742 tag_content, leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $3, $4, $5, $6, $7, $12, $13, $10 || $14 || $20, $16 || $21, $17, $19
743 743 if tag_content
744 744 $&
745 745 else
746 746 link = nil
747 747 project = default_project
748 748 if project_identifier
749 749 project = Project.visible.find_by_identifier(project_identifier)
750 750 end
751 751 if esc.nil?
752 752 if prefix.nil? && sep == 'r'
753 753 if project
754 754 repository = nil
755 755 if repo_identifier
756 756 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
757 757 else
758 758 repository = project.repository
759 759 end
760 760 # project.changesets.visible raises an SQL error because of a double join on repositories
761 761 if repository &&
762 762 (changeset = Changeset.visible.
763 763 find_by_repository_id_and_revision(repository.id, identifier))
764 764 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
765 765 {:only_path => only_path, :controller => 'repositories',
766 766 :action => 'revision', :id => project,
767 767 :repository_id => repository.identifier_param,
768 768 :rev => changeset.revision},
769 769 :class => 'changeset',
770 770 :title => truncate_single_line_raw(changeset.comments, 100))
771 771 end
772 772 end
773 773 elsif sep == '#'
774 774 oid = identifier.to_i
775 775 case prefix
776 776 when nil
777 777 if oid.to_s == identifier &&
778 778 issue = Issue.visible.find_by_id(oid)
779 779 anchor = comment_id ? "note-#{comment_id}" : nil
780 780 link = link_to("##{oid}#{comment_suffix}",
781 781 issue_url(issue, :only_path => only_path, :anchor => anchor),
782 782 :class => issue.css_classes,
783 783 :title => "#{issue.subject.truncate(100)} (#{issue.status.name})")
784 784 end
785 785 when 'document'
786 786 if document = Document.visible.find_by_id(oid)
787 787 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
788 788 end
789 789 when 'version'
790 790 if version = Version.visible.find_by_id(oid)
791 791 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
792 792 end
793 793 when 'message'
794 794 if message = Message.visible.find_by_id(oid)
795 795 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
796 796 end
797 797 when 'forum'
798 798 if board = Board.visible.find_by_id(oid)
799 799 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
800 800 end
801 801 when 'news'
802 802 if news = News.visible.find_by_id(oid)
803 803 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
804 804 end
805 805 when 'project'
806 806 if p = Project.visible.find_by_id(oid)
807 807 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
808 808 end
809 809 end
810 810 elsif sep == ':'
811 811 # removes the double quotes if any
812 812 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
813 813 name = CGI.unescapeHTML(name)
814 814 case prefix
815 815 when 'document'
816 816 if project && document = project.documents.visible.find_by_title(name)
817 817 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
818 818 end
819 819 when 'version'
820 820 if project && version = project.versions.visible.find_by_name(name)
821 821 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
822 822 end
823 823 when 'forum'
824 824 if project && board = project.boards.visible.find_by_name(name)
825 825 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
826 826 end
827 827 when 'news'
828 828 if project && news = project.news.visible.find_by_title(name)
829 829 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
830 830 end
831 831 when 'commit', 'source', 'export'
832 832 if project
833 833 repository = nil
834 834 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
835 835 repo_prefix, repo_identifier, name = $1, $2, $3
836 836 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
837 837 else
838 838 repository = project.repository
839 839 end
840 840 if prefix == 'commit'
841 841 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
842 842 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},
843 843 :class => 'changeset',
844 844 :title => truncate_single_line_raw(changeset.comments, 100)
845 845 end
846 846 else
847 847 if repository && User.current.allowed_to?(:browse_repository, project)
848 848 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
849 849 path, rev, anchor = $1, $3, $5
850 850 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,
851 851 :path => to_path_param(path),
852 852 :rev => rev,
853 853 :anchor => anchor},
854 854 :class => (prefix == 'export' ? 'source download' : 'source')
855 855 end
856 856 end
857 857 repo_prefix = nil
858 858 end
859 859 when 'attachment'
860 860 attachments = options[:attachments] || []
861 861 attachments += obj.attachments if obj.respond_to?(:attachments)
862 862 if attachments && attachment = Attachment.latest_attach(attachments, name)
863 863 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
864 864 end
865 865 when 'project'
866 866 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
867 867 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
868 868 end
869 869 end
870 870 end
871 871 end
872 872 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
873 873 end
874 874 end
875 875 end
876 876
877 877 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
878 878
879 879 def parse_sections(text, project, obj, attr, only_path, options)
880 880 return unless options[:edit_section_links]
881 881 text.gsub!(HEADING_RE) do
882 882 heading = $1
883 883 @current_section += 1
884 884 if @current_section > 1
885 885 content_tag('div',
886 886 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
887 887 :class => 'contextual',
888 888 :title => l(:button_edit_section),
889 889 :id => "section-#{@current_section}") + heading.html_safe
890 890 else
891 891 heading
892 892 end
893 893 end
894 894 end
895 895
896 896 # Headings and TOC
897 897 # Adds ids and links to headings unless options[:headings] is set to false
898 898 def parse_headings(text, project, obj, attr, only_path, options)
899 899 return if options[:headings] == false
900 900
901 901 text.gsub!(HEADING_RE) do
902 902 level, attrs, content = $2.to_i, $3, $4
903 903 item = strip_tags(content).strip
904 904 anchor = sanitize_anchor_name(item)
905 905 # used for single-file wiki export
906 906 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
907 907 @heading_anchors[anchor] ||= 0
908 908 idx = (@heading_anchors[anchor] += 1)
909 909 if idx > 1
910 910 anchor = "#{anchor}-#{idx}"
911 911 end
912 912 @parsed_headings << [level, anchor, item]
913 913 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
914 914 end
915 915 end
916 916
917 917 MACROS_RE = /(
918 918 (!)? # escaping
919 919 (
920 920 \{\{ # opening tag
921 921 ([\w]+) # macro name
922 922 (\(([^\n\r]*?)\))? # optional arguments
923 923 ([\n\r].*?[\n\r])? # optional block of text
924 924 \}\} # closing tag
925 925 )
926 926 )/mx unless const_defined?(:MACROS_RE)
927 927
928 928 MACRO_SUB_RE = /(
929 929 \{\{
930 930 macro\((\d+)\)
931 931 \}\}
932 932 )/x unless const_defined?(:MACRO_SUB_RE)
933 933
934 934 # Extracts macros from text
935 935 def catch_macros(text)
936 936 macros = {}
937 937 text.gsub!(MACROS_RE) do
938 938 all, macro = $1, $4.downcase
939 939 if macro_exists?(macro) || all =~ MACRO_SUB_RE
940 940 index = macros.size
941 941 macros[index] = all
942 942 "{{macro(#{index})}}"
943 943 else
944 944 all
945 945 end
946 946 end
947 947 macros
948 948 end
949 949
950 950 # Executes and replaces macros in text
951 951 def inject_macros(text, obj, macros, execute=true)
952 952 text.gsub!(MACRO_SUB_RE) do
953 953 all, index = $1, $2.to_i
954 954 orig = macros.delete(index)
955 955 if execute && orig && orig =~ MACROS_RE
956 956 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
957 957 if esc.nil?
958 958 h(exec_macro(macro, obj, args, block) || all)
959 959 else
960 960 h(all)
961 961 end
962 962 elsif orig
963 963 h(orig)
964 964 else
965 965 h(all)
966 966 end
967 967 end
968 968 end
969 969
970 970 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
971 971
972 972 # Renders the TOC with given headings
973 973 def replace_toc(text, headings)
974 974 text.gsub!(TOC_RE) do
975 975 left_align, right_align = $2, $3
976 976 # Keep only the 4 first levels
977 977 headings = headings.select{|level, anchor, item| level <= 4}
978 978 if headings.empty?
979 979 ''
980 980 else
981 981 div_class = 'toc'
982 982 div_class << ' right' if right_align
983 983 div_class << ' left' if left_align
984 984 out = "<ul class=\"#{div_class}\"><li>"
985 985 root = headings.map(&:first).min
986 986 current = root
987 987 started = false
988 988 headings.each do |level, anchor, item|
989 989 if level > current
990 990 out << '<ul><li>' * (level - current)
991 991 elsif level < current
992 992 out << "</li></ul>\n" * (current - level) + "</li><li>"
993 993 elsif started
994 994 out << '</li><li>'
995 995 end
996 996 out << "<a href=\"##{anchor}\">#{item}</a>"
997 997 current = level
998 998 started = true
999 999 end
1000 1000 out << '</li></ul>' * (current - root)
1001 1001 out << '</li></ul>'
1002 1002 end
1003 1003 end
1004 1004 end
1005 1005
1006 1006 # Same as Rails' simple_format helper without using paragraphs
1007 1007 def simple_format_without_paragraph(text)
1008 1008 text.to_s.
1009 1009 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1010 1010 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1011 1011 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1012 1012 html_safe
1013 1013 end
1014 1014
1015 1015 def lang_options_for_select(blank=true)
1016 1016 (blank ? [["(auto)", ""]] : []) + languages_options
1017 1017 end
1018 1018
1019 1019 def labelled_form_for(*args, &proc)
1020 1020 args << {} unless args.last.is_a?(Hash)
1021 1021 options = args.last
1022 1022 if args.first.is_a?(Symbol)
1023 1023 options.merge!(:as => args.shift)
1024 1024 end
1025 1025 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1026 1026 form_for(*args, &proc)
1027 1027 end
1028 1028
1029 1029 def labelled_fields_for(*args, &proc)
1030 1030 args << {} unless args.last.is_a?(Hash)
1031 1031 options = args.last
1032 1032 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1033 1033 fields_for(*args, &proc)
1034 1034 end
1035 1035
1036 1036 def error_messages_for(*objects)
1037 1037 html = ""
1038 1038 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1039 1039 errors = objects.map {|o| o.errors.full_messages}.flatten
1040 1040 if errors.any?
1041 1041 html << "<div id='errorExplanation'><ul>\n"
1042 1042 errors.each do |error|
1043 1043 html << "<li>#{h error}</li>\n"
1044 1044 end
1045 1045 html << "</ul></div>\n"
1046 1046 end
1047 1047 html.html_safe
1048 1048 end
1049 1049
1050 1050 def delete_link(url, options={})
1051 1051 options = {
1052 1052 :method => :delete,
1053 1053 :data => {:confirm => l(:text_are_you_sure)},
1054 1054 :class => 'icon icon-del'
1055 1055 }.merge(options)
1056 1056
1057 1057 link_to l(:button_delete), url, options
1058 1058 end
1059 1059
1060 1060 def preview_link(url, form, target='preview', options={})
1061 1061 content_tag 'a', l(:label_preview), {
1062 1062 :href => "#",
1063 1063 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1064 1064 :accesskey => accesskey(:preview)
1065 1065 }.merge(options)
1066 1066 end
1067 1067
1068 1068 def link_to_function(name, function, html_options={})
1069 1069 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1070 1070 end
1071 1071
1072 1072 # Helper to render JSON in views
1073 1073 def raw_json(arg)
1074 1074 arg.to_json.to_s.gsub('/', '\/').html_safe
1075 1075 end
1076 1076
1077 1077 def back_url
1078 1078 url = params[:back_url]
1079 1079 if url.nil? && referer = request.env['HTTP_REFERER']
1080 1080 url = CGI.unescape(referer.to_s)
1081 1081 end
1082 1082 url
1083 1083 end
1084 1084
1085 1085 def back_url_hidden_field_tag
1086 1086 url = back_url
1087 1087 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1088 1088 end
1089 1089
1090 1090 def check_all_links(form_name)
1091 1091 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1092 1092 " | ".html_safe +
1093 1093 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1094 1094 end
1095 1095
1096 1096 def toggle_checkboxes_link(selector)
1097 1097 link_to_function image_tag('toggle_check.png'),
1098 1098 "toggleCheckboxesBySelector('#{selector}')",
1099 1099 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}"
1100 1100 end
1101 1101
1102 1102 def progress_bar(pcts, options={})
1103 1103 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1104 1104 pcts = pcts.collect(&:round)
1105 1105 pcts[1] = pcts[1] - pcts[0]
1106 1106 pcts << (100 - pcts[1] - pcts[0])
1107 1107 width = options[:width] || '100px;'
1108 1108 legend = options[:legend] || ''
1109 1109 content_tag('table',
1110 1110 content_tag('tr',
1111 1111 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1112 1112 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1113 1113 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1114 1114 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1115 1115 content_tag('p', legend, :class => 'percent').html_safe
1116 1116 end
1117 1117
1118 1118 def checked_image(checked=true)
1119 1119 if checked
1120 1120 @checked_image_tag ||= image_tag('toggle_check.png')
1121 1121 end
1122 1122 end
1123 1123
1124 1124 def context_menu(url)
1125 1125 unless @context_menu_included
1126 1126 content_for :header_tags do
1127 1127 javascript_include_tag('context_menu') +
1128 1128 stylesheet_link_tag('context_menu')
1129 1129 end
1130 1130 if l(:direction) == 'rtl'
1131 1131 content_for :header_tags do
1132 1132 stylesheet_link_tag('context_menu_rtl')
1133 1133 end
1134 1134 end
1135 1135 @context_menu_included = true
1136 1136 end
1137 1137 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1138 1138 end
1139 1139
1140 1140 def calendar_for(field_id)
1141 1141 include_calendar_headers_tags
1142 1142 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1143 1143 end
1144 1144
1145 1145 def include_calendar_headers_tags
1146 1146 unless @calendar_headers_tags_included
1147 1147 tags = ''.html_safe
1148 1148 @calendar_headers_tags_included = true
1149 1149 content_for :header_tags do
1150 1150 start_of_week = Setting.start_of_week
1151 1151 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1152 1152 # Redmine uses 1..7 (monday..sunday) in settings and locales
1153 1153 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1154 1154 start_of_week = start_of_week.to_i % 7
1155 1155 tags << javascript_tag(
1156 1156 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1157 1157 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1158 1158 path_to_image('/images/calendar.png') +
1159 1159 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1160 1160 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1161 1161 "beforeShow: beforeShowDatePicker};")
1162 1162 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1163 1163 unless jquery_locale == 'en'
1164 1164 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1165 1165 end
1166 1166 tags
1167 1167 end
1168 1168 end
1169 1169 end
1170 1170
1171 1171 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1172 1172 # Examples:
1173 1173 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1174 1174 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1175 1175 #
1176 1176 def stylesheet_link_tag(*sources)
1177 1177 options = sources.last.is_a?(Hash) ? sources.pop : {}
1178 1178 plugin = options.delete(:plugin)
1179 1179 sources = sources.map do |source|
1180 1180 if plugin
1181 1181 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1182 1182 elsif current_theme && current_theme.stylesheets.include?(source)
1183 1183 current_theme.stylesheet_path(source)
1184 1184 else
1185 1185 source
1186 1186 end
1187 1187 end
1188 1188 super *sources, options
1189 1189 end
1190 1190
1191 1191 # Overrides Rails' image_tag with themes and plugins support.
1192 1192 # Examples:
1193 1193 # image_tag('image.png') # => picks image.png from the current theme or defaults
1194 1194 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1195 1195 #
1196 1196 def image_tag(source, options={})
1197 1197 if plugin = options.delete(:plugin)
1198 1198 source = "/plugin_assets/#{plugin}/images/#{source}"
1199 1199 elsif current_theme && current_theme.images.include?(source)
1200 1200 source = current_theme.image_path(source)
1201 1201 end
1202 1202 super source, options
1203 1203 end
1204 1204
1205 1205 # Overrides Rails' javascript_include_tag with plugins support
1206 1206 # Examples:
1207 1207 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1208 1208 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1209 1209 #
1210 1210 def javascript_include_tag(*sources)
1211 1211 options = sources.last.is_a?(Hash) ? sources.pop : {}
1212 1212 if plugin = options.delete(:plugin)
1213 1213 sources = sources.map do |source|
1214 1214 if plugin
1215 1215 "/plugin_assets/#{plugin}/javascripts/#{source}"
1216 1216 else
1217 1217 source
1218 1218 end
1219 1219 end
1220 1220 end
1221 1221 super *sources, options
1222 1222 end
1223 1223
1224 1224 def sidebar_content?
1225 1225 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1226 1226 end
1227 1227
1228 1228 def view_layouts_base_sidebar_hook_response
1229 1229 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1230 1230 end
1231 1231
1232 1232 def email_delivery_enabled?
1233 1233 !!ActionMailer::Base.perform_deliveries
1234 1234 end
1235 1235
1236 1236 # Returns the avatar image tag for the given +user+ if avatars are enabled
1237 1237 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1238 1238 def avatar(user, options = { })
1239 1239 if Setting.gravatar_enabled?
1240 1240 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1241 1241 email = nil
1242 1242 if user.respond_to?(:mail)
1243 1243 email = user.mail
1244 1244 elsif user.to_s =~ %r{<(.+?)>}
1245 1245 email = $1
1246 1246 end
1247 1247 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1248 1248 else
1249 1249 ''
1250 1250 end
1251 1251 end
1252 1252
1253 1253 def sanitize_anchor_name(anchor)
1254 1254 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1255 1255 end
1256 1256
1257 1257 # Returns the javascript tags that are included in the html layout head
1258 1258 def javascript_heads
1259 1259 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.1', 'application')
1260 1260 unless User.current.pref.warn_on_leaving_unsaved == '0'
1261 1261 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1262 1262 end
1263 1263 tags
1264 1264 end
1265 1265
1266 1266 def favicon
1267 1267 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1268 1268 end
1269 1269
1270 1270 # Returns the path to the favicon
1271 1271 def favicon_path
1272 1272 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1273 1273 image_path(icon)
1274 1274 end
1275 1275
1276 1276 # Returns the full URL to the favicon
1277 1277 def favicon_url
1278 1278 # TODO: use #image_url introduced in Rails4
1279 1279 path = favicon_path
1280 1280 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1281 1281 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1282 1282 end
1283 1283
1284 1284 def robot_exclusion_tag
1285 1285 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1286 1286 end
1287 1287
1288 1288 # Returns true if arg is expected in the API response
1289 1289 def include_in_api_response?(arg)
1290 1290 unless @included_in_api_response
1291 1291 param = params[:include]
1292 1292 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1293 1293 @included_in_api_response.collect!(&:strip)
1294 1294 end
1295 1295 @included_in_api_response.include?(arg.to_s)
1296 1296 end
1297 1297
1298 1298 # Returns options or nil if nometa param or X-Redmine-Nometa header
1299 1299 # was set in the request
1300 1300 def api_meta(options)
1301 1301 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1302 1302 # compatibility mode for activeresource clients that raise
1303 1303 # an error when deserializing an array with attributes
1304 1304 nil
1305 1305 else
1306 1306 options
1307 1307 end
1308 1308 end
1309 1309
1310 def generate_csv(&block)
1311 decimal_separator = l(:general_csv_decimal_separator)
1312 encoding = l(:general_csv_encoding)
1313 end
1314
1310 1315 private
1311 1316
1312 1317 def wiki_helper
1313 1318 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1314 1319 extend helper
1315 1320 return self
1316 1321 end
1317 1322
1318 1323 def link_to_content_update(text, url_params = {}, html_options = {})
1319 1324 link_to(text, url_params, html_options)
1320 1325 end
1321 1326 end
@@ -1,225 +1,223
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2015 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 module QueriesHelper
21 21 include ApplicationHelper
22 22
23 23 def filters_options_for_select(query)
24 24 ungrouped = []
25 25 grouped = {}
26 26 query.available_filters.map do |field, field_options|
27 27 if field_options[:type] == :relation
28 28 group = :label_related_issues
29 29 elsif field =~ /^(.+)\./
30 30 # association filters
31 31 group = "field_#{$1}"
32 32 elsif %w(member_of_group assigned_to_role).include?(field)
33 33 group = :field_assigned_to
34 34 elsif field_options[:type] == :date_past || field_options[:type] == :date
35 35 group = :label_date
36 36 end
37 37 if group
38 38 (grouped[group] ||= []) << [field_options[:name], field]
39 39 else
40 40 ungrouped << [field_options[:name], field]
41 41 end
42 42 end
43 43 # Don't group dates if there's only one (eg. time entries filters)
44 44 if grouped[:label_date].try(:size) == 1
45 45 ungrouped << grouped.delete(:label_date).first
46 46 end
47 47 s = options_for_select([[]] + ungrouped)
48 48 if grouped.present?
49 49 localized_grouped = grouped.map {|k,v| [l(k), v]}
50 50 s << grouped_options_for_select(localized_grouped)
51 51 end
52 52 s
53 53 end
54 54
55 55 def query_filters_hidden_tags(query)
56 56 tags = ''.html_safe
57 57 query.filters.each do |field, options|
58 58 tags << hidden_field_tag("f[]", field, :id => nil)
59 59 tags << hidden_field_tag("op[#{field}]", options[:operator], :id => nil)
60 60 options[:values].each do |value|
61 61 tags << hidden_field_tag("v[#{field}][]", value, :id => nil)
62 62 end
63 63 end
64 64 tags
65 65 end
66 66
67 67 def query_columns_hidden_tags(query)
68 68 tags = ''.html_safe
69 69 query.columns.each do |column|
70 70 tags << hidden_field_tag("c[]", column.name, :id => nil)
71 71 end
72 72 tags
73 73 end
74 74
75 75 def query_hidden_tags(query)
76 76 query_filters_hidden_tags(query) + query_columns_hidden_tags(query)
77 77 end
78 78
79 79 def available_block_columns_tags(query)
80 80 tags = ''.html_safe
81 81 query.available_block_columns.each do |column|
82 82 tags << content_tag('label', check_box_tag('c[]', column.name.to_s, query.has_column?(column), :id => nil) + " #{column.caption}", :class => 'inline')
83 83 end
84 84 tags
85 85 end
86 86
87 87 def query_available_inline_columns_options(query)
88 88 (query.available_inline_columns - query.columns).reject(&:frozen?).collect {|column| [column.caption, column.name]}
89 89 end
90 90
91 91 def query_selected_inline_columns_options(query)
92 92 (query.inline_columns & query.available_inline_columns).reject(&:frozen?).collect {|column| [column.caption, column.name]}
93 93 end
94 94
95 95 def render_query_columns_selection(query, options={})
96 96 tag_name = (options[:name] || 'c') + '[]'
97 97 render :partial => 'queries/columns', :locals => {:query => query, :tag_name => tag_name}
98 98 end
99 99
100 100 def column_header(column)
101 101 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
102 102 :default_order => column.default_order) :
103 103 content_tag('th', h(column.caption))
104 104 end
105 105
106 106 def column_content(column, issue)
107 107 value = column.value_object(issue)
108 108 if value.is_a?(Array)
109 109 value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe
110 110 else
111 111 column_value(column, issue, value)
112 112 end
113 113 end
114 114
115 115 def column_value(column, issue, value)
116 116 case column.name
117 117 when :id
118 118 link_to value, issue_path(issue)
119 119 when :subject
120 120 link_to value, issue_path(issue)
121 121 when :parent
122 122 value ? (value.visible? ? link_to_issue(value, :subject => false) : "##{value.id}") : ''
123 123 when :description
124 124 issue.description? ? content_tag('div', textilizable(issue, :description), :class => "wiki") : ''
125 125 when :done_ratio
126 126 progress_bar(value, :width => '80px')
127 127 when :relations
128 128 content_tag('span',
129 129 value.to_s(issue) {|other| link_to_issue(other, :subject => false, :tracker => false)}.html_safe,
130 130 :class => value.css_classes_for(issue))
131 131 else
132 132 format_object(value)
133 133 end
134 134 end
135 135
136 136 def csv_content(column, issue)
137 137 value = column.value_object(issue)
138 138 if value.is_a?(Array)
139 139 value.collect {|v| csv_value(column, issue, v)}.compact.join(', ')
140 140 else
141 141 csv_value(column, issue, value)
142 142 end
143 143 end
144 144
145 145 def csv_value(column, object, value)
146 146 format_object(value, false) do |value|
147 147 case value.class.name
148 148 when 'Float'
149 149 sprintf("%.2f", value).gsub('.', l(:general_csv_decimal_separator))
150 150 when 'IssueRelation'
151 151 value.to_s(object)
152 152 when 'Issue'
153 153 if object.is_a?(TimeEntry)
154 154 "#{value.tracker} ##{value.id}: #{value.subject}"
155 155 else
156 156 value.id
157 157 end
158 158 else
159 159 value
160 160 end
161 161 end
162 162 end
163 163
164 164 def query_to_csv(items, query, options={})
165 encoding = l(:general_csv_encoding)
166 165 columns = (options[:columns] == 'all' ? query.available_inline_columns : query.inline_columns)
167 166 query.available_block_columns.each do |column|
168 167 if options[column.name].present?
169 168 columns << column
170 169 end
171 170 end
172 171
173 export = CSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
172 Redmine::Export::CSV.generate do |csv|
174 173 # csv header fields
175 csv << columns.collect {|c| Redmine::CodesetUtil.from_utf8(c.caption.to_s, encoding) }
174 csv << columns.map {|c| c.caption.to_s}
176 175 # csv lines
177 176 items.each do |item|
178 csv << columns.collect {|c| Redmine::CodesetUtil.from_utf8(csv_content(c, item), encoding) }
177 csv << columns.map {|c| csv_content(c, item)}
179 178 end
180 179 end
181 export
182 180 end
183 181
184 182 # Retrieve query from session or build a new query
185 183 def retrieve_query
186 184 if !params[:query_id].blank?
187 185 cond = "project_id IS NULL"
188 186 cond << " OR project_id = #{@project.id}" if @project
189 187 @query = IssueQuery.where(cond).find(params[:query_id])
190 188 raise ::Unauthorized unless @query.visible?
191 189 @query.project = @project
192 190 session[:query] = {:id => @query.id, :project_id => @query.project_id}
193 191 sort_clear
194 192 elsif api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
195 193 # Give it a name, required to be valid
196 194 @query = IssueQuery.new(:name => "_")
197 195 @query.project = @project
198 196 @query.build_from_params(params)
199 197 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
200 198 else
201 199 # retrieve from session
202 200 @query = nil
203 201 @query = IssueQuery.find_by_id(session[:query][:id]) if session[:query][:id]
204 202 @query ||= IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
205 203 @query.project = @project
206 204 end
207 205 end
208 206
209 207 def retrieve_query_from_session
210 208 if session[:query]
211 209 if session[:query][:id]
212 210 @query = IssueQuery.find_by_id(session[:query][:id])
213 211 return unless @query
214 212 else
215 213 @query = IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
216 214 end
217 215 if session[:query].has_key?(:project_id)
218 216 @query.project_id = session[:query][:project_id]
219 217 else
220 218 @query.project = @project
221 219 end
222 220 @query
223 221 end
224 222 end
225 223 end
@@ -1,142 +1,135
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2015 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 module TimelogHelper
21 21 include ApplicationHelper
22 22
23 23 def render_timelog_breadcrumb
24 24 links = []
25 25 links << link_to(l(:label_project_all), {:project_id => nil, :issue_id => nil})
26 26 links << link_to(h(@project), {:project_id => @project, :issue_id => nil}) if @project
27 27 if @issue
28 28 if @issue.visible?
29 29 links << link_to_issue(@issue, :subject => false)
30 30 else
31 31 links << "##{@issue.id}"
32 32 end
33 33 end
34 34 breadcrumb links
35 35 end
36 36
37 37 # Returns a collection of activities for a select field. time_entry
38 38 # is optional and will be used to check if the selected TimeEntryActivity
39 39 # is active.
40 40 def activity_collection_for_select_options(time_entry=nil, project=nil)
41 41 project ||= time_entry.try(:project)
42 42 project ||= @project
43 43 if project.nil?
44 44 activities = TimeEntryActivity.shared.active
45 45 else
46 46 activities = project.activities
47 47 end
48 48
49 49 collection = []
50 50 if time_entry && time_entry.activity && !time_entry.activity.active?
51 51 collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ]
52 52 else
53 53 collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ] unless activities.detect(&:is_default)
54 54 end
55 55 activities.each { |a| collection << [a.name, a.id] }
56 56 collection
57 57 end
58 58
59 59 def select_hours(data, criteria, value)
60 60 if value.to_s.empty?
61 61 data.select {|row| row[criteria].blank? }
62 62 else
63 63 data.select {|row| row[criteria].to_s == value.to_s}
64 64 end
65 65 end
66 66
67 67 def sum_hours(data)
68 68 sum = 0
69 69 data.each do |row|
70 70 sum += row['hours'].to_f
71 71 end
72 72 sum
73 73 end
74 74
75 75 def format_criteria_value(criteria_options, value)
76 76 if value.blank?
77 77 "[#{l(:label_none)}]"
78 78 elsif k = criteria_options[:klass]
79 79 obj = k.find_by_id(value.to_i)
80 80 if obj.is_a?(Issue)
81 81 obj.visible? ? "#{obj.tracker} ##{obj.id}: #{obj.subject}" : "##{obj.id}"
82 82 else
83 83 obj
84 84 end
85 85 elsif cf = criteria_options[:custom_field]
86 86 format_value(value, cf)
87 87 else
88 88 value.to_s
89 89 end
90 90 end
91 91
92 92 def report_to_csv(report)
93 decimal_separator = l(:general_csv_decimal_separator)
94 export = CSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
93 Redmine::Export::CSV.generate do |csv|
95 94 # Column headers
96 95 headers = report.criteria.collect {|criteria| l(report.available_criteria[criteria][:label]) }
97 96 headers += report.periods
98 97 headers << l(:label_total_time)
99 csv << headers.collect {|c| Redmine::CodesetUtil.from_utf8(
100 c.to_s,
101 l(:general_csv_encoding) ) }
98 csv << headers
102 99 # Content
103 100 report_criteria_to_csv(csv, report.available_criteria, report.columns, report.criteria, report.periods, report.hours)
104 101 # Total row
105 str_total = Redmine::CodesetUtil.from_utf8(l(:label_total_time), l(:general_csv_encoding))
102 str_total = l(:label_total_time)
106 103 row = [ str_total ] + [''] * (report.criteria.size - 1)
107 104 total = 0
108 105 report.periods.each do |period|
109 106 sum = sum_hours(select_hours(report.hours, report.columns, period.to_s))
110 107 total += sum
111 row << (sum > 0 ? ("%.2f" % sum).gsub('.',decimal_separator) : '')
108 row << (sum > 0 ? sum : '')
112 109 end
113 row << ("%.2f" % total).gsub('.',decimal_separator)
110 row << total
114 111 csv << row
115 112 end
116 export
117 113 end
118 114
119 115 def report_criteria_to_csv(csv, available_criteria, columns, criteria, periods, hours, level=0)
120 decimal_separator = l(:general_csv_decimal_separator)
121 116 hours.collect {|h| h[criteria[level]].to_s}.uniq.each do |value|
122 117 hours_for_value = select_hours(hours, criteria[level], value)
123 118 next if hours_for_value.empty?
124 119 row = [''] * level
125 row << Redmine::CodesetUtil.from_utf8(
126 format_criteria_value(available_criteria[criteria[level]], value).to_s,
127 l(:general_csv_encoding) )
120 row << format_criteria_value(available_criteria[criteria[level]], value).to_s
128 121 row += [''] * (criteria.length - level - 1)
129 122 total = 0
130 123 periods.each do |period|
131 124 sum = sum_hours(select_hours(hours_for_value, columns, period.to_s))
132 125 total += sum
133 row << (sum > 0 ? ("%.2f" % sum).gsub('.',decimal_separator) : '')
126 row << (sum > 0 ? sum : '')
134 127 end
135 row << ("%.2f" % total).gsub('.',decimal_separator)
128 row << total
136 129 csv << row
137 130 if criteria.length > level + 1
138 131 report_criteria_to_csv(csv, available_criteria, columns, criteria, periods, hours_for_value, level + 1)
139 132 end
140 133 end
141 134 end
142 135 end
@@ -1,279 +1,277
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'redmine/core_ext'
19 19
20 20 begin
21 21 require 'rmagick' unless Object.const_defined?(:Magick)
22 22 rescue LoadError
23 23 # RMagick is not available
24 24 end
25 25 begin
26 26 require 'redcarpet' unless Object.const_defined?(:Redcarpet)
27 27 rescue LoadError
28 28 # Redcarpet is not available
29 29 end
30 30
31 31 require 'redmine/scm/base'
32 32 require 'redmine/access_control'
33 33 require 'redmine/access_keys'
34 34 require 'redmine/activity'
35 35 require 'redmine/activity/fetcher'
36 36 require 'redmine/ciphering'
37 37 require 'redmine/codeset_util'
38 38 require 'redmine/field_format'
39 39 require 'redmine/i18n'
40 40 require 'redmine/menu_manager'
41 41 require 'redmine/notifiable'
42 42 require 'redmine/platform'
43 43 require 'redmine/mime_type'
44 44 require 'redmine/notifiable'
45 45 require 'redmine/search'
46 46 require 'redmine/syntax_highlighting'
47 47 require 'redmine/thumbnail'
48 48 require 'redmine/unified_diff'
49 49 require 'redmine/utils'
50 50 require 'redmine/version'
51 51 require 'redmine/wiki_formatting'
52 52
53 53 require 'redmine/default_data/loader'
54 54 require 'redmine/helpers/calendar'
55 55 require 'redmine/helpers/diff'
56 56 require 'redmine/helpers/gantt'
57 57 require 'redmine/helpers/time_report'
58 58 require 'redmine/views/other_formats_builder'
59 59 require 'redmine/views/labelled_form_builder'
60 60 require 'redmine/views/builders'
61 61
62 62 require 'redmine/themes'
63 63 require 'redmine/hook'
64 64 require 'redmine/plugin'
65 65
66 require 'csv'
67
68 66 Redmine::Scm::Base.add "Subversion"
69 67 Redmine::Scm::Base.add "Darcs"
70 68 Redmine::Scm::Base.add "Mercurial"
71 69 Redmine::Scm::Base.add "Cvs"
72 70 Redmine::Scm::Base.add "Bazaar"
73 71 Redmine::Scm::Base.add "Git"
74 72 Redmine::Scm::Base.add "Filesystem"
75 73
76 74 # Permissions
77 75 Redmine::AccessControl.map do |map|
78 76 map.permission :view_project, {:projects => [:show], :activities => [:index]}, :public => true, :read => true
79 77 map.permission :search_project, {:search => :index}, :public => true, :read => true
80 78 map.permission :add_project, {:projects => [:new, :create]}, :require => :loggedin
81 79 map.permission :edit_project, {:projects => [:settings, :edit, :update]}, :require => :member
82 80 map.permission :close_project, {:projects => [:close, :reopen]}, :require => :member, :read => true
83 81 map.permission :select_project_modules, {:projects => :modules}, :require => :member
84 82 map.permission :view_members, {:members => [:index, :show]}, :public => true, :read => true
85 83 map.permission :manage_members, {:projects => :settings, :members => [:index, :show, :new, :create, :update, :destroy, :autocomplete]}, :require => :member
86 84 map.permission :manage_versions, {:projects => :settings, :versions => [:new, :create, :edit, :update, :close_completed, :destroy]}, :require => :member
87 85 map.permission :add_subprojects, {:projects => [:new, :create]}, :require => :member
88 86
89 87 map.project_module :issue_tracking do |map|
90 88 # Issue categories
91 89 map.permission :manage_categories, {:projects => :settings, :issue_categories => [:index, :show, :new, :create, :edit, :update, :destroy]}, :require => :member
92 90 # Issues
93 91 map.permission :view_issues, {:issues => [:index, :show],
94 92 :auto_complete => [:issues],
95 93 :context_menus => [:issues],
96 94 :versions => [:index, :show, :status_by],
97 95 :journals => [:index, :diff],
98 96 :queries => :index,
99 97 :reports => [:issue_report, :issue_report_details]},
100 98 :read => true
101 99 map.permission :add_issues, {:issues => [:new, :create], :attachments => :upload}
102 100 map.permission :edit_issues, {:issues => [:edit, :update, :bulk_edit, :bulk_update], :journals => [:new], :attachments => :upload}
103 101 map.permission :copy_issues, {:issues => [:new, :create, :bulk_edit, :bulk_update], :attachments => :upload}
104 102 map.permission :manage_issue_relations, {:issue_relations => [:index, :show, :create, :destroy]}
105 103 map.permission :manage_subtasks, {}
106 104 map.permission :set_issues_private, {}
107 105 map.permission :set_own_issues_private, {}, :require => :loggedin
108 106 map.permission :add_issue_notes, {:issues => [:edit, :update], :journals => [:new], :attachments => :upload}
109 107 map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
110 108 map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
111 109 map.permission :view_private_notes, {}, :read => true, :require => :member
112 110 map.permission :set_notes_private, {}, :require => :member
113 111 map.permission :delete_issues, {:issues => :destroy}, :require => :member
114 112 # Queries
115 113 map.permission :manage_public_queries, {:queries => [:new, :create, :edit, :update, :destroy]}, :require => :member
116 114 map.permission :save_queries, {:queries => [:new, :create, :edit, :update, :destroy]}, :require => :loggedin
117 115 # Watchers
118 116 map.permission :view_issue_watchers, {}, :read => true
119 117 map.permission :add_issue_watchers, {:watchers => [:new, :create, :append, :autocomplete_for_user]}
120 118 map.permission :delete_issue_watchers, {:watchers => :destroy}
121 119 end
122 120
123 121 map.project_module :time_tracking do |map|
124 122 map.permission :log_time, {:timelog => [:new, :create]}, :require => :loggedin
125 123 map.permission :view_time_entries, {:timelog => [:index, :report, :show]}, :read => true
126 124 map.permission :edit_time_entries, {:timelog => [:edit, :update, :destroy, :bulk_edit, :bulk_update]}, :require => :member
127 125 map.permission :edit_own_time_entries, {:timelog => [:edit, :update, :destroy,:bulk_edit, :bulk_update]}, :require => :loggedin
128 126 map.permission :manage_project_activities, {:project_enumerations => [:update, :destroy]}, :require => :member
129 127 end
130 128
131 129 map.project_module :news do |map|
132 130 map.permission :manage_news, {:news => [:new, :create, :edit, :update, :destroy], :comments => [:destroy], :attachments => :upload}, :require => :member
133 131 map.permission :view_news, {:news => [:index, :show]}, :public => true, :read => true
134 132 map.permission :comment_news, {:comments => :create}
135 133 end
136 134
137 135 map.project_module :documents do |map|
138 136 map.permission :add_documents, {:documents => [:new, :create, :add_attachment], :attachments => :upload}, :require => :loggedin
139 137 map.permission :edit_documents, {:documents => [:edit, :update, :add_attachment], :attachments => :upload}, :require => :loggedin
140 138 map.permission :delete_documents, {:documents => [:destroy]}, :require => :loggedin
141 139 map.permission :view_documents, {:documents => [:index, :show, :download]}, :read => true
142 140 end
143 141
144 142 map.project_module :files do |map|
145 143 map.permission :manage_files, {:files => [:new, :create], :attachments => :upload}, :require => :loggedin
146 144 map.permission :view_files, {:files => :index, :versions => :download}, :read => true
147 145 end
148 146
149 147 map.project_module :wiki do |map|
150 148 map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
151 149 map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
152 150 map.permission :delete_wiki_pages, {:wiki => [:destroy, :destroy_version]}, :require => :member
153 151 map.permission :view_wiki_pages, {:wiki => [:index, :show, :special, :date_index]}, :read => true
154 152 map.permission :export_wiki_pages, {:wiki => [:export]}, :read => true
155 153 map.permission :view_wiki_edits, {:wiki => [:history, :diff, :annotate]}, :read => true
156 154 map.permission :edit_wiki_pages, :wiki => [:edit, :update, :preview, :add_attachment], :attachments => :upload
157 155 map.permission :delete_wiki_pages_attachments, {}
158 156 map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member
159 157 end
160 158
161 159 map.project_module :repository do |map|
162 160 map.permission :manage_repository, {:repositories => [:new, :create, :edit, :update, :committers, :destroy]}, :require => :member
163 161 map.permission :browse_repository, {:repositories => [:show, :browse, :entry, :raw, :annotate, :changes, :diff, :stats, :graph]}, :read => true
164 162 map.permission :view_changesets, {:repositories => [:show, :revisions, :revision]}, :read => true
165 163 map.permission :commit_access, {}
166 164 map.permission :manage_related_issues, {:repositories => [:add_related_issue, :remove_related_issue]}
167 165 end
168 166
169 167 map.project_module :boards do |map|
170 168 map.permission :manage_boards, {:boards => [:new, :create, :edit, :update, :destroy]}, :require => :member
171 169 map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true, :read => true
172 170 map.permission :add_messages, {:messages => [:new, :reply, :quote], :attachments => :upload}
173 171 map.permission :edit_messages, {:messages => :edit, :attachments => :upload}, :require => :member
174 172 map.permission :edit_own_messages, {:messages => :edit, :attachments => :upload}, :require => :loggedin
175 173 map.permission :delete_messages, {:messages => :destroy}, :require => :member
176 174 map.permission :delete_own_messages, {:messages => :destroy}, :require => :loggedin
177 175 end
178 176
179 177 map.project_module :calendar do |map|
180 178 map.permission :view_calendar, {:calendars => [:show, :update]}, :read => true
181 179 end
182 180
183 181 map.project_module :gantt do |map|
184 182 map.permission :view_gantt, {:gantts => [:show, :update]}, :read => true
185 183 end
186 184 end
187 185
188 186 Redmine::MenuManager.map :top_menu do |menu|
189 187 menu.push :home, :home_path
190 188 menu.push :my_page, { :controller => 'my', :action => 'page' }, :if => Proc.new { User.current.logged? }
191 189 menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural
192 190 menu.push :administration, { :controller => 'admin', :action => 'index' }, :if => Proc.new { User.current.admin? }, :last => true
193 191 menu.push :help, Redmine::Info.help_url, :last => true
194 192 end
195 193
196 194 Redmine::MenuManager.map :account_menu do |menu|
197 195 menu.push :login, :signin_path, :if => Proc.new { !User.current.logged? }
198 196 menu.push :register, :register_path, :if => Proc.new { !User.current.logged? && Setting.self_registration? }
199 197 menu.push :my_account, { :controller => 'my', :action => 'account' }, :if => Proc.new { User.current.logged? }
200 198 menu.push :logout, :signout_path, :html => {:method => 'post'}, :if => Proc.new { User.current.logged? }
201 199 end
202 200
203 201 Redmine::MenuManager.map :application_menu do |menu|
204 202 # Empty
205 203 end
206 204
207 205 Redmine::MenuManager.map :admin_menu do |menu|
208 206 menu.push :projects, {:controller => 'admin', :action => 'projects'}, :caption => :label_project_plural
209 207 menu.push :users, {:controller => 'users'}, :caption => :label_user_plural
210 208 menu.push :groups, {:controller => 'groups'}, :caption => :label_group_plural
211 209 menu.push :roles, {:controller => 'roles'}, :caption => :label_role_and_permissions
212 210 menu.push :trackers, {:controller => 'trackers'}, :caption => :label_tracker_plural
213 211 menu.push :issue_statuses, {:controller => 'issue_statuses'}, :caption => :label_issue_status_plural,
214 212 :html => {:class => 'issue_statuses'}
215 213 menu.push :workflows, {:controller => 'workflows', :action => 'edit'}, :caption => :label_workflow
216 214 menu.push :custom_fields, {:controller => 'custom_fields'}, :caption => :label_custom_field_plural,
217 215 :html => {:class => 'custom_fields'}
218 216 menu.push :enumerations, {:controller => 'enumerations'}
219 217 menu.push :settings, {:controller => 'settings'}
220 218 menu.push :ldap_authentication, {:controller => 'auth_sources', :action => 'index'},
221 219 :html => {:class => 'server_authentication'}
222 220 menu.push :plugins, {:controller => 'admin', :action => 'plugins'}, :last => true
223 221 menu.push :info, {:controller => 'admin', :action => 'info'}, :caption => :label_information_plural, :last => true
224 222 end
225 223
226 224 Redmine::MenuManager.map :project_menu do |menu|
227 225 menu.push :overview, { :controller => 'projects', :action => 'show' }
228 226 menu.push :activity, { :controller => 'activities', :action => 'index' }
229 227 menu.push :roadmap, { :controller => 'versions', :action => 'index' }, :param => :project_id,
230 228 :if => Proc.new { |p| p.shared_versions.any? }
231 229 menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
232 230 menu.push :new_issue, { :controller => 'issues', :action => 'new', :copy_from => nil }, :param => :project_id, :caption => :label_issue_new,
233 231 :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) },
234 232 :if => Proc.new { |p| p.trackers.any? },
235 233 :permission => :add_issues
236 234 menu.push :gantt, { :controller => 'gantts', :action => 'show' }, :param => :project_id, :caption => :label_gantt
237 235 menu.push :calendar, { :controller => 'calendars', :action => 'show' }, :param => :project_id, :caption => :label_calendar
238 236 menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural
239 237 menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural
240 238 menu.push :wiki, { :controller => 'wiki', :action => 'show', :id => nil }, :param => :project_id,
241 239 :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
242 240 menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
243 241 :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
244 242 menu.push :files, { :controller => 'files', :action => 'index' }, :caption => :label_file_plural, :param => :project_id
245 243 menu.push :repository, { :controller => 'repositories', :action => 'show', :repository_id => nil, :path => nil, :rev => nil },
246 244 :if => Proc.new { |p| p.repository && !p.repository.new_record? }
247 245 menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true
248 246 end
249 247
250 248 Redmine::Activity.map do |activity|
251 249 activity.register :issues, :class_name => %w(Issue Journal)
252 250 activity.register :changesets
253 251 activity.register :news
254 252 activity.register :documents, :class_name => %w(Document Attachment)
255 253 activity.register :files, :class_name => 'Attachment'
256 254 activity.register :wiki_edits, :class_name => 'WikiContent::Version', :default => false
257 255 activity.register :messages, :default => false
258 256 activity.register :time_entries, :default => false
259 257 end
260 258
261 259 Redmine::Search.map do |search|
262 260 search.register :issues
263 261 search.register :news
264 262 search.register :documents
265 263 search.register :changesets
266 264 search.register :wiki_pages
267 265 search.register :messages
268 266 search.register :projects
269 267 end
270 268
271 269 Redmine::WikiFormatting.map do |format|
272 270 format.register :textile, Redmine::WikiFormatting::Textile::Formatter, Redmine::WikiFormatting::Textile::Helper
273 271 if Object.const_defined?(:Redcarpet)
274 272 format.register :markdown, Redmine::WikiFormatting::Markdown::Formatter, Redmine::WikiFormatting::Markdown::Helper,
275 273 :label => 'Markdown'
276 274 end
277 275 end
278 276
279 277 ActionView::Template.register_template_handler :rsb, Redmine::Views::ApiTemplateHandler
General Comments 0
You need to be logged in to leave comments. Login now