##// END OF EJS Templates
Adds support for macro and Redmine links in PDF export (#13051)....
Jean-Philippe Lang -
r13562:7a0432d85c01
parent child
Show More
@@ -0,0 +1,1
1 <%= raw issues_to_pdf(@issues, @project, @query) %> No newline at end of file
@@ -0,0 +1,1
1 <%= raw issue_to_pdf(@issue, :journals => @journals) %> No newline at end of file
@@ -0,0 +1,1
1 <%= raw wiki_pages_to_pdf(@pages, @project) %> No newline at end of file
@@ -0,0 +1,1
1 <%= raw wiki_page_to_pdf(@page, @project) %> No newline at end of file
This diff has been collapsed as it changes many lines, (508 lines changed) Show them Hide them
@@ -0,0 +1,508
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 module Redmine
21 module Export
22 module PDF
23 module IssuesPdfHelper
24 # Returns a PDF string of a single issue
25 def issue_to_pdf(issue, assoc={})
26 pdf = ITCPDF.new(current_language)
27 pdf.set_title("#{issue.project} - #{issue.tracker} ##{issue.id}")
28 pdf.alias_nb_pages
29 pdf.footer_date = format_date(Date.today)
30 pdf.add_page
31 pdf.SetFontStyle('B',11)
32 buf = "#{issue.project} - #{issue.tracker} ##{issue.id}"
33 pdf.RDMMultiCell(190, 5, buf)
34 pdf.SetFontStyle('',8)
35 base_x = pdf.get_x
36 i = 1
37 issue.ancestors.visible.each do |ancestor|
38 pdf.set_x(base_x + i)
39 buf = "#{ancestor.tracker} # #{ancestor.id} (#{ancestor.status.to_s}): #{ancestor.subject}"
40 pdf.RDMMultiCell(190 - i, 5, buf)
41 i += 1 if i < 35
42 end
43 pdf.SetFontStyle('B',11)
44 pdf.RDMMultiCell(190 - i, 5, issue.subject.to_s)
45 pdf.SetFontStyle('',8)
46 pdf.RDMMultiCell(190, 5, "#{format_time(issue.created_on)} - #{issue.author}")
47 pdf.ln
48
49 left = []
50 left << [l(:field_status), issue.status]
51 left << [l(:field_priority), issue.priority]
52 left << [l(:field_assigned_to), issue.assigned_to] unless issue.disabled_core_fields.include?('assigned_to_id')
53 left << [l(:field_category), issue.category] unless issue.disabled_core_fields.include?('category_id')
54 left << [l(:field_fixed_version), issue.fixed_version] unless issue.disabled_core_fields.include?('fixed_version_id')
55
56 right = []
57 right << [l(:field_start_date), format_date(issue.start_date)] unless issue.disabled_core_fields.include?('start_date')
58 right << [l(:field_due_date), format_date(issue.due_date)] unless issue.disabled_core_fields.include?('due_date')
59 right << [l(:field_done_ratio), "#{issue.done_ratio}%"] unless issue.disabled_core_fields.include?('done_ratio')
60 right << [l(:field_estimated_hours), l_hours(issue.estimated_hours)] unless issue.disabled_core_fields.include?('estimated_hours')
61 right << [l(:label_spent_time), l_hours(issue.total_spent_hours)] if User.current.allowed_to?(:view_time_entries, issue.project)
62
63 rows = left.size > right.size ? left.size : right.size
64 while left.size < rows
65 left << nil
66 end
67 while right.size < rows
68 right << nil
69 end
70
71 half = (issue.visible_custom_field_values.size / 2.0).ceil
72 issue.visible_custom_field_values.each_with_index do |custom_value, i|
73 (i < half ? left : right) << [custom_value.custom_field.name, show_value(custom_value, false)]
74 end
75
76 if pdf.get_rtl
77 border_first_top = 'RT'
78 border_last_top = 'LT'
79 border_first = 'R'
80 border_last = 'L'
81 else
82 border_first_top = 'LT'
83 border_last_top = 'RT'
84 border_first = 'L'
85 border_last = 'R'
86 end
87
88 rows = left.size > right.size ? left.size : right.size
89 rows.times do |i|
90 heights = []
91 pdf.SetFontStyle('B',9)
92 item = left[i]
93 heights << pdf.get_string_height(35, item ? "#{item.first}:" : "")
94 item = right[i]
95 heights << pdf.get_string_height(35, item ? "#{item.first}:" : "")
96 pdf.SetFontStyle('',9)
97 item = left[i]
98 heights << pdf.get_string_height(60, item ? item.last.to_s : "")
99 item = right[i]
100 heights << pdf.get_string_height(60, item ? item.last.to_s : "")
101 height = heights.max
102
103 item = left[i]
104 pdf.SetFontStyle('B',9)
105 pdf.RDMMultiCell(35, height, item ? "#{item.first}:" : "", (i == 0 ? border_first_top : border_first), '', 0, 0)
106 pdf.SetFontStyle('',9)
107 pdf.RDMMultiCell(60, height, item ? item.last.to_s : "", (i == 0 ? border_last_top : border_last), '', 0, 0)
108
109 item = right[i]
110 pdf.SetFontStyle('B',9)
111 pdf.RDMMultiCell(35, height, item ? "#{item.first}:" : "", (i == 0 ? border_first_top : border_first), '', 0, 0)
112 pdf.SetFontStyle('',9)
113 pdf.RDMMultiCell(60, height, item ? item.last.to_s : "", (i == 0 ? border_last_top : border_last), '', 0, 2)
114
115 pdf.set_x(base_x)
116 end
117
118 pdf.SetFontStyle('B',9)
119 pdf.RDMCell(35+155, 5, l(:field_description), "LRT", 1)
120 pdf.SetFontStyle('',9)
121
122 # Set resize image scale
123 pdf.set_image_scale(1.6)
124 pdf.RDMwriteHTMLCell(35+155, 5, '', '',
125 issue.description.to_s, issue.attachments, "LRB")
126
127 unless issue.leaf?
128 truncate_length = (!is_cjk? ? 90 : 65)
129 pdf.SetFontStyle('B',9)
130 pdf.RDMCell(35+155,5, l(:label_subtask_plural) + ":", "LTR")
131 pdf.ln
132 issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level|
133 buf = "#{child.tracker} # #{child.id}: #{child.subject}".
134 truncate(truncate_length)
135 level = 10 if level >= 10
136 pdf.SetFontStyle('',8)
137 pdf.RDMCell(35+135,5, (level >=1 ? " " * level : "") + buf, border_first)
138 pdf.SetFontStyle('B',8)
139 pdf.RDMCell(20,5, child.status.to_s, border_last)
140 pdf.ln
141 end
142 end
143
144 relations = issue.relations.select { |r| r.other_issue(issue).visible? }
145 unless relations.empty?
146 truncate_length = (!is_cjk? ? 80 : 60)
147 pdf.SetFontStyle('B',9)
148 pdf.RDMCell(35+155,5, l(:label_related_issues) + ":", "LTR")
149 pdf.ln
150 relations.each do |relation|
151 buf = relation.to_s(issue) {|other|
152 text = ""
153 if Setting.cross_project_issue_relations?
154 text += "#{relation.other_issue(issue).project} - "
155 end
156 text += "#{other.tracker} ##{other.id}: #{other.subject}"
157 text
158 }
159 buf = buf.truncate(truncate_length)
160 pdf.SetFontStyle('', 8)
161 pdf.RDMCell(35+155-60, 5, buf, border_first)
162 pdf.SetFontStyle('B',8)
163 pdf.RDMCell(20,5, relation.other_issue(issue).status.to_s, "")
164 pdf.RDMCell(20,5, format_date(relation.other_issue(issue).start_date), "")
165 pdf.RDMCell(20,5, format_date(relation.other_issue(issue).due_date), border_last)
166 pdf.ln
167 end
168 end
169 pdf.RDMCell(190,5, "", "T")
170 pdf.ln
171
172 if issue.changesets.any? &&
173 User.current.allowed_to?(:view_changesets, issue.project)
174 pdf.SetFontStyle('B',9)
175 pdf.RDMCell(190,5, l(:label_associated_revisions), "B")
176 pdf.ln
177 for changeset in issue.changesets
178 pdf.SetFontStyle('B',8)
179 csstr = "#{l(:label_revision)} #{changeset.format_identifier} - "
180 csstr += format_time(changeset.committed_on) + " - " + changeset.author.to_s
181 pdf.RDMCell(190, 5, csstr)
182 pdf.ln
183 unless changeset.comments.blank?
184 pdf.SetFontStyle('',8)
185 pdf.RDMwriteHTMLCell(190,5,'','',
186 changeset.comments.to_s, issue.attachments, "")
187 end
188 pdf.ln
189 end
190 end
191
192 if assoc[:journals].present?
193 pdf.SetFontStyle('B',9)
194 pdf.RDMCell(190,5, l(:label_history), "B")
195 pdf.ln
196 assoc[:journals].each do |journal|
197 pdf.SetFontStyle('B',8)
198 title = "##{journal.indice} - #{format_time(journal.created_on)} - #{journal.user}"
199 title << " (#{l(:field_private_notes)})" if journal.private_notes?
200 pdf.RDMCell(190,5, title)
201 pdf.ln
202 pdf.SetFontStyle('I',8)
203 details_to_strings(journal.visible_details, true).each do |string|
204 pdf.RDMMultiCell(190,5, "- " + string)
205 end
206 if journal.notes?
207 pdf.ln unless journal.details.empty?
208 pdf.SetFontStyle('',8)
209 pdf.RDMwriteHTMLCell(190,5,'','',
210 journal.notes.to_s, issue.attachments, "")
211 end
212 pdf.ln
213 end
214 end
215
216 if issue.attachments.any?
217 pdf.SetFontStyle('B',9)
218 pdf.RDMCell(190,5, l(:label_attachment_plural), "B")
219 pdf.ln
220 for attachment in issue.attachments
221 pdf.SetFontStyle('',8)
222 pdf.RDMCell(80,5, attachment.filename)
223 pdf.RDMCell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
224 pdf.RDMCell(25,5, format_date(attachment.created_on),0,0,"R")
225 pdf.RDMCell(65,5, attachment.author.name,0,0,"R")
226 pdf.ln
227 end
228 end
229 pdf.output
230 end
231
232 # Returns a PDF string of a list of issues
233 def issues_to_pdf(issues, project, query)
234 pdf = ITCPDF.new(current_language, "L")
235 title = query.new_record? ? l(:label_issue_plural) : query.name
236 title = "#{project} - #{title}" if project
237 pdf.set_title(title)
238 pdf.alias_nb_pages
239 pdf.footer_date = format_date(Date.today)
240 pdf.set_auto_page_break(false)
241 pdf.add_page("L")
242
243 # Landscape A4 = 210 x 297 mm
244 page_height = pdf.get_page_height # 210
245 page_width = pdf.get_page_width # 297
246 left_margin = pdf.get_original_margins['left'] # 10
247 right_margin = pdf.get_original_margins['right'] # 10
248 bottom_margin = pdf.get_footer_margin
249 row_height = 4
250
251 # column widths
252 table_width = page_width - right_margin - left_margin
253 col_width = []
254 unless query.inline_columns.empty?
255 col_width = calc_col_width(issues, query, table_width, pdf)
256 table_width = col_width.inject(0, :+)
257 end
258
259 # use full width if the description is displayed
260 if table_width > 0 && query.has_column?(:description)
261 col_width = col_width.map {|w| w * (page_width - right_margin - left_margin) / table_width}
262 table_width = col_width.inject(0, :+)
263 end
264
265 # title
266 pdf.SetFontStyle('B',11)
267 pdf.RDMCell(190,10, title)
268 pdf.ln
269
270 render_table_header(pdf, query, col_width, row_height, table_width)
271 previous_group = false
272 issue_list(issues) do |issue, level|
273 if query.grouped? &&
274 (group = query.group_by_column.value(issue)) != previous_group
275 pdf.SetFontStyle('B',10)
276 group_label = group.blank? ? 'None' : group.to_s.dup
277 group_label << " (#{query.issue_count_by_group[group]})"
278 pdf.bookmark group_label, 0, -1
279 pdf.RDMCell(table_width, row_height * 2, group_label, 1, 1, 'L')
280 pdf.SetFontStyle('',8)
281 previous_group = group
282 end
283
284 # fetch row values
285 col_values = fetch_row_values(issue, query, level)
286
287 # make new page if it doesn't fit on the current one
288 base_y = pdf.get_y
289 max_height = get_issues_to_pdf_write_cells(pdf, col_values, col_width)
290 space_left = page_height - base_y - bottom_margin
291 if max_height > space_left
292 pdf.add_page("L")
293 render_table_header(pdf, query, col_width, row_height, table_width)
294 base_y = pdf.get_y
295 end
296
297 # write the cells on page
298 issues_to_pdf_write_cells(pdf, col_values, col_width, max_height)
299 pdf.set_y(base_y + max_height)
300
301 if query.has_column?(:description) && issue.description?
302 pdf.set_x(10)
303 pdf.set_auto_page_break(true, bottom_margin)
304 pdf.RDMwriteHTMLCell(0, 5, 10, '', issue.description.to_s, issue.attachments, "LRBT")
305 pdf.set_auto_page_break(false)
306 end
307 end
308
309 if issues.size == Setting.issues_export_limit.to_i
310 pdf.SetFontStyle('B',10)
311 pdf.RDMCell(0, row_height, '...')
312 end
313 pdf.output
314 end
315
316 def is_cjk?
317 case current_language.to_s.downcase
318 when 'ja', 'zh-tw', 'zh', 'ko'
319 true
320 else
321 false
322 end
323 end
324
325 # fetch row values
326 def fetch_row_values(issue, query, level)
327 query.inline_columns.collect do |column|
328 s = if column.is_a?(QueryCustomFieldColumn)
329 cv = issue.visible_custom_field_values.detect {|v| v.custom_field_id == column.custom_field.id}
330 show_value(cv, false)
331 else
332 value = issue.send(column.name)
333 if column.name == :subject
334 value = " " * level + value
335 end
336 if value.is_a?(Date)
337 format_date(value)
338 elsif value.is_a?(Time)
339 format_time(value)
340 else
341 value
342 end
343 end
344 s.to_s
345 end
346 end
347
348 # calculate columns width
349 def calc_col_width(issues, query, table_width, pdf)
350 # calculate statistics
351 # by captions
352 pdf.SetFontStyle('B',8)
353 margins = pdf.get_margins
354 col_padding = margins['cell']
355 col_width_min = query.inline_columns.map {|v| pdf.get_string_width(v.caption) + col_padding}
356 col_width_max = Array.new(col_width_min)
357 col_width_avg = Array.new(col_width_min)
358 col_min = pdf.get_string_width('OO') + col_padding * 2
359 if table_width > col_min * col_width_avg.length
360 table_width -= col_min * col_width_avg.length
361 else
362 col_min = pdf.get_string_width('O') + col_padding * 2
363 if table_width > col_min * col_width_avg.length
364 table_width -= col_min * col_width_avg.length
365 else
366 ratio = table_width / col_width_avg.inject(0, :+)
367 return col_width = col_width_avg.map {|w| w * ratio}
368 end
369 end
370 word_width_max = query.inline_columns.map {|c|
371 n = 10
372 c.caption.split.each {|w|
373 x = pdf.get_string_width(w) + col_padding
374 n = x if n < x
375 }
376 n
377 }
378
379 # by properties of issues
380 pdf.SetFontStyle('',8)
381 k = 1
382 issue_list(issues) {|issue, level|
383 k += 1
384 values = fetch_row_values(issue, query, level)
385 values.each_with_index {|v,i|
386 n = pdf.get_string_width(v) + col_padding * 2
387 col_width_max[i] = n if col_width_max[i] < n
388 col_width_min[i] = n if col_width_min[i] > n
389 col_width_avg[i] += n
390 v.split.each {|w|
391 x = pdf.get_string_width(w) + col_padding
392 word_width_max[i] = x if word_width_max[i] < x
393 }
394 }
395 }
396 col_width_avg.map! {|x| x / k}
397
398 # calculate columns width
399 ratio = table_width / col_width_avg.inject(0, :+)
400 col_width = col_width_avg.map {|w| w * ratio}
401
402 # correct max word width if too many columns
403 ratio = table_width / word_width_max.inject(0, :+)
404 word_width_max.map! {|v| v * ratio} if ratio < 1
405
406 # correct and lock width of some columns
407 done = 1
408 col_fix = []
409 col_width.each_with_index do |w,i|
410 if w > col_width_max[i]
411 col_width[i] = col_width_max[i]
412 col_fix[i] = 1
413 done = 0
414 elsif w < word_width_max[i]
415 col_width[i] = word_width_max[i]
416 col_fix[i] = 1
417 done = 0
418 else
419 col_fix[i] = 0
420 end
421 end
422
423 # iterate while need to correct and lock coluns width
424 while done == 0
425 # calculate free & locked columns width
426 done = 1
427 ratio = table_width / col_width.inject(0, :+)
428
429 # correct columns width
430 col_width.each_with_index do |w,i|
431 if col_fix[i] == 0
432 col_width[i] = w * ratio
433
434 # check if column width less then max word width
435 if col_width[i] < word_width_max[i]
436 col_width[i] = word_width_max[i]
437 col_fix[i] = 1
438 done = 0
439 elsif col_width[i] > col_width_max[i]
440 col_width[i] = col_width_max[i]
441 col_fix[i] = 1
442 done = 0
443 end
444 end
445 end
446 end
447
448 ratio = table_width / col_width.inject(0, :+)
449 col_width.map! {|v| v * ratio + col_min}
450 col_width
451 end
452
453 def render_table_header(pdf, query, col_width, row_height, table_width)
454 # headers
455 pdf.SetFontStyle('B',8)
456 pdf.set_fill_color(230, 230, 230)
457
458 base_x = pdf.get_x
459 base_y = pdf.get_y
460 max_height = get_issues_to_pdf_write_cells(pdf, query.inline_columns, col_width, true)
461
462 # write the cells on page
463 issues_to_pdf_write_cells(pdf, query.inline_columns, col_width, max_height, true)
464 pdf.set_xy(base_x, base_y + max_height)
465
466 # rows
467 pdf.SetFontStyle('',8)
468 pdf.set_fill_color(255, 255, 255)
469 end
470
471 # returns the maximum height of MultiCells
472 def get_issues_to_pdf_write_cells(pdf, col_values, col_widths, head=false)
473 heights = []
474 col_values.each_with_index do |column, i|
475 heights << pdf.get_string_height(col_widths[i], head ? column.caption : column)
476 end
477 return heights.max
478 end
479
480 # Renders MultiCells and returns the maximum height used
481 def issues_to_pdf_write_cells(pdf, col_values, col_widths, row_height, head=false)
482 col_values.each_with_index do |column, i|
483 pdf.RDMMultiCell(col_widths[i], row_height, head ? column.caption : column.strip, 1, '', 1, 0)
484 end
485 end
486
487 # Draw lines to close the row (MultiCell border drawing in not uniform)
488 #
489 # parameter "col_id_width" is not used. it is kept for compatibility.
490 def issues_to_pdf_draw_borders(pdf, top_x, top_y, lower_y,
491 col_id_width, col_widths, rtl=false)
492 col_x = top_x
493 pdf.line(col_x, top_y, col_x, lower_y) # id right border
494 col_widths.each do |width|
495 if rtl
496 col_x -= width
497 else
498 col_x += width
499 end
500 pdf.line(col_x, top_y, col_x, lower_y) # columns right border
501 end
502 pdf.line(top_x, top_y, top_x, lower_y) # left border
503 pdf.line(top_x, lower_y, col_x, lower_y) # bottom border
504 end
505 end
506 end
507 end
508 end
@@ -0,0 +1,100
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 module Redmine
21 module Export
22 module PDF
23 module WikiPdfHelper
24 # Returns a PDF string of a set of wiki pages
25 def wiki_pages_to_pdf(pages, project)
26 pdf = Redmine::Export::PDF::ITCPDF.new(current_language)
27 pdf.set_title(project.name)
28 pdf.alias_nb_pages
29 pdf.footer_date = format_date(Date.today)
30 pdf.add_page
31 pdf.SetFontStyle('B',11)
32 pdf.RDMMultiCell(190,5, project.name)
33 pdf.ln
34 # Set resize image scale
35 pdf.set_image_scale(1.6)
36 pdf.SetFontStyle('',9)
37 write_page_hierarchy(pdf, pages.group_by(&:parent_id))
38 pdf.output
39 end
40
41 # Returns a PDF string of a single wiki page
42 def wiki_page_to_pdf(page, project)
43 pdf = ITCPDF.new(current_language)
44 pdf.set_title("#{project} - #{page.title}")
45 pdf.alias_nb_pages
46 pdf.footer_date = format_date(Date.today)
47 pdf.add_page
48 pdf.SetFontStyle('B',11)
49 pdf.RDMMultiCell(190,5,
50 "#{project} - #{page.title} - # #{page.content.version}")
51 pdf.ln
52 # Set resize image scale
53 pdf.set_image_scale(1.6)
54 pdf.SetFontStyle('',9)
55 write_wiki_page(pdf, page)
56 pdf.output
57 end
58
59 def write_page_hierarchy(pdf, pages, node=nil, level=0)
60 if pages[node]
61 pages[node].each do |page|
62 if @new_page
63 pdf.add_page
64 else
65 @new_page = true
66 end
67 pdf.bookmark page.title, level
68 write_wiki_page(pdf, page)
69 write_page_hierarchy(pdf, pages, page.id, level + 1) if pages[page.id]
70 end
71 end
72 end
73
74 def write_wiki_page(pdf, page)
75 text = textilizable(page.content, :text,
76 :only_path => false,
77 :edit_section_links => false,
78 :headings => false,
79 :inline_attachments => false
80 )
81 pdf.RDMwriteFormattedCell(190,5,'','', text, page.attachments, 0)
82 if page.attachments.any?
83 pdf.ln(5)
84 pdf.SetFontStyle('B',9)
85 pdf.RDMCell(190,5, l(:label_attachment_plural), "B")
86 pdf.ln
87 for attachment in page.attachments
88 pdf.SetFontStyle('',8)
89 pdf.RDMCell(80,5, attachment.filename)
90 pdf.RDMCell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
91 pdf.RDMCell(25,5, format_date(attachment.created_on),0,0,"R")
92 pdf.RDMCell(65,5, attachment.author.name,0,0,"R")
93 pdf.ln
94 end
95 end
96 end
97 end
98 end
99 end
100 end
@@ -1,502 +1,500
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class IssuesController < ApplicationController
18 class IssuesController < ApplicationController
19 menu_item :new_issue, :only => [:new, :create]
19 menu_item :new_issue, :only => [:new, :create]
20 default_search_scope :issues
20 default_search_scope :issues
21
21
22 before_filter :find_issue, :only => [:show, :edit, :update]
22 before_filter :find_issue, :only => [:show, :edit, :update]
23 before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :destroy]
23 before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :destroy]
24 before_filter :find_project, :only => [:new, :create, :update_form]
24 before_filter :find_project, :only => [:new, :create, :update_form]
25 before_filter :authorize, :except => [:index]
25 before_filter :authorize, :except => [:index]
26 before_filter :find_optional_project, :only => [:index]
26 before_filter :find_optional_project, :only => [:index]
27 before_filter :build_new_issue_from_params, :only => [:new, :create, :update_form]
27 before_filter :build_new_issue_from_params, :only => [:new, :create, :update_form]
28 accept_rss_auth :index, :show
28 accept_rss_auth :index, :show
29 accept_api_auth :index, :show, :create, :update, :destroy
29 accept_api_auth :index, :show, :create, :update, :destroy
30
30
31 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
31 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
32
32
33 helper :journals
33 helper :journals
34 helper :projects
34 helper :projects
35 include ProjectsHelper
35 include ProjectsHelper
36 helper :custom_fields
36 helper :custom_fields
37 include CustomFieldsHelper
37 include CustomFieldsHelper
38 helper :issue_relations
38 helper :issue_relations
39 include IssueRelationsHelper
39 include IssueRelationsHelper
40 helper :watchers
40 helper :watchers
41 include WatchersHelper
41 include WatchersHelper
42 helper :attachments
42 helper :attachments
43 include AttachmentsHelper
43 include AttachmentsHelper
44 helper :queries
44 helper :queries
45 include QueriesHelper
45 include QueriesHelper
46 helper :repositories
46 helper :repositories
47 include RepositoriesHelper
47 include RepositoriesHelper
48 helper :sort
48 helper :sort
49 include SortHelper
49 include SortHelper
50 include IssuesHelper
50 include IssuesHelper
51 helper :timelog
51 helper :timelog
52 include Redmine::Export::PDF
53
52
54 def index
53 def index
55 retrieve_query
54 retrieve_query
56 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
55 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
57 sort_update(@query.sortable_columns)
56 sort_update(@query.sortable_columns)
58 @query.sort_criteria = sort_criteria.to_a
57 @query.sort_criteria = sort_criteria.to_a
59
58
60 if @query.valid?
59 if @query.valid?
61 case params[:format]
60 case params[:format]
62 when 'csv', 'pdf'
61 when 'csv', 'pdf'
63 @limit = Setting.issues_export_limit.to_i
62 @limit = Setting.issues_export_limit.to_i
64 if params[:columns] == 'all'
63 if params[:columns] == 'all'
65 @query.column_names = @query.available_inline_columns.map(&:name)
64 @query.column_names = @query.available_inline_columns.map(&:name)
66 end
65 end
67 when 'atom'
66 when 'atom'
68 @limit = Setting.feeds_limit.to_i
67 @limit = Setting.feeds_limit.to_i
69 when 'xml', 'json'
68 when 'xml', 'json'
70 @offset, @limit = api_offset_and_limit
69 @offset, @limit = api_offset_and_limit
71 @query.column_names = %w(author)
70 @query.column_names = %w(author)
72 else
71 else
73 @limit = per_page_option
72 @limit = per_page_option
74 end
73 end
75
74
76 @issue_count = @query.issue_count
75 @issue_count = @query.issue_count
77 @issue_pages = Paginator.new @issue_count, @limit, params['page']
76 @issue_pages = Paginator.new @issue_count, @limit, params['page']
78 @offset ||= @issue_pages.offset
77 @offset ||= @issue_pages.offset
79 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
78 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
80 :order => sort_clause,
79 :order => sort_clause,
81 :offset => @offset,
80 :offset => @offset,
82 :limit => @limit)
81 :limit => @limit)
83 @issue_count_by_group = @query.issue_count_by_group
82 @issue_count_by_group = @query.issue_count_by_group
84
83
85 respond_to do |format|
84 respond_to do |format|
86 format.html { render :template => 'issues/index', :layout => !request.xhr? }
85 format.html { render :template => 'issues/index', :layout => !request.xhr? }
87 format.api {
86 format.api {
88 Issue.load_visible_relations(@issues) if include_in_api_response?('relations')
87 Issue.load_visible_relations(@issues) if include_in_api_response?('relations')
89 }
88 }
90 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
89 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
91 format.csv { send_data(query_to_csv(@issues, @query, params), :type => 'text/csv; header=present', :filename => 'issues.csv') }
90 format.csv { send_data(query_to_csv(@issues, @query, params), :type => 'text/csv; header=present', :filename => 'issues.csv') }
92 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'issues.pdf') }
91 format.pdf { send_file_headers! :type => 'application/pdf', :filename => 'issues.pdf' }
93 end
92 end
94 else
93 else
95 respond_to do |format|
94 respond_to do |format|
96 format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
95 format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
97 format.any(:atom, :csv, :pdf) { render(:nothing => true) }
96 format.any(:atom, :csv, :pdf) { render(:nothing => true) }
98 format.api { render_validation_errors(@query) }
97 format.api { render_validation_errors(@query) }
99 end
98 end
100 end
99 end
101 rescue ActiveRecord::RecordNotFound
100 rescue ActiveRecord::RecordNotFound
102 render_404
101 render_404
103 end
102 end
104
103
105 def show
104 def show
106 @journals = @issue.journals.includes(:user, :details).
105 @journals = @issue.journals.includes(:user, :details).
107 references(:user, :details).
106 references(:user, :details).
108 reorder("#{Journal.table_name}.id ASC").to_a
107 reorder("#{Journal.table_name}.id ASC").to_a
109 @journals.each_with_index {|j,i| j.indice = i+1}
108 @journals.each_with_index {|j,i| j.indice = i+1}
110 @journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
109 @journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
111 Journal.preload_journals_details_custom_fields(@journals)
110 Journal.preload_journals_details_custom_fields(@journals)
112 @journals.select! {|journal| journal.notes? || journal.visible_details.any?}
111 @journals.select! {|journal| journal.notes? || journal.visible_details.any?}
113 @journals.reverse! if User.current.wants_comments_in_reverse_order?
112 @journals.reverse! if User.current.wants_comments_in_reverse_order?
114
113
115 @changesets = @issue.changesets.visible.to_a
114 @changesets = @issue.changesets.visible.to_a
116 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
115 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
117
116
118 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
117 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
119 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
118 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
120 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
119 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
121 @priorities = IssuePriority.active
120 @priorities = IssuePriority.active
122 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
121 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
123 @relation = IssueRelation.new
122 @relation = IssueRelation.new
124
123
125 respond_to do |format|
124 respond_to do |format|
126 format.html {
125 format.html {
127 retrieve_previous_and_next_issue_ids
126 retrieve_previous_and_next_issue_ids
128 render :template => 'issues/show'
127 render :template => 'issues/show'
129 }
128 }
130 format.api
129 format.api
131 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
130 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
132 format.pdf {
131 format.pdf {
133 pdf = issue_to_pdf(@issue, :journals => @journals)
132 send_file_headers! :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf"
134 send_data(pdf, :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf")
135 }
133 }
136 end
134 end
137 end
135 end
138
136
139 # Add a new issue
137 # Add a new issue
140 # The new issue will be created from an existing one if copy_from parameter is given
138 # The new issue will be created from an existing one if copy_from parameter is given
141 def new
139 def new
142 respond_to do |format|
140 respond_to do |format|
143 format.html { render :action => 'new', :layout => !request.xhr? }
141 format.html { render :action => 'new', :layout => !request.xhr? }
144 end
142 end
145 end
143 end
146
144
147 def create
145 def create
148 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
146 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
149 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
147 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
150 if @issue.save
148 if @issue.save
151 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
149 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
152 respond_to do |format|
150 respond_to do |format|
153 format.html {
151 format.html {
154 render_attachment_warning_if_needed(@issue)
152 render_attachment_warning_if_needed(@issue)
155 flash[:notice] = l(:notice_issue_successful_create, :id => view_context.link_to("##{@issue.id}", issue_path(@issue), :title => @issue.subject))
153 flash[:notice] = l(:notice_issue_successful_create, :id => view_context.link_to("##{@issue.id}", issue_path(@issue), :title => @issue.subject))
156 if params[:continue]
154 if params[:continue]
157 attrs = {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?}
155 attrs = {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?}
158 redirect_to new_project_issue_path(@issue.project, :issue => attrs)
156 redirect_to new_project_issue_path(@issue.project, :issue => attrs)
159 else
157 else
160 redirect_to issue_path(@issue)
158 redirect_to issue_path(@issue)
161 end
159 end
162 }
160 }
163 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
161 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
164 end
162 end
165 return
163 return
166 else
164 else
167 respond_to do |format|
165 respond_to do |format|
168 format.html { render :action => 'new' }
166 format.html { render :action => 'new' }
169 format.api { render_validation_errors(@issue) }
167 format.api { render_validation_errors(@issue) }
170 end
168 end
171 end
169 end
172 end
170 end
173
171
174 def edit
172 def edit
175 return unless update_issue_from_params
173 return unless update_issue_from_params
176
174
177 respond_to do |format|
175 respond_to do |format|
178 format.html { }
176 format.html { }
179 format.xml { }
177 format.xml { }
180 end
178 end
181 end
179 end
182
180
183 def update
181 def update
184 return unless update_issue_from_params
182 return unless update_issue_from_params
185 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
183 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
186 saved = false
184 saved = false
187 begin
185 begin
188 saved = save_issue_with_child_records
186 saved = save_issue_with_child_records
189 rescue ActiveRecord::StaleObjectError
187 rescue ActiveRecord::StaleObjectError
190 @conflict = true
188 @conflict = true
191 if params[:last_journal_id]
189 if params[:last_journal_id]
192 @conflict_journals = @issue.journals_after(params[:last_journal_id]).to_a
190 @conflict_journals = @issue.journals_after(params[:last_journal_id]).to_a
193 @conflict_journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
191 @conflict_journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
194 end
192 end
195 end
193 end
196
194
197 if saved
195 if saved
198 render_attachment_warning_if_needed(@issue)
196 render_attachment_warning_if_needed(@issue)
199 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
197 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
200
198
201 respond_to do |format|
199 respond_to do |format|
202 format.html { redirect_back_or_default issue_path(@issue) }
200 format.html { redirect_back_or_default issue_path(@issue) }
203 format.api { render_api_ok }
201 format.api { render_api_ok }
204 end
202 end
205 else
203 else
206 respond_to do |format|
204 respond_to do |format|
207 format.html { render :action => 'edit' }
205 format.html { render :action => 'edit' }
208 format.api { render_validation_errors(@issue) }
206 format.api { render_validation_errors(@issue) }
209 end
207 end
210 end
208 end
211 end
209 end
212
210
213 # Updates the issue form when changing the project, status or tracker
211 # Updates the issue form when changing the project, status or tracker
214 # on issue creation/update
212 # on issue creation/update
215 def update_form
213 def update_form
216 end
214 end
217
215
218 # Bulk edit/copy a set of issues
216 # Bulk edit/copy a set of issues
219 def bulk_edit
217 def bulk_edit
220 @issues.sort!
218 @issues.sort!
221 @copy = params[:copy].present?
219 @copy = params[:copy].present?
222 @notes = params[:notes]
220 @notes = params[:notes]
223
221
224 if User.current.allowed_to?(:move_issues, @projects)
222 if User.current.allowed_to?(:move_issues, @projects)
225 @allowed_projects = Issue.allowed_target_projects_on_move
223 @allowed_projects = Issue.allowed_target_projects_on_move
226 if params[:issue]
224 if params[:issue]
227 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id].to_s}
225 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id].to_s}
228 if @target_project
226 if @target_project
229 target_projects = [@target_project]
227 target_projects = [@target_project]
230 end
228 end
231 end
229 end
232 end
230 end
233 target_projects ||= @projects
231 target_projects ||= @projects
234
232
235 if @copy
233 if @copy
236 # Copied issues will get their default statuses
234 # Copied issues will get their default statuses
237 @available_statuses = []
235 @available_statuses = []
238 else
236 else
239 @available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
237 @available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
240 end
238 end
241 @custom_fields = target_projects.map{|p|p.all_issue_custom_fields.visible}.reduce(:&)
239 @custom_fields = target_projects.map{|p|p.all_issue_custom_fields.visible}.reduce(:&)
242 @assignables = target_projects.map(&:assignable_users).reduce(:&)
240 @assignables = target_projects.map(&:assignable_users).reduce(:&)
243 @trackers = target_projects.map(&:trackers).reduce(:&)
241 @trackers = target_projects.map(&:trackers).reduce(:&)
244 @versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
242 @versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
245 @categories = target_projects.map {|p| p.issue_categories}.reduce(:&)
243 @categories = target_projects.map {|p| p.issue_categories}.reduce(:&)
246 if @copy
244 if @copy
247 @attachments_present = @issues.detect {|i| i.attachments.any?}.present?
245 @attachments_present = @issues.detect {|i| i.attachments.any?}.present?
248 @subtasks_present = @issues.detect {|i| !i.leaf?}.present?
246 @subtasks_present = @issues.detect {|i| !i.leaf?}.present?
249 end
247 end
250
248
251 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
249 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
252
250
253 @issue_params = params[:issue] || {}
251 @issue_params = params[:issue] || {}
254 @issue_params[:custom_field_values] ||= {}
252 @issue_params[:custom_field_values] ||= {}
255 end
253 end
256
254
257 def bulk_update
255 def bulk_update
258 @issues.sort!
256 @issues.sort!
259 @copy = params[:copy].present?
257 @copy = params[:copy].present?
260 attributes = parse_params_for_bulk_issue_attributes(params)
258 attributes = parse_params_for_bulk_issue_attributes(params)
261
259
262 unsaved_issues = []
260 unsaved_issues = []
263 saved_issues = []
261 saved_issues = []
264
262
265 if @copy && params[:copy_subtasks].present?
263 if @copy && params[:copy_subtasks].present?
266 # Descendant issues will be copied with the parent task
264 # Descendant issues will be copied with the parent task
267 # Don't copy them twice
265 # Don't copy them twice
268 @issues.reject! {|issue| @issues.detect {|other| issue.is_descendant_of?(other)}}
266 @issues.reject! {|issue| @issues.detect {|other| issue.is_descendant_of?(other)}}
269 end
267 end
270
268
271 @issues.each do |orig_issue|
269 @issues.each do |orig_issue|
272 orig_issue.reload
270 orig_issue.reload
273 if @copy
271 if @copy
274 issue = orig_issue.copy({},
272 issue = orig_issue.copy({},
275 :attachments => params[:copy_attachments].present?,
273 :attachments => params[:copy_attachments].present?,
276 :subtasks => params[:copy_subtasks].present?,
274 :subtasks => params[:copy_subtasks].present?,
277 :link => link_copy?(params[:link_copy])
275 :link => link_copy?(params[:link_copy])
278 )
276 )
279 else
277 else
280 issue = orig_issue
278 issue = orig_issue
281 end
279 end
282 journal = issue.init_journal(User.current, params[:notes])
280 journal = issue.init_journal(User.current, params[:notes])
283 issue.safe_attributes = attributes
281 issue.safe_attributes = attributes
284 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
282 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
285 if issue.save
283 if issue.save
286 saved_issues << issue
284 saved_issues << issue
287 else
285 else
288 unsaved_issues << orig_issue
286 unsaved_issues << orig_issue
289 end
287 end
290 end
288 end
291
289
292 if unsaved_issues.empty?
290 if unsaved_issues.empty?
293 flash[:notice] = l(:notice_successful_update) unless saved_issues.empty?
291 flash[:notice] = l(:notice_successful_update) unless saved_issues.empty?
294 if params[:follow]
292 if params[:follow]
295 if @issues.size == 1 && saved_issues.size == 1
293 if @issues.size == 1 && saved_issues.size == 1
296 redirect_to issue_path(saved_issues.first)
294 redirect_to issue_path(saved_issues.first)
297 elsif saved_issues.map(&:project).uniq.size == 1
295 elsif saved_issues.map(&:project).uniq.size == 1
298 redirect_to project_issues_path(saved_issues.map(&:project).first)
296 redirect_to project_issues_path(saved_issues.map(&:project).first)
299 end
297 end
300 else
298 else
301 redirect_back_or_default _project_issues_path(@project)
299 redirect_back_or_default _project_issues_path(@project)
302 end
300 end
303 else
301 else
304 @saved_issues = @issues
302 @saved_issues = @issues
305 @unsaved_issues = unsaved_issues
303 @unsaved_issues = unsaved_issues
306 @issues = Issue.visible.where(:id => @unsaved_issues.map(&:id)).to_a
304 @issues = Issue.visible.where(:id => @unsaved_issues.map(&:id)).to_a
307 bulk_edit
305 bulk_edit
308 render :action => 'bulk_edit'
306 render :action => 'bulk_edit'
309 end
307 end
310 end
308 end
311
309
312 def destroy
310 def destroy
313 @hours = TimeEntry.where(:issue_id => @issues.map(&:id)).sum(:hours).to_f
311 @hours = TimeEntry.where(:issue_id => @issues.map(&:id)).sum(:hours).to_f
314 if @hours > 0
312 if @hours > 0
315 case params[:todo]
313 case params[:todo]
316 when 'destroy'
314 when 'destroy'
317 # nothing to do
315 # nothing to do
318 when 'nullify'
316 when 'nullify'
319 TimeEntry.where(['issue_id IN (?)', @issues]).update_all('issue_id = NULL')
317 TimeEntry.where(['issue_id IN (?)', @issues]).update_all('issue_id = NULL')
320 when 'reassign'
318 when 'reassign'
321 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
319 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
322 if reassign_to.nil?
320 if reassign_to.nil?
323 flash.now[:error] = l(:error_issue_not_found_in_project)
321 flash.now[:error] = l(:error_issue_not_found_in_project)
324 return
322 return
325 else
323 else
326 TimeEntry.where(['issue_id IN (?)', @issues]).
324 TimeEntry.where(['issue_id IN (?)', @issues]).
327 update_all("issue_id = #{reassign_to.id}")
325 update_all("issue_id = #{reassign_to.id}")
328 end
326 end
329 else
327 else
330 # display the destroy form if it's a user request
328 # display the destroy form if it's a user request
331 return unless api_request?
329 return unless api_request?
332 end
330 end
333 end
331 end
334 @issues.each do |issue|
332 @issues.each do |issue|
335 begin
333 begin
336 issue.reload.destroy
334 issue.reload.destroy
337 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
335 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
338 # nothing to do, issue was already deleted (eg. by a parent)
336 # nothing to do, issue was already deleted (eg. by a parent)
339 end
337 end
340 end
338 end
341 respond_to do |format|
339 respond_to do |format|
342 format.html { redirect_back_or_default _project_issues_path(@project) }
340 format.html { redirect_back_or_default _project_issues_path(@project) }
343 format.api { render_api_ok }
341 format.api { render_api_ok }
344 end
342 end
345 end
343 end
346
344
347 private
345 private
348
346
349 def find_project
347 def find_project
350 project_id = params[:project_id] || (params[:issue] && params[:issue][:project_id])
348 project_id = params[:project_id] || (params[:issue] && params[:issue][:project_id])
351 @project = Project.find(project_id)
349 @project = Project.find(project_id)
352 rescue ActiveRecord::RecordNotFound
350 rescue ActiveRecord::RecordNotFound
353 render_404
351 render_404
354 end
352 end
355
353
356 def retrieve_previous_and_next_issue_ids
354 def retrieve_previous_and_next_issue_ids
357 retrieve_query_from_session
355 retrieve_query_from_session
358 if @query
356 if @query
359 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
357 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
360 sort_update(@query.sortable_columns, 'issues_index_sort')
358 sort_update(@query.sortable_columns, 'issues_index_sort')
361 limit = 500
359 limit = 500
362 issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
360 issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
363 if (idx = issue_ids.index(@issue.id)) && idx < limit
361 if (idx = issue_ids.index(@issue.id)) && idx < limit
364 if issue_ids.size < 500
362 if issue_ids.size < 500
365 @issue_position = idx + 1
363 @issue_position = idx + 1
366 @issue_count = issue_ids.size
364 @issue_count = issue_ids.size
367 end
365 end
368 @prev_issue_id = issue_ids[idx - 1] if idx > 0
366 @prev_issue_id = issue_ids[idx - 1] if idx > 0
369 @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
367 @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
370 end
368 end
371 end
369 end
372 end
370 end
373
371
374 # Used by #edit and #update to set some common instance variables
372 # Used by #edit and #update to set some common instance variables
375 # from the params
373 # from the params
376 # TODO: Refactor, not everything in here is needed by #edit
374 # TODO: Refactor, not everything in here is needed by #edit
377 def update_issue_from_params
375 def update_issue_from_params
378 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
376 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
379 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
377 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
380 if params[:time_entry]
378 if params[:time_entry]
381 @time_entry.attributes = params[:time_entry]
379 @time_entry.attributes = params[:time_entry]
382 end
380 end
383
381
384 @issue.init_journal(User.current)
382 @issue.init_journal(User.current)
385
383
386 issue_attributes = params[:issue]
384 issue_attributes = params[:issue]
387 if issue_attributes && params[:conflict_resolution]
385 if issue_attributes && params[:conflict_resolution]
388 case params[:conflict_resolution]
386 case params[:conflict_resolution]
389 when 'overwrite'
387 when 'overwrite'
390 issue_attributes = issue_attributes.dup
388 issue_attributes = issue_attributes.dup
391 issue_attributes.delete(:lock_version)
389 issue_attributes.delete(:lock_version)
392 when 'add_notes'
390 when 'add_notes'
393 issue_attributes = issue_attributes.slice(:notes)
391 issue_attributes = issue_attributes.slice(:notes)
394 when 'cancel'
392 when 'cancel'
395 redirect_to issue_path(@issue)
393 redirect_to issue_path(@issue)
396 return false
394 return false
397 end
395 end
398 end
396 end
399 @issue.safe_attributes = issue_attributes
397 @issue.safe_attributes = issue_attributes
400 @priorities = IssuePriority.active
398 @priorities = IssuePriority.active
401 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
399 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
402 true
400 true
403 end
401 end
404
402
405 # TODO: Refactor, lots of extra code in here
403 # TODO: Refactor, lots of extra code in here
406 # TODO: Changing tracker on an existing issue should not trigger this
404 # TODO: Changing tracker on an existing issue should not trigger this
407 def build_new_issue_from_params
405 def build_new_issue_from_params
408 if params[:id].blank?
406 if params[:id].blank?
409 @issue = Issue.new
407 @issue = Issue.new
410 if params[:copy_from]
408 if params[:copy_from]
411 begin
409 begin
412 @issue.init_journal(User.current)
410 @issue.init_journal(User.current)
413 @copy_from = Issue.visible.find(params[:copy_from])
411 @copy_from = Issue.visible.find(params[:copy_from])
414 @link_copy = link_copy?(params[:link_copy]) || request.get?
412 @link_copy = link_copy?(params[:link_copy]) || request.get?
415 @copy_attachments = params[:copy_attachments].present? || request.get?
413 @copy_attachments = params[:copy_attachments].present? || request.get?
416 @copy_subtasks = params[:copy_subtasks].present? || request.get?
414 @copy_subtasks = params[:copy_subtasks].present? || request.get?
417 @issue.copy_from(@copy_from, :attachments => @copy_attachments, :subtasks => @copy_subtasks, :link => @link_copy)
415 @issue.copy_from(@copy_from, :attachments => @copy_attachments, :subtasks => @copy_subtasks, :link => @link_copy)
418 rescue ActiveRecord::RecordNotFound
416 rescue ActiveRecord::RecordNotFound
419 render_404
417 render_404
420 return
418 return
421 end
419 end
422 end
420 end
423 @issue.project = @project
421 @issue.project = @project
424 @issue.author ||= User.current
422 @issue.author ||= User.current
425 @issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
423 @issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
426 else
424 else
427 @issue = @project.issues.visible.find(params[:id])
425 @issue = @project.issues.visible.find(params[:id])
428 end
426 end
429
427
430 if attrs = params[:issue].deep_dup
428 if attrs = params[:issue].deep_dup
431 if params[:was_default_status] == attrs[:status_id]
429 if params[:was_default_status] == attrs[:status_id]
432 attrs.delete(:status_id)
430 attrs.delete(:status_id)
433 end
431 end
434 @issue.safe_attributes = attrs
432 @issue.safe_attributes = attrs
435 end
433 end
436 @issue.tracker ||= @project.trackers.first
434 @issue.tracker ||= @project.trackers.first
437 if @issue.tracker.nil?
435 if @issue.tracker.nil?
438 render_error l(:error_no_tracker_in_project)
436 render_error l(:error_no_tracker_in_project)
439 return false
437 return false
440 end
438 end
441 if @issue.status.nil?
439 if @issue.status.nil?
442 render_error l(:error_no_default_issue_status)
440 render_error l(:error_no_default_issue_status)
443 return false
441 return false
444 end
442 end
445
443
446 @priorities = IssuePriority.active
444 @priorities = IssuePriority.active
447 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, @issue.new_record?)
445 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, @issue.new_record?)
448 @available_watchers = @issue.watcher_users
446 @available_watchers = @issue.watcher_users
449 if @issue.project.users.count <= 20
447 if @issue.project.users.count <= 20
450 @available_watchers = (@available_watchers + @issue.project.users.sort).uniq
448 @available_watchers = (@available_watchers + @issue.project.users.sort).uniq
451 end
449 end
452 end
450 end
453
451
454 def parse_params_for_bulk_issue_attributes(params)
452 def parse_params_for_bulk_issue_attributes(params)
455 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
453 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
456 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
454 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
457 if custom = attributes[:custom_field_values]
455 if custom = attributes[:custom_field_values]
458 custom.reject! {|k,v| v.blank?}
456 custom.reject! {|k,v| v.blank?}
459 custom.keys.each do |k|
457 custom.keys.each do |k|
460 if custom[k].is_a?(Array)
458 if custom[k].is_a?(Array)
461 custom[k] << '' if custom[k].delete('__none__')
459 custom[k] << '' if custom[k].delete('__none__')
462 else
460 else
463 custom[k] = '' if custom[k] == '__none__'
461 custom[k] = '' if custom[k] == '__none__'
464 end
462 end
465 end
463 end
466 end
464 end
467 attributes
465 attributes
468 end
466 end
469
467
470 # Saves @issue and a time_entry from the parameters
468 # Saves @issue and a time_entry from the parameters
471 def save_issue_with_child_records
469 def save_issue_with_child_records
472 Issue.transaction do
470 Issue.transaction do
473 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, @issue.project)
471 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, @issue.project)
474 time_entry = @time_entry || TimeEntry.new
472 time_entry = @time_entry || TimeEntry.new
475 time_entry.project = @issue.project
473 time_entry.project = @issue.project
476 time_entry.issue = @issue
474 time_entry.issue = @issue
477 time_entry.user = User.current
475 time_entry.user = User.current
478 time_entry.spent_on = User.current.today
476 time_entry.spent_on = User.current.today
479 time_entry.attributes = params[:time_entry]
477 time_entry.attributes = params[:time_entry]
480 @issue.time_entries << time_entry
478 @issue.time_entries << time_entry
481 end
479 end
482
480
483 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
481 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
484 if @issue.save
482 if @issue.save
485 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
483 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
486 else
484 else
487 raise ActiveRecord::Rollback
485 raise ActiveRecord::Rollback
488 end
486 end
489 end
487 end
490 end
488 end
491
489
492 def link_copy?(param)
490 def link_copy?(param)
493 case Setting.link_copied_issue
491 case Setting.link_copied_issue
494 when 'yes'
492 when 'yes'
495 true
493 true
496 when 'no'
494 when 'no'
497 false
495 false
498 when 'ask'
496 when 'ask'
499 param == '1'
497 param == '1'
500 end
498 end
501 end
499 end
502 end
500 end
@@ -1,369 +1,367
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 # The WikiController follows the Rails REST controller pattern but with
18 # The WikiController follows the Rails REST controller pattern but with
19 # a few differences
19 # a few differences
20 #
20 #
21 # * index - shows a list of WikiPages grouped by page or date
21 # * index - shows a list of WikiPages grouped by page or date
22 # * new - not used
22 # * new - not used
23 # * create - not used
23 # * create - not used
24 # * show - will also show the form for creating a new wiki page
24 # * show - will also show the form for creating a new wiki page
25 # * edit - used to edit an existing or new page
25 # * edit - used to edit an existing or new page
26 # * update - used to save a wiki page update to the database, including new pages
26 # * update - used to save a wiki page update to the database, including new pages
27 # * destroy - normal
27 # * destroy - normal
28 #
28 #
29 # Other member and collection methods are also used
29 # Other member and collection methods are also used
30 #
30 #
31 # TODO: still being worked on
31 # TODO: still being worked on
32 class WikiController < ApplicationController
32 class WikiController < ApplicationController
33 default_search_scope :wiki_pages
33 default_search_scope :wiki_pages
34 before_filter :find_wiki, :authorize
34 before_filter :find_wiki, :authorize
35 before_filter :find_existing_or_new_page, :only => [:show, :edit, :update]
35 before_filter :find_existing_or_new_page, :only => [:show, :edit, :update]
36 before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy, :destroy_version]
36 before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy, :destroy_version]
37 accept_api_auth :index, :show, :update, :destroy
37 accept_api_auth :index, :show, :update, :destroy
38 before_filter :find_attachments, :only => [:preview]
38 before_filter :find_attachments, :only => [:preview]
39
39
40 helper :attachments
40 helper :attachments
41 include AttachmentsHelper
41 include AttachmentsHelper
42 helper :watchers
42 helper :watchers
43 include Redmine::Export::PDF
43 include Redmine::Export::PDF
44
44
45 # List of pages, sorted alphabetically and by parent (hierarchy)
45 # List of pages, sorted alphabetically and by parent (hierarchy)
46 def index
46 def index
47 load_pages_for_index
47 load_pages_for_index
48
48
49 respond_to do |format|
49 respond_to do |format|
50 format.html {
50 format.html {
51 @pages_by_parent_id = @pages.group_by(&:parent_id)
51 @pages_by_parent_id = @pages.group_by(&:parent_id)
52 }
52 }
53 format.api
53 format.api
54 end
54 end
55 end
55 end
56
56
57 # List of page, by last update
57 # List of page, by last update
58 def date_index
58 def date_index
59 load_pages_for_index
59 load_pages_for_index
60 @pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
60 @pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
61 end
61 end
62
62
63 # display a page (in editing mode if it doesn't exist)
63 # display a page (in editing mode if it doesn't exist)
64 def show
64 def show
65 if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
65 if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
66 deny_access
66 deny_access
67 return
67 return
68 end
68 end
69 @content = @page.content_for_version(params[:version])
69 @content = @page.content_for_version(params[:version])
70 if @content.nil?
70 if @content.nil?
71 if User.current.allowed_to?(:edit_wiki_pages, @project) && editable? && !api_request?
71 if User.current.allowed_to?(:edit_wiki_pages, @project) && editable? && !api_request?
72 edit
72 edit
73 render :action => 'edit'
73 render :action => 'edit'
74 else
74 else
75 render_404
75 render_404
76 end
76 end
77 return
77 return
78 end
78 end
79 if User.current.allowed_to?(:export_wiki_pages, @project)
79 if User.current.allowed_to?(:export_wiki_pages, @project)
80 if params[:format] == 'pdf'
80 if params[:format] == 'pdf'
81 send_data(wiki_page_to_pdf(@page, @project), :type => 'application/pdf', :filename => "#{@page.title}.pdf")
81 send_file_headers! :type => 'application/pdf', :filename => "#{@page.title}.pdf"
82 return
82 return
83 elsif params[:format] == 'html'
83 elsif params[:format] == 'html'
84 export = render_to_string :action => 'export', :layout => false
84 export = render_to_string :action => 'export', :layout => false
85 send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
85 send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
86 return
86 return
87 elsif params[:format] == 'txt'
87 elsif params[:format] == 'txt'
88 send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
88 send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
89 return
89 return
90 end
90 end
91 end
91 end
92 @editable = editable?
92 @editable = editable?
93 @sections_editable = @editable && User.current.allowed_to?(:edit_wiki_pages, @page.project) &&
93 @sections_editable = @editable && User.current.allowed_to?(:edit_wiki_pages, @page.project) &&
94 @content.current_version? &&
94 @content.current_version? &&
95 Redmine::WikiFormatting.supports_section_edit?
95 Redmine::WikiFormatting.supports_section_edit?
96
96
97 respond_to do |format|
97 respond_to do |format|
98 format.html
98 format.html
99 format.api
99 format.api
100 end
100 end
101 end
101 end
102
102
103 # edit an existing page or a new one
103 # edit an existing page or a new one
104 def edit
104 def edit
105 return render_403 unless editable?
105 return render_403 unless editable?
106 if @page.new_record?
106 if @page.new_record?
107 if params[:parent].present?
107 if params[:parent].present?
108 @page.parent = @page.wiki.find_page(params[:parent].to_s)
108 @page.parent = @page.wiki.find_page(params[:parent].to_s)
109 end
109 end
110 end
110 end
111
111
112 @content = @page.content_for_version(params[:version])
112 @content = @page.content_for_version(params[:version])
113 @content ||= WikiContent.new(:page => @page)
113 @content ||= WikiContent.new(:page => @page)
114 @content.text = initial_page_content(@page) if @content.text.blank?
114 @content.text = initial_page_content(@page) if @content.text.blank?
115 # don't keep previous comment
115 # don't keep previous comment
116 @content.comments = nil
116 @content.comments = nil
117
117
118 # To prevent StaleObjectError exception when reverting to a previous version
118 # To prevent StaleObjectError exception when reverting to a previous version
119 @content.version = @page.content.version if @page.content
119 @content.version = @page.content.version if @page.content
120
120
121 @text = @content.text
121 @text = @content.text
122 if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
122 if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
123 @section = params[:section].to_i
123 @section = params[:section].to_i
124 @text, @section_hash = Redmine::WikiFormatting.formatter.new(@text).get_section(@section)
124 @text, @section_hash = Redmine::WikiFormatting.formatter.new(@text).get_section(@section)
125 render_404 if @text.blank?
125 render_404 if @text.blank?
126 end
126 end
127 end
127 end
128
128
129 # Creates a new page or updates an existing one
129 # Creates a new page or updates an existing one
130 def update
130 def update
131 return render_403 unless editable?
131 return render_403 unless editable?
132 was_new_page = @page.new_record?
132 was_new_page = @page.new_record?
133 @page.safe_attributes = params[:wiki_page]
133 @page.safe_attributes = params[:wiki_page]
134
134
135 @content = @page.content || WikiContent.new(:page => @page)
135 @content = @page.content || WikiContent.new(:page => @page)
136 content_params = params[:content]
136 content_params = params[:content]
137 if content_params.nil? && params[:wiki_page].is_a?(Hash)
137 if content_params.nil? && params[:wiki_page].is_a?(Hash)
138 content_params = params[:wiki_page].slice(:text, :comments, :version)
138 content_params = params[:wiki_page].slice(:text, :comments, :version)
139 end
139 end
140 content_params ||= {}
140 content_params ||= {}
141
141
142 @content.comments = content_params[:comments]
142 @content.comments = content_params[:comments]
143 @text = content_params[:text]
143 @text = content_params[:text]
144 if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
144 if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
145 @section = params[:section].to_i
145 @section = params[:section].to_i
146 @section_hash = params[:section_hash]
146 @section_hash = params[:section_hash]
147 @content.text = Redmine::WikiFormatting.formatter.new(@content.text).update_section(@section, @text, @section_hash)
147 @content.text = Redmine::WikiFormatting.formatter.new(@content.text).update_section(@section, @text, @section_hash)
148 else
148 else
149 @content.version = content_params[:version] if content_params[:version]
149 @content.version = content_params[:version] if content_params[:version]
150 @content.text = @text
150 @content.text = @text
151 end
151 end
152 @content.author = User.current
152 @content.author = User.current
153
153
154 if @page.save_with_content(@content)
154 if @page.save_with_content(@content)
155 attachments = Attachment.attach_files(@page, params[:attachments])
155 attachments = Attachment.attach_files(@page, params[:attachments])
156 render_attachment_warning_if_needed(@page)
156 render_attachment_warning_if_needed(@page)
157 call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
157 call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
158
158
159 respond_to do |format|
159 respond_to do |format|
160 format.html {
160 format.html {
161 anchor = @section ? "section-#{@section}" : nil
161 anchor = @section ? "section-#{@section}" : nil
162 redirect_to project_wiki_page_path(@project, @page.title, :anchor => anchor)
162 redirect_to project_wiki_page_path(@project, @page.title, :anchor => anchor)
163 }
163 }
164 format.api {
164 format.api {
165 if was_new_page
165 if was_new_page
166 render :action => 'show', :status => :created, :location => project_wiki_page_path(@project, @page.title)
166 render :action => 'show', :status => :created, :location => project_wiki_page_path(@project, @page.title)
167 else
167 else
168 render_api_ok
168 render_api_ok
169 end
169 end
170 }
170 }
171 end
171 end
172 else
172 else
173 respond_to do |format|
173 respond_to do |format|
174 format.html { render :action => 'edit' }
174 format.html { render :action => 'edit' }
175 format.api { render_validation_errors(@content) }
175 format.api { render_validation_errors(@content) }
176 end
176 end
177 end
177 end
178
178
179 rescue ActiveRecord::StaleObjectError, Redmine::WikiFormatting::StaleSectionError
179 rescue ActiveRecord::StaleObjectError, Redmine::WikiFormatting::StaleSectionError
180 # Optimistic locking exception
180 # Optimistic locking exception
181 respond_to do |format|
181 respond_to do |format|
182 format.html {
182 format.html {
183 flash.now[:error] = l(:notice_locking_conflict)
183 flash.now[:error] = l(:notice_locking_conflict)
184 render :action => 'edit'
184 render :action => 'edit'
185 }
185 }
186 format.api { render_api_head :conflict }
186 format.api { render_api_head :conflict }
187 end
187 end
188 end
188 end
189
189
190 # rename a page
190 # rename a page
191 def rename
191 def rename
192 return render_403 unless editable?
192 return render_403 unless editable?
193 @page.redirect_existing_links = true
193 @page.redirect_existing_links = true
194 # used to display the *original* title if some AR validation errors occur
194 # used to display the *original* title if some AR validation errors occur
195 @original_title = @page.pretty_title
195 @original_title = @page.pretty_title
196 @page.safe_attributes = params[:wiki_page]
196 @page.safe_attributes = params[:wiki_page]
197 if request.post? && @page.save
197 if request.post? && @page.save
198 flash[:notice] = l(:notice_successful_update)
198 flash[:notice] = l(:notice_successful_update)
199 redirect_to project_wiki_page_path(@page.project, @page.title)
199 redirect_to project_wiki_page_path(@page.project, @page.title)
200 end
200 end
201 end
201 end
202
202
203 def protect
203 def protect
204 @page.update_attribute :protected, params[:protected]
204 @page.update_attribute :protected, params[:protected]
205 redirect_to project_wiki_page_path(@project, @page.title)
205 redirect_to project_wiki_page_path(@project, @page.title)
206 end
206 end
207
207
208 # show page history
208 # show page history
209 def history
209 def history
210 @version_count = @page.content.versions.count
210 @version_count = @page.content.versions.count
211 @version_pages = Paginator.new @version_count, per_page_option, params['page']
211 @version_pages = Paginator.new @version_count, per_page_option, params['page']
212 # don't load text
212 # don't load text
213 @versions = @page.content.versions.
213 @versions = @page.content.versions.
214 select("id, author_id, comments, updated_on, version").
214 select("id, author_id, comments, updated_on, version").
215 reorder('version DESC').
215 reorder('version DESC').
216 limit(@version_pages.per_page + 1).
216 limit(@version_pages.per_page + 1).
217 offset(@version_pages.offset).
217 offset(@version_pages.offset).
218 to_a
218 to_a
219
219
220 render :layout => false if request.xhr?
220 render :layout => false if request.xhr?
221 end
221 end
222
222
223 def diff
223 def diff
224 @diff = @page.diff(params[:version], params[:version_from])
224 @diff = @page.diff(params[:version], params[:version_from])
225 render_404 unless @diff
225 render_404 unless @diff
226 end
226 end
227
227
228 def annotate
228 def annotate
229 @annotate = @page.annotate(params[:version])
229 @annotate = @page.annotate(params[:version])
230 render_404 unless @annotate
230 render_404 unless @annotate
231 end
231 end
232
232
233 # Removes a wiki page and its history
233 # Removes a wiki page and its history
234 # Children can be either set as root pages, removed or reassigned to another parent page
234 # Children can be either set as root pages, removed or reassigned to another parent page
235 def destroy
235 def destroy
236 return render_403 unless editable?
236 return render_403 unless editable?
237
237
238 @descendants_count = @page.descendants.size
238 @descendants_count = @page.descendants.size
239 if @descendants_count > 0
239 if @descendants_count > 0
240 case params[:todo]
240 case params[:todo]
241 when 'nullify'
241 when 'nullify'
242 # Nothing to do
242 # Nothing to do
243 when 'destroy'
243 when 'destroy'
244 # Removes all its descendants
244 # Removes all its descendants
245 @page.descendants.each(&:destroy)
245 @page.descendants.each(&:destroy)
246 when 'reassign'
246 when 'reassign'
247 # Reassign children to another parent page
247 # Reassign children to another parent page
248 reassign_to = @wiki.pages.find_by_id(params[:reassign_to_id].to_i)
248 reassign_to = @wiki.pages.find_by_id(params[:reassign_to_id].to_i)
249 return unless reassign_to
249 return unless reassign_to
250 @page.children.each do |child|
250 @page.children.each do |child|
251 child.update_attribute(:parent, reassign_to)
251 child.update_attribute(:parent, reassign_to)
252 end
252 end
253 else
253 else
254 @reassignable_to = @wiki.pages - @page.self_and_descendants
254 @reassignable_to = @wiki.pages - @page.self_and_descendants
255 # display the destroy form if it's a user request
255 # display the destroy form if it's a user request
256 return unless api_request?
256 return unless api_request?
257 end
257 end
258 end
258 end
259 @page.destroy
259 @page.destroy
260 respond_to do |format|
260 respond_to do |format|
261 format.html { redirect_to project_wiki_index_path(@project) }
261 format.html { redirect_to project_wiki_index_path(@project) }
262 format.api { render_api_ok }
262 format.api { render_api_ok }
263 end
263 end
264 end
264 end
265
265
266 def destroy_version
266 def destroy_version
267 return render_403 unless editable?
267 return render_403 unless editable?
268
268
269 @content = @page.content_for_version(params[:version])
269 @content = @page.content_for_version(params[:version])
270 @content.destroy
270 @content.destroy
271 redirect_to_referer_or history_project_wiki_page_path(@project, @page.title)
271 redirect_to_referer_or history_project_wiki_page_path(@project, @page.title)
272 end
272 end
273
273
274 # Export wiki to a single pdf or html file
274 # Export wiki to a single pdf or html file
275 def export
275 def export
276 @pages = @wiki.pages.
276 @pages = @wiki.pages.
277 order('title').
277 order('title').
278 includes([:content, {:attachments => :author}]).
278 includes([:content, {:attachments => :author}]).
279 to_a
279 to_a
280 respond_to do |format|
280 respond_to do |format|
281 format.html {
281 format.html {
282 export = render_to_string :action => 'export_multiple', :layout => false
282 export = render_to_string :action => 'export_multiple', :layout => false
283 send_data(export, :type => 'text/html', :filename => "wiki.html")
283 send_data(export, :type => 'text/html', :filename => "wiki.html")
284 }
284 }
285 format.pdf {
285 format.pdf {
286 send_data(wiki_pages_to_pdf(@pages, @project),
286 send_file_headers! :type => 'application/pdf', :filename => "#{@project.identifier}.pdf"
287 :type => 'application/pdf',
288 :filename => "#{@project.identifier}.pdf")
289 }
287 }
290 end
288 end
291 end
289 end
292
290
293 def preview
291 def preview
294 page = @wiki.find_page(params[:id])
292 page = @wiki.find_page(params[:id])
295 # page is nil when previewing a new page
293 # page is nil when previewing a new page
296 return render_403 unless page.nil? || editable?(page)
294 return render_403 unless page.nil? || editable?(page)
297 if page
295 if page
298 @attachments += page.attachments
296 @attachments += page.attachments
299 @previewed = page.content
297 @previewed = page.content
300 end
298 end
301 @text = params[:content][:text]
299 @text = params[:content][:text]
302 render :partial => 'common/preview'
300 render :partial => 'common/preview'
303 end
301 end
304
302
305 def add_attachment
303 def add_attachment
306 return render_403 unless editable?
304 return render_403 unless editable?
307 attachments = Attachment.attach_files(@page, params[:attachments])
305 attachments = Attachment.attach_files(@page, params[:attachments])
308 render_attachment_warning_if_needed(@page)
306 render_attachment_warning_if_needed(@page)
309 redirect_to :action => 'show', :id => @page.title, :project_id => @project
307 redirect_to :action => 'show', :id => @page.title, :project_id => @project
310 end
308 end
311
309
312 private
310 private
313
311
314 def find_wiki
312 def find_wiki
315 @project = Project.find(params[:project_id])
313 @project = Project.find(params[:project_id])
316 @wiki = @project.wiki
314 @wiki = @project.wiki
317 render_404 unless @wiki
315 render_404 unless @wiki
318 rescue ActiveRecord::RecordNotFound
316 rescue ActiveRecord::RecordNotFound
319 render_404
317 render_404
320 end
318 end
321
319
322 # Finds the requested page or a new page if it doesn't exist
320 # Finds the requested page or a new page if it doesn't exist
323 def find_existing_or_new_page
321 def find_existing_or_new_page
324 @page = @wiki.find_or_new_page(params[:id])
322 @page = @wiki.find_or_new_page(params[:id])
325 if @wiki.page_found_with_redirect?
323 if @wiki.page_found_with_redirect?
326 redirect_to_page @page
324 redirect_to_page @page
327 end
325 end
328 end
326 end
329
327
330 # Finds the requested page and returns a 404 error if it doesn't exist
328 # Finds the requested page and returns a 404 error if it doesn't exist
331 def find_existing_page
329 def find_existing_page
332 @page = @wiki.find_page(params[:id])
330 @page = @wiki.find_page(params[:id])
333 if @page.nil?
331 if @page.nil?
334 render_404
332 render_404
335 return
333 return
336 end
334 end
337 if @wiki.page_found_with_redirect?
335 if @wiki.page_found_with_redirect?
338 redirect_to_page @page
336 redirect_to_page @page
339 end
337 end
340 end
338 end
341
339
342 def redirect_to_page(page)
340 def redirect_to_page(page)
343 if page.project && page.project.visible?
341 if page.project && page.project.visible?
344 redirect_to :action => action_name, :project_id => page.project, :id => page.title
342 redirect_to :action => action_name, :project_id => page.project, :id => page.title
345 else
343 else
346 render_404
344 render_404
347 end
345 end
348 end
346 end
349
347
350 # Returns true if the current user is allowed to edit the page, otherwise false
348 # Returns true if the current user is allowed to edit the page, otherwise false
351 def editable?(page = @page)
349 def editable?(page = @page)
352 page.editable_by?(User.current)
350 page.editable_by?(User.current)
353 end
351 end
354
352
355 # Returns the default content of a new wiki page
353 # Returns the default content of a new wiki page
356 def initial_page_content(page)
354 def initial_page_content(page)
357 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
355 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
358 extend helper unless self.instance_of?(helper)
356 extend helper unless self.instance_of?(helper)
359 helper.instance_method(:initial_page_content).bind(self).call(page)
357 helper.instance_method(:initial_page_content).bind(self).call(page)
360 end
358 end
361
359
362 def load_pages_for_index
360 def load_pages_for_index
363 @pages = @wiki.pages.with_updated_on.
361 @pages = @wiki.pages.with_updated_on.
364 reorder("#{WikiPage.table_name}.title").
362 reorder("#{WikiPage.table_name}.title").
365 includes(:wiki => :project).
363 includes(:wiki => :project).
366 includes(:parent).
364 includes(:parent).
367 to_a
365 to_a
368 end
366 end
369 end
367 end
@@ -1,1316 +1,1318
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
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
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.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require 'forwardable'
20 require 'forwardable'
21 require 'cgi'
21 require 'cgi'
22
22
23 module ApplicationHelper
23 module ApplicationHelper
24 include Redmine::WikiFormatting::Macros::Definitions
24 include Redmine::WikiFormatting::Macros::Definitions
25 include Redmine::I18n
25 include Redmine::I18n
26 include GravatarHelper::PublicMethods
26 include GravatarHelper::PublicMethods
27 include Redmine::Pagination::Helper
27 include Redmine::Pagination::Helper
28
28
29 extend Forwardable
29 extend Forwardable
30 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
30 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
31
31
32 # Return true if user is authorized for controller/action, otherwise false
32 # Return true if user is authorized for controller/action, otherwise false
33 def authorize_for(controller, action)
33 def authorize_for(controller, action)
34 User.current.allowed_to?({:controller => controller, :action => action}, @project)
34 User.current.allowed_to?({:controller => controller, :action => action}, @project)
35 end
35 end
36
36
37 # Display a link if user is authorized
37 # Display a link if user is authorized
38 #
38 #
39 # @param [String] name Anchor text (passed to link_to)
39 # @param [String] name Anchor text (passed to link_to)
40 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
40 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
41 # @param [optional, Hash] html_options Options passed to link_to
41 # @param [optional, Hash] html_options Options passed to link_to
42 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
42 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
43 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
43 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
44 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
44 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
45 end
45 end
46
46
47 # Displays a link to user's account page if active
47 # Displays a link to user's account page if active
48 def link_to_user(user, options={})
48 def link_to_user(user, options={})
49 if user.is_a?(User)
49 if user.is_a?(User)
50 name = h(user.name(options[:format]))
50 name = h(user.name(options[:format]))
51 if user.active? || (User.current.admin? && user.logged?)
51 if user.active? || (User.current.admin? && user.logged?)
52 link_to name, user_path(user), :class => user.css_classes
52 link_to name, user_path(user), :class => user.css_classes
53 else
53 else
54 name
54 name
55 end
55 end
56 else
56 else
57 h(user.to_s)
57 h(user.to_s)
58 end
58 end
59 end
59 end
60
60
61 # Displays a link to +issue+ with its subject.
61 # Displays a link to +issue+ with its subject.
62 # Examples:
62 # Examples:
63 #
63 #
64 # link_to_issue(issue) # => Defect #6: This is the subject
64 # link_to_issue(issue) # => Defect #6: This is the subject
65 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
65 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
66 # link_to_issue(issue, :subject => false) # => Defect #6
66 # link_to_issue(issue, :subject => false) # => Defect #6
67 # link_to_issue(issue, :project => true) # => Foo - Defect #6
67 # link_to_issue(issue, :project => true) # => Foo - Defect #6
68 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
68 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
69 #
69 #
70 def link_to_issue(issue, options={})
70 def link_to_issue(issue, options={})
71 title = nil
71 title = nil
72 subject = nil
72 subject = nil
73 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
73 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
74 if options[:subject] == false
74 if options[:subject] == false
75 title = issue.subject.truncate(60)
75 title = issue.subject.truncate(60)
76 else
76 else
77 subject = issue.subject
77 subject = issue.subject
78 if truncate_length = options[:truncate]
78 if truncate_length = options[:truncate]
79 subject = subject.truncate(truncate_length)
79 subject = subject.truncate(truncate_length)
80 end
80 end
81 end
81 end
82 only_path = options[:only_path].nil? ? true : options[:only_path]
82 only_path = options[:only_path].nil? ? true : options[:only_path]
83 s = link_to(text, issue_url(issue, :only_path => only_path),
83 s = link_to(text, issue_url(issue, :only_path => only_path),
84 :class => issue.css_classes, :title => title)
84 :class => issue.css_classes, :title => title)
85 s << h(": #{subject}") if subject
85 s << h(": #{subject}") if subject
86 s = h("#{issue.project} - ") + s if options[:project]
86 s = h("#{issue.project} - ") + s if options[:project]
87 s
87 s
88 end
88 end
89
89
90 # Generates a link to an attachment.
90 # Generates a link to an attachment.
91 # Options:
91 # Options:
92 # * :text - Link text (default to attachment filename)
92 # * :text - Link text (default to attachment filename)
93 # * :download - Force download (default: false)
93 # * :download - Force download (default: false)
94 def link_to_attachment(attachment, options={})
94 def link_to_attachment(attachment, options={})
95 text = options.delete(:text) || attachment.filename
95 text = options.delete(:text) || attachment.filename
96 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
96 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
97 html_options = options.slice!(:only_path)
97 html_options = options.slice!(:only_path)
98 options[:only_path] = true unless options.key?(:only_path)
98 options[:only_path] = true unless options.key?(:only_path)
99 url = send(route_method, attachment, attachment.filename, options)
99 url = send(route_method, attachment, attachment.filename, options)
100 link_to text, url, html_options
100 link_to text, url, html_options
101 end
101 end
102
102
103 # Generates a link to a SCM revision
103 # Generates a link to a SCM revision
104 # Options:
104 # Options:
105 # * :text - Link text (default to the formatted revision)
105 # * :text - Link text (default to the formatted revision)
106 def link_to_revision(revision, repository, options={})
106 def link_to_revision(revision, repository, options={})
107 if repository.is_a?(Project)
107 if repository.is_a?(Project)
108 repository = repository.repository
108 repository = repository.repository
109 end
109 end
110 text = options.delete(:text) || format_revision(revision)
110 text = options.delete(:text) || format_revision(revision)
111 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
111 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
112 link_to(
112 link_to(
113 h(text),
113 h(text),
114 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
114 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
115 :title => l(:label_revision_id, format_revision(revision)),
115 :title => l(:label_revision_id, format_revision(revision)),
116 :accesskey => options[:accesskey]
116 :accesskey => options[:accesskey]
117 )
117 )
118 end
118 end
119
119
120 # Generates a link to a message
120 # Generates a link to a message
121 def link_to_message(message, options={}, html_options = nil)
121 def link_to_message(message, options={}, html_options = nil)
122 link_to(
122 link_to(
123 message.subject.truncate(60),
123 message.subject.truncate(60),
124 board_message_url(message.board_id, message.parent_id || message.id, {
124 board_message_url(message.board_id, message.parent_id || message.id, {
125 :r => (message.parent_id && message.id),
125 :r => (message.parent_id && message.id),
126 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
126 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
127 :only_path => true
127 :only_path => true
128 }.merge(options)),
128 }.merge(options)),
129 html_options
129 html_options
130 )
130 )
131 end
131 end
132
132
133 # Generates a link to a project if active
133 # Generates a link to a project if active
134 # Examples:
134 # Examples:
135 #
135 #
136 # link_to_project(project) # => link to the specified project overview
136 # link_to_project(project) # => link to the specified project overview
137 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
137 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
138 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
138 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
139 #
139 #
140 def link_to_project(project, options={}, html_options = nil)
140 def link_to_project(project, options={}, html_options = nil)
141 if project.archived?
141 if project.archived?
142 h(project.name)
142 h(project.name)
143 else
143 else
144 link_to project.name,
144 link_to project.name,
145 project_url(project, {:only_path => true}.merge(options)),
145 project_url(project, {:only_path => true}.merge(options)),
146 html_options
146 html_options
147 end
147 end
148 end
148 end
149
149
150 # Generates a link to a project settings if active
150 # Generates a link to a project settings if active
151 def link_to_project_settings(project, options={}, html_options=nil)
151 def link_to_project_settings(project, options={}, html_options=nil)
152 if project.active?
152 if project.active?
153 link_to project.name, settings_project_path(project, options), html_options
153 link_to project.name, settings_project_path(project, options), html_options
154 elsif project.archived?
154 elsif project.archived?
155 h(project.name)
155 h(project.name)
156 else
156 else
157 link_to project.name, project_path(project, options), html_options
157 link_to project.name, project_path(project, options), html_options
158 end
158 end
159 end
159 end
160
160
161 # Generates a link to a version
161 # Generates a link to a version
162 def link_to_version(version, options = {})
162 def link_to_version(version, options = {})
163 return '' unless version && version.is_a?(Version)
163 return '' unless version && version.is_a?(Version)
164 options = {:title => format_date(version.effective_date)}.merge(options)
164 options = {:title => format_date(version.effective_date)}.merge(options)
165 link_to_if version.visible?, format_version_name(version), version_path(version), options
165 link_to_if version.visible?, format_version_name(version), version_path(version), options
166 end
166 end
167
167
168 # Helper that formats object for html or text rendering
168 # Helper that formats object for html or text rendering
169 def format_object(object, html=true, &block)
169 def format_object(object, html=true, &block)
170 if block_given?
170 if block_given?
171 object = yield object
171 object = yield object
172 end
172 end
173 case object.class.name
173 case object.class.name
174 when 'Array'
174 when 'Array'
175 object.map {|o| format_object(o, html)}.join(', ').html_safe
175 object.map {|o| format_object(o, html)}.join(', ').html_safe
176 when 'Time'
176 when 'Time'
177 format_time(object)
177 format_time(object)
178 when 'Date'
178 when 'Date'
179 format_date(object)
179 format_date(object)
180 when 'Fixnum'
180 when 'Fixnum'
181 object.to_s
181 object.to_s
182 when 'Float'
182 when 'Float'
183 sprintf "%.2f", object
183 sprintf "%.2f", object
184 when 'User'
184 when 'User'
185 html ? link_to_user(object) : object.to_s
185 html ? link_to_user(object) : object.to_s
186 when 'Project'
186 when 'Project'
187 html ? link_to_project(object) : object.to_s
187 html ? link_to_project(object) : object.to_s
188 when 'Version'
188 when 'Version'
189 html ? link_to_version(object) : object.to_s
189 html ? link_to_version(object) : object.to_s
190 when 'TrueClass'
190 when 'TrueClass'
191 l(:general_text_Yes)
191 l(:general_text_Yes)
192 when 'FalseClass'
192 when 'FalseClass'
193 l(:general_text_No)
193 l(:general_text_No)
194 when 'Issue'
194 when 'Issue'
195 object.visible? && html ? link_to_issue(object) : "##{object.id}"
195 object.visible? && html ? link_to_issue(object) : "##{object.id}"
196 when 'CustomValue', 'CustomFieldValue'
196 when 'CustomValue', 'CustomFieldValue'
197 if object.custom_field
197 if object.custom_field
198 f = object.custom_field.format.formatted_custom_value(self, object, html)
198 f = object.custom_field.format.formatted_custom_value(self, object, html)
199 if f.nil? || f.is_a?(String)
199 if f.nil? || f.is_a?(String)
200 f
200 f
201 else
201 else
202 format_object(f, html, &block)
202 format_object(f, html, &block)
203 end
203 end
204 else
204 else
205 object.value.to_s
205 object.value.to_s
206 end
206 end
207 else
207 else
208 html ? h(object) : object.to_s
208 html ? h(object) : object.to_s
209 end
209 end
210 end
210 end
211
211
212 def wiki_page_path(page, options={})
212 def wiki_page_path(page, options={})
213 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
213 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
214 end
214 end
215
215
216 def thumbnail_tag(attachment)
216 def thumbnail_tag(attachment)
217 link_to image_tag(thumbnail_path(attachment)),
217 link_to image_tag(thumbnail_path(attachment)),
218 named_attachment_path(attachment, attachment.filename),
218 named_attachment_path(attachment, attachment.filename),
219 :title => attachment.filename
219 :title => attachment.filename
220 end
220 end
221
221
222 def toggle_link(name, id, options={})
222 def toggle_link(name, id, options={})
223 onclick = "$('##{id}').toggle(); "
223 onclick = "$('##{id}').toggle(); "
224 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
224 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
225 onclick << "return false;"
225 onclick << "return false;"
226 link_to(name, "#", :onclick => onclick)
226 link_to(name, "#", :onclick => onclick)
227 end
227 end
228
228
229 def format_activity_title(text)
229 def format_activity_title(text)
230 h(truncate_single_line_raw(text, 100))
230 h(truncate_single_line_raw(text, 100))
231 end
231 end
232
232
233 def format_activity_day(date)
233 def format_activity_day(date)
234 date == User.current.today ? l(:label_today).titleize : format_date(date)
234 date == User.current.today ? l(:label_today).titleize : format_date(date)
235 end
235 end
236
236
237 def format_activity_description(text)
237 def format_activity_description(text)
238 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
238 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
239 ).gsub(/[\r\n]+/, "<br />").html_safe
239 ).gsub(/[\r\n]+/, "<br />").html_safe
240 end
240 end
241
241
242 def format_version_name(version)
242 def format_version_name(version)
243 if !version.shared? || version.project == @project
243 if !version.shared? || version.project == @project
244 h(version)
244 h(version)
245 else
245 else
246 h("#{version.project} - #{version}")
246 h("#{version.project} - #{version}")
247 end
247 end
248 end
248 end
249
249
250 def due_date_distance_in_words(date)
250 def due_date_distance_in_words(date)
251 if date
251 if date
252 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
252 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
253 end
253 end
254 end
254 end
255
255
256 # Renders a tree of projects as a nested set of unordered lists
256 # Renders a tree of projects as a nested set of unordered lists
257 # The given collection may be a subset of the whole project tree
257 # The given collection may be a subset of the whole project tree
258 # (eg. some intermediate nodes are private and can not be seen)
258 # (eg. some intermediate nodes are private and can not be seen)
259 def render_project_nested_lists(projects, &block)
259 def render_project_nested_lists(projects, &block)
260 s = ''
260 s = ''
261 if projects.any?
261 if projects.any?
262 ancestors = []
262 ancestors = []
263 original_project = @project
263 original_project = @project
264 projects.sort_by(&:lft).each do |project|
264 projects.sort_by(&:lft).each do |project|
265 # set the project environment to please macros.
265 # set the project environment to please macros.
266 @project = project
266 @project = project
267 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
267 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
268 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
268 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
269 else
269 else
270 ancestors.pop
270 ancestors.pop
271 s << "</li>"
271 s << "</li>"
272 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
272 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
273 ancestors.pop
273 ancestors.pop
274 s << "</ul></li>\n"
274 s << "</ul></li>\n"
275 end
275 end
276 end
276 end
277 classes = (ancestors.empty? ? 'root' : 'child')
277 classes = (ancestors.empty? ? 'root' : 'child')
278 s << "<li class='#{classes}'><div class='#{classes}'>"
278 s << "<li class='#{classes}'><div class='#{classes}'>"
279 s << h(block_given? ? capture(project, &block) : project.name)
279 s << h(block_given? ? capture(project, &block) : project.name)
280 s << "</div>\n"
280 s << "</div>\n"
281 ancestors << project
281 ancestors << project
282 end
282 end
283 s << ("</li></ul>\n" * ancestors.size)
283 s << ("</li></ul>\n" * ancestors.size)
284 @project = original_project
284 @project = original_project
285 end
285 end
286 s.html_safe
286 s.html_safe
287 end
287 end
288
288
289 def render_page_hierarchy(pages, node=nil, options={})
289 def render_page_hierarchy(pages, node=nil, options={})
290 content = ''
290 content = ''
291 if pages[node]
291 if pages[node]
292 content << "<ul class=\"pages-hierarchy\">\n"
292 content << "<ul class=\"pages-hierarchy\">\n"
293 pages[node].each do |page|
293 pages[node].each do |page|
294 content << "<li>"
294 content << "<li>"
295 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
295 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
296 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
296 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
297 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
297 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
298 content << "</li>\n"
298 content << "</li>\n"
299 end
299 end
300 content << "</ul>\n"
300 content << "</ul>\n"
301 end
301 end
302 content.html_safe
302 content.html_safe
303 end
303 end
304
304
305 # Renders flash messages
305 # Renders flash messages
306 def render_flash_messages
306 def render_flash_messages
307 s = ''
307 s = ''
308 flash.each do |k,v|
308 flash.each do |k,v|
309 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
309 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
310 end
310 end
311 s.html_safe
311 s.html_safe
312 end
312 end
313
313
314 # Renders tabs and their content
314 # Renders tabs and their content
315 def render_tabs(tabs, selected=params[:tab])
315 def render_tabs(tabs, selected=params[:tab])
316 if tabs.any?
316 if tabs.any?
317 unless tabs.detect {|tab| tab[:name] == selected}
317 unless tabs.detect {|tab| tab[:name] == selected}
318 selected = nil
318 selected = nil
319 end
319 end
320 selected ||= tabs.first[:name]
320 selected ||= tabs.first[:name]
321 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
321 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
322 else
322 else
323 content_tag 'p', l(:label_no_data), :class => "nodata"
323 content_tag 'p', l(:label_no_data), :class => "nodata"
324 end
324 end
325 end
325 end
326
326
327 # Renders the project quick-jump box
327 # Renders the project quick-jump box
328 def render_project_jump_box
328 def render_project_jump_box
329 return unless User.current.logged?
329 return unless User.current.logged?
330 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
330 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
331 if projects.any?
331 if projects.any?
332 options =
332 options =
333 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
333 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
334 '<option value="" disabled="disabled">---</option>').html_safe
334 '<option value="" disabled="disabled">---</option>').html_safe
335
335
336 options << project_tree_options_for_select(projects, :selected => @project) do |p|
336 options << project_tree_options_for_select(projects, :selected => @project) do |p|
337 { :value => project_path(:id => p, :jump => current_menu_item) }
337 { :value => project_path(:id => p, :jump => current_menu_item) }
338 end
338 end
339
339
340 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
340 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
341 end
341 end
342 end
342 end
343
343
344 def project_tree_options_for_select(projects, options = {})
344 def project_tree_options_for_select(projects, options = {})
345 s = ''.html_safe
345 s = ''.html_safe
346 if options[:include_blank]
346 if options[:include_blank]
347 s << content_tag('option', '&nbsp;'.html_safe, :value => '')
347 s << content_tag('option', '&nbsp;'.html_safe, :value => '')
348 end
348 end
349 project_tree(projects) do |project, level|
349 project_tree(projects) do |project, level|
350 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
350 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
351 tag_options = {:value => project.id}
351 tag_options = {:value => project.id}
352 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
352 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
353 tag_options[:selected] = 'selected'
353 tag_options[:selected] = 'selected'
354 else
354 else
355 tag_options[:selected] = nil
355 tag_options[:selected] = nil
356 end
356 end
357 tag_options.merge!(yield(project)) if block_given?
357 tag_options.merge!(yield(project)) if block_given?
358 s << content_tag('option', name_prefix + h(project), tag_options)
358 s << content_tag('option', name_prefix + h(project), tag_options)
359 end
359 end
360 s.html_safe
360 s.html_safe
361 end
361 end
362
362
363 # Yields the given block for each project with its level in the tree
363 # Yields the given block for each project with its level in the tree
364 #
364 #
365 # Wrapper for Project#project_tree
365 # Wrapper for Project#project_tree
366 def project_tree(projects, &block)
366 def project_tree(projects, &block)
367 Project.project_tree(projects, &block)
367 Project.project_tree(projects, &block)
368 end
368 end
369
369
370 def principals_check_box_tags(name, principals)
370 def principals_check_box_tags(name, principals)
371 s = ''
371 s = ''
372 principals.each do |principal|
372 principals.each do |principal|
373 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
373 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
374 end
374 end
375 s.html_safe
375 s.html_safe
376 end
376 end
377
377
378 # Returns a string for users/groups option tags
378 # Returns a string for users/groups option tags
379 def principals_options_for_select(collection, selected=nil)
379 def principals_options_for_select(collection, selected=nil)
380 s = ''
380 s = ''
381 if collection.include?(User.current)
381 if collection.include?(User.current)
382 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
382 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
383 end
383 end
384 groups = ''
384 groups = ''
385 collection.sort.each do |element|
385 collection.sort.each do |element|
386 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
386 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
387 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
387 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
388 end
388 end
389 unless groups.empty?
389 unless groups.empty?
390 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
390 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
391 end
391 end
392 s.html_safe
392 s.html_safe
393 end
393 end
394
394
395 def option_tag(name, text, value, selected=nil, options={})
395 def option_tag(name, text, value, selected=nil, options={})
396 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
396 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
397 end
397 end
398
398
399 def truncate_single_line_raw(string, length)
399 def truncate_single_line_raw(string, length)
400 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
400 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
401 end
401 end
402
402
403 # Truncates at line break after 250 characters or options[:length]
403 # Truncates at line break after 250 characters or options[:length]
404 def truncate_lines(string, options={})
404 def truncate_lines(string, options={})
405 length = options[:length] || 250
405 length = options[:length] || 250
406 if string.to_s =~ /\A(.{#{length}}.*?)$/m
406 if string.to_s =~ /\A(.{#{length}}.*?)$/m
407 "#{$1}..."
407 "#{$1}..."
408 else
408 else
409 string
409 string
410 end
410 end
411 end
411 end
412
412
413 def anchor(text)
413 def anchor(text)
414 text.to_s.gsub(' ', '_')
414 text.to_s.gsub(' ', '_')
415 end
415 end
416
416
417 def html_hours(text)
417 def html_hours(text)
418 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
418 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
419 end
419 end
420
420
421 def authoring(created, author, options={})
421 def authoring(created, author, options={})
422 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
422 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
423 end
423 end
424
424
425 def time_tag(time)
425 def time_tag(time)
426 text = distance_of_time_in_words(Time.now, time)
426 text = distance_of_time_in_words(Time.now, time)
427 if @project
427 if @project
428 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
428 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
429 else
429 else
430 content_tag('abbr', text, :title => format_time(time))
430 content_tag('abbr', text, :title => format_time(time))
431 end
431 end
432 end
432 end
433
433
434 def syntax_highlight_lines(name, content)
434 def syntax_highlight_lines(name, content)
435 lines = []
435 lines = []
436 syntax_highlight(name, content).each_line { |line| lines << line }
436 syntax_highlight(name, content).each_line { |line| lines << line }
437 lines
437 lines
438 end
438 end
439
439
440 def syntax_highlight(name, content)
440 def syntax_highlight(name, content)
441 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
441 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
442 end
442 end
443
443
444 def to_path_param(path)
444 def to_path_param(path)
445 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
445 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
446 str.blank? ? nil : str
446 str.blank? ? nil : str
447 end
447 end
448
448
449 def reorder_links(name, url, method = :post)
449 def reorder_links(name, url, method = :post)
450 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
450 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
451 url.merge({"#{name}[move_to]" => 'highest'}),
451 url.merge({"#{name}[move_to]" => 'highest'}),
452 :method => method, :title => l(:label_sort_highest)) +
452 :method => method, :title => l(:label_sort_highest)) +
453 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
453 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
454 url.merge({"#{name}[move_to]" => 'higher'}),
454 url.merge({"#{name}[move_to]" => 'higher'}),
455 :method => method, :title => l(:label_sort_higher)) +
455 :method => method, :title => l(:label_sort_higher)) +
456 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
456 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
457 url.merge({"#{name}[move_to]" => 'lower'}),
457 url.merge({"#{name}[move_to]" => 'lower'}),
458 :method => method, :title => l(:label_sort_lower)) +
458 :method => method, :title => l(:label_sort_lower)) +
459 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
459 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
460 url.merge({"#{name}[move_to]" => 'lowest'}),
460 url.merge({"#{name}[move_to]" => 'lowest'}),
461 :method => method, :title => l(:label_sort_lowest))
461 :method => method, :title => l(:label_sort_lowest))
462 end
462 end
463
463
464 def breadcrumb(*args)
464 def breadcrumb(*args)
465 elements = args.flatten
465 elements = args.flatten
466 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
466 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
467 end
467 end
468
468
469 def other_formats_links(&block)
469 def other_formats_links(&block)
470 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
470 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
471 yield Redmine::Views::OtherFormatsBuilder.new(self)
471 yield Redmine::Views::OtherFormatsBuilder.new(self)
472 concat('</p>'.html_safe)
472 concat('</p>'.html_safe)
473 end
473 end
474
474
475 def page_header_title
475 def page_header_title
476 if @project.nil? || @project.new_record?
476 if @project.nil? || @project.new_record?
477 h(Setting.app_title)
477 h(Setting.app_title)
478 else
478 else
479 b = []
479 b = []
480 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
480 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
481 if ancestors.any?
481 if ancestors.any?
482 root = ancestors.shift
482 root = ancestors.shift
483 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
483 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
484 if ancestors.size > 2
484 if ancestors.size > 2
485 b << "\xe2\x80\xa6"
485 b << "\xe2\x80\xa6"
486 ancestors = ancestors[-2, 2]
486 ancestors = ancestors[-2, 2]
487 end
487 end
488 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
488 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
489 end
489 end
490 b << h(@project)
490 b << h(@project)
491 b.join(" \xc2\xbb ").html_safe
491 b.join(" \xc2\xbb ").html_safe
492 end
492 end
493 end
493 end
494
494
495 # Returns a h2 tag and sets the html title with the given arguments
495 # Returns a h2 tag and sets the html title with the given arguments
496 def title(*args)
496 def title(*args)
497 strings = args.map do |arg|
497 strings = args.map do |arg|
498 if arg.is_a?(Array) && arg.size >= 2
498 if arg.is_a?(Array) && arg.size >= 2
499 link_to(*arg)
499 link_to(*arg)
500 else
500 else
501 h(arg.to_s)
501 h(arg.to_s)
502 end
502 end
503 end
503 end
504 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
504 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
505 content_tag('h2', strings.join(' &#187; ').html_safe)
505 content_tag('h2', strings.join(' &#187; ').html_safe)
506 end
506 end
507
507
508 # Sets the html title
508 # Sets the html title
509 # Returns the html title when called without arguments
509 # Returns the html title when called without arguments
510 # Current project name and app_title and automatically appended
510 # Current project name and app_title and automatically appended
511 # Exemples:
511 # Exemples:
512 # html_title 'Foo', 'Bar'
512 # html_title 'Foo', 'Bar'
513 # html_title # => 'Foo - Bar - My Project - Redmine'
513 # html_title # => 'Foo - Bar - My Project - Redmine'
514 def html_title(*args)
514 def html_title(*args)
515 if args.empty?
515 if args.empty?
516 title = @html_title || []
516 title = @html_title || []
517 title << @project.name if @project
517 title << @project.name if @project
518 title << Setting.app_title unless Setting.app_title == title.last
518 title << Setting.app_title unless Setting.app_title == title.last
519 title.reject(&:blank?).join(' - ')
519 title.reject(&:blank?).join(' - ')
520 else
520 else
521 @html_title ||= []
521 @html_title ||= []
522 @html_title += args
522 @html_title += args
523 end
523 end
524 end
524 end
525
525
526 # Returns the theme, controller name, and action as css classes for the
526 # Returns the theme, controller name, and action as css classes for the
527 # HTML body.
527 # HTML body.
528 def body_css_classes
528 def body_css_classes
529 css = []
529 css = []
530 if theme = Redmine::Themes.theme(Setting.ui_theme)
530 if theme = Redmine::Themes.theme(Setting.ui_theme)
531 css << 'theme-' + theme.name
531 css << 'theme-' + theme.name
532 end
532 end
533
533
534 css << 'project-' + @project.identifier if @project && @project.identifier.present?
534 css << 'project-' + @project.identifier if @project && @project.identifier.present?
535 css << 'controller-' + controller_name
535 css << 'controller-' + controller_name
536 css << 'action-' + action_name
536 css << 'action-' + action_name
537 css.join(' ')
537 css.join(' ')
538 end
538 end
539
539
540 def accesskey(s)
540 def accesskey(s)
541 @used_accesskeys ||= []
541 @used_accesskeys ||= []
542 key = Redmine::AccessKeys.key_for(s)
542 key = Redmine::AccessKeys.key_for(s)
543 return nil if @used_accesskeys.include?(key)
543 return nil if @used_accesskeys.include?(key)
544 @used_accesskeys << key
544 @used_accesskeys << key
545 key
545 key
546 end
546 end
547
547
548 # Formats text according to system settings.
548 # Formats text according to system settings.
549 # 2 ways to call this method:
549 # 2 ways to call this method:
550 # * with a String: textilizable(text, options)
550 # * with a String: textilizable(text, options)
551 # * with an object and one of its attribute: textilizable(issue, :description, options)
551 # * with an object and one of its attribute: textilizable(issue, :description, options)
552 def textilizable(*args)
552 def textilizable(*args)
553 options = args.last.is_a?(Hash) ? args.pop : {}
553 options = args.last.is_a?(Hash) ? args.pop : {}
554 case args.size
554 case args.size
555 when 1
555 when 1
556 obj = options[:object]
556 obj = options[:object]
557 text = args.shift
557 text = args.shift
558 when 2
558 when 2
559 obj = args.shift
559 obj = args.shift
560 attr = args.shift
560 attr = args.shift
561 text = obj.send(attr).to_s
561 text = obj.send(attr).to_s
562 else
562 else
563 raise ArgumentError, 'invalid arguments to textilizable'
563 raise ArgumentError, 'invalid arguments to textilizable'
564 end
564 end
565 return '' if text.blank?
565 return '' if text.blank?
566 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
566 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
567 @only_path = only_path = options.delete(:only_path) == false ? false : true
567 @only_path = only_path = options.delete(:only_path) == false ? false : true
568
568
569 text = text.dup
569 text = text.dup
570 macros = catch_macros(text)
570 macros = catch_macros(text)
571 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
571 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
572
572
573 @parsed_headings = []
573 @parsed_headings = []
574 @heading_anchors = {}
574 @heading_anchors = {}
575 @current_section = 0 if options[:edit_section_links]
575 @current_section = 0 if options[:edit_section_links]
576
576
577 parse_sections(text, project, obj, attr, only_path, options)
577 parse_sections(text, project, obj, attr, only_path, options)
578 text = parse_non_pre_blocks(text, obj, macros) do |text|
578 text = parse_non_pre_blocks(text, obj, macros) do |text|
579 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
579 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
580 send method_name, text, project, obj, attr, only_path, options
580 send method_name, text, project, obj, attr, only_path, options
581 end
581 end
582 end
582 end
583 parse_headings(text, project, obj, attr, only_path, options)
583 parse_headings(text, project, obj, attr, only_path, options)
584
584
585 if @parsed_headings.any?
585 if @parsed_headings.any?
586 replace_toc(text, @parsed_headings)
586 replace_toc(text, @parsed_headings)
587 end
587 end
588
588
589 text.html_safe
589 text.html_safe
590 end
590 end
591
591
592 def parse_non_pre_blocks(text, obj, macros)
592 def parse_non_pre_blocks(text, obj, macros)
593 s = StringScanner.new(text)
593 s = StringScanner.new(text)
594 tags = []
594 tags = []
595 parsed = ''
595 parsed = ''
596 while !s.eos?
596 while !s.eos?
597 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
597 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
598 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
598 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
599 if tags.empty?
599 if tags.empty?
600 yield text
600 yield text
601 inject_macros(text, obj, macros) if macros.any?
601 inject_macros(text, obj, macros) if macros.any?
602 else
602 else
603 inject_macros(text, obj, macros, false) if macros.any?
603 inject_macros(text, obj, macros, false) if macros.any?
604 end
604 end
605 parsed << text
605 parsed << text
606 if tag
606 if tag
607 if closing
607 if closing
608 if tags.last == tag.downcase
608 if tags.last == tag.downcase
609 tags.pop
609 tags.pop
610 end
610 end
611 else
611 else
612 tags << tag.downcase
612 tags << tag.downcase
613 end
613 end
614 parsed << full_tag
614 parsed << full_tag
615 end
615 end
616 end
616 end
617 # Close any non closing tags
617 # Close any non closing tags
618 while tag = tags.pop
618 while tag = tags.pop
619 parsed << "</#{tag}>"
619 parsed << "</#{tag}>"
620 end
620 end
621 parsed
621 parsed
622 end
622 end
623
623
624 def parse_inline_attachments(text, project, obj, attr, only_path, options)
624 def parse_inline_attachments(text, project, obj, attr, only_path, options)
625 return if options[:inline_attachments] == false
626
625 # when using an image link, try to use an attachment, if possible
627 # when using an image link, try to use an attachment, if possible
626 attachments = options[:attachments] || []
628 attachments = options[:attachments] || []
627 attachments += obj.attachments if obj.respond_to?(:attachments)
629 attachments += obj.attachments if obj.respond_to?(:attachments)
628 if attachments.present?
630 if attachments.present?
629 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
631 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
630 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
632 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
631 # search for the picture in attachments
633 # search for the picture in attachments
632 if found = Attachment.latest_attach(attachments, filename)
634 if found = Attachment.latest_attach(attachments, filename)
633 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
635 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
634 desc = found.description.to_s.gsub('"', '')
636 desc = found.description.to_s.gsub('"', '')
635 if !desc.blank? && alttext.blank?
637 if !desc.blank? && alttext.blank?
636 alt = " title=\"#{desc}\" alt=\"#{desc}\""
638 alt = " title=\"#{desc}\" alt=\"#{desc}\""
637 end
639 end
638 "src=\"#{image_url}\"#{alt}"
640 "src=\"#{image_url}\"#{alt}"
639 else
641 else
640 m
642 m
641 end
643 end
642 end
644 end
643 end
645 end
644 end
646 end
645
647
646 # Wiki links
648 # Wiki links
647 #
649 #
648 # Examples:
650 # Examples:
649 # [[mypage]]
651 # [[mypage]]
650 # [[mypage|mytext]]
652 # [[mypage|mytext]]
651 # wiki links can refer other project wikis, using project name or identifier:
653 # wiki links can refer other project wikis, using project name or identifier:
652 # [[project:]] -> wiki starting page
654 # [[project:]] -> wiki starting page
653 # [[project:|mytext]]
655 # [[project:|mytext]]
654 # [[project:mypage]]
656 # [[project:mypage]]
655 # [[project:mypage|mytext]]
657 # [[project:mypage|mytext]]
656 def parse_wiki_links(text, project, obj, attr, only_path, options)
658 def parse_wiki_links(text, project, obj, attr, only_path, options)
657 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
659 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
658 link_project = project
660 link_project = project
659 esc, all, page, title = $1, $2, $3, $5
661 esc, all, page, title = $1, $2, $3, $5
660 if esc.nil?
662 if esc.nil?
661 if page =~ /^([^\:]+)\:(.*)$/
663 if page =~ /^([^\:]+)\:(.*)$/
662 identifier, page = $1, $2
664 identifier, page = $1, $2
663 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
665 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
664 title ||= identifier if page.blank?
666 title ||= identifier if page.blank?
665 end
667 end
666
668
667 if link_project && link_project.wiki
669 if link_project && link_project.wiki
668 # extract anchor
670 # extract anchor
669 anchor = nil
671 anchor = nil
670 if page =~ /^(.+?)\#(.+)$/
672 if page =~ /^(.+?)\#(.+)$/
671 page, anchor = $1, $2
673 page, anchor = $1, $2
672 end
674 end
673 anchor = sanitize_anchor_name(anchor) if anchor.present?
675 anchor = sanitize_anchor_name(anchor) if anchor.present?
674 # check if page exists
676 # check if page exists
675 wiki_page = link_project.wiki.find_page(page)
677 wiki_page = link_project.wiki.find_page(page)
676 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
678 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
677 "##{anchor}"
679 "##{anchor}"
678 else
680 else
679 case options[:wiki_links]
681 case options[:wiki_links]
680 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
682 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
681 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
683 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
682 else
684 else
683 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
685 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
684 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
686 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
685 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
687 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
686 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
688 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
687 end
689 end
688 end
690 end
689 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
691 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
690 else
692 else
691 # project or wiki doesn't exist
693 # project or wiki doesn't exist
692 all
694 all
693 end
695 end
694 else
696 else
695 all
697 all
696 end
698 end
697 end
699 end
698 end
700 end
699
701
700 # Redmine links
702 # Redmine links
701 #
703 #
702 # Examples:
704 # Examples:
703 # Issues:
705 # Issues:
704 # #52 -> Link to issue #52
706 # #52 -> Link to issue #52
705 # Changesets:
707 # Changesets:
706 # r52 -> Link to revision 52
708 # r52 -> Link to revision 52
707 # commit:a85130f -> Link to scmid starting with a85130f
709 # commit:a85130f -> Link to scmid starting with a85130f
708 # Documents:
710 # Documents:
709 # document#17 -> Link to document with id 17
711 # document#17 -> Link to document with id 17
710 # document:Greetings -> Link to the document with title "Greetings"
712 # document:Greetings -> Link to the document with title "Greetings"
711 # document:"Some document" -> Link to the document with title "Some document"
713 # document:"Some document" -> Link to the document with title "Some document"
712 # Versions:
714 # Versions:
713 # version#3 -> Link to version with id 3
715 # version#3 -> Link to version with id 3
714 # version:1.0.0 -> Link to version named "1.0.0"
716 # version:1.0.0 -> Link to version named "1.0.0"
715 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
717 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
716 # Attachments:
718 # Attachments:
717 # attachment:file.zip -> Link to the attachment of the current object named file.zip
719 # attachment:file.zip -> Link to the attachment of the current object named file.zip
718 # Source files:
720 # Source files:
719 # source:some/file -> Link to the file located at /some/file in the project's repository
721 # source:some/file -> Link to the file located at /some/file in the project's repository
720 # source:some/file@52 -> Link to the file's revision 52
722 # source:some/file@52 -> Link to the file's revision 52
721 # source:some/file#L120 -> Link to line 120 of the file
723 # source:some/file#L120 -> Link to line 120 of the file
722 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
724 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
723 # export:some/file -> Force the download of the file
725 # export:some/file -> Force the download of the file
724 # Forum messages:
726 # Forum messages:
725 # message#1218 -> Link to message with id 1218
727 # message#1218 -> Link to message with id 1218
726 # Projects:
728 # Projects:
727 # project:someproject -> Link to project named "someproject"
729 # project:someproject -> Link to project named "someproject"
728 # project#3 -> Link to project with id 3
730 # project#3 -> Link to project with id 3
729 #
731 #
730 # Links can refer other objects from other projects, using project identifier:
732 # Links can refer other objects from other projects, using project identifier:
731 # identifier:r52
733 # identifier:r52
732 # identifier:document:"Some document"
734 # identifier:document:"Some document"
733 # identifier:version:1.0.0
735 # identifier:version:1.0.0
734 # identifier:source:some/file
736 # identifier:source:some/file
735 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
737 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
736 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|
738 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|
737 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
739 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
738 if tag_content
740 if tag_content
739 $&
741 $&
740 else
742 else
741 link = nil
743 link = nil
742 project = default_project
744 project = default_project
743 if project_identifier
745 if project_identifier
744 project = Project.visible.find_by_identifier(project_identifier)
746 project = Project.visible.find_by_identifier(project_identifier)
745 end
747 end
746 if esc.nil?
748 if esc.nil?
747 if prefix.nil? && sep == 'r'
749 if prefix.nil? && sep == 'r'
748 if project
750 if project
749 repository = nil
751 repository = nil
750 if repo_identifier
752 if repo_identifier
751 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
753 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
752 else
754 else
753 repository = project.repository
755 repository = project.repository
754 end
756 end
755 # project.changesets.visible raises an SQL error because of a double join on repositories
757 # project.changesets.visible raises an SQL error because of a double join on repositories
756 if repository &&
758 if repository &&
757 (changeset = Changeset.visible.
759 (changeset = Changeset.visible.
758 find_by_repository_id_and_revision(repository.id, identifier))
760 find_by_repository_id_and_revision(repository.id, identifier))
759 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
761 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
760 {:only_path => only_path, :controller => 'repositories',
762 {:only_path => only_path, :controller => 'repositories',
761 :action => 'revision', :id => project,
763 :action => 'revision', :id => project,
762 :repository_id => repository.identifier_param,
764 :repository_id => repository.identifier_param,
763 :rev => changeset.revision},
765 :rev => changeset.revision},
764 :class => 'changeset',
766 :class => 'changeset',
765 :title => truncate_single_line_raw(changeset.comments, 100))
767 :title => truncate_single_line_raw(changeset.comments, 100))
766 end
768 end
767 end
769 end
768 elsif sep == '#'
770 elsif sep == '#'
769 oid = identifier.to_i
771 oid = identifier.to_i
770 case prefix
772 case prefix
771 when nil
773 when nil
772 if oid.to_s == identifier &&
774 if oid.to_s == identifier &&
773 issue = Issue.visible.find_by_id(oid)
775 issue = Issue.visible.find_by_id(oid)
774 anchor = comment_id ? "note-#{comment_id}" : nil
776 anchor = comment_id ? "note-#{comment_id}" : nil
775 link = link_to("##{oid}#{comment_suffix}",
777 link = link_to("##{oid}#{comment_suffix}",
776 issue_url(issue, :only_path => only_path, :anchor => anchor),
778 issue_url(issue, :only_path => only_path, :anchor => anchor),
777 :class => issue.css_classes,
779 :class => issue.css_classes,
778 :title => "#{issue.subject.truncate(100)} (#{issue.status.name})")
780 :title => "#{issue.subject.truncate(100)} (#{issue.status.name})")
779 end
781 end
780 when 'document'
782 when 'document'
781 if document = Document.visible.find_by_id(oid)
783 if document = Document.visible.find_by_id(oid)
782 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
784 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
783 end
785 end
784 when 'version'
786 when 'version'
785 if version = Version.visible.find_by_id(oid)
787 if version = Version.visible.find_by_id(oid)
786 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
788 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
787 end
789 end
788 when 'message'
790 when 'message'
789 if message = Message.visible.find_by_id(oid)
791 if message = Message.visible.find_by_id(oid)
790 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
792 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
791 end
793 end
792 when 'forum'
794 when 'forum'
793 if board = Board.visible.find_by_id(oid)
795 if board = Board.visible.find_by_id(oid)
794 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
796 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
795 end
797 end
796 when 'news'
798 when 'news'
797 if news = News.visible.find_by_id(oid)
799 if news = News.visible.find_by_id(oid)
798 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
800 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
799 end
801 end
800 when 'project'
802 when 'project'
801 if p = Project.visible.find_by_id(oid)
803 if p = Project.visible.find_by_id(oid)
802 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
804 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
803 end
805 end
804 end
806 end
805 elsif sep == ':'
807 elsif sep == ':'
806 # removes the double quotes if any
808 # removes the double quotes if any
807 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
809 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
808 name = CGI.unescapeHTML(name)
810 name = CGI.unescapeHTML(name)
809 case prefix
811 case prefix
810 when 'document'
812 when 'document'
811 if project && document = project.documents.visible.find_by_title(name)
813 if project && document = project.documents.visible.find_by_title(name)
812 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
814 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
813 end
815 end
814 when 'version'
816 when 'version'
815 if project && version = project.versions.visible.find_by_name(name)
817 if project && version = project.versions.visible.find_by_name(name)
816 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
818 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
817 end
819 end
818 when 'forum'
820 when 'forum'
819 if project && board = project.boards.visible.find_by_name(name)
821 if project && board = project.boards.visible.find_by_name(name)
820 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
822 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
821 end
823 end
822 when 'news'
824 when 'news'
823 if project && news = project.news.visible.find_by_title(name)
825 if project && news = project.news.visible.find_by_title(name)
824 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
826 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
825 end
827 end
826 when 'commit', 'source', 'export'
828 when 'commit', 'source', 'export'
827 if project
829 if project
828 repository = nil
830 repository = nil
829 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
831 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
830 repo_prefix, repo_identifier, name = $1, $2, $3
832 repo_prefix, repo_identifier, name = $1, $2, $3
831 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
833 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
832 else
834 else
833 repository = project.repository
835 repository = project.repository
834 end
836 end
835 if prefix == 'commit'
837 if prefix == 'commit'
836 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
838 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
837 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},
839 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},
838 :class => 'changeset',
840 :class => 'changeset',
839 :title => truncate_single_line_raw(changeset.comments, 100)
841 :title => truncate_single_line_raw(changeset.comments, 100)
840 end
842 end
841 else
843 else
842 if repository && User.current.allowed_to?(:browse_repository, project)
844 if repository && User.current.allowed_to?(:browse_repository, project)
843 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
845 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
844 path, rev, anchor = $1, $3, $5
846 path, rev, anchor = $1, $3, $5
845 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,
847 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,
846 :path => to_path_param(path),
848 :path => to_path_param(path),
847 :rev => rev,
849 :rev => rev,
848 :anchor => anchor},
850 :anchor => anchor},
849 :class => (prefix == 'export' ? 'source download' : 'source')
851 :class => (prefix == 'export' ? 'source download' : 'source')
850 end
852 end
851 end
853 end
852 repo_prefix = nil
854 repo_prefix = nil
853 end
855 end
854 when 'attachment'
856 when 'attachment'
855 attachments = options[:attachments] || []
857 attachments = options[:attachments] || []
856 attachments += obj.attachments if obj.respond_to?(:attachments)
858 attachments += obj.attachments if obj.respond_to?(:attachments)
857 if attachments && attachment = Attachment.latest_attach(attachments, name)
859 if attachments && attachment = Attachment.latest_attach(attachments, name)
858 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
860 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
859 end
861 end
860 when 'project'
862 when 'project'
861 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
863 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
862 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
864 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
863 end
865 end
864 end
866 end
865 end
867 end
866 end
868 end
867 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
869 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
868 end
870 end
869 end
871 end
870 end
872 end
871
873
872 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
874 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
873
875
874 def parse_sections(text, project, obj, attr, only_path, options)
876 def parse_sections(text, project, obj, attr, only_path, options)
875 return unless options[:edit_section_links]
877 return unless options[:edit_section_links]
876 text.gsub!(HEADING_RE) do
878 text.gsub!(HEADING_RE) do
877 heading = $1
879 heading = $1
878 @current_section += 1
880 @current_section += 1
879 if @current_section > 1
881 if @current_section > 1
880 content_tag('div',
882 content_tag('div',
881 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
883 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
882 :class => 'contextual',
884 :class => 'contextual',
883 :title => l(:button_edit_section),
885 :title => l(:button_edit_section),
884 :id => "section-#{@current_section}") + heading.html_safe
886 :id => "section-#{@current_section}") + heading.html_safe
885 else
887 else
886 heading
888 heading
887 end
889 end
888 end
890 end
889 end
891 end
890
892
891 # Headings and TOC
893 # Headings and TOC
892 # Adds ids and links to headings unless options[:headings] is set to false
894 # Adds ids and links to headings unless options[:headings] is set to false
893 def parse_headings(text, project, obj, attr, only_path, options)
895 def parse_headings(text, project, obj, attr, only_path, options)
894 return if options[:headings] == false
896 return if options[:headings] == false
895
897
896 text.gsub!(HEADING_RE) do
898 text.gsub!(HEADING_RE) do
897 level, attrs, content = $2.to_i, $3, $4
899 level, attrs, content = $2.to_i, $3, $4
898 item = strip_tags(content).strip
900 item = strip_tags(content).strip
899 anchor = sanitize_anchor_name(item)
901 anchor = sanitize_anchor_name(item)
900 # used for single-file wiki export
902 # used for single-file wiki export
901 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
903 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
902 @heading_anchors[anchor] ||= 0
904 @heading_anchors[anchor] ||= 0
903 idx = (@heading_anchors[anchor] += 1)
905 idx = (@heading_anchors[anchor] += 1)
904 if idx > 1
906 if idx > 1
905 anchor = "#{anchor}-#{idx}"
907 anchor = "#{anchor}-#{idx}"
906 end
908 end
907 @parsed_headings << [level, anchor, item]
909 @parsed_headings << [level, anchor, item]
908 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
910 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
909 end
911 end
910 end
912 end
911
913
912 MACROS_RE = /(
914 MACROS_RE = /(
913 (!)? # escaping
915 (!)? # escaping
914 (
916 (
915 \{\{ # opening tag
917 \{\{ # opening tag
916 ([\w]+) # macro name
918 ([\w]+) # macro name
917 (\(([^\n\r]*?)\))? # optional arguments
919 (\(([^\n\r]*?)\))? # optional arguments
918 ([\n\r].*?[\n\r])? # optional block of text
920 ([\n\r].*?[\n\r])? # optional block of text
919 \}\} # closing tag
921 \}\} # closing tag
920 )
922 )
921 )/mx unless const_defined?(:MACROS_RE)
923 )/mx unless const_defined?(:MACROS_RE)
922
924
923 MACRO_SUB_RE = /(
925 MACRO_SUB_RE = /(
924 \{\{
926 \{\{
925 macro\((\d+)\)
927 macro\((\d+)\)
926 \}\}
928 \}\}
927 )/x unless const_defined?(:MACRO_SUB_RE)
929 )/x unless const_defined?(:MACRO_SUB_RE)
928
930
929 # Extracts macros from text
931 # Extracts macros from text
930 def catch_macros(text)
932 def catch_macros(text)
931 macros = {}
933 macros = {}
932 text.gsub!(MACROS_RE) do
934 text.gsub!(MACROS_RE) do
933 all, macro = $1, $4.downcase
935 all, macro = $1, $4.downcase
934 if macro_exists?(macro) || all =~ MACRO_SUB_RE
936 if macro_exists?(macro) || all =~ MACRO_SUB_RE
935 index = macros.size
937 index = macros.size
936 macros[index] = all
938 macros[index] = all
937 "{{macro(#{index})}}"
939 "{{macro(#{index})}}"
938 else
940 else
939 all
941 all
940 end
942 end
941 end
943 end
942 macros
944 macros
943 end
945 end
944
946
945 # Executes and replaces macros in text
947 # Executes and replaces macros in text
946 def inject_macros(text, obj, macros, execute=true)
948 def inject_macros(text, obj, macros, execute=true)
947 text.gsub!(MACRO_SUB_RE) do
949 text.gsub!(MACRO_SUB_RE) do
948 all, index = $1, $2.to_i
950 all, index = $1, $2.to_i
949 orig = macros.delete(index)
951 orig = macros.delete(index)
950 if execute && orig && orig =~ MACROS_RE
952 if execute && orig && orig =~ MACROS_RE
951 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
953 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
952 if esc.nil?
954 if esc.nil?
953 h(exec_macro(macro, obj, args, block) || all)
955 h(exec_macro(macro, obj, args, block) || all)
954 else
956 else
955 h(all)
957 h(all)
956 end
958 end
957 elsif orig
959 elsif orig
958 h(orig)
960 h(orig)
959 else
961 else
960 h(all)
962 h(all)
961 end
963 end
962 end
964 end
963 end
965 end
964
966
965 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
967 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
966
968
967 # Renders the TOC with given headings
969 # Renders the TOC with given headings
968 def replace_toc(text, headings)
970 def replace_toc(text, headings)
969 text.gsub!(TOC_RE) do
971 text.gsub!(TOC_RE) do
970 left_align, right_align = $2, $3
972 left_align, right_align = $2, $3
971 # Keep only the 4 first levels
973 # Keep only the 4 first levels
972 headings = headings.select{|level, anchor, item| level <= 4}
974 headings = headings.select{|level, anchor, item| level <= 4}
973 if headings.empty?
975 if headings.empty?
974 ''
976 ''
975 else
977 else
976 div_class = 'toc'
978 div_class = 'toc'
977 div_class << ' right' if right_align
979 div_class << ' right' if right_align
978 div_class << ' left' if left_align
980 div_class << ' left' if left_align
979 out = "<ul class=\"#{div_class}\"><li>"
981 out = "<ul class=\"#{div_class}\"><li>"
980 root = headings.map(&:first).min
982 root = headings.map(&:first).min
981 current = root
983 current = root
982 started = false
984 started = false
983 headings.each do |level, anchor, item|
985 headings.each do |level, anchor, item|
984 if level > current
986 if level > current
985 out << '<ul><li>' * (level - current)
987 out << '<ul><li>' * (level - current)
986 elsif level < current
988 elsif level < current
987 out << "</li></ul>\n" * (current - level) + "</li><li>"
989 out << "</li></ul>\n" * (current - level) + "</li><li>"
988 elsif started
990 elsif started
989 out << '</li><li>'
991 out << '</li><li>'
990 end
992 end
991 out << "<a href=\"##{anchor}\">#{item}</a>"
993 out << "<a href=\"##{anchor}\">#{item}</a>"
992 current = level
994 current = level
993 started = true
995 started = true
994 end
996 end
995 out << '</li></ul>' * (current - root)
997 out << '</li></ul>' * (current - root)
996 out << '</li></ul>'
998 out << '</li></ul>'
997 end
999 end
998 end
1000 end
999 end
1001 end
1000
1002
1001 # Same as Rails' simple_format helper without using paragraphs
1003 # Same as Rails' simple_format helper without using paragraphs
1002 def simple_format_without_paragraph(text)
1004 def simple_format_without_paragraph(text)
1003 text.to_s.
1005 text.to_s.
1004 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1006 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1005 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1007 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1006 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1008 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1007 html_safe
1009 html_safe
1008 end
1010 end
1009
1011
1010 def lang_options_for_select(blank=true)
1012 def lang_options_for_select(blank=true)
1011 (blank ? [["(auto)", ""]] : []) + languages_options
1013 (blank ? [["(auto)", ""]] : []) + languages_options
1012 end
1014 end
1013
1015
1014 def labelled_form_for(*args, &proc)
1016 def labelled_form_for(*args, &proc)
1015 args << {} unless args.last.is_a?(Hash)
1017 args << {} unless args.last.is_a?(Hash)
1016 options = args.last
1018 options = args.last
1017 if args.first.is_a?(Symbol)
1019 if args.first.is_a?(Symbol)
1018 options.merge!(:as => args.shift)
1020 options.merge!(:as => args.shift)
1019 end
1021 end
1020 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1022 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1021 form_for(*args, &proc)
1023 form_for(*args, &proc)
1022 end
1024 end
1023
1025
1024 def labelled_fields_for(*args, &proc)
1026 def labelled_fields_for(*args, &proc)
1025 args << {} unless args.last.is_a?(Hash)
1027 args << {} unless args.last.is_a?(Hash)
1026 options = args.last
1028 options = args.last
1027 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1029 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1028 fields_for(*args, &proc)
1030 fields_for(*args, &proc)
1029 end
1031 end
1030
1032
1031 def error_messages_for(*objects)
1033 def error_messages_for(*objects)
1032 html = ""
1034 html = ""
1033 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1035 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1034 errors = objects.map {|o| o.errors.full_messages}.flatten
1036 errors = objects.map {|o| o.errors.full_messages}.flatten
1035 if errors.any?
1037 if errors.any?
1036 html << "<div id='errorExplanation'><ul>\n"
1038 html << "<div id='errorExplanation'><ul>\n"
1037 errors.each do |error|
1039 errors.each do |error|
1038 html << "<li>#{h error}</li>\n"
1040 html << "<li>#{h error}</li>\n"
1039 end
1041 end
1040 html << "</ul></div>\n"
1042 html << "</ul></div>\n"
1041 end
1043 end
1042 html.html_safe
1044 html.html_safe
1043 end
1045 end
1044
1046
1045 def delete_link(url, options={})
1047 def delete_link(url, options={})
1046 options = {
1048 options = {
1047 :method => :delete,
1049 :method => :delete,
1048 :data => {:confirm => l(:text_are_you_sure)},
1050 :data => {:confirm => l(:text_are_you_sure)},
1049 :class => 'icon icon-del'
1051 :class => 'icon icon-del'
1050 }.merge(options)
1052 }.merge(options)
1051
1053
1052 link_to l(:button_delete), url, options
1054 link_to l(:button_delete), url, options
1053 end
1055 end
1054
1056
1055 def preview_link(url, form, target='preview', options={})
1057 def preview_link(url, form, target='preview', options={})
1056 content_tag 'a', l(:label_preview), {
1058 content_tag 'a', l(:label_preview), {
1057 :href => "#",
1059 :href => "#",
1058 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1060 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1059 :accesskey => accesskey(:preview)
1061 :accesskey => accesskey(:preview)
1060 }.merge(options)
1062 }.merge(options)
1061 end
1063 end
1062
1064
1063 def link_to_function(name, function, html_options={})
1065 def link_to_function(name, function, html_options={})
1064 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1066 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1065 end
1067 end
1066
1068
1067 # Helper to render JSON in views
1069 # Helper to render JSON in views
1068 def raw_json(arg)
1070 def raw_json(arg)
1069 arg.to_json.to_s.gsub('/', '\/').html_safe
1071 arg.to_json.to_s.gsub('/', '\/').html_safe
1070 end
1072 end
1071
1073
1072 def back_url
1074 def back_url
1073 url = params[:back_url]
1075 url = params[:back_url]
1074 if url.nil? && referer = request.env['HTTP_REFERER']
1076 if url.nil? && referer = request.env['HTTP_REFERER']
1075 url = CGI.unescape(referer.to_s)
1077 url = CGI.unescape(referer.to_s)
1076 end
1078 end
1077 url
1079 url
1078 end
1080 end
1079
1081
1080 def back_url_hidden_field_tag
1082 def back_url_hidden_field_tag
1081 url = back_url
1083 url = back_url
1082 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1084 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1083 end
1085 end
1084
1086
1085 def check_all_links(form_name)
1087 def check_all_links(form_name)
1086 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1088 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1087 " | ".html_safe +
1089 " | ".html_safe +
1088 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1090 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1089 end
1091 end
1090
1092
1091 def toggle_checkboxes_link(selector)
1093 def toggle_checkboxes_link(selector)
1092 link_to_function image_tag('toggle_check.png'),
1094 link_to_function image_tag('toggle_check.png'),
1093 "toggleCheckboxesBySelector('#{selector}')",
1095 "toggleCheckboxesBySelector('#{selector}')",
1094 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}"
1096 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}"
1095 end
1097 end
1096
1098
1097 def progress_bar(pcts, options={})
1099 def progress_bar(pcts, options={})
1098 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1100 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1099 pcts = pcts.collect(&:round)
1101 pcts = pcts.collect(&:round)
1100 pcts[1] = pcts[1] - pcts[0]
1102 pcts[1] = pcts[1] - pcts[0]
1101 pcts << (100 - pcts[1] - pcts[0])
1103 pcts << (100 - pcts[1] - pcts[0])
1102 width = options[:width] || '100px;'
1104 width = options[:width] || '100px;'
1103 legend = options[:legend] || ''
1105 legend = options[:legend] || ''
1104 content_tag('table',
1106 content_tag('table',
1105 content_tag('tr',
1107 content_tag('tr',
1106 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1108 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1107 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1109 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1108 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1110 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1109 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1111 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1110 content_tag('p', legend, :class => 'percent').html_safe
1112 content_tag('p', legend, :class => 'percent').html_safe
1111 end
1113 end
1112
1114
1113 def checked_image(checked=true)
1115 def checked_image(checked=true)
1114 if checked
1116 if checked
1115 image_tag 'toggle_check.png'
1117 image_tag 'toggle_check.png'
1116 end
1118 end
1117 end
1119 end
1118
1120
1119 def context_menu(url)
1121 def context_menu(url)
1120 unless @context_menu_included
1122 unless @context_menu_included
1121 content_for :header_tags do
1123 content_for :header_tags do
1122 javascript_include_tag('context_menu') +
1124 javascript_include_tag('context_menu') +
1123 stylesheet_link_tag('context_menu')
1125 stylesheet_link_tag('context_menu')
1124 end
1126 end
1125 if l(:direction) == 'rtl'
1127 if l(:direction) == 'rtl'
1126 content_for :header_tags do
1128 content_for :header_tags do
1127 stylesheet_link_tag('context_menu_rtl')
1129 stylesheet_link_tag('context_menu_rtl')
1128 end
1130 end
1129 end
1131 end
1130 @context_menu_included = true
1132 @context_menu_included = true
1131 end
1133 end
1132 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1134 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1133 end
1135 end
1134
1136
1135 def calendar_for(field_id)
1137 def calendar_for(field_id)
1136 include_calendar_headers_tags
1138 include_calendar_headers_tags
1137 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1139 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1138 end
1140 end
1139
1141
1140 def include_calendar_headers_tags
1142 def include_calendar_headers_tags
1141 unless @calendar_headers_tags_included
1143 unless @calendar_headers_tags_included
1142 tags = ''.html_safe
1144 tags = ''.html_safe
1143 @calendar_headers_tags_included = true
1145 @calendar_headers_tags_included = true
1144 content_for :header_tags do
1146 content_for :header_tags do
1145 start_of_week = Setting.start_of_week
1147 start_of_week = Setting.start_of_week
1146 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1148 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1147 # Redmine uses 1..7 (monday..sunday) in settings and locales
1149 # Redmine uses 1..7 (monday..sunday) in settings and locales
1148 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1150 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1149 start_of_week = start_of_week.to_i % 7
1151 start_of_week = start_of_week.to_i % 7
1150 tags << javascript_tag(
1152 tags << javascript_tag(
1151 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1153 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1152 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1154 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1153 path_to_image('/images/calendar.png') +
1155 path_to_image('/images/calendar.png') +
1154 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1156 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1155 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1157 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1156 "beforeShow: beforeShowDatePicker};")
1158 "beforeShow: beforeShowDatePicker};")
1157 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1159 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1158 unless jquery_locale == 'en'
1160 unless jquery_locale == 'en'
1159 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1161 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1160 end
1162 end
1161 tags
1163 tags
1162 end
1164 end
1163 end
1165 end
1164 end
1166 end
1165
1167
1166 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1168 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1167 # Examples:
1169 # Examples:
1168 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1170 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1169 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1171 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1170 #
1172 #
1171 def stylesheet_link_tag(*sources)
1173 def stylesheet_link_tag(*sources)
1172 options = sources.last.is_a?(Hash) ? sources.pop : {}
1174 options = sources.last.is_a?(Hash) ? sources.pop : {}
1173 plugin = options.delete(:plugin)
1175 plugin = options.delete(:plugin)
1174 sources = sources.map do |source|
1176 sources = sources.map do |source|
1175 if plugin
1177 if plugin
1176 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1178 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1177 elsif current_theme && current_theme.stylesheets.include?(source)
1179 elsif current_theme && current_theme.stylesheets.include?(source)
1178 current_theme.stylesheet_path(source)
1180 current_theme.stylesheet_path(source)
1179 else
1181 else
1180 source
1182 source
1181 end
1183 end
1182 end
1184 end
1183 super *sources, options
1185 super *sources, options
1184 end
1186 end
1185
1187
1186 # Overrides Rails' image_tag with themes and plugins support.
1188 # Overrides Rails' image_tag with themes and plugins support.
1187 # Examples:
1189 # Examples:
1188 # image_tag('image.png') # => picks image.png from the current theme or defaults
1190 # image_tag('image.png') # => picks image.png from the current theme or defaults
1189 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1191 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1190 #
1192 #
1191 def image_tag(source, options={})
1193 def image_tag(source, options={})
1192 if plugin = options.delete(:plugin)
1194 if plugin = options.delete(:plugin)
1193 source = "/plugin_assets/#{plugin}/images/#{source}"
1195 source = "/plugin_assets/#{plugin}/images/#{source}"
1194 elsif current_theme && current_theme.images.include?(source)
1196 elsif current_theme && current_theme.images.include?(source)
1195 source = current_theme.image_path(source)
1197 source = current_theme.image_path(source)
1196 end
1198 end
1197 super source, options
1199 super source, options
1198 end
1200 end
1199
1201
1200 # Overrides Rails' javascript_include_tag with plugins support
1202 # Overrides Rails' javascript_include_tag with plugins support
1201 # Examples:
1203 # Examples:
1202 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1204 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1203 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1205 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1204 #
1206 #
1205 def javascript_include_tag(*sources)
1207 def javascript_include_tag(*sources)
1206 options = sources.last.is_a?(Hash) ? sources.pop : {}
1208 options = sources.last.is_a?(Hash) ? sources.pop : {}
1207 if plugin = options.delete(:plugin)
1209 if plugin = options.delete(:plugin)
1208 sources = sources.map do |source|
1210 sources = sources.map do |source|
1209 if plugin
1211 if plugin
1210 "/plugin_assets/#{plugin}/javascripts/#{source}"
1212 "/plugin_assets/#{plugin}/javascripts/#{source}"
1211 else
1213 else
1212 source
1214 source
1213 end
1215 end
1214 end
1216 end
1215 end
1217 end
1216 super *sources, options
1218 super *sources, options
1217 end
1219 end
1218
1220
1219 def sidebar_content?
1221 def sidebar_content?
1220 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1222 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1221 end
1223 end
1222
1224
1223 def view_layouts_base_sidebar_hook_response
1225 def view_layouts_base_sidebar_hook_response
1224 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1226 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1225 end
1227 end
1226
1228
1227 def email_delivery_enabled?
1229 def email_delivery_enabled?
1228 !!ActionMailer::Base.perform_deliveries
1230 !!ActionMailer::Base.perform_deliveries
1229 end
1231 end
1230
1232
1231 # Returns the avatar image tag for the given +user+ if avatars are enabled
1233 # Returns the avatar image tag for the given +user+ if avatars are enabled
1232 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1234 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1233 def avatar(user, options = { })
1235 def avatar(user, options = { })
1234 if Setting.gravatar_enabled?
1236 if Setting.gravatar_enabled?
1235 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1237 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1236 email = nil
1238 email = nil
1237 if user.respond_to?(:mail)
1239 if user.respond_to?(:mail)
1238 email = user.mail
1240 email = user.mail
1239 elsif user.to_s =~ %r{<(.+?)>}
1241 elsif user.to_s =~ %r{<(.+?)>}
1240 email = $1
1242 email = $1
1241 end
1243 end
1242 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1244 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1243 else
1245 else
1244 ''
1246 ''
1245 end
1247 end
1246 end
1248 end
1247
1249
1248 def sanitize_anchor_name(anchor)
1250 def sanitize_anchor_name(anchor)
1249 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1251 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1250 end
1252 end
1251
1253
1252 # Returns the javascript tags that are included in the html layout head
1254 # Returns the javascript tags that are included in the html layout head
1253 def javascript_heads
1255 def javascript_heads
1254 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.1', 'application')
1256 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.1', 'application')
1255 unless User.current.pref.warn_on_leaving_unsaved == '0'
1257 unless User.current.pref.warn_on_leaving_unsaved == '0'
1256 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1258 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1257 end
1259 end
1258 tags
1260 tags
1259 end
1261 end
1260
1262
1261 def favicon
1263 def favicon
1262 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1264 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1263 end
1265 end
1264
1266
1265 # Returns the path to the favicon
1267 # Returns the path to the favicon
1266 def favicon_path
1268 def favicon_path
1267 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1269 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1268 image_path(icon)
1270 image_path(icon)
1269 end
1271 end
1270
1272
1271 # Returns the full URL to the favicon
1273 # Returns the full URL to the favicon
1272 def favicon_url
1274 def favicon_url
1273 # TODO: use #image_url introduced in Rails4
1275 # TODO: use #image_url introduced in Rails4
1274 path = favicon_path
1276 path = favicon_path
1275 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1277 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1276 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1278 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1277 end
1279 end
1278
1280
1279 def robot_exclusion_tag
1281 def robot_exclusion_tag
1280 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1282 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1281 end
1283 end
1282
1284
1283 # Returns true if arg is expected in the API response
1285 # Returns true if arg is expected in the API response
1284 def include_in_api_response?(arg)
1286 def include_in_api_response?(arg)
1285 unless @included_in_api_response
1287 unless @included_in_api_response
1286 param = params[:include]
1288 param = params[:include]
1287 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1289 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1288 @included_in_api_response.collect!(&:strip)
1290 @included_in_api_response.collect!(&:strip)
1289 end
1291 end
1290 @included_in_api_response.include?(arg.to_s)
1292 @included_in_api_response.include?(arg.to_s)
1291 end
1293 end
1292
1294
1293 # Returns options or nil if nometa param or X-Redmine-Nometa header
1295 # Returns options or nil if nometa param or X-Redmine-Nometa header
1294 # was set in the request
1296 # was set in the request
1295 def api_meta(options)
1297 def api_meta(options)
1296 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1298 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1297 # compatibility mode for activeresource clients that raise
1299 # compatibility mode for activeresource clients that raise
1298 # an error when deserializing an array with attributes
1300 # an error when deserializing an array with attributes
1299 nil
1301 nil
1300 else
1302 else
1301 options
1303 options
1302 end
1304 end
1303 end
1305 end
1304
1306
1305 private
1307 private
1306
1308
1307 def wiki_helper
1309 def wiki_helper
1308 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1310 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1309 extend helper
1311 extend helper
1310 return self
1312 return self
1311 end
1313 end
1312
1314
1313 def link_to_content_update(text, url_params = {}, html_options = {})
1315 def link_to_content_update(text, url_params = {}, html_options = {})
1314 link_to(text, url_params, html_options)
1316 link_to(text, url_params, html_options)
1315 end
1317 end
1316 end
1318 end
@@ -1,435 +1,436
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
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
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.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 module IssuesHelper
20 module IssuesHelper
21 include ApplicationHelper
21 include ApplicationHelper
22 include Redmine::Export::PDF::IssuesPdfHelper
22
23
23 def issue_list(issues, &block)
24 def issue_list(issues, &block)
24 ancestors = []
25 ancestors = []
25 issues.each do |issue|
26 issues.each do |issue|
26 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
27 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
27 ancestors.pop
28 ancestors.pop
28 end
29 end
29 yield issue, ancestors.size
30 yield issue, ancestors.size
30 ancestors << issue unless issue.leaf?
31 ancestors << issue unless issue.leaf?
31 end
32 end
32 end
33 end
33
34
34 # Renders a HTML/CSS tooltip
35 # Renders a HTML/CSS tooltip
35 #
36 #
36 # To use, a trigger div is needed. This is a div with the class of "tooltip"
37 # To use, a trigger div is needed. This is a div with the class of "tooltip"
37 # that contains this method wrapped in a span with the class of "tip"
38 # that contains this method wrapped in a span with the class of "tip"
38 #
39 #
39 # <div class="tooltip"><%= link_to_issue(issue) %>
40 # <div class="tooltip"><%= link_to_issue(issue) %>
40 # <span class="tip"><%= render_issue_tooltip(issue) %></span>
41 # <span class="tip"><%= render_issue_tooltip(issue) %></span>
41 # </div>
42 # </div>
42 #
43 #
43 def render_issue_tooltip(issue)
44 def render_issue_tooltip(issue)
44 @cached_label_status ||= l(:field_status)
45 @cached_label_status ||= l(:field_status)
45 @cached_label_start_date ||= l(:field_start_date)
46 @cached_label_start_date ||= l(:field_start_date)
46 @cached_label_due_date ||= l(:field_due_date)
47 @cached_label_due_date ||= l(:field_due_date)
47 @cached_label_assigned_to ||= l(:field_assigned_to)
48 @cached_label_assigned_to ||= l(:field_assigned_to)
48 @cached_label_priority ||= l(:field_priority)
49 @cached_label_priority ||= l(:field_priority)
49 @cached_label_project ||= l(:field_project)
50 @cached_label_project ||= l(:field_project)
50
51
51 link_to_issue(issue) + "<br /><br />".html_safe +
52 link_to_issue(issue) + "<br /><br />".html_safe +
52 "<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />".html_safe +
53 "<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />".html_safe +
53 "<strong>#{@cached_label_status}</strong>: #{h(issue.status.name)}<br />".html_safe +
54 "<strong>#{@cached_label_status}</strong>: #{h(issue.status.name)}<br />".html_safe +
54 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />".html_safe +
55 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />".html_safe +
55 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />".html_safe +
56 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />".html_safe +
56 "<strong>#{@cached_label_assigned_to}</strong>: #{h(issue.assigned_to)}<br />".html_safe +
57 "<strong>#{@cached_label_assigned_to}</strong>: #{h(issue.assigned_to)}<br />".html_safe +
57 "<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}".html_safe
58 "<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}".html_safe
58 end
59 end
59
60
60 def issue_heading(issue)
61 def issue_heading(issue)
61 h("#{issue.tracker} ##{issue.id}")
62 h("#{issue.tracker} ##{issue.id}")
62 end
63 end
63
64
64 def render_issue_subject_with_tree(issue)
65 def render_issue_subject_with_tree(issue)
65 s = ''
66 s = ''
66 ancestors = issue.root? ? [] : issue.ancestors.visible.to_a
67 ancestors = issue.root? ? [] : issue.ancestors.visible.to_a
67 ancestors.each do |ancestor|
68 ancestors.each do |ancestor|
68 s << '<div>' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id)))
69 s << '<div>' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id)))
69 end
70 end
70 s << '<div>'
71 s << '<div>'
71 subject = h(issue.subject)
72 subject = h(issue.subject)
72 if issue.is_private?
73 if issue.is_private?
73 subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject
74 subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject
74 end
75 end
75 s << content_tag('h3', subject)
76 s << content_tag('h3', subject)
76 s << '</div>' * (ancestors.size + 1)
77 s << '</div>' * (ancestors.size + 1)
77 s.html_safe
78 s.html_safe
78 end
79 end
79
80
80 def render_descendants_tree(issue)
81 def render_descendants_tree(issue)
81 s = '<form><table class="list issues">'
82 s = '<form><table class="list issues">'
82 issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level|
83 issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level|
83 css = "issue issue-#{child.id} hascontextmenu"
84 css = "issue issue-#{child.id} hascontextmenu"
84 css << " idnt idnt-#{level}" if level > 0
85 css << " idnt idnt-#{level}" if level > 0
85 s << content_tag('tr',
86 s << content_tag('tr',
86 content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
87 content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
87 content_tag('td', link_to_issue(child, :project => (issue.project_id != child.project_id)), :class => 'subject', :style => 'width: 50%') +
88 content_tag('td', link_to_issue(child, :project => (issue.project_id != child.project_id)), :class => 'subject', :style => 'width: 50%') +
88 content_tag('td', h(child.status)) +
89 content_tag('td', h(child.status)) +
89 content_tag('td', link_to_user(child.assigned_to)) +
90 content_tag('td', link_to_user(child.assigned_to)) +
90 content_tag('td', progress_bar(child.done_ratio, :width => '80px')),
91 content_tag('td', progress_bar(child.done_ratio, :width => '80px')),
91 :class => css)
92 :class => css)
92 end
93 end
93 s << '</table></form>'
94 s << '</table></form>'
94 s.html_safe
95 s.html_safe
95 end
96 end
96
97
97 # Returns an array of error messages for bulk edited issues
98 # Returns an array of error messages for bulk edited issues
98 def bulk_edit_error_messages(issues)
99 def bulk_edit_error_messages(issues)
99 messages = {}
100 messages = {}
100 issues.each do |issue|
101 issues.each do |issue|
101 issue.errors.full_messages.each do |message|
102 issue.errors.full_messages.each do |message|
102 messages[message] ||= []
103 messages[message] ||= []
103 messages[message] << issue
104 messages[message] << issue
104 end
105 end
105 end
106 end
106 messages.map { |message, issues|
107 messages.map { |message, issues|
107 "#{message}: " + issues.map {|i| "##{i.id}"}.join(', ')
108 "#{message}: " + issues.map {|i| "##{i.id}"}.join(', ')
108 }
109 }
109 end
110 end
110
111
111 # Returns a link for adding a new subtask to the given issue
112 # Returns a link for adding a new subtask to the given issue
112 def link_to_new_subtask(issue)
113 def link_to_new_subtask(issue)
113 attrs = {
114 attrs = {
114 :tracker_id => issue.tracker,
115 :tracker_id => issue.tracker,
115 :parent_issue_id => issue
116 :parent_issue_id => issue
116 }
117 }
117 link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs))
118 link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs))
118 end
119 end
119
120
120 class IssueFieldsRows
121 class IssueFieldsRows
121 include ActionView::Helpers::TagHelper
122 include ActionView::Helpers::TagHelper
122
123
123 def initialize
124 def initialize
124 @left = []
125 @left = []
125 @right = []
126 @right = []
126 end
127 end
127
128
128 def left(*args)
129 def left(*args)
129 args.any? ? @left << cells(*args) : @left
130 args.any? ? @left << cells(*args) : @left
130 end
131 end
131
132
132 def right(*args)
133 def right(*args)
133 args.any? ? @right << cells(*args) : @right
134 args.any? ? @right << cells(*args) : @right
134 end
135 end
135
136
136 def size
137 def size
137 @left.size > @right.size ? @left.size : @right.size
138 @left.size > @right.size ? @left.size : @right.size
138 end
139 end
139
140
140 def to_html
141 def to_html
141 html = ''.html_safe
142 html = ''.html_safe
142 blank = content_tag('th', '') + content_tag('td', '')
143 blank = content_tag('th', '') + content_tag('td', '')
143 size.times do |i|
144 size.times do |i|
144 left = @left[i] || blank
145 left = @left[i] || blank
145 right = @right[i] || blank
146 right = @right[i] || blank
146 html << content_tag('tr', left + right)
147 html << content_tag('tr', left + right)
147 end
148 end
148 html
149 html
149 end
150 end
150
151
151 def cells(label, text, options={})
152 def cells(label, text, options={})
152 content_tag('th', "#{label}:", options) + content_tag('td', text, options)
153 content_tag('th', "#{label}:", options) + content_tag('td', text, options)
153 end
154 end
154 end
155 end
155
156
156 def issue_fields_rows
157 def issue_fields_rows
157 r = IssueFieldsRows.new
158 r = IssueFieldsRows.new
158 yield r
159 yield r
159 r.to_html
160 r.to_html
160 end
161 end
161
162
162 def render_custom_fields_rows(issue)
163 def render_custom_fields_rows(issue)
163 values = issue.visible_custom_field_values
164 values = issue.visible_custom_field_values
164 return if values.empty?
165 return if values.empty?
165 ordered_values = []
166 ordered_values = []
166 half = (values.size / 2.0).ceil
167 half = (values.size / 2.0).ceil
167 half.times do |i|
168 half.times do |i|
168 ordered_values << values[i]
169 ordered_values << values[i]
169 ordered_values << values[i + half]
170 ordered_values << values[i + half]
170 end
171 end
171 s = "<tr>\n"
172 s = "<tr>\n"
172 n = 0
173 n = 0
173 ordered_values.compact.each do |value|
174 ordered_values.compact.each do |value|
174 css = "cf_#{value.custom_field.id}"
175 css = "cf_#{value.custom_field.id}"
175 s << "</tr>\n<tr>\n" if n > 0 && (n % 2) == 0
176 s << "</tr>\n<tr>\n" if n > 0 && (n % 2) == 0
176 s << "\t<th class=\"#{css}\">#{ h(value.custom_field.name) }:</th><td class=\"#{css}\">#{ h(show_value(value)) }</td>\n"
177 s << "\t<th class=\"#{css}\">#{ h(value.custom_field.name) }:</th><td class=\"#{css}\">#{ h(show_value(value)) }</td>\n"
177 n += 1
178 n += 1
178 end
179 end
179 s << "</tr>\n"
180 s << "</tr>\n"
180 s.html_safe
181 s.html_safe
181 end
182 end
182
183
183 # Returns the number of descendants for an array of issues
184 # Returns the number of descendants for an array of issues
184 def issues_descendant_count(issues)
185 def issues_descendant_count(issues)
185 ids = issues.reject(&:leaf?).map {|issue| issue.descendants.ids}.flatten.uniq
186 ids = issues.reject(&:leaf?).map {|issue| issue.descendants.ids}.flatten.uniq
186 ids -= issues.map(&:id)
187 ids -= issues.map(&:id)
187 ids.size
188 ids.size
188 end
189 end
189
190
190 def issues_destroy_confirmation_message(issues)
191 def issues_destroy_confirmation_message(issues)
191 issues = [issues] unless issues.is_a?(Array)
192 issues = [issues] unless issues.is_a?(Array)
192 message = l(:text_issues_destroy_confirmation)
193 message = l(:text_issues_destroy_confirmation)
193
194
194 descendant_count = issues_descendant_count(issues)
195 descendant_count = issues_descendant_count(issues)
195 if descendant_count > 0
196 if descendant_count > 0
196 message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count)
197 message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count)
197 end
198 end
198 message
199 message
199 end
200 end
200
201
201 def sidebar_queries
202 def sidebar_queries
202 unless @sidebar_queries
203 unless @sidebar_queries
203 @sidebar_queries = IssueQuery.visible.
204 @sidebar_queries = IssueQuery.visible.
204 order("#{Query.table_name}.name ASC").
205 order("#{Query.table_name}.name ASC").
205 # Project specific queries and global queries
206 # Project specific queries and global queries
206 where(@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id]).
207 where(@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id]).
207 to_a
208 to_a
208 end
209 end
209 @sidebar_queries
210 @sidebar_queries
210 end
211 end
211
212
212 def query_links(title, queries)
213 def query_links(title, queries)
213 return '' if queries.empty?
214 return '' if queries.empty?
214 # links to #index on issues/show
215 # links to #index on issues/show
215 url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : params
216 url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : params
216
217
217 content_tag('h3', title) + "\n" +
218 content_tag('h3', title) + "\n" +
218 content_tag('ul',
219 content_tag('ul',
219 queries.collect {|query|
220 queries.collect {|query|
220 css = 'query'
221 css = 'query'
221 css << ' selected' if query == @query
222 css << ' selected' if query == @query
222 content_tag('li', link_to(query.name, url_params.merge(:query_id => query), :class => css))
223 content_tag('li', link_to(query.name, url_params.merge(:query_id => query), :class => css))
223 }.join("\n").html_safe,
224 }.join("\n").html_safe,
224 :class => 'queries'
225 :class => 'queries'
225 ) + "\n"
226 ) + "\n"
226 end
227 end
227
228
228 def render_sidebar_queries
229 def render_sidebar_queries
229 out = ''.html_safe
230 out = ''.html_safe
230 out << query_links(l(:label_my_queries), sidebar_queries.select(&:is_private?))
231 out << query_links(l(:label_my_queries), sidebar_queries.select(&:is_private?))
231 out << query_links(l(:label_query_plural), sidebar_queries.reject(&:is_private?))
232 out << query_links(l(:label_query_plural), sidebar_queries.reject(&:is_private?))
232 out
233 out
233 end
234 end
234
235
235 def email_issue_attributes(issue, user)
236 def email_issue_attributes(issue, user)
236 items = []
237 items = []
237 %w(author status priority assigned_to category fixed_version).each do |attribute|
238 %w(author status priority assigned_to category fixed_version).each do |attribute|
238 unless issue.disabled_core_fields.include?(attribute+"_id")
239 unless issue.disabled_core_fields.include?(attribute+"_id")
239 items << "#{l("field_#{attribute}")}: #{issue.send attribute}"
240 items << "#{l("field_#{attribute}")}: #{issue.send attribute}"
240 end
241 end
241 end
242 end
242 issue.visible_custom_field_values(user).each do |value|
243 issue.visible_custom_field_values(user).each do |value|
243 items << "#{value.custom_field.name}: #{show_value(value, false)}"
244 items << "#{value.custom_field.name}: #{show_value(value, false)}"
244 end
245 end
245 items
246 items
246 end
247 end
247
248
248 def render_email_issue_attributes(issue, user, html=false)
249 def render_email_issue_attributes(issue, user, html=false)
249 items = email_issue_attributes(issue, user)
250 items = email_issue_attributes(issue, user)
250 if html
251 if html
251 content_tag('ul', items.map{|s| content_tag('li', s)}.join("\n").html_safe)
252 content_tag('ul', items.map{|s| content_tag('li', s)}.join("\n").html_safe)
252 else
253 else
253 items.map{|s| "* #{s}"}.join("\n")
254 items.map{|s| "* #{s}"}.join("\n")
254 end
255 end
255 end
256 end
256
257
257 # Returns the textual representation of a journal details
258 # Returns the textual representation of a journal details
258 # as an array of strings
259 # as an array of strings
259 def details_to_strings(details, no_html=false, options={})
260 def details_to_strings(details, no_html=false, options={})
260 options[:only_path] = (options[:only_path] == false ? false : true)
261 options[:only_path] = (options[:only_path] == false ? false : true)
261 strings = []
262 strings = []
262 values_by_field = {}
263 values_by_field = {}
263 details.each do |detail|
264 details.each do |detail|
264 if detail.property == 'cf'
265 if detail.property == 'cf'
265 field = detail.custom_field
266 field = detail.custom_field
266 if field && field.multiple?
267 if field && field.multiple?
267 values_by_field[field] ||= {:added => [], :deleted => []}
268 values_by_field[field] ||= {:added => [], :deleted => []}
268 if detail.old_value
269 if detail.old_value
269 values_by_field[field][:deleted] << detail.old_value
270 values_by_field[field][:deleted] << detail.old_value
270 end
271 end
271 if detail.value
272 if detail.value
272 values_by_field[field][:added] << detail.value
273 values_by_field[field][:added] << detail.value
273 end
274 end
274 next
275 next
275 end
276 end
276 end
277 end
277 strings << show_detail(detail, no_html, options)
278 strings << show_detail(detail, no_html, options)
278 end
279 end
279 values_by_field.each do |field, changes|
280 values_by_field.each do |field, changes|
280 detail = JournalDetail.new(:property => 'cf', :prop_key => field.id.to_s)
281 detail = JournalDetail.new(:property => 'cf', :prop_key => field.id.to_s)
281 detail.instance_variable_set "@custom_field", field
282 detail.instance_variable_set "@custom_field", field
282 if changes[:added].any?
283 if changes[:added].any?
283 detail.value = changes[:added]
284 detail.value = changes[:added]
284 strings << show_detail(detail, no_html, options)
285 strings << show_detail(detail, no_html, options)
285 elsif changes[:deleted].any?
286 elsif changes[:deleted].any?
286 detail.old_value = changes[:deleted]
287 detail.old_value = changes[:deleted]
287 strings << show_detail(detail, no_html, options)
288 strings << show_detail(detail, no_html, options)
288 end
289 end
289 end
290 end
290 strings
291 strings
291 end
292 end
292
293
293 # Returns the textual representation of a single journal detail
294 # Returns the textual representation of a single journal detail
294 def show_detail(detail, no_html=false, options={})
295 def show_detail(detail, no_html=false, options={})
295 multiple = false
296 multiple = false
296 case detail.property
297 case detail.property
297 when 'attr'
298 when 'attr'
298 field = detail.prop_key.to_s.gsub(/\_id$/, "")
299 field = detail.prop_key.to_s.gsub(/\_id$/, "")
299 label = l(("field_" + field).to_sym)
300 label = l(("field_" + field).to_sym)
300 case detail.prop_key
301 case detail.prop_key
301 when 'due_date', 'start_date'
302 when 'due_date', 'start_date'
302 value = format_date(detail.value.to_date) if detail.value
303 value = format_date(detail.value.to_date) if detail.value
303 old_value = format_date(detail.old_value.to_date) if detail.old_value
304 old_value = format_date(detail.old_value.to_date) if detail.old_value
304
305
305 when 'project_id', 'status_id', 'tracker_id', 'assigned_to_id',
306 when 'project_id', 'status_id', 'tracker_id', 'assigned_to_id',
306 'priority_id', 'category_id', 'fixed_version_id'
307 'priority_id', 'category_id', 'fixed_version_id'
307 value = find_name_by_reflection(field, detail.value)
308 value = find_name_by_reflection(field, detail.value)
308 old_value = find_name_by_reflection(field, detail.old_value)
309 old_value = find_name_by_reflection(field, detail.old_value)
309
310
310 when 'estimated_hours'
311 when 'estimated_hours'
311 value = "%0.02f" % detail.value.to_f unless detail.value.blank?
312 value = "%0.02f" % detail.value.to_f unless detail.value.blank?
312 old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
313 old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
313
314
314 when 'parent_id'
315 when 'parent_id'
315 label = l(:field_parent_issue)
316 label = l(:field_parent_issue)
316 value = "##{detail.value}" unless detail.value.blank?
317 value = "##{detail.value}" unless detail.value.blank?
317 old_value = "##{detail.old_value}" unless detail.old_value.blank?
318 old_value = "##{detail.old_value}" unless detail.old_value.blank?
318
319
319 when 'is_private'
320 when 'is_private'
320 value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank?
321 value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank?
321 old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
322 old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
322 end
323 end
323 when 'cf'
324 when 'cf'
324 custom_field = detail.custom_field
325 custom_field = detail.custom_field
325 if custom_field
326 if custom_field
326 multiple = custom_field.multiple?
327 multiple = custom_field.multiple?
327 label = custom_field.name
328 label = custom_field.name
328 value = format_value(detail.value, custom_field) if detail.value
329 value = format_value(detail.value, custom_field) if detail.value
329 old_value = format_value(detail.old_value, custom_field) if detail.old_value
330 old_value = format_value(detail.old_value, custom_field) if detail.old_value
330 end
331 end
331 when 'attachment'
332 when 'attachment'
332 label = l(:label_attachment)
333 label = l(:label_attachment)
333 when 'relation'
334 when 'relation'
334 if detail.value && !detail.old_value
335 if detail.value && !detail.old_value
335 rel_issue = Issue.visible.find_by_id(detail.value)
336 rel_issue = Issue.visible.find_by_id(detail.value)
336 value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.value}" :
337 value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.value}" :
337 (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
338 (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
338 elsif detail.old_value && !detail.value
339 elsif detail.old_value && !detail.value
339 rel_issue = Issue.visible.find_by_id(detail.old_value)
340 rel_issue = Issue.visible.find_by_id(detail.old_value)
340 old_value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.old_value}" :
341 old_value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.old_value}" :
341 (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
342 (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
342 end
343 end
343 relation_type = IssueRelation::TYPES[detail.prop_key]
344 relation_type = IssueRelation::TYPES[detail.prop_key]
344 label = l(relation_type[:name]) if relation_type
345 label = l(relation_type[:name]) if relation_type
345 end
346 end
346 call_hook(:helper_issues_show_detail_after_setting,
347 call_hook(:helper_issues_show_detail_after_setting,
347 {:detail => detail, :label => label, :value => value, :old_value => old_value })
348 {:detail => detail, :label => label, :value => value, :old_value => old_value })
348
349
349 label ||= detail.prop_key
350 label ||= detail.prop_key
350 value ||= detail.value
351 value ||= detail.value
351 old_value ||= detail.old_value
352 old_value ||= detail.old_value
352
353
353 unless no_html
354 unless no_html
354 label = content_tag('strong', label)
355 label = content_tag('strong', label)
355 old_value = content_tag("i", h(old_value)) if detail.old_value
356 old_value = content_tag("i", h(old_value)) if detail.old_value
356 if detail.old_value && detail.value.blank? && detail.property != 'relation'
357 if detail.old_value && detail.value.blank? && detail.property != 'relation'
357 old_value = content_tag("del", old_value)
358 old_value = content_tag("del", old_value)
358 end
359 end
359 if detail.property == 'attachment' && value.present? &&
360 if detail.property == 'attachment' && value.present? &&
360 atta = detail.journal.journalized.attachments.detect {|a| a.id == detail.prop_key.to_i}
361 atta = detail.journal.journalized.attachments.detect {|a| a.id == detail.prop_key.to_i}
361 # Link to the attachment if it has not been removed
362 # Link to the attachment if it has not been removed
362 value = link_to_attachment(atta, :download => true, :only_path => options[:only_path])
363 value = link_to_attachment(atta, :download => true, :only_path => options[:only_path])
363 if options[:only_path] != false && atta.is_text?
364 if options[:only_path] != false && atta.is_text?
364 value += link_to(
365 value += link_to(
365 image_tag('magnifier.png'),
366 image_tag('magnifier.png'),
366 :controller => 'attachments', :action => 'show',
367 :controller => 'attachments', :action => 'show',
367 :id => atta, :filename => atta.filename
368 :id => atta, :filename => atta.filename
368 )
369 )
369 end
370 end
370 else
371 else
371 value = content_tag("i", h(value)) if value
372 value = content_tag("i", h(value)) if value
372 end
373 end
373 end
374 end
374
375
375 if detail.property == 'attr' && detail.prop_key == 'description'
376 if detail.property == 'attr' && detail.prop_key == 'description'
376 s = l(:text_journal_changed_no_detail, :label => label)
377 s = l(:text_journal_changed_no_detail, :label => label)
377 unless no_html
378 unless no_html
378 diff_link = link_to 'diff',
379 diff_link = link_to 'diff',
379 {:controller => 'journals', :action => 'diff', :id => detail.journal_id,
380 {:controller => 'journals', :action => 'diff', :id => detail.journal_id,
380 :detail_id => detail.id, :only_path => options[:only_path]},
381 :detail_id => detail.id, :only_path => options[:only_path]},
381 :title => l(:label_view_diff)
382 :title => l(:label_view_diff)
382 s << " (#{ diff_link })"
383 s << " (#{ diff_link })"
383 end
384 end
384 s.html_safe
385 s.html_safe
385 elsif detail.value.present?
386 elsif detail.value.present?
386 case detail.property
387 case detail.property
387 when 'attr', 'cf'
388 when 'attr', 'cf'
388 if detail.old_value.present?
389 if detail.old_value.present?
389 l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe
390 l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe
390 elsif multiple
391 elsif multiple
391 l(:text_journal_added, :label => label, :value => value).html_safe
392 l(:text_journal_added, :label => label, :value => value).html_safe
392 else
393 else
393 l(:text_journal_set_to, :label => label, :value => value).html_safe
394 l(:text_journal_set_to, :label => label, :value => value).html_safe
394 end
395 end
395 when 'attachment', 'relation'
396 when 'attachment', 'relation'
396 l(:text_journal_added, :label => label, :value => value).html_safe
397 l(:text_journal_added, :label => label, :value => value).html_safe
397 end
398 end
398 else
399 else
399 l(:text_journal_deleted, :label => label, :old => old_value).html_safe
400 l(:text_journal_deleted, :label => label, :old => old_value).html_safe
400 end
401 end
401 end
402 end
402
403
403 # Find the name of an associated record stored in the field attribute
404 # Find the name of an associated record stored in the field attribute
404 def find_name_by_reflection(field, id)
405 def find_name_by_reflection(field, id)
405 unless id.present?
406 unless id.present?
406 return nil
407 return nil
407 end
408 end
408 @detail_value_name_by_reflection ||= Hash.new do |hash, key|
409 @detail_value_name_by_reflection ||= Hash.new do |hash, key|
409 association = Issue.reflect_on_association(key.first.to_sym)
410 association = Issue.reflect_on_association(key.first.to_sym)
410 if association
411 if association
411 record = association.klass.find_by_id(key.last)
412 record = association.klass.find_by_id(key.last)
412 if record
413 if record
413 record.name.force_encoding('UTF-8')
414 record.name.force_encoding('UTF-8')
414 hash[key] = record.name
415 hash[key] = record.name
415 end
416 end
416 end
417 end
417 hash[key] ||= nil
418 hash[key] ||= nil
418 end
419 end
419 @detail_value_name_by_reflection[[field, id]]
420 @detail_value_name_by_reflection[[field, id]]
420 end
421 end
421
422
422 # Renders issue children recursively
423 # Renders issue children recursively
423 def render_api_issue_children(issue, api)
424 def render_api_issue_children(issue, api)
424 return if issue.leaf?
425 return if issue.leaf?
425 api.array :children do
426 api.array :children do
426 issue.children.each do |child|
427 issue.children.each do |child|
427 api.issue(:id => child.id) do
428 api.issue(:id => child.id) do
428 api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil?
429 api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil?
429 api.subject child.subject
430 api.subject child.subject
430 render_api_issue_children(child, api)
431 render_api_issue_children(child, api)
431 end
432 end
432 end
433 end
433 end
434 end
434 end
435 end
435 end
436 end
@@ -1,53 +1,54
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
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
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.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 module WikiHelper
20 module WikiHelper
21 include Redmine::Export::PDF::WikiPdfHelper
21
22
22 def wiki_page_options_for_select(pages, selected = nil, parent = nil, level = 0)
23 def wiki_page_options_for_select(pages, selected = nil, parent = nil, level = 0)
23 pages = pages.group_by(&:parent) unless pages.is_a?(Hash)
24 pages = pages.group_by(&:parent) unless pages.is_a?(Hash)
24 s = ''.html_safe
25 s = ''.html_safe
25 if pages.has_key?(parent)
26 if pages.has_key?(parent)
26 pages[parent].each do |page|
27 pages[parent].each do |page|
27 attrs = "value='#{page.id}'"
28 attrs = "value='#{page.id}'"
28 attrs << " selected='selected'" if selected == page
29 attrs << " selected='selected'" if selected == page
29 indent = (level > 0) ? ('&nbsp;' * level * 2 + '&#187; ') : ''
30 indent = (level > 0) ? ('&nbsp;' * level * 2 + '&#187; ') : ''
30
31
31 s << content_tag('option', (indent + h(page.pretty_title)).html_safe, :value => page.id.to_s, :selected => selected == page) +
32 s << content_tag('option', (indent + h(page.pretty_title)).html_safe, :value => page.id.to_s, :selected => selected == page) +
32 wiki_page_options_for_select(pages, selected, page, level + 1)
33 wiki_page_options_for_select(pages, selected, page, level + 1)
33 end
34 end
34 end
35 end
35 s
36 s
36 end
37 end
37
38
38 def wiki_page_wiki_options_for_select(page)
39 def wiki_page_wiki_options_for_select(page)
39 projects = Project.allowed_to(:rename_wiki_pages).joins(:wiki).preload(:wiki).to_a
40 projects = Project.allowed_to(:rename_wiki_pages).joins(:wiki).preload(:wiki).to_a
40 projects << page.project unless projects.include?(page.project)
41 projects << page.project unless projects.include?(page.project)
41
42
42 project_tree_options_for_select(projects, :selected => page.project) do |project|
43 project_tree_options_for_select(projects, :selected => page.project) do |project|
43 wiki_id = project.wiki.try(:id)
44 wiki_id = project.wiki.try(:id)
44 {:value => wiki_id, :selected => wiki_id == page.wiki_id}
45 {:value => wiki_id, :selected => wiki_id == page.wiki_id}
45 end
46 end
46 end
47 end
47
48
48 def wiki_page_breadcrumb(page)
49 def wiki_page_breadcrumb(page)
49 breadcrumb(page.ancestors.reverse.collect {|parent|
50 breadcrumb(page.ancestors.reverse.collect {|parent|
50 link_to(h(parent.pretty_title), {:controller => 'wiki', :action => 'show', :id => parent.title, :project_id => parent.project, :version => nil})
51 link_to(h(parent.pretty_title), {:controller => 'wiki', :action => 'show', :id => parent.title, :project_id => parent.project, :version => nil})
51 })
52 })
52 end
53 end
53 end
54 end
This diff has been collapsed as it changes many lines, (573 lines changed) Show them Hide them
@@ -1,698 +1,145
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
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
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.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require 'rbpdf'
20 require 'rbpdf'
21
21
22 module Redmine
22 module Redmine
23 module Export
23 module Export
24 module PDF
24 module PDF
25 include ActionView::Helpers::TextHelper
26 include ActionView::Helpers::NumberHelper
27 include IssuesHelper
28
29 class ITCPDF < RBPDF
25 class ITCPDF < RBPDF
30 include Redmine::I18n
26 include Redmine::I18n
31 attr_accessor :footer_date
27 attr_accessor :footer_date
32
28
33 def initialize(lang, orientation='P')
29 def initialize(lang, orientation='P')
34 @@k_path_cache = Rails.root.join('tmp', 'pdf')
30 @@k_path_cache = Rails.root.join('tmp', 'pdf')
35 FileUtils.mkdir_p @@k_path_cache unless File::exist?(@@k_path_cache)
31 FileUtils.mkdir_p @@k_path_cache unless File::exist?(@@k_path_cache)
36 set_language_if_valid lang
32 set_language_if_valid lang
37 super(orientation, 'mm', 'A4')
33 super(orientation, 'mm', 'A4')
38 set_print_header(false)
34 set_print_header(false)
39 set_rtl(l(:direction) == 'rtl')
35 set_rtl(l(:direction) == 'rtl')
40 set_temp_rtl(l(:direction) == 'rtl' ? 'R' : 'L')
36 set_temp_rtl(l(:direction) == 'rtl' ? 'R' : 'L')
41
37
42 @font_for_content = l(:general_pdf_fontname)
38 @font_for_content = l(:general_pdf_fontname)
43 @font_for_footer = l(:general_pdf_fontname)
39 @font_for_footer = l(:general_pdf_fontname)
44 set_creator(Redmine::Info.app_name)
40 set_creator(Redmine::Info.app_name)
45 set_font(@font_for_content)
41 set_font(@font_for_content)
46
42
47 set_header_font([@font_for_content, '', 10])
43 set_header_font([@font_for_content, '', 10])
48 set_footer_font([@font_for_content, '', 8])
44 set_footer_font([@font_for_content, '', 8])
49 set_default_monospaced_font(@font_for_content)
45 set_default_monospaced_font(@font_for_content)
50 end
46 end
51
47
52 def SetFontStyle(style, size)
48 def SetFontStyle(style, size)
53 style.delete!('B') if current_language.to_s.downcase == 'th' # FreeSerif Bold Thai font has problem.
49 style.delete!('B') if current_language.to_s.downcase == 'th' # FreeSerif Bold Thai font has problem.
54 set_font(@font_for_content, style, size)
50 set_font(@font_for_content, style, size)
55 end
51 end
56
52
57 def fix_text_encoding(txt)
53 def fix_text_encoding(txt)
58 RDMPdfEncoding::rdm_from_utf8(txt, "UTF-8")
54 RDMPdfEncoding::rdm_from_utf8(txt, "UTF-8")
59 end
55 end
60
56
61 def formatted_text(text)
57 def formatted_text(text)
62 html = Redmine::WikiFormatting.to_html(Setting.text_formatting, text)
58 Redmine::WikiFormatting.to_html(Setting.text_formatting, text)
63 # Strip {{toc}} tags
64 html.gsub!(/<p>\{\{([<>]?)toc\}\}<\/p>/i, '')
65 html
66 end
59 end
67
60
68 def RDMCell(w ,h=0, txt='', border=0, ln=0, align='', fill=0, link='')
61 def RDMCell(w ,h=0, txt='', border=0, ln=0, align='', fill=0, link='')
69 cell(w, h, txt, border, ln, align, fill, link)
62 cell(w, h, txt, border, ln, align, fill, link)
70 end
63 end
71
64
72 def RDMMultiCell(w, h=0, txt='', border=0, align='', fill=0, ln=1)
65 def RDMMultiCell(w, h=0, txt='', border=0, align='', fill=0, ln=1)
73 multi_cell(w, h, txt, border, align, fill, ln)
66 multi_cell(w, h, txt, border, align, fill, ln)
74 end
67 end
75
68
76 def RDMwriteHTMLCell(w, h, x, y, txt='', attachments=[], border=0, ln=1, fill=0)
69 def RDMwriteFormattedCell(w, h, x, y, txt='', attachments=[], border=0, ln=1, fill=0)
77 @attachments = attachments
70 @attachments = attachments
78
71
79 css_tag = ' <style>
72 css_tag = ' <style>
80 table, td {
73 table, td {
81 border: 2px #ff0000 solid;
74 border: 2px #ff0000 solid;
82 }
75 }
83 th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; text-align: center; font-style: bold;}
76 th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; text-align: center; font-style: bold;}
84 pre {
77 pre {
85 background-color: #fafafa;
78 background-color: #fafafa;
86 }
79 }
87 </style>'
80 </style>'
88
81
89 writeHTMLCell(w, h, x, y,
82 # Strip {{toc}} tags
90 css_tag + formatted_text(txt),
83 txt.gsub!(/<p>\{\{([<>]?)toc\}\}<\/p>/i, '')
91 border, ln, fill)
84 writeHTMLCell(w, h, x, y, css_tag + txt, border, ln, fill)
85 end
86
87 def RDMwriteHTMLCell(w, h, x, y, txt='', attachments=[], border=0, ln=1, fill=0)
88 txt = formatted_text(txt)
89 RDMwriteFormattedCell(w, h, x, y, txt, attachments, border, ln, fill)
92 end
90 end
93
91
94 def get_image_filename(attrname)
92 def get_image_filename(attrname)
95 atta = RDMPdfEncoding.attach(@attachments, attrname, "UTF-8")
93 atta = RDMPdfEncoding.attach(@attachments, attrname, "UTF-8")
96 if atta
94 if atta
97 return atta.diskfile
95 return atta.diskfile
98 else
96 else
99 return nil
97 return nil
100 end
98 end
101 end
99 end
102
100
103 def get_sever_url(url)
101 def get_sever_url(url)
104 if !empty_string(url) and (url[0, 1] == '/')
102 if !empty_string(url) and (url[0, 1] == '/')
105 Setting.host_name.split('/')[0] + url
103 Setting.host_name.split('/')[0] + url
106 else
104 else
107 url
105 url
108 end
106 end
109 end
107 end
110
108
111 def Footer
109 def Footer
112 set_font(@font_for_footer, 'I', 8)
110 set_font(@font_for_footer, 'I', 8)
113 set_x(15)
111 set_x(15)
114 if get_rtl
112 if get_rtl
115 RDMCell(0, 5, @footer_date, 0, 0, 'R')
113 RDMCell(0, 5, @footer_date, 0, 0, 'R')
116 else
114 else
117 RDMCell(0, 5, @footer_date, 0, 0, 'L')
115 RDMCell(0, 5, @footer_date, 0, 0, 'L')
118 end
116 end
119 set_x(-30)
117 set_x(-30)
120 RDMCell(0, 5, get_alias_num_page() + '/' + get_alias_nb_pages(), 0, 0, 'C')
118 RDMCell(0, 5, get_alias_num_page() + '/' + get_alias_nb_pages(), 0, 0, 'C')
121 end
119 end
122 end
120 end
123
121
124 def is_cjk?
125 case current_language.to_s.downcase
126 when 'ja', 'zh-tw', 'zh', 'ko'
127 true
128 else
129 false
130 end
131 end
132
133 # fetch row values
134 def fetch_row_values(issue, query, level)
135 query.inline_columns.collect do |column|
136 s = if column.is_a?(QueryCustomFieldColumn)
137 cv = issue.visible_custom_field_values.detect {|v| v.custom_field_id == column.custom_field.id}
138 show_value(cv, false)
139 else
140 value = issue.send(column.name)
141 if column.name == :subject
142 value = " " * level + value
143 end
144 if value.is_a?(Date)
145 format_date(value)
146 elsif value.is_a?(Time)
147 format_time(value)
148 else
149 value
150 end
151 end
152 s.to_s
153 end
154 end
155
156 # calculate columns width
157 def calc_col_width(issues, query, table_width, pdf)
158 # calculate statistics
159 # by captions
160 pdf.SetFontStyle('B',8)
161 margins = pdf.get_margins
162 col_padding = margins['cell']
163 col_width_min = query.inline_columns.map {|v| pdf.get_string_width(v.caption) + col_padding}
164 col_width_max = Array.new(col_width_min)
165 col_width_avg = Array.new(col_width_min)
166 col_min = pdf.get_string_width('OO') + col_padding * 2
167 if table_width > col_min * col_width_avg.length
168 table_width -= col_min * col_width_avg.length
169 else
170 col_min = pdf.get_string_width('O') + col_padding * 2
171 if table_width > col_min * col_width_avg.length
172 table_width -= col_min * col_width_avg.length
173 else
174 ratio = table_width / col_width_avg.inject(0, :+)
175 return col_width = col_width_avg.map {|w| w * ratio}
176 end
177 end
178 word_width_max = query.inline_columns.map {|c|
179 n = 10
180 c.caption.split.each {|w|
181 x = pdf.get_string_width(w) + col_padding
182 n = x if n < x
183 }
184 n
185 }
186
187 # by properties of issues
188 pdf.SetFontStyle('',8)
189 k = 1
190 issue_list(issues) {|issue, level|
191 k += 1
192 values = fetch_row_values(issue, query, level)
193 values.each_with_index {|v,i|
194 n = pdf.get_string_width(v) + col_padding * 2
195 col_width_max[i] = n if col_width_max[i] < n
196 col_width_min[i] = n if col_width_min[i] > n
197 col_width_avg[i] += n
198 v.split.each {|w|
199 x = pdf.get_string_width(w) + col_padding
200 word_width_max[i] = x if word_width_max[i] < x
201 }
202 }
203 }
204 col_width_avg.map! {|x| x / k}
205
206 # calculate columns width
207 ratio = table_width / col_width_avg.inject(0, :+)
208 col_width = col_width_avg.map {|w| w * ratio}
209
210 # correct max word width if too many columns
211 ratio = table_width / word_width_max.inject(0, :+)
212 word_width_max.map! {|v| v * ratio} if ratio < 1
213
214 # correct and lock width of some columns
215 done = 1
216 col_fix = []
217 col_width.each_with_index do |w,i|
218 if w > col_width_max[i]
219 col_width[i] = col_width_max[i]
220 col_fix[i] = 1
221 done = 0
222 elsif w < word_width_max[i]
223 col_width[i] = word_width_max[i]
224 col_fix[i] = 1
225 done = 0
226 else
227 col_fix[i] = 0
228 end
229 end
230
231 # iterate while need to correct and lock coluns width
232 while done == 0
233 # calculate free & locked columns width
234 done = 1
235 ratio = table_width / col_width.inject(0, :+)
236
237 # correct columns width
238 col_width.each_with_index do |w,i|
239 if col_fix[i] == 0
240 col_width[i] = w * ratio
241
242 # check if column width less then max word width
243 if col_width[i] < word_width_max[i]
244 col_width[i] = word_width_max[i]
245 col_fix[i] = 1
246 done = 0
247 elsif col_width[i] > col_width_max[i]
248 col_width[i] = col_width_max[i]
249 col_fix[i] = 1
250 done = 0
251 end
252 end
253 end
254 end
255
256 ratio = table_width / col_width.inject(0, :+)
257 col_width.map! {|v| v * ratio + col_min}
258 col_width
259 end
260
261 def render_table_header(pdf, query, col_width, row_height, table_width)
262 # headers
263 pdf.SetFontStyle('B',8)
264 pdf.set_fill_color(230, 230, 230)
265
266 base_x = pdf.get_x
267 base_y = pdf.get_y
268 max_height = get_issues_to_pdf_write_cells(pdf, query.inline_columns, col_width, true)
269
270 # write the cells on page
271 issues_to_pdf_write_cells(pdf, query.inline_columns, col_width, max_height, true)
272 pdf.set_xy(base_x, base_y + max_height)
273
274 # rows
275 pdf.SetFontStyle('',8)
276 pdf.set_fill_color(255, 255, 255)
277 end
278
279 # Returns a PDF string of a list of issues
280 def issues_to_pdf(issues, project, query)
281 pdf = ITCPDF.new(current_language, "L")
282 title = query.new_record? ? l(:label_issue_plural) : query.name
283 title = "#{project} - #{title}" if project
284 pdf.set_title(title)
285 pdf.alias_nb_pages
286 pdf.footer_date = format_date(Date.today)
287 pdf.set_auto_page_break(false)
288 pdf.add_page("L")
289
290 # Landscape A4 = 210 x 297 mm
291 page_height = pdf.get_page_height # 210
292 page_width = pdf.get_page_width # 297
293 left_margin = pdf.get_original_margins['left'] # 10
294 right_margin = pdf.get_original_margins['right'] # 10
295 bottom_margin = pdf.get_footer_margin
296 row_height = 4
297
298 # column widths
299 table_width = page_width - right_margin - left_margin
300 col_width = []
301 unless query.inline_columns.empty?
302 col_width = calc_col_width(issues, query, table_width, pdf)
303 table_width = col_width.inject(0, :+)
304 end
305
306 # use full width if the description is displayed
307 if table_width > 0 && query.has_column?(:description)
308 col_width = col_width.map {|w| w * (page_width - right_margin - left_margin) / table_width}
309 table_width = col_width.inject(0, :+)
310 end
311
312 # title
313 pdf.SetFontStyle('B',11)
314 pdf.RDMCell(190,10, title)
315 pdf.ln
316
317 render_table_header(pdf, query, col_width, row_height, table_width)
318 previous_group = false
319 issue_list(issues) do |issue, level|
320 if query.grouped? &&
321 (group = query.group_by_column.value(issue)) != previous_group
322 pdf.SetFontStyle('B',10)
323 group_label = group.blank? ? 'None' : group.to_s.dup
324 group_label << " (#{query.issue_count_by_group[group]})"
325 pdf.bookmark group_label, 0, -1
326 pdf.RDMCell(table_width, row_height * 2, group_label, 1, 1, 'L')
327 pdf.SetFontStyle('',8)
328 previous_group = group
329 end
330
331 # fetch row values
332 col_values = fetch_row_values(issue, query, level)
333
334 # make new page if it doesn't fit on the current one
335 base_y = pdf.get_y
336 max_height = get_issues_to_pdf_write_cells(pdf, col_values, col_width)
337 space_left = page_height - base_y - bottom_margin
338 if max_height > space_left
339 pdf.add_page("L")
340 render_table_header(pdf, query, col_width, row_height, table_width)
341 base_y = pdf.get_y
342 end
343
344 # write the cells on page
345 issues_to_pdf_write_cells(pdf, col_values, col_width, max_height)
346 pdf.set_y(base_y + max_height)
347
348 if query.has_column?(:description) && issue.description?
349 pdf.set_x(10)
350 pdf.set_auto_page_break(true, bottom_margin)
351 pdf.RDMwriteHTMLCell(0, 5, 10, '', issue.description.to_s, issue.attachments, "LRBT")
352 pdf.set_auto_page_break(false)
353 end
354 end
355
356 if issues.size == Setting.issues_export_limit.to_i
357 pdf.SetFontStyle('B',10)
358 pdf.RDMCell(0, row_height, '...')
359 end
360 pdf.output
361 end
362
363 # returns the maximum height of MultiCells
364 def get_issues_to_pdf_write_cells(pdf, col_values, col_widths, head=false)
365 heights = []
366 col_values.each_with_index do |column, i|
367 heights << pdf.get_string_height(col_widths[i], head ? column.caption : column)
368 end
369 return heights.max
370 end
371
372 # Renders MultiCells and returns the maximum height used
373 def issues_to_pdf_write_cells(pdf, col_values, col_widths, row_height, head=false)
374 col_values.each_with_index do |column, i|
375 pdf.RDMMultiCell(col_widths[i], row_height, head ? column.caption : column.strip, 1, '', 1, 0)
376 end
377 end
378
379 # Draw lines to close the row (MultiCell border drawing in not uniform)
380 #
381 # parameter "col_id_width" is not used. it is kept for compatibility.
382 def issues_to_pdf_draw_borders(pdf, top_x, top_y, lower_y,
383 col_id_width, col_widths, rtl=false)
384 col_x = top_x
385 pdf.line(col_x, top_y, col_x, lower_y) # id right border
386 col_widths.each do |width|
387 if rtl
388 col_x -= width
389 else
390 col_x += width
391 end
392 pdf.line(col_x, top_y, col_x, lower_y) # columns right border
393 end
394 pdf.line(top_x, top_y, top_x, lower_y) # left border
395 pdf.line(top_x, lower_y, col_x, lower_y) # bottom border
396 end
397
398 # Returns a PDF string of a single issue
399 def issue_to_pdf(issue, assoc={})
400 pdf = ITCPDF.new(current_language)
401 pdf.set_title("#{issue.project} - #{issue.tracker} ##{issue.id}")
402 pdf.alias_nb_pages
403 pdf.footer_date = format_date(Date.today)
404 pdf.add_page
405 pdf.SetFontStyle('B',11)
406 buf = "#{issue.project} - #{issue.tracker} ##{issue.id}"
407 pdf.RDMMultiCell(190, 5, buf)
408 pdf.SetFontStyle('',8)
409 base_x = pdf.get_x
410 i = 1
411 issue.ancestors.visible.each do |ancestor|
412 pdf.set_x(base_x + i)
413 buf = "#{ancestor.tracker} # #{ancestor.id} (#{ancestor.status.to_s}): #{ancestor.subject}"
414 pdf.RDMMultiCell(190 - i, 5, buf)
415 i += 1 if i < 35
416 end
417 pdf.SetFontStyle('B',11)
418 pdf.RDMMultiCell(190 - i, 5, issue.subject.to_s)
419 pdf.SetFontStyle('',8)
420 pdf.RDMMultiCell(190, 5, "#{format_time(issue.created_on)} - #{issue.author}")
421 pdf.ln
422
423 left = []
424 left << [l(:field_status), issue.status]
425 left << [l(:field_priority), issue.priority]
426 left << [l(:field_assigned_to), issue.assigned_to] unless issue.disabled_core_fields.include?('assigned_to_id')
427 left << [l(:field_category), issue.category] unless issue.disabled_core_fields.include?('category_id')
428 left << [l(:field_fixed_version), issue.fixed_version] unless issue.disabled_core_fields.include?('fixed_version_id')
429
430 right = []
431 right << [l(:field_start_date), format_date(issue.start_date)] unless issue.disabled_core_fields.include?('start_date')
432 right << [l(:field_due_date), format_date(issue.due_date)] unless issue.disabled_core_fields.include?('due_date')
433 right << [l(:field_done_ratio), "#{issue.done_ratio}%"] unless issue.disabled_core_fields.include?('done_ratio')
434 right << [l(:field_estimated_hours), l_hours(issue.estimated_hours)] unless issue.disabled_core_fields.include?('estimated_hours')
435 right << [l(:label_spent_time), l_hours(issue.total_spent_hours)] if User.current.allowed_to?(:view_time_entries, issue.project)
436
437 rows = left.size > right.size ? left.size : right.size
438 while left.size < rows
439 left << nil
440 end
441 while right.size < rows
442 right << nil
443 end
444
445 half = (issue.visible_custom_field_values.size / 2.0).ceil
446 issue.visible_custom_field_values.each_with_index do |custom_value, i|
447 (i < half ? left : right) << [custom_value.custom_field.name, show_value(custom_value, false)]
448 end
449
450 if pdf.get_rtl
451 border_first_top = 'RT'
452 border_last_top = 'LT'
453 border_first = 'R'
454 border_last = 'L'
455 else
456 border_first_top = 'LT'
457 border_last_top = 'RT'
458 border_first = 'L'
459 border_last = 'R'
460 end
461
462 rows = left.size > right.size ? left.size : right.size
463 rows.times do |i|
464 heights = []
465 pdf.SetFontStyle('B',9)
466 item = left[i]
467 heights << pdf.get_string_height(35, item ? "#{item.first}:" : "")
468 item = right[i]
469 heights << pdf.get_string_height(35, item ? "#{item.first}:" : "")
470 pdf.SetFontStyle('',9)
471 item = left[i]
472 heights << pdf.get_string_height(60, item ? item.last.to_s : "")
473 item = right[i]
474 heights << pdf.get_string_height(60, item ? item.last.to_s : "")
475 height = heights.max
476
477 item = left[i]
478 pdf.SetFontStyle('B',9)
479 pdf.RDMMultiCell(35, height, item ? "#{item.first}:" : "", (i == 0 ? border_first_top : border_first), '', 0, 0)
480 pdf.SetFontStyle('',9)
481 pdf.RDMMultiCell(60, height, item ? item.last.to_s : "", (i == 0 ? border_last_top : border_last), '', 0, 0)
482
483 item = right[i]
484 pdf.SetFontStyle('B',9)
485 pdf.RDMMultiCell(35, height, item ? "#{item.first}:" : "", (i == 0 ? border_first_top : border_first), '', 0, 0)
486 pdf.SetFontStyle('',9)
487 pdf.RDMMultiCell(60, height, item ? item.last.to_s : "", (i == 0 ? border_last_top : border_last), '', 0, 2)
488
489 pdf.set_x(base_x)
490 end
491
492 pdf.SetFontStyle('B',9)
493 pdf.RDMCell(35+155, 5, l(:field_description), "LRT", 1)
494 pdf.SetFontStyle('',9)
495
496 # Set resize image scale
497 pdf.set_image_scale(1.6)
498 pdf.RDMwriteHTMLCell(35+155, 5, '', '',
499 issue.description.to_s, issue.attachments, "LRB")
500
501 unless issue.leaf?
502 truncate_length = (!is_cjk? ? 90 : 65)
503 pdf.SetFontStyle('B',9)
504 pdf.RDMCell(35+155,5, l(:label_subtask_plural) + ":", "LTR")
505 pdf.ln
506 issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level|
507 buf = "#{child.tracker} # #{child.id}: #{child.subject}".
508 truncate(truncate_length)
509 level = 10 if level >= 10
510 pdf.SetFontStyle('',8)
511 pdf.RDMCell(35+135,5, (level >=1 ? " " * level : "") + buf, border_first)
512 pdf.SetFontStyle('B',8)
513 pdf.RDMCell(20,5, child.status.to_s, border_last)
514 pdf.ln
515 end
516 end
517
518 relations = issue.relations.select { |r| r.other_issue(issue).visible? }
519 unless relations.empty?
520 truncate_length = (!is_cjk? ? 80 : 60)
521 pdf.SetFontStyle('B',9)
522 pdf.RDMCell(35+155,5, l(:label_related_issues) + ":", "LTR")
523 pdf.ln
524 relations.each do |relation|
525 buf = relation.to_s(issue) {|other|
526 text = ""
527 if Setting.cross_project_issue_relations?
528 text += "#{relation.other_issue(issue).project} - "
529 end
530 text += "#{other.tracker} ##{other.id}: #{other.subject}"
531 text
532 }
533 buf = buf.truncate(truncate_length)
534 pdf.SetFontStyle('', 8)
535 pdf.RDMCell(35+155-60, 5, buf, border_first)
536 pdf.SetFontStyle('B',8)
537 pdf.RDMCell(20,5, relation.other_issue(issue).status.to_s, "")
538 pdf.RDMCell(20,5, format_date(relation.other_issue(issue).start_date), "")
539 pdf.RDMCell(20,5, format_date(relation.other_issue(issue).due_date), border_last)
540 pdf.ln
541 end
542 end
543 pdf.RDMCell(190,5, "", "T")
544 pdf.ln
545
546 if issue.changesets.any? &&
547 User.current.allowed_to?(:view_changesets, issue.project)
548 pdf.SetFontStyle('B',9)
549 pdf.RDMCell(190,5, l(:label_associated_revisions), "B")
550 pdf.ln
551 for changeset in issue.changesets
552 pdf.SetFontStyle('B',8)
553 csstr = "#{l(:label_revision)} #{changeset.format_identifier} - "
554 csstr += format_time(changeset.committed_on) + " - " + changeset.author.to_s
555 pdf.RDMCell(190, 5, csstr)
556 pdf.ln
557 unless changeset.comments.blank?
558 pdf.SetFontStyle('',8)
559 pdf.RDMwriteHTMLCell(190,5,'','',
560 changeset.comments.to_s, issue.attachments, "")
561 end
562 pdf.ln
563 end
564 end
565
566 if assoc[:journals].present?
567 pdf.SetFontStyle('B',9)
568 pdf.RDMCell(190,5, l(:label_history), "B")
569 pdf.ln
570 assoc[:journals].each do |journal|
571 pdf.SetFontStyle('B',8)
572 title = "##{journal.indice} - #{format_time(journal.created_on)} - #{journal.user}"
573 title << " (#{l(:field_private_notes)})" if journal.private_notes?
574 pdf.RDMCell(190,5, title)
575 pdf.ln
576 pdf.SetFontStyle('I',8)
577 details_to_strings(journal.visible_details, true).each do |string|
578 pdf.RDMMultiCell(190,5, "- " + string)
579 end
580 if journal.notes?
581 pdf.ln unless journal.details.empty?
582 pdf.SetFontStyle('',8)
583 pdf.RDMwriteHTMLCell(190,5,'','',
584 journal.notes.to_s, issue.attachments, "")
585 end
586 pdf.ln
587 end
588 end
589
590 if issue.attachments.any?
591 pdf.SetFontStyle('B',9)
592 pdf.RDMCell(190,5, l(:label_attachment_plural), "B")
593 pdf.ln
594 for attachment in issue.attachments
595 pdf.SetFontStyle('',8)
596 pdf.RDMCell(80,5, attachment.filename)
597 pdf.RDMCell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
598 pdf.RDMCell(25,5, format_date(attachment.created_on),0,0,"R")
599 pdf.RDMCell(65,5, attachment.author.name,0,0,"R")
600 pdf.ln
601 end
602 end
603 pdf.output
604 end
605
606 # Returns a PDF string of a set of wiki pages
607 def wiki_pages_to_pdf(pages, project)
608 pdf = ITCPDF.new(current_language)
609 pdf.set_title(project.name)
610 pdf.alias_nb_pages
611 pdf.footer_date = format_date(Date.today)
612 pdf.add_page
613 pdf.SetFontStyle('B',11)
614 pdf.RDMMultiCell(190,5, project.name)
615 pdf.ln
616 # Set resize image scale
617 pdf.set_image_scale(1.6)
618 pdf.SetFontStyle('',9)
619 write_page_hierarchy(pdf, pages.group_by(&:parent_id))
620 pdf.output
621 end
622
623 # Returns a PDF string of a single wiki page
624 def wiki_page_to_pdf(page, project)
625 pdf = ITCPDF.new(current_language)
626 pdf.set_title("#{project} - #{page.title}")
627 pdf.alias_nb_pages
628 pdf.footer_date = format_date(Date.today)
629 pdf.add_page
630 pdf.SetFontStyle('B',11)
631 pdf.RDMMultiCell(190,5,
632 "#{project} - #{page.title} - # #{page.content.version}")
633 pdf.ln
634 # Set resize image scale
635 pdf.set_image_scale(1.6)
636 pdf.SetFontStyle('',9)
637 write_wiki_page(pdf, page)
638 pdf.output
639 end
640
641 def write_page_hierarchy(pdf, pages, node=nil, level=0)
642 if pages[node]
643 pages[node].each do |page|
644 if @new_page
645 pdf.add_page
646 else
647 @new_page = true
648 end
649 pdf.bookmark page.title, level
650 write_wiki_page(pdf, page)
651 write_page_hierarchy(pdf, pages, page.id, level + 1) if pages[page.id]
652 end
653 end
654 end
655
656 def write_wiki_page(pdf, page)
657 pdf.RDMwriteHTMLCell(190,5,'','',
658 page.content.text.to_s, page.attachments, 0)
659 if page.attachments.any?
660 pdf.ln(5)
661 pdf.SetFontStyle('B',9)
662 pdf.RDMCell(190,5, l(:label_attachment_plural), "B")
663 pdf.ln
664 for attachment in page.attachments
665 pdf.SetFontStyle('',8)
666 pdf.RDMCell(80,5, attachment.filename)
667 pdf.RDMCell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
668 pdf.RDMCell(25,5, format_date(attachment.created_on),0,0,"R")
669 pdf.RDMCell(65,5, attachment.author.name,0,0,"R")
670 pdf.ln
671 end
672 end
673 end
674
675 class RDMPdfEncoding
122 class RDMPdfEncoding
676 def self.rdm_from_utf8(txt, encoding)
123 def self.rdm_from_utf8(txt, encoding)
677 txt ||= ''
124 txt ||= ''
678 txt = Redmine::CodesetUtil.from_utf8(txt, encoding)
125 txt = Redmine::CodesetUtil.from_utf8(txt, encoding)
679 txt.force_encoding('ASCII-8BIT')
126 txt.force_encoding('ASCII-8BIT')
680 txt
127 txt
681 end
128 end
682
129
683 def self.attach(attachments, filename, encoding)
130 def self.attach(attachments, filename, encoding)
684 filename_utf8 = Redmine::CodesetUtil.to_utf8(filename, encoding)
131 filename_utf8 = Redmine::CodesetUtil.to_utf8(filename, encoding)
685 atta = nil
132 atta = nil
686 if filename_utf8 =~ /^[^\/"]+\.(gif|jpg|jpe|jpeg|png)$/i
133 if filename_utf8 =~ /^[^\/"]+\.(gif|jpg|jpe|jpeg|png)$/i
687 atta = Attachment.latest_attach(attachments, filename_utf8)
134 atta = Attachment.latest_attach(attachments, filename_utf8)
688 end
135 end
689 if atta && atta.readable? && atta.visible?
136 if atta && atta.readable? && atta.visible?
690 return atta
137 return atta
691 else
138 else
692 return nil
139 return nil
693 end
140 end
694 end
141 end
695 end
142 end
696 end
143 end
697 end
144 end
698 end
145 end
General Comments 0
You need to be logged in to leave comments. Login now