##// END OF EJS Templates
pdf: expand relational issues status column width of issue pdf (#5634)...
Toshi MARUYAMA -
r7845:9dfaa562fc44
parent child
Show More
@@ -1,549 +1,549
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 pdf.RDMCell(35+155-50,5, buf, "L")
421 pdf.RDMCell(35+155-60, 5, buf, "L")
422 422 pdf.SetFontStyle('B',8)
423 pdf.RDMCell(10,5, relation.other_issue(issue).status.to_s, "")
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 for journal in issue.journals.find(
456 456 :all, :include => [:user, :details],
457 457 :order => "#{Journal.table_name}.created_on ASC")
458 458 pdf.SetFontStyle('B',8)
459 459 pdf.RDMCell(190,5,
460 460 format_time(journal.created_on) + " - " + journal.user.name)
461 461 pdf.Ln
462 462 pdf.SetFontStyle('I',8)
463 463 for detail in journal.details
464 464 pdf.RDMMultiCell(190,5, "- " + show_detail(detail, true))
465 465 end
466 466 if journal.notes?
467 467 pdf.Ln unless journal.details.empty?
468 468 pdf.SetFontStyle('',8)
469 469 pdf.RDMwriteHTMLCell(190,5,0,0,
470 470 journal.notes.to_s, issue.attachments, "")
471 471 end
472 472 pdf.Ln
473 473 end
474 474
475 475 if issue.attachments.any?
476 476 pdf.SetFontStyle('B',9)
477 477 pdf.RDMCell(190,5, l(:label_attachment_plural), "B")
478 478 pdf.Ln
479 479 for attachment in issue.attachments
480 480 pdf.SetFontStyle('',8)
481 481 pdf.RDMCell(80,5, attachment.filename)
482 482 pdf.RDMCell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
483 483 pdf.RDMCell(25,5, format_date(attachment.created_on),0,0,"R")
484 484 pdf.RDMCell(65,5, attachment.author.name,0,0,"R")
485 485 pdf.Ln
486 486 end
487 487 end
488 488 pdf.Output
489 489 end
490 490
491 491 # Returns a PDF string of a single wiki page
492 492 def wiki_to_pdf(page, project)
493 493 pdf = ITCPDF.new(current_language)
494 494 pdf.SetTitle("#{project} - #{page.title}")
495 495 pdf.alias_nb_pages
496 496 pdf.footer_date = format_date(Date.today)
497 497 pdf.AddPage
498 498 pdf.SetFontStyle('B',11)
499 499 pdf.RDMMultiCell(190,5,
500 500 "#{project} - #{page.title} - # #{page.content.version}")
501 501 pdf.Ln
502 502 # Set resize image scale
503 503 pdf.SetImageScale(1.6)
504 504 pdf.SetFontStyle('',9)
505 505 pdf.RDMwriteHTMLCell(190,5,0,0,
506 506 page.content.text.to_s, page.attachments, "TLRB")
507 507 if page.attachments.any?
508 508 pdf.Ln
509 509 pdf.SetFontStyle('B',9)
510 510 pdf.RDMCell(190,5, l(:label_attachment_plural), "B")
511 511 pdf.Ln
512 512 for attachment in page.attachments
513 513 pdf.SetFontStyle('',8)
514 514 pdf.RDMCell(80,5, attachment.filename)
515 515 pdf.RDMCell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
516 516 pdf.RDMCell(25,5, format_date(attachment.created_on),0,0,"R")
517 517 pdf.RDMCell(65,5, attachment.author.name,0,0,"R")
518 518 pdf.Ln
519 519 end
520 520 end
521 521 pdf.Output
522 522 end
523 523
524 524 class RDMPdfEncoding
525 525 def self.rdm_from_utf8(txt, encoding)
526 526 txt ||= ''
527 527 txt = Redmine::CodesetUtil.from_utf8(txt, encoding)
528 528 if txt.respond_to?(:force_encoding)
529 529 txt.force_encoding('ASCII-8BIT')
530 530 end
531 531 txt
532 532 end
533 533
534 534 def self.attach(attachments, filename, encoding)
535 535 filename_utf8 = Redmine::CodesetUtil.to_utf8(filename, encoding)
536 536 atta = nil
537 537 if filename_utf8 =~ /^[^\/"]+\.(gif|jpg|jpe|jpeg|png)$/i
538 538 atta = Attachment.latest_attach(attachments, filename_utf8)
539 539 end
540 540 if atta && atta.readable? && atta.visible?
541 541 return atta
542 542 else
543 543 return nil
544 544 end
545 545 end
546 546 end
547 547 end
548 548 end
549 549 end
General Comments 0
You need to be logged in to leave comments. Login now