Skip to content

Commit 499b05d

Browse files
Add user settings key/value DB table (go-gitea#16834)
1 parent a159c31 commit 499b05d

File tree

8 files changed

+198
-2
lines changed

8 files changed

+198
-2
lines changed

cmd/web_https.go

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"code.gitea.io/gitea/modules/graceful"
1414
"code.gitea.io/gitea/modules/log"
1515
"code.gitea.io/gitea/modules/setting"
16+
1617
"github.com/klauspost/cpuid/v2"
1718
)
1819

models/migrations/migrations.go

+2
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,8 @@ var migrations = []Migration{
357357
NewMigration("Add table app_state", addTableAppState),
358358
// v201 -> v202
359359
NewMigration("Drop table remote_version (if exists)", dropTableRemoteVersion),
360+
// v202 -> v203
361+
NewMigration("Create key/value table for user settings", createUserSettingsTable),
360362
}
361363

362364
// GetCurrentDBVersion returns the current db version

models/migrations/v202.go

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright 2021 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package migrations
6+
7+
import (
8+
"fmt"
9+
10+
"xorm.io/xorm"
11+
)
12+
13+
func createUserSettingsTable(x *xorm.Engine) error {
14+
type UserSetting struct {
15+
ID int64 `xorm:"pk autoincr"`
16+
UserID int64 `xorm:"index unique(key_userid)"` // to load all of someone's settings
17+
SettingKey string `xorm:"varchar(255) index unique(key_userid)"` // ensure key is always lowercase
18+
SettingValue string `xorm:"text"`
19+
}
20+
if err := x.Sync2(new(UserSetting)); err != nil {
21+
return fmt.Errorf("sync2: %v", err)
22+
}
23+
return nil
24+
25+
}

models/user.go

+1
Original file line numberDiff line numberDiff line change
@@ -1192,6 +1192,7 @@ func DeleteUser(ctx context.Context, u *User) (err error) {
11921192
&TeamUser{UID: u.ID},
11931193
&Collaboration{UserID: u.ID},
11941194
&Stopwatch{UserID: u.ID},
1195+
&user_model.Setting{UserID: u.ID},
11951196
); err != nil {
11961197
return fmt.Errorf("deleteBeans: %v", err)
11971198
}

models/user/main_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2020 The Gitea Authors. All rights reserved.
1+
// Copyright 2021 The Gitea Authors. All rights reserved.
22
// Use of this source code is governed by a MIT-style
33
// license that can be found in the LICENSE file.
44

models/user/setting.go

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Copyright 2021 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package user
6+
7+
import (
8+
"context"
9+
"fmt"
10+
"strings"
11+
12+
"code.gitea.io/gitea/models/db"
13+
14+
"xorm.io/builder"
15+
)
16+
17+
// Setting is a key value store of user settings
18+
type Setting struct {
19+
ID int64 `xorm:"pk autoincr"`
20+
UserID int64 `xorm:"index unique(key_userid)"` // to load all of someone's settings
21+
SettingKey string `xorm:"varchar(255) index unique(key_userid)"` // ensure key is always lowercase
22+
SettingValue string `xorm:"text"`
23+
}
24+
25+
// TableName sets the table name for the settings struct
26+
func (s *Setting) TableName() string {
27+
return "user_setting"
28+
}
29+
30+
func init() {
31+
db.RegisterModel(new(Setting))
32+
}
33+
34+
// GetSettings returns specific settings from user
35+
func GetSettings(uid int64, keys []string) (map[string]*Setting, error) {
36+
settings := make([]*Setting, 0, len(keys))
37+
if err := db.GetEngine(db.DefaultContext).
38+
Where("user_id=?", uid).
39+
And(builder.In("setting_key", keys)).
40+
Find(&settings); err != nil {
41+
return nil, err
42+
}
43+
settingsMap := make(map[string]*Setting)
44+
for _, s := range settings {
45+
settingsMap[s.SettingKey] = s
46+
}
47+
return settingsMap, nil
48+
}
49+
50+
// GetUserAllSettings returns all settings from user
51+
func GetUserAllSettings(uid int64) (map[string]*Setting, error) {
52+
settings := make([]*Setting, 0, 5)
53+
if err := db.GetEngine(db.DefaultContext).
54+
Where("user_id=?", uid).
55+
Find(&settings); err != nil {
56+
return nil, err
57+
}
58+
settingsMap := make(map[string]*Setting)
59+
for _, s := range settings {
60+
settingsMap[s.SettingKey] = s
61+
}
62+
return settingsMap, nil
63+
}
64+
65+
// DeleteSetting deletes a specific setting for a user
66+
func DeleteSetting(setting *Setting) error {
67+
_, err := db.GetEngine(db.DefaultContext).Delete(setting)
68+
return err
69+
}
70+
71+
// SetSetting updates a users' setting for a specific key
72+
func SetSetting(setting *Setting) error {
73+
if strings.ToLower(setting.SettingKey) != setting.SettingKey {
74+
return fmt.Errorf("setting key should be lowercase")
75+
}
76+
return upsertSettingValue(setting.UserID, setting.SettingKey, setting.SettingValue)
77+
}
78+
79+
func upsertSettingValue(userID int64, key string, value string) error {
80+
return db.WithTx(func(ctx context.Context) error {
81+
e := db.GetEngine(ctx)
82+
83+
// here we use a general method to do a safe upsert for different databases (and most transaction levels)
84+
// 1. try to UPDATE the record and acquire the transaction write lock
85+
// if UPDATE returns non-zero rows are changed, OK, the setting is saved correctly
86+
// if UPDATE returns "0 rows changed", two possibilities: (a) record doesn't exist (b) value is not changed
87+
// 2. do a SELECT to check if the row exists or not (we already have the transaction lock)
88+
// 3. if the row doesn't exist, do an INSERT (we are still protected by the transaction lock, so it's safe)
89+
//
90+
// to optimize the SELECT in step 2, we can use an extra column like `revision=revision+1`
91+
// to make sure the UPDATE always returns a non-zero value for existing (unchanged) records.
92+
93+
res, err := e.Exec("UPDATE user_setting SET setting_value=? WHERE setting_key=? AND user_id=?", value, key, userID)
94+
if err != nil {
95+
return err
96+
}
97+
rows, _ := res.RowsAffected()
98+
if rows > 0 {
99+
// the existing row is updated, so we can return
100+
return nil
101+
}
102+
103+
// in case the value isn't changed, update would return 0 rows changed, so we need this check
104+
has, err := e.Exist(&Setting{UserID: userID, SettingKey: key})
105+
if err != nil {
106+
return err
107+
}
108+
if has {
109+
return nil
110+
}
111+
112+
// if no existing row, insert a new row
113+
_, err = e.Insert(&Setting{UserID: userID, SettingKey: key, SettingValue: value})
114+
return err
115+
})
116+
}

models/user/setting_test.go

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright 2021 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package user
6+
7+
import (
8+
"testing"
9+
10+
"code.gitea.io/gitea/models/unittest"
11+
12+
"github.com/stretchr/testify/assert"
13+
)
14+
15+
func TestSettings(t *testing.T) {
16+
keyName := "test_user_setting"
17+
assert.NoError(t, unittest.PrepareTestDatabase())
18+
19+
newSetting := &Setting{UserID: 99, SettingKey: keyName, SettingValue: "Gitea User Setting Test"}
20+
21+
// create setting
22+
err := SetSetting(newSetting)
23+
assert.NoError(t, err)
24+
// test about saving unchanged values
25+
err = SetSetting(newSetting)
26+
assert.NoError(t, err)
27+
28+
// get specific setting
29+
settings, err := GetSettings(99, []string{keyName})
30+
assert.NoError(t, err)
31+
assert.Len(t, settings, 1)
32+
assert.EqualValues(t, newSetting.SettingValue, settings[keyName].SettingValue)
33+
34+
// updated setting
35+
updatedSetting := &Setting{UserID: 99, SettingKey: keyName, SettingValue: "Updated"}
36+
err = SetSetting(updatedSetting)
37+
assert.NoError(t, err)
38+
39+
// get all settings
40+
settings, err = GetUserAllSettings(99)
41+
assert.NoError(t, err)
42+
assert.Len(t, settings, 1)
43+
assert.EqualValues(t, updatedSetting.SettingValue, settings[updatedSetting.SettingKey].SettingValue)
44+
45+
// delete setting
46+
err = DeleteSetting(&Setting{UserID: 99, SettingKey: keyName})
47+
assert.NoError(t, err)
48+
settings, err = GetUserAllSettings(99)
49+
assert.NoError(t, err)
50+
assert.Len(t, settings, 0)
51+
}

modules/avatar/identicon/identicon_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// license that can be found in the LICENSE file.
44

55
//go:build test_avatar_identicon
6-
// +build test_avatar_identicon
6+
// +build test_avatar_identicon
77

88
package identicon
99

0 commit comments

Comments
 (0)