Add system setting table with cache and also add cache supports for user setting (#18058)
parent
5d3dbffa15
commit
f860a6d2e4
@ -1,117 +0,0 @@ |
|||||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a MIT-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package admin_test |
|
||||||
|
|
||||||
import ( |
|
||||||
"testing" |
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/admin" |
|
||||||
"code.gitea.io/gitea/models/db" |
|
||||||
"code.gitea.io/gitea/models/unittest" |
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert" |
|
||||||
) |
|
||||||
|
|
||||||
func TestNotice_TrStr(t *testing.T) { |
|
||||||
notice := &admin.Notice{ |
|
||||||
Type: admin.NoticeRepository, |
|
||||||
Description: "test description", |
|
||||||
} |
|
||||||
assert.Equal(t, "admin.notices.type_1", notice.TrStr()) |
|
||||||
} |
|
||||||
|
|
||||||
func TestCreateNotice(t *testing.T) { |
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase()) |
|
||||||
|
|
||||||
noticeBean := &admin.Notice{ |
|
||||||
Type: admin.NoticeRepository, |
|
||||||
Description: "test description", |
|
||||||
} |
|
||||||
unittest.AssertNotExistsBean(t, noticeBean) |
|
||||||
assert.NoError(t, admin.CreateNotice(db.DefaultContext, noticeBean.Type, noticeBean.Description)) |
|
||||||
unittest.AssertExistsAndLoadBean(t, noticeBean) |
|
||||||
} |
|
||||||
|
|
||||||
func TestCreateRepositoryNotice(t *testing.T) { |
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase()) |
|
||||||
|
|
||||||
noticeBean := &admin.Notice{ |
|
||||||
Type: admin.NoticeRepository, |
|
||||||
Description: "test description", |
|
||||||
} |
|
||||||
unittest.AssertNotExistsBean(t, noticeBean) |
|
||||||
assert.NoError(t, admin.CreateRepositoryNotice(noticeBean.Description)) |
|
||||||
unittest.AssertExistsAndLoadBean(t, noticeBean) |
|
||||||
} |
|
||||||
|
|
||||||
// TODO TestRemoveAllWithNotice
|
|
||||||
|
|
||||||
func TestCountNotices(t *testing.T) { |
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase()) |
|
||||||
assert.Equal(t, int64(3), admin.CountNotices()) |
|
||||||
} |
|
||||||
|
|
||||||
func TestNotices(t *testing.T) { |
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase()) |
|
||||||
|
|
||||||
notices, err := admin.Notices(1, 2) |
|
||||||
assert.NoError(t, err) |
|
||||||
if assert.Len(t, notices, 2) { |
|
||||||
assert.Equal(t, int64(3), notices[0].ID) |
|
||||||
assert.Equal(t, int64(2), notices[1].ID) |
|
||||||
} |
|
||||||
|
|
||||||
notices, err = admin.Notices(2, 2) |
|
||||||
assert.NoError(t, err) |
|
||||||
if assert.Len(t, notices, 1) { |
|
||||||
assert.Equal(t, int64(1), notices[0].ID) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func TestDeleteNotice(t *testing.T) { |
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase()) |
|
||||||
|
|
||||||
unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 3}) |
|
||||||
assert.NoError(t, admin.DeleteNotice(3)) |
|
||||||
unittest.AssertNotExistsBean(t, &admin.Notice{ID: 3}) |
|
||||||
} |
|
||||||
|
|
||||||
func TestDeleteNotices(t *testing.T) { |
|
||||||
// delete a non-empty range
|
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase()) |
|
||||||
|
|
||||||
unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 1}) |
|
||||||
unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 2}) |
|
||||||
unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 3}) |
|
||||||
assert.NoError(t, admin.DeleteNotices(1, 2)) |
|
||||||
unittest.AssertNotExistsBean(t, &admin.Notice{ID: 1}) |
|
||||||
unittest.AssertNotExistsBean(t, &admin.Notice{ID: 2}) |
|
||||||
unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 3}) |
|
||||||
} |
|
||||||
|
|
||||||
func TestDeleteNotices2(t *testing.T) { |
|
||||||
// delete an empty range
|
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase()) |
|
||||||
|
|
||||||
unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 1}) |
|
||||||
unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 2}) |
|
||||||
unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 3}) |
|
||||||
assert.NoError(t, admin.DeleteNotices(3, 2)) |
|
||||||
unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 1}) |
|
||||||
unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 2}) |
|
||||||
unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 3}) |
|
||||||
} |
|
||||||
|
|
||||||
func TestDeleteNoticesByIDs(t *testing.T) { |
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase()) |
|
||||||
|
|
||||||
unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 1}) |
|
||||||
unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 2}) |
|
||||||
unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 3}) |
|
||||||
assert.NoError(t, admin.DeleteNoticesByIDs([]int64{1, 3})) |
|
||||||
unittest.AssertNotExistsBean(t, &admin.Notice{ID: 1}) |
|
||||||
unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 2}) |
|
||||||
unittest.AssertNotExistsBean(t, &admin.Notice{ID: 3}) |
|
||||||
} |
|
@ -0,0 +1,15 @@ |
|||||||
|
- |
||||||
|
id: 1 |
||||||
|
setting_key: 'disable_gravatar' |
||||||
|
setting_value: 'false' |
||||||
|
version: 1 |
||||||
|
created: 1653533198 |
||||||
|
updated: 1653533198 |
||||||
|
|
||||||
|
- |
||||||
|
id: 2 |
||||||
|
setting_key: 'enable_federated_avatar' |
||||||
|
setting_value: 'false' |
||||||
|
version: 1 |
||||||
|
created: 1653533198 |
||||||
|
updated: 1653533198 |
@ -0,0 +1,64 @@ |
|||||||
|
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package migrations |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"strconv" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/setting" |
||||||
|
"code.gitea.io/gitea/modules/timeutil" |
||||||
|
|
||||||
|
"xorm.io/xorm" |
||||||
|
) |
||||||
|
|
||||||
|
type SystemSetting struct { |
||||||
|
ID int64 `xorm:"pk autoincr"` |
||||||
|
SettingKey string `xorm:"varchar(255) unique"` // ensure key is always lowercase
|
||||||
|
SettingValue string `xorm:"text"` |
||||||
|
Version int `xorm:"version"` // prevent to override
|
||||||
|
Created timeutil.TimeStamp `xorm:"created"` |
||||||
|
Updated timeutil.TimeStamp `xorm:"updated"` |
||||||
|
} |
||||||
|
|
||||||
|
func insertSettingsIfNotExist(x *xorm.Engine, sysSettings []*SystemSetting) error { |
||||||
|
sess := x.NewSession() |
||||||
|
defer sess.Close() |
||||||
|
if err := sess.Begin(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
for _, setting := range sysSettings { |
||||||
|
exist, err := sess.Table("system_setting").Where("setting_key=?", setting.SettingKey).Exist() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if !exist { |
||||||
|
if _, err := sess.Insert(setting); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return sess.Commit() |
||||||
|
} |
||||||
|
|
||||||
|
func createSystemSettingsTable(x *xorm.Engine) error { |
||||||
|
if err := x.Sync2(new(SystemSetting)); err != nil { |
||||||
|
return fmt.Errorf("sync2: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
// migrate xx to database
|
||||||
|
sysSettings := []*SystemSetting{ |
||||||
|
{ |
||||||
|
SettingKey: "picture.disable_gravatar", |
||||||
|
SettingValue: strconv.FormatBool(setting.DisableGravatar), |
||||||
|
}, |
||||||
|
{ |
||||||
|
SettingKey: "picture.enable_federated_avatar", |
||||||
|
SettingValue: strconv.FormatBool(setting.EnableFederatedAvatar), |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
return insertSettingsIfNotExist(x, sysSettings) |
||||||
|
} |
@ -0,0 +1,21 @@ |
|||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package system_test |
||||||
|
|
||||||
|
import ( |
||||||
|
"path/filepath" |
||||||
|
"testing" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/unittest" |
||||||
|
|
||||||
|
_ "code.gitea.io/gitea/models" // register models
|
||||||
|
_ "code.gitea.io/gitea/models/system" // register models of system
|
||||||
|
) |
||||||
|
|
||||||
|
func TestMain(m *testing.M) { |
||||||
|
unittest.MainTest(m, &unittest.TestOptions{ |
||||||
|
GiteaRootPath: filepath.Join("..", ".."), |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,117 @@ |
|||||||
|
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package system_test |
||||||
|
|
||||||
|
import ( |
||||||
|
"testing" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db" |
||||||
|
"code.gitea.io/gitea/models/system" |
||||||
|
"code.gitea.io/gitea/models/unittest" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
func TestNotice_TrStr(t *testing.T) { |
||||||
|
notice := &system.Notice{ |
||||||
|
Type: system.NoticeRepository, |
||||||
|
Description: "test description", |
||||||
|
} |
||||||
|
assert.Equal(t, "admin.notices.type_1", notice.TrStr()) |
||||||
|
} |
||||||
|
|
||||||
|
func TestCreateNotice(t *testing.T) { |
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase()) |
||||||
|
|
||||||
|
noticeBean := &system.Notice{ |
||||||
|
Type: system.NoticeRepository, |
||||||
|
Description: "test description", |
||||||
|
} |
||||||
|
unittest.AssertNotExistsBean(t, noticeBean) |
||||||
|
assert.NoError(t, system.CreateNotice(db.DefaultContext, noticeBean.Type, noticeBean.Description)) |
||||||
|
unittest.AssertExistsAndLoadBean(t, noticeBean) |
||||||
|
} |
||||||
|
|
||||||
|
func TestCreateRepositoryNotice(t *testing.T) { |
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase()) |
||||||
|
|
||||||
|
noticeBean := &system.Notice{ |
||||||
|
Type: system.NoticeRepository, |
||||||
|
Description: "test description", |
||||||
|
} |
||||||
|
unittest.AssertNotExistsBean(t, noticeBean) |
||||||
|
assert.NoError(t, system.CreateRepositoryNotice(noticeBean.Description)) |
||||||
|
unittest.AssertExistsAndLoadBean(t, noticeBean) |
||||||
|
} |
||||||
|
|
||||||
|
// TODO TestRemoveAllWithNotice
|
||||||
|
|
||||||
|
func TestCountNotices(t *testing.T) { |
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase()) |
||||||
|
assert.Equal(t, int64(3), system.CountNotices()) |
||||||
|
} |
||||||
|
|
||||||
|
func TestNotices(t *testing.T) { |
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase()) |
||||||
|
|
||||||
|
notices, err := system.Notices(1, 2) |
||||||
|
assert.NoError(t, err) |
||||||
|
if assert.Len(t, notices, 2) { |
||||||
|
assert.Equal(t, int64(3), notices[0].ID) |
||||||
|
assert.Equal(t, int64(2), notices[1].ID) |
||||||
|
} |
||||||
|
|
||||||
|
notices, err = system.Notices(2, 2) |
||||||
|
assert.NoError(t, err) |
||||||
|
if assert.Len(t, notices, 1) { |
||||||
|
assert.Equal(t, int64(1), notices[0].ID) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestDeleteNotice(t *testing.T) { |
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase()) |
||||||
|
|
||||||
|
unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 3}) |
||||||
|
assert.NoError(t, system.DeleteNotice(3)) |
||||||
|
unittest.AssertNotExistsBean(t, &system.Notice{ID: 3}) |
||||||
|
} |
||||||
|
|
||||||
|
func TestDeleteNotices(t *testing.T) { |
||||||
|
// delete a non-empty range
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase()) |
||||||
|
|
||||||
|
unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 1}) |
||||||
|
unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 2}) |
||||||
|
unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 3}) |
||||||
|
assert.NoError(t, system.DeleteNotices(1, 2)) |
||||||
|
unittest.AssertNotExistsBean(t, &system.Notice{ID: 1}) |
||||||
|
unittest.AssertNotExistsBean(t, &system.Notice{ID: 2}) |
||||||
|
unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 3}) |
||||||
|
} |
||||||
|
|
||||||
|
func TestDeleteNotices2(t *testing.T) { |
||||||
|
// delete an empty range
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase()) |
||||||
|
|
||||||
|
unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 1}) |
||||||
|
unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 2}) |
||||||
|
unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 3}) |
||||||
|
assert.NoError(t, system.DeleteNotices(3, 2)) |
||||||
|
unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 1}) |
||||||
|
unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 2}) |
||||||
|
unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 3}) |
||||||
|
} |
||||||
|
|
||||||
|
func TestDeleteNoticesByIDs(t *testing.T) { |
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase()) |
||||||
|
|
||||||
|
unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 1}) |
||||||
|
unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 2}) |
||||||
|
unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 3}) |
||||||
|
assert.NoError(t, system.DeleteNoticesByIDs([]int64{1, 3})) |
||||||
|
unittest.AssertNotExistsBean(t, &system.Notice{ID: 1}) |
||||||
|
unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 2}) |
||||||
|
unittest.AssertNotExistsBean(t, &system.Notice{ID: 3}) |
||||||
|
} |
@ -0,0 +1,261 @@ |
|||||||
|
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package system |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"fmt" |
||||||
|
"net/url" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db" |
||||||
|
"code.gitea.io/gitea/modules/setting" |
||||||
|
"code.gitea.io/gitea/modules/timeutil" |
||||||
|
|
||||||
|
"strk.kbt.io/projects/go/libravatar" |
||||||
|
"xorm.io/builder" |
||||||
|
) |
||||||
|
|
||||||
|
// Setting is a key value store of user settings
|
||||||
|
type Setting struct { |
||||||
|
ID int64 `xorm:"pk autoincr"` |
||||||
|
SettingKey string `xorm:"varchar(255) unique"` // ensure key is always lowercase
|
||||||
|
SettingValue string `xorm:"text"` |
||||||
|
Version int `xorm:"version"` // prevent to override
|
||||||
|
Created timeutil.TimeStamp `xorm:"created"` |
||||||
|
Updated timeutil.TimeStamp `xorm:"updated"` |
||||||
|
} |
||||||
|
|
||||||
|
// TableName sets the table name for the settings struct
|
||||||
|
func (s *Setting) TableName() string { |
||||||
|
return "system_setting" |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Setting) GetValueBool() bool { |
||||||
|
b, _ := strconv.ParseBool(s.SettingValue) |
||||||
|
return b |
||||||
|
} |
||||||
|
|
||||||
|
func init() { |
||||||
|
db.RegisterModel(new(Setting)) |
||||||
|
} |
||||||
|
|
||||||
|
// ErrSettingIsNotExist represents an error that a setting is not exist with special key
|
||||||
|
type ErrSettingIsNotExist struct { |
||||||
|
Key string |
||||||
|
} |
||||||
|
|
||||||
|
// Error implements error
|
||||||
|
func (err ErrSettingIsNotExist) Error() string { |
||||||
|
return fmt.Sprintf("System setting[%s] is not exist", err.Key) |
||||||
|
} |
||||||
|
|
||||||
|
// IsErrSettingIsNotExist return true if err is ErrSettingIsNotExist
|
||||||
|
func IsErrSettingIsNotExist(err error) bool { |
||||||
|
_, ok := err.(ErrSettingIsNotExist) |
||||||
|
return ok |
||||||
|
} |
||||||
|
|
||||||
|
// ErrDataExpired represents an error that update a record which has been updated by another thread
|
||||||
|
type ErrDataExpired struct { |
||||||
|
Key string |
||||||
|
} |
||||||
|
|
||||||
|
// Error implements error
|
||||||
|
func (err ErrDataExpired) Error() string { |
||||||
|
return fmt.Sprintf("System setting[%s] has been updated by another thread", err.Key) |
||||||
|
} |
||||||
|
|
||||||
|
// IsErrDataExpired return true if err is ErrDataExpired
|
||||||
|
func IsErrDataExpired(err error) bool { |
||||||
|
_, ok := err.(ErrDataExpired) |
||||||
|
return ok |
||||||
|
} |
||||||
|
|
||||||
|
// GetSetting returns specific setting
|
||||||
|
func GetSetting(key string) (*Setting, error) { |
||||||
|
v, err := GetSettings([]string{key}) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if len(v) == 0 { |
||||||
|
return nil, ErrSettingIsNotExist{key} |
||||||
|
} |
||||||
|
return v[key], nil |
||||||
|
} |
||||||
|
|
||||||
|
// GetSettings returns specific settings
|
||||||
|
func GetSettings(keys []string) (map[string]*Setting, error) { |
||||||
|
for i := 0; i < len(keys); i++ { |
||||||
|
keys[i] = strings.ToLower(keys[i]) |
||||||
|
} |
||||||
|
settings := make([]*Setting, 0, len(keys)) |
||||||
|
if err := db.GetEngine(db.DefaultContext). |
||||||
|
Where(builder.In("setting_key", keys)). |
||||||
|
Find(&settings); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
settingsMap := make(map[string]*Setting) |
||||||
|
for _, s := range settings { |
||||||
|
settingsMap[s.SettingKey] = s |
||||||
|
} |
||||||
|
return settingsMap, nil |
||||||
|
} |
||||||
|
|
||||||
|
type AllSettings map[string]*Setting |
||||||
|
|
||||||
|
func (settings AllSettings) Get(key string) Setting { |
||||||
|
if v, ok := settings[key]; ok { |
||||||
|
return *v |
||||||
|
} |
||||||
|
return Setting{} |
||||||
|
} |
||||||
|
|
||||||
|
func (settings AllSettings) GetBool(key string) bool { |
||||||
|
b, _ := strconv.ParseBool(settings.Get(key).SettingValue) |
||||||
|
return b |
||||||
|
} |
||||||
|
|
||||||
|
func (settings AllSettings) GetVersion(key string) int { |
||||||
|
return settings.Get(key).Version |
||||||
|
} |
||||||
|
|
||||||
|
// GetAllSettings returns all settings from user
|
||||||
|
func GetAllSettings() (AllSettings, error) { |
||||||
|
settings := make([]*Setting, 0, 5) |
||||||
|
if err := db.GetEngine(db.DefaultContext). |
||||||
|
Find(&settings); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
settingsMap := make(map[string]*Setting) |
||||||
|
for _, s := range settings { |
||||||
|
settingsMap[s.SettingKey] = s |
||||||
|
} |
||||||
|
return settingsMap, nil |
||||||
|
} |
||||||
|
|
||||||
|
// DeleteSetting deletes a specific setting for a user
|
||||||
|
func DeleteSetting(setting *Setting) error { |
||||||
|
_, err := db.GetEngine(db.DefaultContext).Delete(setting) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
func SetSettingNoVersion(key, value string) error { |
||||||
|
s, err := GetSetting(key) |
||||||
|
if IsErrSettingIsNotExist(err) { |
||||||
|
return SetSetting(&Setting{ |
||||||
|
SettingKey: key, |
||||||
|
SettingValue: value, |
||||||
|
}) |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
s.SettingValue = value |
||||||
|
return SetSetting(s) |
||||||
|
} |
||||||
|
|
||||||
|
// SetSetting updates a users' setting for a specific key
|
||||||
|
func SetSetting(setting *Setting) error { |
||||||
|
if err := upsertSettingValue(strings.ToLower(setting.SettingKey), setting.SettingValue, setting.Version); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
setting.Version++ |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func upsertSettingValue(key, value string, version int) error { |
||||||
|
return db.WithTx(func(ctx context.Context) error { |
||||||
|
e := db.GetEngine(ctx) |
||||||
|
|
||||||
|
// here we use a general method to do a safe upsert for different databases (and most transaction levels)
|
||||||
|
// 1. try to UPDATE the record and acquire the transaction write lock
|
||||||
|
// if UPDATE returns non-zero rows are changed, OK, the setting is saved correctly
|
||||||
|
// if UPDATE returns "0 rows changed", two possibilities: (a) record doesn't exist (b) value is not changed
|
||||||
|
// 2. do a SELECT to check if the row exists or not (we already have the transaction lock)
|
||||||
|
// 3. if the row doesn't exist, do an INSERT (we are still protected by the transaction lock, so it's safe)
|
||||||
|
//
|
||||||
|
// to optimize the SELECT in step 2, we can use an extra column like `revision=revision+1`
|
||||||
|
// to make sure the UPDATE always returns a non-zero value for existing (unchanged) records.
|
||||||
|
|
||||||
|
res, err := e.Exec("UPDATE system_setting SET setting_value=?, version = version+1 WHERE setting_key=? AND version=?", value, key, version) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
rows, _ := res.RowsAffected() |
||||||
|
if rows > 0 { |
||||||
|
// the existing row is updated, so we can return
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// in case the value isn't changed, update would return 0 rows changed, so we need this check
|
||||||
|
has, err := e.Exist(&Setting{SettingKey: key}) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if has { |
||||||
|
return ErrDataExpired{Key: key} |
||||||
|
} |
||||||
|
|
||||||
|
// if no existing row, insert a new row
|
||||||
|
_, err = e.Insert(&Setting{SettingKey: key, SettingValue: value}) |
||||||
|
return err |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
var ( |
||||||
|
GravatarSourceURL *url.URL |
||||||
|
LibravatarService *libravatar.Libravatar |
||||||
|
) |
||||||
|
|
||||||
|
func Init() error { |
||||||
|
var disableGravatar bool |
||||||
|
disableGravatarSetting, err := GetSetting(KeyPictureDisableGravatar) |
||||||
|
if IsErrSettingIsNotExist(err) { |
||||||
|
disableGravatar = setting.GetDefaultDisableGravatar() |
||||||
|
disableGravatarSetting = &Setting{SettingValue: strconv.FormatBool(disableGravatar)} |
||||||
|
} else if err != nil { |
||||||
|
return err |
||||||
|
} else { |
||||||
|
disableGravatar = disableGravatarSetting.GetValueBool() |
||||||
|
} |
||||||
|
|
||||||
|
var enableFederatedAvatar bool |
||||||
|
enableFederatedAvatarSetting, err := GetSetting(KeyPictureEnableFederatedAvatar) |
||||||
|
if IsErrSettingIsNotExist(err) { |
||||||
|
enableFederatedAvatar = setting.GetDefaultEnableFederatedAvatar(disableGravatar) |
||||||
|
enableFederatedAvatarSetting = &Setting{SettingValue: strconv.FormatBool(enableFederatedAvatar)} |
||||||
|
} else if err != nil { |
||||||
|
return err |
||||||
|
} else { |
||||||
|
enableFederatedAvatar = disableGravatarSetting.GetValueBool() |
||||||
|
} |
||||||
|
|
||||||
|
if setting.OfflineMode { |
||||||
|
disableGravatar = true |
||||||
|
enableFederatedAvatar = false |
||||||
|
} |
||||||
|
|
||||||
|
if disableGravatar || !enableFederatedAvatar { |
||||||
|
var err error |
||||||
|
GravatarSourceURL, err = url.Parse(setting.GravatarSource) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("Failed to parse Gravatar URL(%s): %v", setting.GravatarSource, err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if enableFederatedAvatarSetting.GetValueBool() { |
||||||
|
LibravatarService = libravatar.New() |
||||||
|
if GravatarSourceURL.Scheme == "https" { |
||||||
|
LibravatarService.SetUseHTTPS(true) |
||||||
|
LibravatarService.SetSecureFallbackHost(GravatarSourceURL.Host) |
||||||
|
} else { |
||||||
|
LibravatarService.SetUseHTTPS(false) |
||||||
|
LibravatarService.SetFallbackHost(GravatarSourceURL.Host) |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,11 @@ |
|||||||
|
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package system |
||||||
|
|
||||||
|
// enumerate all system setting keys
|
||||||
|
const ( |
||||||
|
KeyPictureDisableGravatar = "picture.disable_gravatar" |
||||||
|
KeyPictureEnableFederatedAvatar = "picture.enable_federated_avatar" |
||||||
|
) |
@ -0,0 +1,53 @@ |
|||||||
|
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package system_test |
||||||
|
|
||||||
|
import ( |
||||||
|
"strings" |
||||||
|
"testing" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/system" |
||||||
|
"code.gitea.io/gitea/models/unittest" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
func TestSettings(t *testing.T) { |
||||||
|
keyName := "server.LFS_LOCKS_PAGING_NUM" |
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase()) |
||||||
|
|
||||||
|
newSetting := &system.Setting{SettingKey: keyName, SettingValue: "50"} |
||||||
|
|
||||||
|
// create setting
|
||||||
|
err := system.SetSetting(newSetting) |
||||||
|
assert.NoError(t, err) |
||||||
|
// test about saving unchanged values
|
||||||
|
err = system.SetSetting(newSetting) |
||||||
|
assert.NoError(t, err) |
||||||
|
|
||||||
|
// get specific setting
|
||||||
|
settings, err := system.GetSettings([]string{keyName}) |
||||||
|
assert.NoError(t, err) |
||||||
|
assert.Len(t, settings, 1) |
||||||
|
assert.EqualValues(t, newSetting.SettingValue, settings[strings.ToLower(keyName)].SettingValue) |
||||||
|
|
||||||
|
// updated setting
|
||||||
|
updatedSetting := &system.Setting{SettingKey: keyName, SettingValue: "100", Version: newSetting.Version} |
||||||
|
err = system.SetSetting(updatedSetting) |
||||||
|
assert.NoError(t, err) |
||||||
|
|
||||||
|
// get all settings
|
||||||
|
settings, err = system.GetAllSettings() |
||||||
|
assert.NoError(t, err) |
||||||
|
assert.Len(t, settings, 3) |
||||||
|
assert.EqualValues(t, updatedSetting.SettingValue, settings[strings.ToLower(updatedSetting.SettingKey)].SettingValue) |
||||||
|
|
||||||
|
// delete setting
|
||||||
|
err = system.DeleteSetting(&system.Setting{SettingKey: strings.ToLower(keyName)}) |
||||||
|
assert.NoError(t, err) |
||||||
|
settings, err = system.GetAllSettings() |
||||||
|
assert.NoError(t, err) |
||||||
|
assert.Len(t, settings, 2) |
||||||
|
} |
@ -0,0 +1,46 @@ |
|||||||
|
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package system |
||||||
|
|
||||||
|
import ( |
||||||
|
"strconv" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/system" |
||||||
|
"code.gitea.io/gitea/modules/cache" |
||||||
|
) |
||||||
|
|
||||||
|
func genKey(key string) string { |
||||||
|
return "system.setting." + key |
||||||
|
} |
||||||
|
|
||||||
|
// GetSetting returns the setting value via the key
|
||||||
|
func GetSetting(key string) (string, error) { |
||||||
|
return cache.GetString(genKey(key), func() (string, error) { |
||||||
|
res, err := system.GetSetting(key) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
return res.SettingValue, nil |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// GetSettingBool return bool value of setting,
|
||||||
|
// none existing keys and errors are ignored and result in false
|
||||||
|
func GetSettingBool(key string) bool { |
||||||
|
s, _ := GetSetting(key) |
||||||
|
b, _ := strconv.ParseBool(s) |
||||||
|
return b |
||||||
|
} |
||||||
|
|
||||||
|
// SetSetting sets the setting value
|
||||||
|
func SetSetting(key, value string, version int) error { |
||||||
|
cache.Remove(genKey(key)) |
||||||
|
|
||||||
|
return system.SetSetting(&system.Setting{ |
||||||
|
SettingKey: key, |
||||||
|
SettingValue: value, |
||||||
|
Version: version, |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,34 @@ |
|||||||
|
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package system |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/user" |
||||||
|
"code.gitea.io/gitea/modules/cache" |
||||||
|
) |
||||||
|
|
||||||
|
func genUserKey(userID int64, key string) string { |
||||||
|
return fmt.Sprintf("user_%d.setting.%s", userID, key) |
||||||
|
} |
||||||
|
|
||||||
|
// GetUserSetting returns the user setting value via the key
|
||||||
|
func GetUserSetting(userID int64, key string) (string, error) { |
||||||
|
return cache.GetString(genUserKey(userID, key), func() (string, error) { |
||||||
|
res, err := user.GetSetting(userID, key) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
return res.SettingValue, nil |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// SetUserSetting sets the user setting value
|
||||||
|
func SetUserSetting(userID int64, key, value string) error { |
||||||
|
cache.Remove(genUserKey(userID, key)) |
||||||
|
|
||||||
|
return user.SetUserSetting(userID, key, value) |
||||||
|
} |
@ -0,0 +1,217 @@ |
|||||||
|
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package admin |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/http" |
||||||
|
"net/url" |
||||||
|
"os" |
||||||
|
"strings" |
||||||
|
|
||||||
|
system_model "code.gitea.io/gitea/models/system" |
||||||
|
"code.gitea.io/gitea/modules/base" |
||||||
|
"code.gitea.io/gitea/modules/context" |
||||||
|
"code.gitea.io/gitea/modules/git" |
||||||
|
"code.gitea.io/gitea/modules/json" |
||||||
|
"code.gitea.io/gitea/modules/log" |
||||||
|
"code.gitea.io/gitea/modules/setting" |
||||||
|
system_module "code.gitea.io/gitea/modules/system" |
||||||
|
"code.gitea.io/gitea/modules/util" |
||||||
|
"code.gitea.io/gitea/services/mailer" |
||||||
|
|
||||||
|
"gitea.com/go-chi/session" |
||||||
|
) |
||||||
|
|
||||||
|
const tplConfig base.TplName = "admin/config" |
||||||
|
|
||||||
|
// SendTestMail send test mail to confirm mail service is OK
|
||||||
|
func SendTestMail(ctx *context.Context) { |
||||||
|
email := ctx.FormString("email") |
||||||
|
// Send a test email to the user's email address and redirect back to Config
|
||||||
|
if err := mailer.SendTestMail(email); err != nil { |
||||||
|
ctx.Flash.Error(ctx.Tr("admin.config.test_mail_failed", email, err)) |
||||||
|
} else { |
||||||
|
ctx.Flash.Info(ctx.Tr("admin.config.test_mail_sent", email)) |
||||||
|
} |
||||||
|
|
||||||
|
ctx.Redirect(setting.AppSubURL + "/admin/config") |
||||||
|
} |
||||||
|
|
||||||
|
func shadowPasswordKV(cfgItem, splitter string) string { |
||||||
|
fields := strings.Split(cfgItem, splitter) |
||||||
|
for i := 0; i < len(fields); i++ { |
||||||
|
if strings.HasPrefix(fields[i], "password=") { |
||||||
|
fields[i] = "password=******" |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
return strings.Join(fields, splitter) |
||||||
|
} |
||||||
|
|
||||||
|
func shadowURL(provider, cfgItem string) string { |
||||||
|
u, err := url.Parse(cfgItem) |
||||||
|
if err != nil { |
||||||
|
log.Error("Shadowing Password for %v failed: %v", provider, err) |
||||||
|
return cfgItem |
||||||
|
} |
||||||
|
if u.User != nil { |
||||||
|
atIdx := strings.Index(cfgItem, "@") |
||||||
|
if atIdx > 0 { |
||||||
|
colonIdx := strings.LastIndex(cfgItem[:atIdx], ":") |
||||||
|
if colonIdx > 0 { |
||||||
|
return cfgItem[:colonIdx+1] + "******" + cfgItem[atIdx:] |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return cfgItem |
||||||
|
} |
||||||
|
|
||||||
|
func shadowPassword(provider, cfgItem string) string { |
||||||
|
switch provider { |
||||||
|
case "redis": |
||||||
|
return shadowPasswordKV(cfgItem, ",") |
||||||
|
case "mysql": |
||||||
|
// root:@tcp(localhost:3306)/macaron?charset=utf8
|
||||||
|
atIdx := strings.Index(cfgItem, "@") |
||||||
|
if atIdx > 0 { |
||||||
|
colonIdx := strings.Index(cfgItem[:atIdx], ":") |
||||||
|
if colonIdx > 0 { |
||||||
|
return cfgItem[:colonIdx+1] + "******" + cfgItem[atIdx:] |
||||||
|
} |
||||||
|
} |
||||||
|
return cfgItem |
||||||
|
case "postgres": |
||||||
|
// user=jiahuachen dbname=macaron port=5432 sslmode=disable
|
||||||
|
if !strings.HasPrefix(cfgItem, "postgres://") { |
||||||
|
return shadowPasswordKV(cfgItem, " ") |
||||||
|
} |
||||||
|
fallthrough |
||||||
|
case "couchbase": |
||||||
|
return shadowURL(provider, cfgItem) |
||||||
|
// postgres://pqgotest:password@localhost/pqgotest?sslmode=verify-full
|
||||||
|
// Notice: use shadowURL
|
||||||
|
} |
||||||
|
return cfgItem |
||||||
|
} |
||||||
|
|
||||||
|
// Config show admin config page
|
||||||
|
func Config(ctx *context.Context) { |
||||||
|
ctx.Data["Title"] = ctx.Tr("admin.config") |
||||||
|
ctx.Data["PageIsAdmin"] = true |
||||||
|
ctx.Data["PageIsAdminConfig"] = true |
||||||
|
|
||||||
|
systemSettings, err := system_model.GetAllSettings() |
||||||
|
if err != nil { |
||||||
|
ctx.ServerError("system_model.GetAllSettings", err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// All editable settings from UI
|
||||||
|
ctx.Data["SystemSettings"] = systemSettings |
||||||
|
ctx.PageData["adminConfigPage"] = true |
||||||
|
|
||||||
|
ctx.Data["CustomConf"] = setting.CustomConf |
||||||
|
ctx.Data["AppUrl"] = setting.AppURL |
||||||
|
ctx.Data["Domain"] = setting.Domain |
||||||
|
ctx.Data["OfflineMode"] = setting.OfflineMode |
||||||
|
ctx.Data["DisableRouterLog"] = setting.DisableRouterLog |
||||||
|
ctx.Data["RunUser"] = setting.RunUser |
||||||
|
ctx.Data["RunMode"] = util.ToTitleCase(setting.RunMode) |
||||||
|
ctx.Data["GitVersion"] = git.VersionInfo() |
||||||
|
|
||||||
|
ctx.Data["RepoRootPath"] = setting.RepoRootPath |
||||||
|
ctx.Data["CustomRootPath"] = setting.CustomPath |
||||||
|
ctx.Data["StaticRootPath"] = setting.StaticRootPath |
||||||
|
ctx.Data["LogRootPath"] = setting.LogRootPath |
||||||
|
ctx.Data["ScriptType"] = setting.ScriptType |
||||||
|
ctx.Data["ReverseProxyAuthUser"] = setting.ReverseProxyAuthUser |
||||||
|
ctx.Data["ReverseProxyAuthEmail"] = setting.ReverseProxyAuthEmail |
||||||
|
|
||||||
|
ctx.Data["SSH"] = setting.SSH |
||||||
|
ctx.Data["LFS"] = setting.LFS |
||||||
|
|
||||||
|
ctx.Data["Service"] = setting.Service |
||||||
|
ctx.Data["DbCfg"] = setting.Database |
||||||
|
ctx.Data["Webhook"] = setting.Webhook |
||||||
|
|
||||||
|
ctx.Data["MailerEnabled"] = false |
||||||
|
if setting.MailService != nil { |
||||||
|
ctx.Data["MailerEnabled"] = true |
||||||
|
ctx.Data["Mailer"] = setting.MailService |
||||||
|
} |
||||||
|
|
||||||
|
ctx.Data["CacheAdapter"] = setting.CacheService.Adapter |
||||||
|
ctx.Data["CacheInterval"] = setting.CacheService.Interval |
||||||
|
|
||||||
|
ctx.Data["CacheConn"] = shadowPassword(setting.CacheService.Adapter, setting.CacheService.Conn) |
||||||
|
ctx.Data["CacheItemTTL"] = setting.CacheService.TTL |
||||||
|
|
||||||
|
sessionCfg := setting.SessionConfig |
||||||
|
if sessionCfg.Provider == "VirtualSession" { |
||||||
|
var realSession session.Options |
||||||
|
if err := json.Unmarshal([]byte(sessionCfg.ProviderConfig), &realSession); err != nil { |
||||||
|
log.Error("Unable to unmarshall session config for virtual provider config: %s\nError: %v", sessionCfg.ProviderConfig, err) |
||||||
|
} |
||||||
|
sessionCfg.Provider = realSession.Provider |
||||||
|
sessionCfg.ProviderConfig = realSession.ProviderConfig |
||||||
|
sessionCfg.CookieName = realSession.CookieName |
||||||
|
sessionCfg.CookiePath = realSession.CookiePath |
||||||
|
sessionCfg.Gclifetime = realSession.Gclifetime |
||||||
|
sessionCfg.Maxlifetime = realSession.Maxlifetime |
||||||
|
sessionCfg.Secure = realSession.Secure |
||||||
|
sessionCfg.Domain = realSession.Domain |
||||||
|
} |
||||||
|
sessionCfg.ProviderConfig = shadowPassword(sessionCfg.Provider, sessionCfg.ProviderConfig) |
||||||
|
ctx.Data["SessionConfig"] = sessionCfg |
||||||
|
|
||||||
|
ctx.Data["Git"] = setting.Git |
||||||
|
|
||||||
|
type envVar struct { |
||||||
|
Name, Value string |
||||||
|
} |
||||||
|
|
||||||
|
envVars := map[string]*envVar{} |
||||||
|
if len(os.Getenv("GITEA_WORK_DIR")) > 0 { |
||||||
|
envVars["GITEA_WORK_DIR"] = &envVar{"GITEA_WORK_DIR", os.Getenv("GITEA_WORK_DIR")} |
||||||
|
} |
||||||
|
if len(os.Getenv("GITEA_CUSTOM")) > 0 { |
||||||
|
envVars["GITEA_CUSTOM"] = &envVar{"GITEA_CUSTOM", os.Getenv("GITEA_CUSTOM")} |
||||||
|
} |
||||||
|
|
||||||
|
ctx.Data["EnvVars"] = envVars |
||||||
|
ctx.Data["Loggers"] = setting.GetLogDescriptions() |
||||||
|
ctx.Data["EnableAccessLog"] = setting.EnableAccessLog |
||||||
|
ctx.Data["AccessLogTemplate"] = setting.AccessLogTemplate |
||||||
|
ctx.Data["DisableRouterLog"] = setting.DisableRouterLog |
||||||
|
ctx.Data["EnableXORMLog"] = setting.EnableXORMLog |
||||||
|
ctx.Data["LogSQL"] = setting.Database.LogSQL |
||||||
|
|
||||||
|
ctx.HTML(http.StatusOK, tplConfig) |
||||||
|
} |
||||||
|
|
||||||
|
func ChangeConfig(ctx *context.Context) { |
||||||
|
key := strings.TrimSpace(ctx.FormString("key")) |
||||||
|
if key == "" { |
||||||
|
ctx.JSON(http.StatusOK, map[string]string{ |
||||||
|
"redirect": ctx.Req.URL.String(), |
||||||
|
}) |
||||||
|
return |
||||||
|
} |
||||||
|
value := ctx.FormString("value") |
||||||
|
version := ctx.FormInt("version") |
||||||
|
|
||||||
|
if err := system_module.SetSetting(key, value, version); err != nil { |
||||||
|
log.Error("set setting failed: %v", err) |
||||||
|
ctx.JSON(http.StatusOK, map[string]string{ |
||||||
|
"err": ctx.Tr("admin.config.set_setting_failed", key), |
||||||
|
}) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, map[string]interface{}{ |
||||||
|
"version": version + 1, |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,37 @@ |
|||||||
|
import $ from 'jquery'; |
||||||
|
import {showTemporaryTooltip} from '../../modules/tippy.js'; |
||||||
|
|
||||||
|
const {appSubUrl, csrfToken, pageData} = window.config; |
||||||
|
|
||||||
|
export function initAdminConfigs() { |
||||||
|
const isAdminConfigPage = pageData?.adminConfigPage; |
||||||
|
if (!isAdminConfigPage) return; |
||||||
|
|
||||||
|
$("input[type='checkbox']").on('change', (e) => { |
||||||
|
const $this = $(e.currentTarget); |
||||||
|
$.ajax({ |
||||||
|
url: `${appSubUrl}/admin/config`, |
||||||
|
type: 'POST', |
||||||
|
data: { |
||||||
|
_csrf: csrfToken, |
||||||
|
key: $this.attr('name'), |
||||||
|
value: $this.is(':checked'), |
||||||
|
version: $this.attr('version'), |
||||||
|
} |
||||||
|
}).done((resp) => { |
||||||
|
if (resp) { |
||||||
|
if (resp.redirect) { |
||||||
|
window.location.href = resp.redirect; |
||||||
|
} else if (resp.version) { |
||||||
|
$this.attr('version', resp.version); |
||||||
|
} else if (resp.err) { |
||||||
|
showTemporaryTooltip(e.currentTarget, resp.err); |
||||||
|
$this.prop('checked', !$this.is(':checked')); |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
e.preventDefault(); |
||||||
|
return false; |
||||||
|
}); |
||||||
|
} |
Loading…
Reference in new issue