Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add iCalender export #14

Merged
merged 1 commit into from
Jan 7, 2025
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
6 changes: 5 additions & 1 deletion cmd/create_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const (
CVS Format = iota
Table
JSON
ICS
)

func RunCreateShiftPlan(writer io.Writer, arguments []string) error {
Expand All @@ -35,6 +36,9 @@ func RunCreateShiftPlan(writer io.Writer, arguments []string) error {
JSON: func() apis.Exporter {
return export.NewJSONExporter()
},
ICS: func() apis.Exporter {
return export.NewICSExporter()
},
}

start := new(time.Time)
Expand All @@ -52,7 +56,7 @@ func RunCreateShiftPlan(writer io.Writer, arguments []string) error {
createCommand.Func("start", "(required) start time of the schedule plan", cli.TimeValueVar(start))
createCommand.Func("end", "(required) end time of the schedule plan", cli.TimeValueVar(end))
createCommand.Func("team-file", "(required) path to the file that contain all on-call duties", cli.FilePathVar(teamFilePath))
createCommand.Func("output", "output format. One of (cvs, table, json)", cli.EnumValueVar(enums, &outputFormat))
createCommand.Func("output", "output format. One of (cvs, table, json, ics)", cli.EnumValueVar(enums, &outputFormat))
createCommand.StringVar(primaryRules, "primary-rules", "vacation", "Rule to decide which employee should be on-call for the next shift")
createCommand.StringVar(secondaryRules, "secondary-rules", "vacation", "Rule to decide which employee should be on-call for the next shift")
createCommand.Usage = func() {
Expand Down
76 changes: 76 additions & 0 deletions internal/export/ics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package export

import (
_ "embed"
"errors"
"fmt"
"io"
"text/template"

"github.com/orltom/on-call-schedule/pkg/apis"
)

//go:embed ics.tmpl
var templateICSFile string

var _ apis.Exporter = &TableExporter{}

type event struct {
apis.Shift
UID string
Category string
XColor string
Attendee string
Summary string
Description string
}

type ICSExporter struct {
template *template.Template
}

func NewICSExporter() *ICSExporter {
tmpl, _ := template.New("ical").Parse(templateICSFile)
return &ICSExporter{template: tmpl}
}

func (e *ICSExporter) Write(plan []apis.Shift, writer io.Writer) error {
if plan == nil {
return errors.New("shifts must not be nil")
}

events := make([]event, 0, len(plan))
for idx := range plan {
s := plan[idx]
events = append(events, mapPrimaryEvent(s), mapSecondaryEvent(s))
}

if err := e.template.ExecuteTemplate(writer, "icalCalendar", events); err != nil {
return fmt.Errorf("can not generate ICS: %w", err)
}
return nil
}

func mapPrimaryEvent(shift apis.Shift) event {
return event{
Shift: shift,
UID: "primary-" + shift.Start.Format("20060102"),
Category: "Primary",
XColor: "#FF5733",
Attendee: string(shift.Primary),
Summary: "Primary On-Call: " + string(shift.Primary),
Description: fmt.Sprintf("Primary: %s\\nSecondary: %s", shift.Primary, shift.Secondary),
}
}

func mapSecondaryEvent(shift apis.Shift) event {
return event{
Shift: shift,
UID: "secondary-" + shift.Start.Format("20060102"),
Category: "Secondary",
XColor: "#33FF57",
Attendee: string(shift.Secondary),
Summary: "Secondary On-Call: " + string(shift.Secondary),
Description: fmt.Sprintf("Primary: %s\\nSecondary: %s", shift.Primary, shift.Secondary),
}
}
29 changes: 29 additions & 0 deletions internal/export/ics.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{{- define "icalEvent" }}
BEGIN:VEVENT
UID:{{ .UID }}
DTSTART:{{ .Start.Format "20060102T150405Z" }}
DTEND:{{ .End.Format "20060102T150405Z" }}
SUMMARY:{{ .Summary }}
DESCRIPTION:Primary: {{ .Primary }}\nSecondary: {{ .Secondary }}
CATEGORIES:{{ .Category }}
ATTENDEE:mailto:{{ .Attendee }}
X-COLOR:{{ .XColor }}
BEGIN:VALARM
TRIGGER:-PT8H
ACTION:DISPLAY
DESCRIPTION:Reminder: {{ .Summary }} starts in 8 hours.
END:VALARM
END:VEVENT
{{ end -}}

{{- define "icalCalendar" -}}
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//ocsctl//on-call-shift//EN
CALSCALE:GREGORIAN
{{ range . }}
{{- template "icalEvent" . }}
{{- end }}
END:VCALENDAR

{{- end -}}
95 changes: 95 additions & 0 deletions internal/export/ics_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package export

import (
"bytes"
"testing"

"github.com/orltom/on-call-schedule/pkg/apis"
)

func TestICSExporter_Write(t *testing.T) {
type args struct {
plan []apis.Shift
}
tests := []struct {
name string
args args
wantWriter string
wantErr bool
}{
{
name: "When plan is nil, throw an error",
args: args{
plan: nil,
},
wantWriter: "",
wantErr: true,
},
{
name: "Print ICS result according shift plan",
args: args{
plan: []apis.Shift{
{
Start: date("2024-12-25"),
End: date("2024-12-26"),
Primary: "a",
Secondary: "b",
},
},
},
wantWriter: `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//ocsctl//on-call-shift//EN
CALSCALE:GREGORIAN

BEGIN:VEVENT
UID:primary-20241225
DTSTART:20241225T000000Z
DTEND:20241226T000000Z
SUMMARY:Primary On-Call: a
DESCRIPTION:Primary: a\nSecondary: b
CATEGORIES:Primary
ATTENDEE:mailto:a
X-COLOR:#FF5733
BEGIN:VALARM
TRIGGER:-PT8H
ACTION:DISPLAY
DESCRIPTION:Reminder: Primary On-Call: a starts in 8 hours.
END:VALARM
END:VEVENT

BEGIN:VEVENT
UID:secondary-20241225
DTSTART:20241225T000000Z
DTEND:20241226T000000Z
SUMMARY:Secondary On-Call: b
DESCRIPTION:Primary: a\nSecondary: b
CATEGORIES:Secondary
ATTENDEE:mailto:b
X-COLOR:#33FF57
BEGIN:VALARM
TRIGGER:-PT8H
ACTION:DISPLAY
DESCRIPTION:Reminder: Secondary On-Call: b starts in 8 hours.
END:VALARM
END:VEVENT

END:VCALENDAR`,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := NewICSExporter()
writer := &bytes.Buffer{}
err := e.Write(tt.args.plan, writer)
if (err != nil) != tt.wantErr {
t.Errorf("Write() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotWriter := writer.String(); gotWriter != tt.wantWriter {
t.Errorf("Write() gotWriter = %v, want %v", gotWriter, tt.wantWriter)
}
})
}
}
Loading