apps/habbits/handlers_export.go 2.0 K raw
1
package main
2
3
import (
4
	"encoding/csv"
5
	"net/http"
6
	"strings"
7
	"time"
8
9
	"github.com/stevedylandev/andromeda/pkg/web"
10
)
11
12
// exportHandler streams records as CSV. With ?habit=<short_id> it exports a
13
// single habit; otherwise every habit's records. Rows are chronological.
14
func (a *App) exportHandler(w http.ResponseWriter, r *http.Request) {
15
	habitShortID := strings.TrimSpace(r.URL.Query().Get("habit"))
16
17
	filename := "habbits-export.csv"
18
	if habitShortID != "" {
19
		habit, err := getHabitByShortID(a.DB, habitShortID)
20
		if err != nil {
21
			a.Log.Error("export get habit", "err", err)
22
			web.RedirectWithError(w, r, "/settings", "Failed to export")
23
			return
24
		}
25
		if habit == nil {
26
			http.NotFound(w, r)
27
			return
28
		}
29
		filename = "habbits-" + slugify(habit.Name) + ".csv"
30
	}
31
32
	records, err := recordsForExport(a.DB, habitShortID)
33
	if err != nil {
34
		a.Log.Error("export records", "err", err)
35
		web.RedirectWithError(w, r, "/settings", "Failed to export")
36
		return
37
	}
38
39
	w.Header().Set("Content-Type", "text/csv; charset=utf-8")
40
	w.Header().Set("Content-Disposition", `attachment; filename="`+filename+`"`)
41
42
	cw := csv.NewWriter(w)
43
	_ = cw.Write([]string{"recorded_at", "habit", "value_type", "unit", "value"})
44
	for _, rec := range records {
45
		_ = cw.Write([]string{
46
			time.Unix(rec.RecordedAt, 0).Local().Format(time.RFC3339),
47
			rec.HabitName,
48
			rec.ValueType,
49
			rec.Unit,
50
			rec.Value,
51
		})
52
	}
53
	cw.Flush()
54
	if err := cw.Error(); err != nil {
55
		a.Log.Error("export csv write", "err", err)
56
	}
57
}
58
59
// slugify makes a filesystem-safe token from a habit name for the download
60
// filename, e.g. "Sleep hours" -> "sleep-hours".
61
func slugify(s string) string {
62
	var b strings.Builder
63
	prevDash := false
64
	for _, r := range strings.ToLower(strings.TrimSpace(s)) {
65
		switch {
66
		case r >= 'a' && r <= 'z', r >= '0' && r <= '9':
67
			b.WriteRune(r)
68
			prevDash = false
69
		default:
70
			if !prevDash && b.Len() > 0 {
71
				b.WriteByte('-')
72
				prevDash = true
73
			}
74
		}
75
	}
76
	out := strings.Trim(b.String(), "-")
77
	if out == "" {
78
		return "habit"
79
	}
80
	return out
81
}