forked from arp242/goatcounter
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsettings.go
329 lines (299 loc) · 8.44 KB
/
settings.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
// Copyright © 2019 Martin Tournoij – This file is part of GoatCounter and
// published under the terms of a slightly modified EUPL v1.2 license, which can
// be found in the LICENSE file or at https://license.goatcounter.com
package goatcounter
import (
"database/sql/driver"
"fmt"
"strconv"
"strings"
"zgo.at/json"
"zgo.at/tz"
"zgo.at/zdb"
"zgo.at/zstd/zint"
"zgo.at/zstd/zjson"
)
// Settings.Collect values (bitmask)
// Note: also update CollectFlags() method below.
const (
_ zint.Bitflag16 = 1 << iota // Skip 1 so we can pass that from tests as "empty".
CollectReferrer // 2
CollectUserAgent // 4
CollectScreenSize // 8
CollectLocation // 16
CollectLocationRegion // 32
CollectLanguage // 64
)
type (
// SiteSettings contains all the user-configurable settings for a site, with
// the exception of the domain and billing settings.
//
// This is stored as JSON in the database.
SiteSettings struct {
// Global site settings.
Public bool `json:"public"`
AllowCounter bool `json:"allow_counter"`
AllowAdmin bool `json:"allow_admin"`
DataRetention int `json:"data_retention"`
Campaigns zdb.Strings `json:"campaigns"`
IgnoreIPs zdb.Strings `json:"ignore_ips"`
Collect zint.Bitflag16 `json:"collect"`
// User preferences.
TwentyFourHours bool `json:"twenty_four_hours"`
SundayStartsWeek bool `json:"sunday_starts_week"`
DateFormat string `json:"date_format"`
NumberFormat rune `json:"number_format"`
Timezone *tz.Zone `json:"timezone"`
Widgets Widgets `json:"widgets"`
Views Views `json:"views"`
}
// Widgets is a list of widgets to be printed, in order.
Widgets []Widget
Widget map[string]interface{}
widgetSettings map[string]widgetSetting
widgetSetting struct {
Type string
Label string
Help string
Value interface{}
}
// Views for the dashboard; these settings apply to all widget and are
// configurable in the yellow box at the top.
Views []View
View struct {
Name string `json:"name"`
Filter string `json:"filter"`
Daily bool `json:"daily"`
AsText bool `json:"as-text"`
Period string `json:"period"` // "week", "week-cur", or n days: "8"
}
)
// Default widgets for new sites.
//
// This *must* return a list of all configurable widgets; even if it's off by
// default.
//
// As a function to ensure a global map isn't accidentally modified.
func defaultWidgets() Widgets {
s := defaultWidgetSettings()
w := Widgets{}
for _, n := range []string{"pages", "totalpages", "toprefs", "browsers", "systems", "sizes", "locations"} {
w = append(w, map[string]interface{}{"on": true, "name": n, "s": s[n].getMap()})
}
return w
}
// List of all settings for widgets with some data.
func defaultWidgetSettings() map[string]widgetSettings {
return map[string]widgetSettings{
"pages": map[string]widgetSetting{
"limit_pages": widgetSetting{
Type: "number",
Label: "Page size",
Help: "Number of pages to load",
Value: float64(10),
},
"limit_refs": widgetSetting{
Type: "number",
Label: "Referrers page size",
Help: "Number of referrers to load when clicking on a path",
Value: float64(10),
},
},
"totalpages": map[string]widgetSetting{
"align": widgetSetting{
Type: "checkbox",
Label: "Align with pages",
Help: "Add margin to the left so it aligns with pages charts",
Value: false,
},
"no-events": widgetSetting{
Type: "checkbox",
Label: "Exclude events",
Help: "Don't include events in the Totals overview",
Value: false,
},
},
}
}
func (ss SiteSettings) String() string { return string(zjson.MustMarshal(ss)) }
// Value implements the SQL Value function to determine what to store in the DB.
func (ss SiteSettings) Value() (driver.Value, error) { return json.Marshal(ss) }
// Scan converts the data returned from the DB into the struct.
func (ss *SiteSettings) Scan(v interface{}) error {
switch vv := v.(type) {
case []byte:
return json.Unmarshal(vv, ss)
case string:
return json.Unmarshal([]byte(vv), ss)
default:
panic(fmt.Sprintf("unsupported type: %T", v))
}
}
func (ss *SiteSettings) Defaults() {
if ss.DateFormat == "" {
ss.DateFormat = "2 Jan ’06"
}
if ss.NumberFormat == 0 {
ss.NumberFormat = 0x202f
}
if ss.Timezone == nil {
ss.Timezone = tz.UTC
}
if ss.Campaigns == nil {
ss.Campaigns = []string{"utm_campaign", "utm_source", "ref"}
}
if len(ss.Widgets) == 0 {
ss.Widgets = defaultWidgets()
}
if len(ss.Views) == 0 {
ss.Views = Views{{Name: "default", Period: "week"}}
}
if ss.Collect == 0 {
ss.Collect = CollectReferrer | CollectUserAgent | CollectScreenSize | CollectLocation | CollectLocationRegion
}
// Collecting region without country makes no sense.
if ss.Collect.Has(CollectLocationRegion) {
ss.Collect |= CollectLocation
}
}
type CollectFlag struct {
Label, Help string
Flag zint.Bitflag16
}
// CollectFlags returns a list of all flags we know for the Collect settings.
//func (ss SiteSettings) CollectFlags() []zint.Bitflag8 {
func (ss SiteSettings) CollectFlags() []CollectFlag {
return []CollectFlag{
{
Label: "Referrer",
Help: "Referrer header and campaign parameters",
Flag: CollectReferrer,
},
{
Label: "User-Agent",
Help: "Browser and OS from User-Agent",
Flag: CollectUserAgent,
},
{
Label: "Size",
Help: "Screen size",
Flag: CollectScreenSize,
},
{
Label: "Country",
Help: "Country name (i.e. Belgium, Indonesia, etc.)",
Flag: CollectLocation,
},
{
Label: "Region",
Help: "Region (i.e. Texas, Bali, etc.)",
Flag: CollectLocationRegion,
},
// {
// Label: "Language",
// Help: "Supported languages from Accept-Language",
// Flag: CollectLanguage,
// },
}
}
func (s widgetSettings) getMap() map[string]interface{} {
m := make(map[string]interface{})
for k, v := range s {
m[k] = v.Value
}
return m
}
// SetSettings set the setting "setting" for widget "widget" to "value".
//
// The value is converted to the correct type for this setting.
func (w Widget) SetSetting(widget, setting, value string) error {
defW, ok := defaultWidgetSettings()[widget]
if !ok {
return fmt.Errorf("Widget.SetSetting: no such widget %q", widget)
}
def, ok := defW[setting]
if !ok {
return fmt.Errorf("Widget.SetSetting: no such setting %q for widget %q", setting, widget)
}
s, ok := w["s"].(map[string]interface{})
if !ok {
s = make(map[string]interface{})
}
switch def.Type {
case "number":
n, err := strconv.Atoi(value)
if err != nil {
return err
}
s[setting] = float64(n)
case "checkbox":
s[setting] = value == "on"
case "text":
s[setting] = value
}
w["s"] = s
return nil
}
// Get a widget from the list by name.
func (w Widgets) Get(name string) Widget {
for _, v := range w {
if v["name"] == name {
return v
}
}
return nil
}
// GetSettings gets all setting for this widget.
func (w Widgets) GetSettings(name string) widgetSettings {
for _, v := range w {
if v["name"] == name {
def := defaultWidgetSettings()[name]
s, ok := v["s"]
if ok {
// {"limit_pages": 10, "limit_refs": 10}},
ss := s.(map[string]interface{})
for k, v := range ss {
if v != nil {
d := def[k]
d.Value = v
def[k] = d
}
}
}
return def
}
}
return make(widgetSettings)
}
// On reports if this setting should be displayed.
func (w Widgets) On(name string) bool {
ww := w.Get(name)
b, ok := ww["on"].(bool)
if !ok {
return false
}
return b
}
// Get a view for this site by name and returns the view and index.
// Returns -1 if this view doesn't exist.
func (v Views) Get(name string) (View, int) {
for i, vv := range v {
if strings.EqualFold(vv.Name, name) {
return vv, i
}
}
return View{}, -1
}
// Some shortcuts for getting the settings.
func (ss SiteSettings) LimitPages() int {
return int(ss.Widgets.GetSettings("pages")["limit_pages"].Value.(float64))
}
func (ss SiteSettings) LimitRefs() int {
return int(ss.Widgets.GetSettings("pages")["limit_refs"].Value.(float64))
}
func (ss SiteSettings) TotalsAlign() bool {
return ss.Widgets.GetSettings("totalpages")["align"].Value.(bool)
}
func (ss SiteSettings) TotalsNoEvents() bool {
return ss.Widgets.GetSettings("totalpages")["no-events"].Value.(bool)
}