Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions internal/schedule/schedule.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ type ScheduleParams struct {
fx.In
Service *service.Service
Config *config.Config
Logger *zap.Logger
Lifecycle fx.Lifecycle
}

func New(p ScheduleParams) *Schedule {
s := &Schedule{
service: p.Service,
config: p.Config,
logger: p.Logger,
stopCh: make(chan struct{}),
}

Expand Down
57 changes: 57 additions & 0 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"database/sql"
"fmt"
"net/http"
"strings"

"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
Expand Down Expand Up @@ -707,6 +708,61 @@ func (ct *Controller) voteAnswer(c *gin.Context) {

}

// exportRetrospective godoc
//
// @Summary Export Retrospective
// @Tags Retrospective
// @Accept json
// @Produce json
// @Produce text/markdown
// @Param export body types.RetrospectiveExportRequest true "Export Retrospective"
// @Success 200 {object} types.Retrospective "Retrospective Object (JSON) or Markdown file"
// @Failure 400 {string} string "Invalid input"
// @Failure 404 {string} string "Not Found"
// @Failure 500 {string} string "Internal error"
// @Router /retrospective/export [post]
func (ct *Controller) exportRetrospective(c *gin.Context) {
var input types.RetrospectiveExportRequest
if err := c.BindJSON(&input); err != nil {
ct.logger.Error("error parsing body content", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid body content"})
return
}

if err := input.Validate(); err != nil {
ct.logger.Error("invalid input", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

retro, err := ct.service.GetRetrospective(c, input.RetrospectiveID)
if err == sql.ErrNoRows {
ct.logger.Error("retrospective not found", zap.String("id", input.RetrospectiveID.String()))
c.JSON(http.StatusNotFound, gin.H{"error": "restrospective not found"})
return
}

if err != nil {
ct.logger.Error("error getting retrospective", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
return
}

filename := fmt.Sprintf("retrospective-%s", strings.ReplaceAll(retro.Name, " ", "_"))
switch input.ExportType {
case types.ExportTypeMarkdown:

markdown := ct.service.ConvertRetrospectiveToMarkdown(c, retro)
c.Header("Content-Type", "text/markdown")
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.md\"", filename))
c.String(http.StatusOK, markdown)
default:
c.Header("Content-Type", "application/json")
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.json\"", filename))
c.JSON(http.StatusOK, retro)
}
}

// @license.name MIT
// @license.url https://github.com/simple-retro/api/blob/master/LICENSE
func (ct *Controller) Start() {
Expand Down Expand Up @@ -739,6 +795,7 @@ func (ct *Controller) Start() {
api.GET("/retrospective/:id", ct.getRetrospective)
api.PATCH("/retrospective/:id", ct.updateRetrospective)
api.DELETE("/retrospective/:id", ct.deleteRetrospective)
api.POST("/retrospective/export", ct.exportRetrospective)
api.GET("/hello/:id", ct.subscribeChanges)
api.GET("/limits", ct.getLimits)

Expand Down
46 changes: 46 additions & 0 deletions internal/service/convert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package service

import (
"api/types"
"context"
"fmt"
"sort"
"strings"
)

func (s *Service) ConvertRetrospectiveToMarkdown(ctx context.Context, retro *types.Retrospective) string {
var sb strings.Builder

sb.WriteString("# Simple Retro\n\n")

sb.WriteString("## " + retro.Name + "\n\n")

if retro.Description != "" {
sb.WriteString(retro.Description + "\n\n")
}

sb.WriteString("*Created on " + retro.CreatedAt.Format("January 2, 2006 at 3:04 PM") + "*\n\n")

sb.WriteString("---\n\n")

for _, question := range retro.Questions {
sb.WriteString("### " + question.Text + "\n\n")

answers := make([]types.Answer, len(question.Answers))
copy(answers, question.Answers)
sort.Slice(answers, func(i, j int) bool {
return answers[i].Position < answers[j].Position
})

for _, answer := range answers {
if answer.Votes > 0 {
sb.WriteString(fmt.Sprintf("- %s (%d Up votes)\n", answer.Text, answer.Votes))
} else {
sb.WriteString("- " + answer.Text + "\n")
}
}
sb.WriteString("\n")
}

return sb.String()
}
9 changes: 9 additions & 0 deletions types/retrospective.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ import (
)

type VoteAction string
type ExportType string

const (
VoteAdd VoteAction = "ADD_VOTE"
VoteRemove VoteAction = "REMOVE_VOTE"

ExportTypeJSON ExportType = "JSON"
ExportTypeMarkdown ExportType = "MARKDOWN"
)

type Retrospective struct {
Expand Down Expand Up @@ -55,6 +59,11 @@ type AnswerVoteRequest struct {
Action VoteAction `json:"action"`
}

type RetrospectiveExportRequest struct {
RetrospectiveID uuid.UUID `json:"retrospective_id"`
ExportType ExportType `json:"export_type"`
}

func (v VoteAction) String() string {
return string(v)
}
24 changes: 23 additions & 1 deletion types/validations.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package types

import "fmt"
import (
"fmt"

"github.com/google/uuid"
)

const (
NAME_LIMIT = 100
Expand Down Expand Up @@ -106,3 +110,21 @@ func (a *AnswerVoteRequest) Validate() error {
return fmt.Errorf("invalid vote action")
}
}

func (r RetrospectiveExportRequest) Validate() error {
if r.RetrospectiveID.String() == "" {
return fmt.Errorf("retrospective id cannot be empty")
}

if r.RetrospectiveID == uuid.Nil {
return fmt.Errorf("retrospective id cannot be nil")
}

switch r.ExportType {
case ExportTypeJSON, ExportTypeMarkdown:
return nil
default:
return fmt.Errorf("invalid export type")
}

}