Skip to content

Commit 101e8d3

Browse files
committed
Add functionality to export retrospective as PDF
1 parent cabc9b6 commit 101e8d3

6 files changed

Lines changed: 92 additions & 4 deletions

File tree

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ require (
1919
github.com/bytedance/sonic/loader v0.2.4 // indirect
2020
github.com/cloudwego/base64x v0.1.5 // indirect
2121
github.com/davecgh/go-spew v1.1.1 // indirect
22+
github.com/go-pdf/fpdf v0.9.0 // indirect
2223
github.com/gorilla/context v1.1.2 // indirect
2324
github.com/gorilla/securecookie v1.1.2 // indirect
2425
github.com/gorilla/sessions v1.4.0 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ github.com/go-openapi/spec v0.20.14 h1:7CBlRnw+mtjFGlPDRZmAMnq35cRzI91xj03HVyUi/
3232
github.com/go-openapi/spec v0.20.14/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw=
3333
github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZCE=
3434
github.com/go-openapi/swag v0.22.9/go.mod h1:3/OXnFfnMAwBD099SwYRk7GD3xOrr1iL7d/XNLXVVwE=
35+
github.com/go-pdf/fpdf v0.9.0 h1:PPvSaUuo1iMi9KkaAn90NuKi+P4gwMedWPHhj8YlJQw=
36+
github.com/go-pdf/fpdf v0.9.0/go.mod h1:oO8N111TkmKb9D7VvWGLvLJlaZUQVPM+6V42pp3iV4Y=
3537
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
3638
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
3739
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=

internal/server/server.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -715,8 +715,9 @@ func (ct *Controller) voteAnswer(c *gin.Context) {
715715
// @Accept json
716716
// @Produce json
717717
// @Produce text/markdown
718+
// @Produce application/pdf
718719
// @Param export body types.RetrospectiveExportRequest true "Export Retrospective"
719-
// @Success 200 {object} types.Retrospective "Retrospective Object (JSON) or Markdown file"
720+
// @Success 200 {object} types.Retrospective "Retrospective Object (JSON), Markdown file, or PDF file"
720721
// @Failure 400 {string} string "Invalid input"
721722
// @Failure 404 {string} string "Not Found"
722723
// @Failure 500 {string} string "Internal error"
@@ -751,11 +752,20 @@ func (ct *Controller) exportRetrospective(c *gin.Context) {
751752
filename := fmt.Sprintf("retrospective-%s", strings.ReplaceAll(retro.Name, " ", "_"))
752753
switch input.ExportType {
753754
case types.ExportTypeMarkdown:
754-
755755
markdown := ct.service.ConvertRetrospectiveToMarkdown(c, retro)
756756
c.Header("Content-Type", "text/markdown")
757757
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.md\"", filename))
758758
c.String(http.StatusOK, markdown)
759+
case types.ExportTypePDF:
760+
pdfBytes, err := ct.service.ConvertRetrospectiveToPDF(c, retro)
761+
if err != nil {
762+
ct.logger.Error("error converting retrospective to PDF", zap.Error(err))
763+
c.JSON(http.StatusInternalServerError, gin.H{"error": "error generating PDF"})
764+
return
765+
}
766+
c.Header("Content-Type", "application/pdf")
767+
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.pdf\"", filename))
768+
c.Data(http.StatusOK, "application/pdf", pdfBytes)
759769
default:
760770
c.Header("Content-Type", "application/json")
761771
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.json\"", filename))

internal/service/convert.go

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
package service
22

33
import (
4-
"api/types"
4+
"bytes"
55
"context"
66
"fmt"
77
"sort"
88
"strings"
9+
10+
"api/types"
11+
12+
"github.com/go-pdf/fpdf"
913
)
1014

1115
func (s *Service) ConvertRetrospectiveToMarkdown(ctx context.Context, retro *types.Retrospective) string {
@@ -44,3 +48,73 @@ func (s *Service) ConvertRetrospectiveToMarkdown(ctx context.Context, retro *typ
4448

4549
return sb.String()
4650
}
51+
52+
func (s *Service) ConvertRetrospectiveToPDF(ctx context.Context, retro *types.Retrospective) ([]byte, error) {
53+
pdf := fpdf.New("P", "mm", "A4", "")
54+
pdf.SetMargins(20, 20, 20)
55+
pdf.AddPage()
56+
57+
// Title
58+
pdf.SetFont("Arial", "B", 24)
59+
pdf.Cell(0, 12, "Simple Retro")
60+
pdf.Ln(16)
61+
62+
// Subtitle (retrospective name)
63+
pdf.SetFont("Arial", "B", 18)
64+
pdf.Cell(0, 10, retro.Name)
65+
pdf.Ln(12)
66+
67+
// Description
68+
if retro.Description != "" {
69+
pdf.SetFont("Arial", "", 12)
70+
pdf.MultiCell(0, 6, retro.Description, "", "", false)
71+
pdf.Ln(4)
72+
}
73+
74+
// Created at
75+
pdf.SetFont("Arial", "I", 10)
76+
pdf.SetTextColor(128, 128, 128)
77+
pdf.Cell(0, 6, "Created on "+retro.CreatedAt.Format("January 2, 2006 at 3:04 PM"))
78+
pdf.Ln(10)
79+
pdf.SetTextColor(0, 0, 0)
80+
81+
// Separator line
82+
pdf.Line(20, pdf.GetY(), 190, pdf.GetY())
83+
pdf.Ln(8)
84+
85+
// Questions and answers
86+
for _, question := range retro.Questions {
87+
// Question title
88+
pdf.SetFont("Arial", "B", 14)
89+
pdf.MultiCell(0, 8, question.Text, "", "", false)
90+
pdf.Ln(4)
91+
92+
// Sort answers by position
93+
answers := make([]types.Answer, len(question.Answers))
94+
copy(answers, question.Answers)
95+
sort.Slice(answers, func(i, j int) bool {
96+
return answers[i].Position < answers[j].Position
97+
})
98+
99+
pdf.SetFont("Arial", "", 11)
100+
for _, answer := range answers {
101+
var text string
102+
if answer.Votes > 0 {
103+
text = fmt.Sprintf(" - %s (%d votes)", answer.Text, answer.Votes)
104+
} else {
105+
text = fmt.Sprintf(" - %s", answer.Text)
106+
}
107+
pdf.MultiCell(0, 6, text, "", "", false)
108+
pdf.Ln(1)
109+
}
110+
pdf.Ln(6)
111+
}
112+
113+
var buf bytes.Buffer
114+
err := pdf.Output(&buf)
115+
if err != nil {
116+
return nil, err
117+
}
118+
119+
return buf.Bytes(), nil
120+
}

types/retrospective.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const (
1515

1616
ExportTypeJSON ExportType = "JSON"
1717
ExportTypeMarkdown ExportType = "MARKDOWN"
18+
ExportTypePDF ExportType = "PDF"
1819
)
1920

2021
type Retrospective struct {

types/validations.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ func (r RetrospectiveExportRequest) Validate() error {
121121
}
122122

123123
switch r.ExportType {
124-
case ExportTypeJSON, ExportTypeMarkdown:
124+
case ExportTypeJSON, ExportTypeMarkdown, ExportTypePDF:
125125
return nil
126126
default:
127127
return fmt.Errorf("invalid export type")

0 commit comments

Comments
 (0)