##// END OF EJS Templates
Merged r8099 from trunk...
Toshi MARUYAMA -
r7980:011d9587a5ec
parent child
Show More
@@ -1,552 +1,553
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2011 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 require 'iconv'
21 21 require 'fpdf/chinese'
22 22 require 'fpdf/japanese'
23 23 require 'fpdf/korean'
24 24 require 'core/rmagick'
25 25
26 26 module Redmine
27 27 module Export
28 28 module PDF
29 29 include ActionView::Helpers::TextHelper
30 30 include ActionView::Helpers::NumberHelper
31 31 include IssuesHelper
32 32
33 33 class ITCPDF < TCPDF
34 34 include Redmine::I18n
35 35 attr_accessor :footer_date
36 36
37 37 def initialize(lang)
38 38 @@k_path_cache = Rails.root.join('tmp', 'pdf')
39 39 FileUtils.mkdir_p @@k_path_cache unless File::exist?(@@k_path_cache)
40 40 set_language_if_valid lang
41 41 pdf_encoding = l(:general_pdf_encoding).upcase
42 42 super('P', 'mm', 'A4', (pdf_encoding == 'UTF-8'), pdf_encoding)
43 43 case current_language.to_s.downcase
44 44 when 'vi'
45 45 @font_for_content = 'DejaVuSans'
46 46 @font_for_footer = 'DejaVuSans'
47 47 else
48 48 case pdf_encoding
49 49 when 'UTF-8'
50 50 @font_for_content = 'FreeSans'
51 51 @font_for_footer = 'FreeSans'
52 52 when 'CP949'
53 53 extend(PDF_Korean)
54 54 AddUHCFont()
55 55 @font_for_content = 'UHC'
56 56 @font_for_footer = 'UHC'
57 57 when 'CP932', 'SJIS', 'SHIFT_JIS'
58 58 extend(PDF_Japanese)
59 59 AddSJISFont()
60 60 @font_for_content = 'SJIS'
61 61 @font_for_footer = 'SJIS'
62 62 when 'GB18030'
63 63 extend(PDF_Chinese)
64 64 AddGBFont()
65 65 @font_for_content = 'GB'
66 66 @font_for_footer = 'GB'
67 67 when 'BIG5'
68 68 extend(PDF_Chinese)
69 69 AddBig5Font()
70 70 @font_for_content = 'Big5'
71 71 @font_for_footer = 'Big5'
72 72 else
73 73 @font_for_content = 'Arial'
74 74 @font_for_footer = 'Helvetica'
75 75 end
76 76 end
77 77 SetCreator(Redmine::Info.app_name)
78 78 SetFont(@font_for_content)
79 79 end
80 80
81 81 def SetFontStyle(style, size)
82 82 SetFont(@font_for_content, style, size)
83 83 end
84 84
85 85 def SetTitle(txt)
86 86 txt = begin
87 87 utf16txt = Iconv.conv('UTF-16BE', 'UTF-8', txt)
88 88 hextxt = "<FEFF" # FEFF is BOM
89 89 hextxt << utf16txt.unpack("C*").map {|x| sprintf("%02X",x) }.join
90 90 hextxt << ">"
91 91 rescue
92 92 txt
93 93 end || ''
94 94 super(txt)
95 95 end
96 96
97 97 def textstring(s)
98 98 # Format a text string
99 99 if s =~ /^</ # This means the string is hex-dumped.
100 100 return s
101 101 else
102 102 return '('+escape(s)+')'
103 103 end
104 104 end
105 105
106 106 def fix_text_encoding(txt)
107 107 RDMPdfEncoding::rdm_from_utf8(txt, l(:general_pdf_encoding))
108 108 end
109 109
110 110 def RDMCell(w ,h=0, txt='', border=0, ln=0, align='', fill=0, link='')
111 111 Cell(w, h, fix_text_encoding(txt), border, ln, align, fill, link)
112 112 end
113 113
114 114 def RDMMultiCell(w, h=0, txt='', border=0, align='', fill=0, ln=1)
115 115 MultiCell(w, h, fix_text_encoding(txt), border, align, fill, ln)
116 116 end
117 117
118 118 def RDMwriteHTMLCell(w, h, x, y, txt='', attachments=[], border=0, ln=1, fill=0)
119 119 @attachments = attachments
120 120 writeHTMLCell(w, h, x, y,
121 121 fix_text_encoding(
122 122 Redmine::WikiFormatting.to_html(Setting.text_formatting, txt)),
123 123 border, ln, fill)
124 124 end
125 125
126 126 def getImageFilename(attrname)
127 127 # attrname: general_pdf_encoding string file/uri name
128 128 atta = RDMPdfEncoding.attach(@attachments, attrname, l(:general_pdf_encoding))
129 129 if atta
130 130 return atta.diskfile
131 131 else
132 132 return nil
133 133 end
134 134 end
135 135
136 136 def Footer
137 137 SetFont(@font_for_footer, 'I', 8)
138 138 SetY(-15)
139 139 SetX(15)
140 140 RDMCell(0, 5, @footer_date, 0, 0, 'L')
141 141 SetY(-15)
142 142 SetX(-30)
143 143 RDMCell(0, 5, PageNo().to_s + '/{nb}', 0, 0, 'C')
144 144 end
145 145 end
146 146
147 147 # Returns a PDF string of a list of issues
148 148 def issues_to_pdf(issues, project, query)
149 149 pdf = ITCPDF.new(current_language)
150 150 title = query.new_record? ? l(:label_issue_plural) : query.name
151 151 title = "#{project} - #{title}" if project
152 152 pdf.SetTitle(title)
153 153 pdf.alias_nb_pages
154 154 pdf.footer_date = format_date(Date.today)
155 155 pdf.SetAutoPageBreak(false)
156 156 pdf.AddPage("L")
157 157
158 158 # Landscape A4 = 210 x 297 mm
159 159 page_height = 210
160 160 page_width = 297
161 161 right_margin = 10
162 162 bottom_margin = 20
163 163 col_id_width = 10
164 164 row_height = 5
165 165
166 166 # column widths
167 167 table_width = page_width - right_margin - 10 # fixed left margin
168 168 col_width = []
169 169 unless query.columns.empty?
170 170 col_width = query.columns.collect do |c|
171 171 (c.name == :subject || (c.is_a?(QueryCustomFieldColumn) &&
172 172 ['string', 'text'].include?(c.custom_field.field_format))) ? 4.0 : 1.0
173 173 end
174 174 ratio = (table_width - col_id_width) / col_width.inject(0) {|s,w| s += w}
175 175 col_width = col_width.collect {|w| w * ratio}
176 176 end
177 177
178 178 # title
179 179 pdf.SetFontStyle('B',11)
180 180 pdf.RDMCell(190,10, title)
181 181 pdf.Ln
182 182
183 183 # headers
184 184 pdf.SetFontStyle('B',8)
185 185 pdf.SetFillColor(230, 230, 230)
186 186
187 187 # render it background to find the max height used
188 188 base_x = pdf.GetX
189 189 base_y = pdf.GetY
190 190 max_height = issues_to_pdf_write_cells(pdf, query.columns, col_width, row_height, true)
191 191 pdf.Rect(base_x, base_y, table_width, max_height, 'FD');
192 192 pdf.SetXY(base_x, base_y);
193 193
194 194 # write the cells on page
195 195 pdf.RDMCell(col_id_width, row_height, "#", "T", 0, 'C', 1)
196 196 issues_to_pdf_write_cells(pdf, query.columns, col_width, row_height, true)
197 197 issues_to_pdf_draw_borders(pdf, base_x, base_y, base_y + max_height, col_id_width, col_width)
198 198 pdf.SetY(base_y + max_height);
199 199
200 200 # rows
201 201 pdf.SetFontStyle('',8)
202 202 pdf.SetFillColor(255, 255, 255)
203 203 previous_group = false
204 204 issue_list(issues) do |issue, level|
205 205 if query.grouped? &&
206 206 (group = query.group_by_column.value(issue)) != previous_group
207 207 pdf.SetFontStyle('B',9)
208 208 pdf.RDMCell(277, row_height,
209 209 (group.blank? ? 'None' : group.to_s) + " (#{query.issue_count_by_group[group]})",
210 210 1, 1, 'L')
211 211 pdf.SetFontStyle('',8)
212 212 previous_group = group
213 213 end
214 214 # fetch all the row values
215 215 col_values = query.columns.collect do |column|
216 216 s = if column.is_a?(QueryCustomFieldColumn)
217 217 cv = issue.custom_values.detect {|v| v.custom_field_id == column.custom_field.id}
218 218 show_value(cv)
219 219 else
220 220 value = issue.send(column.name)
221 221 if column.name == :subject
222 222 value = " " * level + value
223 223 end
224 224 if value.is_a?(Date)
225 225 format_date(value)
226 226 elsif value.is_a?(Time)
227 227 format_time(value)
228 228 else
229 229 value
230 230 end
231 231 end
232 232 s.to_s
233 233 end
234 234
235 235 # render it off-page to find the max height used
236 236 base_x = pdf.GetX
237 237 base_y = pdf.GetY
238 238 pdf.SetY(2 * page_height)
239 239 max_height = issues_to_pdf_write_cells(pdf, col_values, col_width, row_height)
240 240 pdf.SetXY(base_x, base_y)
241 241
242 242 # make new page if it doesn't fit on the current one
243 243 space_left = page_height - base_y - bottom_margin
244 244 if max_height > space_left
245 245 pdf.AddPage("L")
246 246 base_x = pdf.GetX
247 247 base_y = pdf.GetY
248 248 end
249 249
250 250 # write the cells on page
251 251 pdf.RDMCell(col_id_width, row_height, issue.id.to_s, "T", 0, 'C', 1)
252 252 issues_to_pdf_write_cells(pdf, col_values, col_width, row_height)
253 253 issues_to_pdf_draw_borders(pdf, base_x, base_y, base_y + max_height, col_id_width, col_width)
254 254 pdf.SetY(base_y + max_height);
255 255 end
256 256
257 257 if issues.size == Setting.issues_export_limit.to_i
258 258 pdf.SetFontStyle('B',10)
259 259 pdf.RDMCell(0, row_height, '...')
260 260 end
261 261 pdf.Output
262 262 end
263 263
264 264 # Renders MultiCells and returns the maximum height used
265 265 def issues_to_pdf_write_cells(pdf, col_values, col_widths,
266 266 row_height, head=false)
267 267 base_y = pdf.GetY
268 268 max_height = row_height
269 269 col_values.each_with_index do |column, i|
270 270 col_x = pdf.GetX
271 271 if head == true
272 272 pdf.RDMMultiCell(col_widths[i], row_height, column.caption, "T", 'L', 1)
273 273 else
274 274 pdf.RDMMultiCell(col_widths[i], row_height, column, "T", 'L', 1)
275 275 end
276 276 max_height = (pdf.GetY - base_y) if (pdf.GetY - base_y) > max_height
277 277 pdf.SetXY(col_x + col_widths[i], base_y);
278 278 end
279 279 return max_height
280 280 end
281 281
282 282 # Draw lines to close the row (MultiCell border drawing in not uniform)
283 283 def issues_to_pdf_draw_borders(pdf, top_x, top_y, lower_y,
284 284 id_width, col_widths)
285 285 col_x = top_x + id_width
286 286 pdf.Line(col_x, top_y, col_x, lower_y) # id right border
287 287 col_widths.each do |width|
288 288 col_x += width
289 289 pdf.Line(col_x, top_y, col_x, lower_y) # columns right border
290 290 end
291 291 pdf.Line(top_x, top_y, top_x, lower_y) # left border
292 292 pdf.Line(top_x, lower_y, col_x, lower_y) # bottom border
293 293 end
294 294
295 295 # Returns a PDF string of a single issue
296 296 def issue_to_pdf(issue)
297 297 pdf = ITCPDF.new(current_language)
298 298 pdf.SetTitle("#{issue.project} - ##{issue.tracker} #{issue.id}")
299 299 pdf.alias_nb_pages
300 300 pdf.footer_date = format_date(Date.today)
301 301 pdf.AddPage
302 302 pdf.SetFontStyle('B',11)
303 303 buf = "#{issue.project} - #{issue.tracker} # #{issue.id}"
304 304 pdf.RDMMultiCell(190, 5, buf)
305 305 pdf.Ln
306 306 pdf.SetFontStyle('',8)
307 307 base_x = pdf.GetX
308 308 i = 1
309 309 issue.ancestors.each do |ancestor|
310 310 pdf.SetX(base_x + i)
311 311 buf = "#{ancestor.tracker} # #{ancestor.id} (#{ancestor.status.to_s}): #{ancestor.subject}"
312 312 pdf.RDMMultiCell(190 - i, 5, buf)
313 313 i += 1 if i < 35
314 314 end
315 315 pdf.Ln
316 316
317 317 pdf.SetFontStyle('B',9)
318 318 pdf.RDMCell(35,5, l(:field_status) + ":","LT")
319 319 pdf.SetFontStyle('',9)
320 320 pdf.RDMCell(60,5, issue.status.to_s,"RT")
321 321 pdf.SetFontStyle('B',9)
322 322 pdf.RDMCell(35,5, l(:field_priority) + ":","LT")
323 323 pdf.SetFontStyle('',9)
324 324 pdf.RDMCell(60,5, issue.priority.to_s,"RT")
325 325 pdf.Ln
326 326
327 327 pdf.SetFontStyle('B',9)
328 328 pdf.RDMCell(35,5, l(:field_author) + ":","L")
329 329 pdf.SetFontStyle('',9)
330 330 pdf.RDMCell(60,5, issue.author.to_s,"R")
331 331 pdf.SetFontStyle('B',9)
332 332 pdf.RDMCell(35,5, l(:field_category) + ":","L")
333 333 pdf.SetFontStyle('',9)
334 334 pdf.RDMCell(60,5, issue.category.to_s,"R")
335 335 pdf.Ln
336 336
337 337 pdf.SetFontStyle('B',9)
338 338 pdf.RDMCell(35,5, l(:field_created_on) + ":","L")
339 339 pdf.SetFontStyle('',9)
340 340 pdf.RDMCell(60,5, format_date(issue.created_on),"R")
341 341 pdf.SetFontStyle('B',9)
342 342 pdf.RDMCell(35,5, l(:field_assigned_to) + ":","L")
343 343 pdf.SetFontStyle('',9)
344 344 pdf.RDMCell(60,5, issue.assigned_to.to_s,"R")
345 345 pdf.Ln
346 346
347 347 pdf.SetFontStyle('B',9)
348 348 pdf.RDMCell(35,5, l(:field_updated_on) + ":","LB")
349 349 pdf.SetFontStyle('',9)
350 350 pdf.RDMCell(60,5, format_date(issue.updated_on),"RB")
351 351 pdf.SetFontStyle('B',9)
352 352 pdf.RDMCell(35,5, l(:field_due_date) + ":","LB")
353 353 pdf.SetFontStyle('',9)
354 354 pdf.RDMCell(60,5, format_date(issue.due_date),"RB")
355 355 pdf.Ln
356 356
357 357 for custom_value in issue.custom_field_values
358 358 pdf.SetFontStyle('B',9)
359 359 pdf.RDMCell(35,5, custom_value.custom_field.name + ":","L")
360 360 pdf.SetFontStyle('',9)
361 361 pdf.RDMMultiCell(155,5, (show_value custom_value),"R")
362 362 end
363 363
364 364 y0 = pdf.GetY
365 365
366 366 pdf.SetFontStyle('B',9)
367 367 pdf.RDMCell(35,5, l(:field_subject) + ":","LT")
368 368 pdf.SetFontStyle('',9)
369 369 pdf.RDMMultiCell(155,5, issue.subject,"RT")
370 370 pdf.Line(pdf.GetX, y0, pdf.GetX, pdf.GetY)
371 371
372 372 pdf.SetFontStyle('B',9)
373 373 pdf.RDMCell(35+155, 5, l(:field_description), "LRT", 1)
374 374 pdf.SetFontStyle('',9)
375 375
376 376 # Set resize image scale
377 377 pdf.SetImageScale(1.6)
378 378 pdf.RDMwriteHTMLCell(35+155, 5, 0, 0,
379 379 issue.description.to_s, issue.attachments, "LRB")
380 380
381 381 unless issue.leaf?
382 382 # for CJK
383 383 truncate_length = ( l(:general_pdf_encoding).upcase == "UTF-8" ? 90 : 65 )
384 384
385 385 pdf.SetFontStyle('B',9)
386 386 pdf.RDMCell(35+155,5, l(:label_subtask_plural) + ":", "LTR")
387 387 pdf.Ln
388 388 issue_list(issue.descendants.sort_by(&:lft)) do |child, level|
389 389 buf = truncate("#{child.tracker} # #{child.id}: #{child.subject}",
390 390 :length => truncate_length)
391 391 level = 10 if level >= 10
392 392 pdf.SetFontStyle('',8)
393 393 pdf.RDMCell(35+135,5, (level >=1 ? " " * level : "") + buf, "L")
394 394 pdf.SetFontStyle('B',8)
395 395 pdf.RDMCell(20,5, child.status.to_s, "R")
396 396 pdf.Ln
397 397 end
398 398 end
399 399
400 400 relations = issue.relations.select { |r| r.other_issue(issue).visible? }
401 401 unless relations.empty?
402 402 # for CJK
403 403 truncate_length = ( l(:general_pdf_encoding).upcase == "UTF-8" ? 80 : 60 )
404 404
405 405 pdf.SetFontStyle('B',9)
406 406 pdf.RDMCell(35+155,5, l(:label_related_issues) + ":", "LTR")
407 407 pdf.Ln
408 408 relations.each do |relation|
409 409 buf = ""
410 410 buf += "#{l(relation.label_for(issue))} "
411 411 if relation.delay && relation.delay != 0
412 412 buf += "(#{l('datetime.distance_in_words.x_days', :count => relation.delay)}) "
413 413 end
414 414 if Setting.cross_project_issue_relations?
415 415 buf += "#{relation.other_issue(issue).project} - "
416 416 end
417 417 buf += "#{relation.other_issue(issue).tracker}" +
418 418 " # #{relation.other_issue(issue).id}: #{relation.other_issue(issue).subject}"
419 419 buf = truncate(buf, :length => truncate_length)
420 420 pdf.SetFontStyle('', 8)
421 421 pdf.RDMCell(35+155-60, 5, buf, "L")
422 422 pdf.SetFontStyle('B',8)
423 423 pdf.RDMCell(20,5, relation.other_issue(issue).status.to_s, "")
424 424 pdf.RDMCell(20,5, format_date(relation.other_issue(issue).start_date), "")
425 425 pdf.RDMCell(20,5, format_date(relation.other_issue(issue).due_date), "R")
426 426 pdf.Ln
427 427 end
428 428 end
429 429 pdf.RDMCell(190,5, "", "T")
430 430 pdf.Ln
431 431
432 432 if issue.changesets.any? &&
433 433 User.current.allowed_to?(:view_changesets, issue.project)
434 434 pdf.SetFontStyle('B',9)
435 435 pdf.RDMCell(190,5, l(:label_associated_revisions), "B")
436 436 pdf.Ln
437 437 for changeset in issue.changesets
438 438 pdf.SetFontStyle('B',8)
439 439 csstr = "#{l(:label_revision)} #{changeset.format_identifier} - "
440 440 csstr += format_time(changeset.committed_on) + " - " + changeset.author.to_s
441 441 pdf.RDMCell(190, 5, csstr)
442 442 pdf.Ln
443 443 unless changeset.comments.blank?
444 444 pdf.SetFontStyle('',8)
445 445 pdf.RDMwriteHTMLCell(190,5,0,0,
446 446 changeset.comments.to_s, issue.attachments, "")
447 447 end
448 448 pdf.Ln
449 449 end
450 450 end
451 451
452 452 pdf.SetFontStyle('B',9)
453 453 pdf.RDMCell(190,5, l(:label_history), "B")
454 454 pdf.Ln
455 455 indice = 0
456 456 for journal in issue.journals.find(
457 457 :all, :include => [:user, :details],
458 458 :order => "#{Journal.table_name}.created_on ASC")
459 459 indice = indice + 1
460 460 pdf.SetFontStyle('B',8)
461 461 pdf.RDMCell(190,5,
462 format_time(journal.created_on) + " - " +
463 journal.user.name + " #" + indice.to_s )
462 "#" + indice.to_s +
463 " - " + format_time(journal.created_on) +
464 " - " + journal.user.name)
464 465 pdf.Ln
465 466 pdf.SetFontStyle('I',8)
466 467 for detail in journal.details
467 468 pdf.RDMMultiCell(190,5, "- " + show_detail(detail, true))
468 469 end
469 470 if journal.notes?
470 471 pdf.Ln unless journal.details.empty?
471 472 pdf.SetFontStyle('',8)
472 473 pdf.RDMwriteHTMLCell(190,5,0,0,
473 474 journal.notes.to_s, issue.attachments, "")
474 475 end
475 476 pdf.Ln
476 477 end
477 478
478 479 if issue.attachments.any?
479 480 pdf.SetFontStyle('B',9)
480 481 pdf.RDMCell(190,5, l(:label_attachment_plural), "B")
481 482 pdf.Ln
482 483 for attachment in issue.attachments
483 484 pdf.SetFontStyle('',8)
484 485 pdf.RDMCell(80,5, attachment.filename)
485 486 pdf.RDMCell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
486 487 pdf.RDMCell(25,5, format_date(attachment.created_on),0,0,"R")
487 488 pdf.RDMCell(65,5, attachment.author.name,0,0,"R")
488 489 pdf.Ln
489 490 end
490 491 end
491 492 pdf.Output
492 493 end
493 494
494 495 # Returns a PDF string of a single wiki page
495 496 def wiki_to_pdf(page, project)
496 497 pdf = ITCPDF.new(current_language)
497 498 pdf.SetTitle("#{project} - #{page.title}")
498 499 pdf.alias_nb_pages
499 500 pdf.footer_date = format_date(Date.today)
500 501 pdf.AddPage
501 502 pdf.SetFontStyle('B',11)
502 503 pdf.RDMMultiCell(190,5,
503 504 "#{project} - #{page.title} - # #{page.content.version}")
504 505 pdf.Ln
505 506 # Set resize image scale
506 507 pdf.SetImageScale(1.6)
507 508 pdf.SetFontStyle('',9)
508 509 pdf.RDMwriteHTMLCell(190,5,0,0,
509 510 page.content.text.to_s, page.attachments, "TLRB")
510 511 if page.attachments.any?
511 512 pdf.Ln
512 513 pdf.SetFontStyle('B',9)
513 514 pdf.RDMCell(190,5, l(:label_attachment_plural), "B")
514 515 pdf.Ln
515 516 for attachment in page.attachments
516 517 pdf.SetFontStyle('',8)
517 518 pdf.RDMCell(80,5, attachment.filename)
518 519 pdf.RDMCell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
519 520 pdf.RDMCell(25,5, format_date(attachment.created_on),0,0,"R")
520 521 pdf.RDMCell(65,5, attachment.author.name,0,0,"R")
521 522 pdf.Ln
522 523 end
523 524 end
524 525 pdf.Output
525 526 end
526 527
527 528 class RDMPdfEncoding
528 529 def self.rdm_from_utf8(txt, encoding)
529 530 txt ||= ''
530 531 txt = Redmine::CodesetUtil.from_utf8(txt, encoding)
531 532 if txt.respond_to?(:force_encoding)
532 533 txt.force_encoding('ASCII-8BIT')
533 534 end
534 535 txt
535 536 end
536 537
537 538 def self.attach(attachments, filename, encoding)
538 539 filename_utf8 = Redmine::CodesetUtil.to_utf8(filename, encoding)
539 540 atta = nil
540 541 if filename_utf8 =~ /^[^\/"]+\.(gif|jpg|jpe|jpeg|png)$/i
541 542 atta = Attachment.latest_attach(attachments, filename_utf8)
542 543 end
543 544 if atta && atta.readable? && atta.visible?
544 545 return atta
545 546 else
546 547 return nil
547 548 end
548 549 end
549 550 end
550 551 end
551 552 end
552 553 end
General Comments 0
You need to be logged in to leave comments. Login now