##// END OF EJS Templates
pdf: add the new feature to include attachment images (#3261)...
Toshi MARUYAMA -
r7795:b200fcfdae3c
parent child
Show More
@@ -1,538 +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 @attachments = attachments
119 120 writeHTMLCell(w, h, x, y,
120 121 fix_text_encoding(
121 122 Redmine::WikiFormatting.to_html(Setting.text_formatting, txt)),
122 123 border, ln, fill)
123 124 end
124 125
126 def getImageFilename(attrname)
127 # attrname: general_pdf_encoding string file/uri name
128 atta = RDMPdfEncoding.attach(@attachments, attrname, l(:general_pdf_encoding))
129 if atta
130 return atta.diskfile
131 else
132 return nil
133 end
134 end
135
125 136 def Footer
126 137 SetFont(@font_for_footer, 'I', 8)
127 138 SetY(-15)
128 139 SetX(15)
129 140 RDMCell(0, 5, @footer_date, 0, 0, 'L')
130 141 SetY(-15)
131 142 SetX(-30)
132 143 RDMCell(0, 5, PageNo().to_s + '/{nb}', 0, 0, 'C')
133 144 end
134 145 end
135 146
136 147 # Returns a PDF string of a list of issues
137 148 def issues_to_pdf(issues, project, query)
138 149 pdf = ITCPDF.new(current_language)
139 150 title = query.new_record? ? l(:label_issue_plural) : query.name
140 151 title = "#{project} - #{title}" if project
141 152 pdf.SetTitle(title)
142 153 pdf.alias_nb_pages
143 154 pdf.footer_date = format_date(Date.today)
144 155 pdf.SetAutoPageBreak(false)
145 156 pdf.AddPage("L")
146 157
147 158 # Landscape A4 = 210 x 297 mm
148 159 page_height = 210
149 160 page_width = 297
150 161 right_margin = 10
151 162 bottom_margin = 20
152 163 col_id_width = 10
153 164 row_height = 5
154 165
155 166 # column widths
156 167 table_width = page_width - right_margin - 10 # fixed left margin
157 168 col_width = []
158 169 unless query.columns.empty?
159 170 col_width = query.columns.collect do |c|
160 171 (c.name == :subject || (c.is_a?(QueryCustomFieldColumn) &&
161 172 ['string', 'text'].include?(c.custom_field.field_format))) ? 4.0 : 1.0
162 173 end
163 174 ratio = (table_width - col_id_width) / col_width.inject(0) {|s,w| s += w}
164 175 col_width = col_width.collect {|w| w * ratio}
165 176 end
166 177
167 178 # title
168 179 pdf.SetFontStyle('B',11)
169 180 pdf.RDMCell(190,10, title)
170 181 pdf.Ln
171 182
172 183 # headers
173 184 pdf.SetFontStyle('B',8)
174 185 pdf.SetFillColor(230, 230, 230)
175 186
176 187 # render it background to find the max height used
177 188 base_x = pdf.GetX
178 189 base_y = pdf.GetY
179 190 max_height = issues_to_pdf_write_cells(pdf, query.columns, col_width, row_height, true)
180 191 pdf.Rect(base_x, base_y, table_width, max_height, 'FD');
181 192 pdf.SetXY(base_x, base_y);
182 193
183 194 # write the cells on page
184 195 pdf.RDMCell(col_id_width, row_height, "#", "T", 0, 'C', 1)
185 196 issues_to_pdf_write_cells(pdf, query.columns, col_width, row_height, true)
186 197 issues_to_pdf_draw_borders(pdf, base_x, base_y, base_y + max_height, col_id_width, col_width)
187 198 pdf.SetY(base_y + max_height);
188 199
189 200 # rows
190 201 pdf.SetFontStyle('',8)
191 202 pdf.SetFillColor(255, 255, 255)
192 203 previous_group = false
193 204 issue_list(issues) do |issue, level|
194 205 if query.grouped? &&
195 206 (group = query.group_by_column.value(issue)) != previous_group
196 207 pdf.SetFontStyle('B',9)
197 208 pdf.RDMCell(277, row_height,
198 209 (group.blank? ? 'None' : group.to_s) + " (#{query.issue_count_by_group[group]})",
199 210 1, 1, 'L')
200 211 pdf.SetFontStyle('',8)
201 212 previous_group = group
202 213 end
203 214 # fetch all the row values
204 215 col_values = query.columns.collect do |column|
205 216 s = if column.is_a?(QueryCustomFieldColumn)
206 217 cv = issue.custom_values.detect {|v| v.custom_field_id == column.custom_field.id}
207 218 show_value(cv)
208 219 else
209 220 value = issue.send(column.name)
210 221 if column.name == :subject
211 222 value = " " * level + value
212 223 end
213 224 if value.is_a?(Date)
214 225 format_date(value)
215 226 elsif value.is_a?(Time)
216 227 format_time(value)
217 228 else
218 229 value
219 230 end
220 231 end
221 232 s.to_s
222 233 end
223 234
224 235 # render it off-page to find the max height used
225 236 base_x = pdf.GetX
226 237 base_y = pdf.GetY
227 238 pdf.SetY(2 * page_height)
228 239 max_height = issues_to_pdf_write_cells(pdf, col_values, col_width, row_height)
229 240 pdf.SetXY(base_x, base_y)
230 241
231 242 # make new page if it doesn't fit on the current one
232 243 space_left = page_height - base_y - bottom_margin
233 244 if max_height > space_left
234 245 pdf.AddPage("L")
235 246 base_x = pdf.GetX
236 247 base_y = pdf.GetY
237 248 end
238 249
239 250 # write the cells on page
240 251 pdf.RDMCell(col_id_width, row_height, issue.id.to_s, "T", 0, 'C', 1)
241 252 issues_to_pdf_write_cells(pdf, col_values, col_width, row_height)
242 253 issues_to_pdf_draw_borders(pdf, base_x, base_y, base_y + max_height, col_id_width, col_width)
243 254 pdf.SetY(base_y + max_height);
244 255 end
245 256
246 257 if issues.size == Setting.issues_export_limit.to_i
247 258 pdf.SetFontStyle('B',10)
248 259 pdf.RDMCell(0, row_height, '...')
249 260 end
250 261 pdf.Output
251 262 end
252 263
253 264 # Renders MultiCells and returns the maximum height used
254 265 def issues_to_pdf_write_cells(pdf, col_values, col_widths,
255 266 row_height, head=false)
256 267 base_y = pdf.GetY
257 268 max_height = row_height
258 269 col_values.each_with_index do |column, i|
259 270 col_x = pdf.GetX
260 271 if head == true
261 272 pdf.RDMMultiCell(col_widths[i], row_height, column.caption, "T", 'L', 1)
262 273 else
263 274 pdf.RDMMultiCell(col_widths[i], row_height, column, "T", 'L', 1)
264 275 end
265 276 max_height = (pdf.GetY - base_y) if (pdf.GetY - base_y) > max_height
266 277 pdf.SetXY(col_x + col_widths[i], base_y);
267 278 end
268 279 return max_height
269 280 end
270 281
271 282 # Draw lines to close the row (MultiCell border drawing in not uniform)
272 283 def issues_to_pdf_draw_borders(pdf, top_x, top_y, lower_y,
273 284 id_width, col_widths)
274 285 col_x = top_x + id_width
275 286 pdf.Line(col_x, top_y, col_x, lower_y) # id right border
276 287 col_widths.each do |width|
277 288 col_x += width
278 289 pdf.Line(col_x, top_y, col_x, lower_y) # columns right border
279 290 end
280 291 pdf.Line(top_x, top_y, top_x, lower_y) # left border
281 292 pdf.Line(top_x, lower_y, col_x, lower_y) # bottom border
282 293 end
283 294
284 295 # Returns a PDF string of a single issue
285 296 def issue_to_pdf(issue)
286 297 pdf = ITCPDF.new(current_language)
287 298 pdf.SetTitle("#{issue.project} - ##{issue.tracker} #{issue.id}")
288 299 pdf.alias_nb_pages
289 300 pdf.footer_date = format_date(Date.today)
290 301 pdf.AddPage
291 302 pdf.SetFontStyle('B',11)
292 303 buf = "#{issue.project} - #{issue.tracker} # #{issue.id}"
293 304 pdf.RDMMultiCell(190, 5, buf)
294 305 pdf.Ln
295 306 pdf.SetFontStyle('',8)
296 307 base_x = pdf.GetX
297 308 i = 1
298 309 issue.ancestors.each do |ancestor|
299 310 pdf.SetX(base_x + i)
300 311 buf = "#{ancestor.tracker} # #{ancestor.id} (#{ancestor.status.to_s}): #{ancestor.subject}"
301 312 pdf.RDMMultiCell(190 - i, 5, buf)
302 313 i += 1 if i < 35
303 314 end
304 315 pdf.Ln
305 316
306 317 pdf.SetFontStyle('B',9)
307 318 pdf.RDMCell(35,5, l(:field_status) + ":","LT")
308 319 pdf.SetFontStyle('',9)
309 320 pdf.RDMCell(60,5, issue.status.to_s,"RT")
310 321 pdf.SetFontStyle('B',9)
311 322 pdf.RDMCell(35,5, l(:field_priority) + ":","LT")
312 323 pdf.SetFontStyle('',9)
313 324 pdf.RDMCell(60,5, issue.priority.to_s,"RT")
314 325 pdf.Ln
315 326
316 327 pdf.SetFontStyle('B',9)
317 328 pdf.RDMCell(35,5, l(:field_author) + ":","L")
318 329 pdf.SetFontStyle('',9)
319 330 pdf.RDMCell(60,5, issue.author.to_s,"R")
320 331 pdf.SetFontStyle('B',9)
321 332 pdf.RDMCell(35,5, l(:field_category) + ":","L")
322 333 pdf.SetFontStyle('',9)
323 334 pdf.RDMCell(60,5, issue.category.to_s,"R")
324 335 pdf.Ln
325 336
326 337 pdf.SetFontStyle('B',9)
327 338 pdf.RDMCell(35,5, l(:field_created_on) + ":","L")
328 339 pdf.SetFontStyle('',9)
329 340 pdf.RDMCell(60,5, format_date(issue.created_on),"R")
330 341 pdf.SetFontStyle('B',9)
331 342 pdf.RDMCell(35,5, l(:field_assigned_to) + ":","L")
332 343 pdf.SetFontStyle('',9)
333 344 pdf.RDMCell(60,5, issue.assigned_to.to_s,"R")
334 345 pdf.Ln
335 346
336 347 pdf.SetFontStyle('B',9)
337 348 pdf.RDMCell(35,5, l(:field_updated_on) + ":","LB")
338 349 pdf.SetFontStyle('',9)
339 350 pdf.RDMCell(60,5, format_date(issue.updated_on),"RB")
340 351 pdf.SetFontStyle('B',9)
341 352 pdf.RDMCell(35,5, l(:field_due_date) + ":","LB")
342 353 pdf.SetFontStyle('',9)
343 354 pdf.RDMCell(60,5, format_date(issue.due_date),"RB")
344 355 pdf.Ln
345 356
346 357 for custom_value in issue.custom_field_values
347 358 pdf.SetFontStyle('B',9)
348 359 pdf.RDMCell(35,5, custom_value.custom_field.name + ":","L")
349 360 pdf.SetFontStyle('',9)
350 361 pdf.RDMMultiCell(155,5, (show_value custom_value),"R")
351 362 end
352 363
353 364 y0 = pdf.GetY
354 365
355 366 pdf.SetFontStyle('B',9)
356 367 pdf.RDMCell(35,5, l(:field_subject) + ":","LT")
357 368 pdf.SetFontStyle('',9)
358 369 pdf.RDMMultiCell(155,5, issue.subject,"RT")
359 370 pdf.Line(pdf.GetX, y0, pdf.GetX, pdf.GetY)
360 371
361 372 pdf.SetFontStyle('B',9)
362 373 pdf.RDMCell(35+155, 5, l(:field_description), "LRT", 1)
363 374 pdf.SetFontStyle('',9)
364 375
365 376 # Set resize image scale
366 377 pdf.SetImageScale(1.6)
367 378 pdf.RDMwriteHTMLCell(35+155, 5, 0, 0,
368 379 issue.description.to_s, issue.attachments, "LRB")
369 380
370 381 unless issue.leaf?
371 382 # for CJK
372 383 truncate_length = ( l(:general_pdf_encoding).upcase == "UTF-8" ? 90 : 65 )
373 384
374 385 pdf.SetFontStyle('B',9)
375 386 pdf.RDMCell(35+155,5, l(:label_subtask_plural) + ":", "LTR")
376 387 pdf.Ln
377 388 issue_list(issue.descendants.sort_by(&:lft)) do |child, level|
378 389 buf = truncate("#{child.tracker} # #{child.id}: #{child.subject}",
379 390 :length => truncate_length)
380 391 level = 10 if level >= 10
381 392 pdf.SetFontStyle('',8)
382 393 pdf.RDMCell(35+135,5, (level >=1 ? " " * level : "") + buf, "L")
383 394 pdf.SetFontStyle('B',8)
384 395 pdf.RDMCell(20,5, child.status.to_s, "R")
385 396 pdf.Ln
386 397 end
387 398 end
388 399
389 400 relations = issue.relations.select { |r| r.other_issue(issue).visible? }
390 401 unless relations.empty?
391 402 # for CJK
392 403 truncate_length = ( l(:general_pdf_encoding).upcase == "UTF-8" ? 80 : 60 )
393 404
394 405 pdf.SetFontStyle('B',9)
395 406 pdf.RDMCell(35+155,5, l(:label_related_issues) + ":", "LTR")
396 407 pdf.Ln
397 408 relations.each do |relation|
398 409 buf = ""
399 410 buf += "#{l(relation.label_for(issue))} "
400 411 if relation.delay && relation.delay != 0
401 412 buf += "(#{l('datetime.distance_in_words.x_days', :count => relation.delay)}) "
402 413 end
403 414 if Setting.cross_project_issue_relations?
404 415 buf += "#{relation.other_issue(issue).project} - "
405 416 end
406 417 buf += "#{relation.other_issue(issue).tracker}" +
407 418 " # #{relation.other_issue(issue).id}: #{relation.other_issue(issue).subject}"
408 419 buf = truncate(buf, :length => truncate_length)
409 420 pdf.SetFontStyle('', 8)
410 421 pdf.RDMCell(35+155-50,5, buf, "L")
411 422 pdf.SetFontStyle('B',8)
412 423 pdf.RDMCell(10,5, relation.other_issue(issue).status.to_s, "")
413 424 pdf.RDMCell(20,5, format_date(relation.other_issue(issue).start_date), "")
414 425 pdf.RDMCell(20,5, format_date(relation.other_issue(issue).due_date), "R")
415 426 pdf.Ln
416 427 end
417 428 end
418 429 pdf.RDMCell(190,5, "", "T")
419 430 pdf.Ln
420 431
421 432 if issue.changesets.any? &&
422 433 User.current.allowed_to?(:view_changesets, issue.project)
423 434 pdf.SetFontStyle('B',9)
424 435 pdf.RDMCell(190,5, l(:label_associated_revisions), "B")
425 436 pdf.Ln
426 437 for changeset in issue.changesets
427 438 pdf.SetFontStyle('B',8)
428 439 csstr = "#{l(:label_revision)} #{changeset.format_identifier} - "
429 440 csstr += format_time(changeset.committed_on) + " - " + changeset.author.to_s
430 441 pdf.RDMCell(190, 5, csstr)
431 442 pdf.Ln
432 443 unless changeset.comments.blank?
433 444 pdf.SetFontStyle('',8)
434 445 pdf.RDMwriteHTMLCell(190,5,0,0,
435 446 changeset.comments.to_s, issue.attachments, "")
436 447 end
437 448 pdf.Ln
438 449 end
439 450 end
440 451
441 452 pdf.SetFontStyle('B',9)
442 453 pdf.RDMCell(190,5, l(:label_history), "B")
443 454 pdf.Ln
444 455 for journal in issue.journals.find(
445 456 :all, :include => [:user, :details],
446 457 :order => "#{Journal.table_name}.created_on ASC")
447 458 pdf.SetFontStyle('B',8)
448 459 pdf.RDMCell(190,5,
449 460 format_time(journal.created_on) + " - " + journal.user.name)
450 461 pdf.Ln
451 462 pdf.SetFontStyle('I',8)
452 463 for detail in journal.details
453 464 pdf.RDMMultiCell(190,5, "- " + show_detail(detail, true))
454 465 end
455 466 if journal.notes?
456 467 pdf.Ln unless journal.details.empty?
457 468 pdf.SetFontStyle('',8)
458 469 pdf.RDMwriteHTMLCell(190,5,0,0,
459 470 journal.notes.to_s, issue.attachments, "")
460 471 end
461 472 pdf.Ln
462 473 end
463 474
464 475 if issue.attachments.any?
465 476 pdf.SetFontStyle('B',9)
466 477 pdf.RDMCell(190,5, l(:label_attachment_plural), "B")
467 478 pdf.Ln
468 479 for attachment in issue.attachments
469 480 pdf.SetFontStyle('',8)
470 481 pdf.RDMCell(80,5, attachment.filename)
471 482 pdf.RDMCell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
472 483 pdf.RDMCell(25,5, format_date(attachment.created_on),0,0,"R")
473 484 pdf.RDMCell(65,5, attachment.author.name,0,0,"R")
474 485 pdf.Ln
475 486 end
476 487 end
477 488 pdf.Output
478 489 end
479 490
480 491 # Returns a PDF string of a single wiki page
481 492 def wiki_to_pdf(page, project)
482 493 pdf = ITCPDF.new(current_language)
483 494 pdf.SetTitle("#{project} - #{page.title}")
484 495 pdf.alias_nb_pages
485 496 pdf.footer_date = format_date(Date.today)
486 497 pdf.AddPage
487 498 pdf.SetFontStyle('B',11)
488 499 pdf.RDMMultiCell(190,5,
489 500 "#{project} - #{page.title} - # #{page.content.version}")
490 501 pdf.Ln
491 502 # Set resize image scale
492 503 pdf.SetImageScale(1.6)
493 504 pdf.SetFontStyle('',9)
494 505 pdf.RDMwriteHTMLCell(190,5,0,0,
495 506 page.content.text.to_s, page.attachments, "TLRB")
496 507 if page.attachments.any?
497 508 pdf.Ln
498 509 pdf.SetFontStyle('B',9)
499 510 pdf.RDMCell(190,5, l(:label_attachment_plural), "B")
500 511 pdf.Ln
501 512 for attachment in page.attachments
502 513 pdf.SetFontStyle('',8)
503 514 pdf.RDMCell(80,5, attachment.filename)
504 515 pdf.RDMCell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
505 516 pdf.RDMCell(25,5, format_date(attachment.created_on),0,0,"R")
506 517 pdf.RDMCell(65,5, attachment.author.name,0,0,"R")
507 518 pdf.Ln
508 519 end
509 520 end
510 521 pdf.Output
511 522 end
512 523
513 524 class RDMPdfEncoding
514 525 def self.rdm_from_utf8(txt, encoding)
515 526 txt ||= ''
516 527 txt = Redmine::CodesetUtil.from_utf8(txt, encoding)
517 528 if txt.respond_to?(:force_encoding)
518 529 txt.force_encoding('ASCII-8BIT')
519 530 end
520 531 txt
521 532 end
522 533
523 534 def self.attach(attachments, filename, encoding)
524 535 filename_utf8 = Redmine::CodesetUtil.to_utf8(filename, encoding)
525 536 atta = nil
526 537 if filename_utf8 =~ /^[^\/"]+\.(gif|jpg|jpe|jpeg|png)$/i
527 538 atta = Attachment.latest_attach(attachments, filename_utf8)
528 539 end
529 540 if atta && atta.readable? && atta.visible?
530 541 return atta
531 542 else
532 543 return nil
533 544 end
534 545 end
535 546 end
536 547 end
537 548 end
538 549 end
General Comments 0
You need to be logged in to leave comments. Login now