Skip to content
This repository was archived by the owner on Jul 12, 2023. It is now read-only.

Commit b440f01

Browse files
authored
Return CSV if requested from realm stats. (#922)
Supports 4 modes: ${SERVER}/realm/stats.csv ${SERVER}/realm/stats.csv?user ${SERVER}/realm/stats.json ${SERVER}/realm/stats.json?user Fixes #916
1 parent 1b54fa9 commit b440f01

File tree

4 files changed

+145
-27
lines changed

4 files changed

+145
-27
lines changed

internal/routes/server.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,9 @@ func Server(
302302
realmSub.Handle("/settings", realmadminController.HandleSettings()).Methods("GET", "POST")
303303
realmSub.Handle("/settings/enable-express", realmadminController.HandleEnableExpress()).Methods("POST")
304304
realmSub.Handle("/settings/disable-express", realmadminController.HandleDisableExpress()).Methods("POST")
305-
realmSub.Handle("/stats", realmadminController.HandleShow()).Methods("GET")
305+
realmSub.Handle("/stats", realmadminController.HandleShow(realmadmin.HTML)).Methods("GET")
306+
realmSub.Handle("/stats.json", realmadminController.HandleShow(realmadmin.JSON)).Methods("GET")
307+
realmSub.Handle("/stats.csv", realmadminController.HandleShow(realmadmin.CSV)).Methods("GET")
306308
realmSub.Handle("/stats/{date}", realmadminController.HandleStats()).Methods("GET")
307309
realmSub.Handle("/events", realmadminController.HandleEvents()).Methods("GET")
308310

pkg/controller/realmadmin/show.go

Lines changed: 115 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package realmadmin
1616

1717
import (
1818
"context"
19+
"encoding/csv"
1920
"net/http"
2021
"strconv"
2122
"time"
@@ -27,46 +28,87 @@ import (
2728

2829
var cacheTimeout = 5 * time.Minute
2930

30-
func (c *Controller) HandleShow() http.Handler {
31+
// ResultType specfies which type of renderer you want.
32+
type ResultType int
33+
34+
const (
35+
HTML ResultType = iota
36+
JSON
37+
CSV
38+
)
39+
40+
// wantUser returns true if we want per-user requests.
41+
func wantUser(r *http.Request) bool {
42+
_, has := r.URL.Query()["user"]
43+
return has
44+
}
45+
46+
// getRealmStats returns the realm stats for a given date range.
47+
func (c *Controller) getRealmStats(ctx context.Context, realm *database.Realm, now, past time.Time) ([]*database.RealmStats, error) {
48+
var stats []*database.RealmStats
49+
cacheKey := &cache.Key{
50+
Namespace: "stats:realm",
51+
Key: strconv.FormatUint(uint64(realm.ID), 10),
52+
}
53+
if err := c.cacher.Fetch(ctx, cacheKey, &stats, cacheTimeout, func() (interface{}, error) {
54+
return realm.Stats(c.db, past, now)
55+
}); err != nil {
56+
return nil, err
57+
}
58+
59+
return stats, nil
60+
}
61+
62+
// getUserStats gets the per-user realm stats for a given date range.
63+
func (c *Controller) getUserStats(ctx context.Context, realm *database.Realm, now, past time.Time) ([]*database.RealmUserStats, error) {
64+
var userStats []*database.RealmUserStats
65+
cacheKey := &cache.Key{
66+
Namespace: "stats:realm:per_user",
67+
Key: strconv.FormatUint(uint64(realm.ID), 10),
68+
}
69+
if err := c.cacher.Fetch(ctx, cacheKey, &userStats, cacheTimeout, func() (interface{}, error) {
70+
return realm.CodesPerUser(c.db, past, now)
71+
}); err != nil {
72+
return nil, err
73+
}
74+
return userStats, nil
75+
}
76+
77+
func (c *Controller) HandleShow(result ResultType) http.Handler {
3178
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
3279
ctx := r.Context()
3380

81+
now := time.Now().UTC()
82+
past := now.Add(-30 * 24 * time.Hour)
83+
3484
realm := controller.RealmFromContext(ctx)
3585
if realm == nil {
3686
controller.MissingRealm(w, r, c.h)
37-
return
3887
}
3988

40-
now := time.Now().UTC()
41-
past := now.Add(-30 * 24 * time.Hour)
42-
43-
// Get and cache the stats for this realm.
44-
var stats []*database.RealmStats
45-
cacheKey := &cache.Key{
46-
Namespace: "stats:realm",
47-
Key: strconv.FormatUint(uint64(realm.ID), 10),
48-
}
49-
if err := c.cacher.Fetch(ctx, cacheKey, &stats, cacheTimeout, func() (interface{}, error) {
50-
return realm.Stats(c.db, past, now)
51-
}); err != nil {
89+
// Get the realm stats.
90+
stats, err := c.getRealmStats(ctx, realm, now, past)
91+
if err != nil {
5292
controller.InternalError(w, r, c.h, err)
53-
return
5493
}
5594

5695
// Also get the per-user stats.
57-
var userStats []*database.RealmUserStats
58-
cacheKey = &cache.Key{
59-
Namespace: "stats:realm:per_user",
60-
Key: strconv.FormatUint(uint64(realm.ID), 10),
61-
}
62-
if err := c.cacher.Fetch(ctx, cacheKey, &userStats, cacheTimeout, func() (interface{}, error) {
63-
return realm.CodesPerUser(c.db, past, now)
64-
}); err != nil {
96+
userStats, err := c.getUserStats(ctx, realm, now, past)
97+
if err != nil {
6598
controller.InternalError(w, r, c.h, err)
66-
return
6799
}
68100

69-
c.renderShow(ctx, w, realm, stats, userStats)
101+
switch result {
102+
case CSV:
103+
err = c.renderCSV(r, w, stats, userStats)
104+
case JSON:
105+
err = c.renderJSON(r, w, stats, userStats)
106+
case HTML:
107+
err = c.renderHTML(ctx, w, realm, stats, userStats)
108+
}
109+
if err != nil {
110+
controller.InternalError(w, r, c.h, err)
111+
}
70112
})
71113
}
72114

@@ -107,12 +149,59 @@ func formatData(userStats []*database.RealmUserStats) ([]string, [][]interface{}
107149
return names, data
108150
}
109151

110-
func (c *Controller) renderShow(ctx context.Context, w http.ResponseWriter, realm *database.Realm, stats []*database.RealmStats, userStats []*database.RealmUserStats) {
152+
func (c *Controller) renderHTML(ctx context.Context, w http.ResponseWriter, realm *database.Realm, stats []*database.RealmStats, userStats []*database.RealmUserStats) error {
111153
names, format := formatData(userStats)
112154
m := controller.TemplateMapFromContext(ctx)
113155
m["user"] = realm
114156
m["stats"] = stats
115157
m["names"] = names
116158
m["userStats"] = format
117159
c.h.RenderHTML(w, "realmadmin/show", m)
160+
161+
return nil
162+
}
163+
164+
// renderCSV renders a CSV response.
165+
func (c *Controller) renderCSV(r *http.Request, w http.ResponseWriter, stats []*database.RealmStats, userStats []*database.RealmUserStats) error {
166+
wr := csv.NewWriter(w)
167+
defer wr.Flush()
168+
169+
// Check if we want the realm stats or the per-user stats. We
170+
// default to realm stats.
171+
if wantUser(r) {
172+
if err := wr.Write(database.RealmUserStatsCSVHeader); err != nil {
173+
return err
174+
}
175+
176+
for _, u := range userStats {
177+
if err := wr.Write(u.CSV()); err != nil {
178+
return err
179+
}
180+
}
181+
} else {
182+
if err := wr.Write(database.RealmStatsCSVHeader); err != nil {
183+
return err
184+
}
185+
186+
for _, s := range stats {
187+
if err := wr.Write(s.CSV()); err != nil {
188+
return err
189+
}
190+
}
191+
}
192+
193+
w.Header().Set("Content-Type", "text/csv")
194+
w.Header().Set("Content-Disposition", "attachment;filename=stats.csv")
195+
return nil
196+
}
197+
198+
// renderJSON renders a JSON response.
199+
func (c *Controller) renderJSON(r *http.Request, w http.ResponseWriter, stats []*database.RealmStats, userStats []*database.RealmUserStats) error {
200+
if wantUser(r) {
201+
c.h.RenderJSON(w, http.StatusOK, userStats)
202+
} else {
203+
c.h.RenderJSON(w, http.StatusOK, stats)
204+
}
205+
w.Header().Set("Content-Disposition", "attachment;filename=stats.json")
206+
return nil
118207
}

pkg/database/realm.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1319,6 +1319,19 @@ type RealmUserStats struct {
13191319
Date time.Time `json:"date"`
13201320
}
13211321

1322+
// RealmUserStatsCSVHeader is a header for CSV stats
1323+
var RealmUserStatsCSVHeader = []string{"User ID", "Name", "Codes Issued", "Date"}
1324+
1325+
// CSV returns a slice of the data from a RealmUserStats for CSV writing.
1326+
func (s *RealmUserStats) CSV() []string {
1327+
return []string{
1328+
fmt.Sprintf("%d", s.UserID),
1329+
s.Name,
1330+
fmt.Sprintf("%d", s.CodesIssued),
1331+
s.Date.Format("2006-01-02"),
1332+
}
1333+
}
1334+
13221335
// CodesPerUser returns a set of UserStats for a given date range.
13231336
func (r *Realm) CodesPerUser(db *Database, start, stop time.Time) ([]*RealmUserStats, error) {
13241337
start = timeutils.UTCMidnight(start)

pkg/database/realm_stats.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package database
1616

1717
import (
18+
"fmt"
1819
"time"
1920
)
2021

@@ -26,6 +27,19 @@ type RealmStats struct {
2627
CodesClaimed uint `gorm:"codes_claimed; default: 0"`
2728
}
2829

30+
// RealmStatsCSVHeader is a header for CSV files for RealmStats.
31+
var RealmStatsCSVHeader = []string{"Date", "Realm ID", "Codes Issued", "Codes Claimed"}
32+
33+
// CSV returns the CSV encoded values for a RealmStats.
34+
func (r *RealmStats) CSV() []string {
35+
return []string{
36+
r.Date.Format("2006-01-02"),
37+
fmt.Sprintf("%d", r.RealmID),
38+
fmt.Sprintf("%d", r.CodesIssued),
39+
fmt.Sprintf("%d", r.CodesClaimed),
40+
}
41+
}
42+
2943
// TableName sets the RealmStats table name
3044
func (RealmStats) TableName() string {
3145
return "realm_stats"

0 commit comments

Comments
 (0)