add default language admin ui functionality
This commit is contained in:
parent
e941e10f24
commit
c97b7d44c3
|
@ -20,6 +20,8 @@ import (
|
|||
type RoomConfig interface {
|
||||
GetPrivacyMode(context.Context) (PrivacyMode, error)
|
||||
SetPrivacyMode(context.Context, PrivacyMode) error
|
||||
GetDefaultLanguage(context.Context) (string, error)
|
||||
SetDefaultLanguage(context.Context, string) error
|
||||
}
|
||||
|
||||
// AuthFallbackService allows password authentication which might be helpful for scenarios
|
||||
|
|
|
@ -9,6 +9,19 @@ import (
|
|||
)
|
||||
|
||||
type FakeRoomConfig struct {
|
||||
GetDefaultLanguageStub func(context.Context) (string, error)
|
||||
getDefaultLanguageMutex sync.RWMutex
|
||||
getDefaultLanguageArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
}
|
||||
getDefaultLanguageReturns struct {
|
||||
result1 string
|
||||
result2 error
|
||||
}
|
||||
getDefaultLanguageReturnsOnCall map[int]struct {
|
||||
result1 string
|
||||
result2 error
|
||||
}
|
||||
GetPrivacyModeStub func(context.Context) (roomdb.PrivacyMode, error)
|
||||
getPrivacyModeMutex sync.RWMutex
|
||||
getPrivacyModeArgsForCall []struct {
|
||||
|
@ -22,6 +35,18 @@ type FakeRoomConfig struct {
|
|||
result1 roomdb.PrivacyMode
|
||||
result2 error
|
||||
}
|
||||
SetDefaultLanguageStub func(context.Context, string) error
|
||||
setDefaultLanguageMutex sync.RWMutex
|
||||
setDefaultLanguageArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 string
|
||||
}
|
||||
setDefaultLanguageReturns struct {
|
||||
result1 error
|
||||
}
|
||||
setDefaultLanguageReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
SetPrivacyModeStub func(context.Context, roomdb.PrivacyMode) error
|
||||
setPrivacyModeMutex sync.RWMutex
|
||||
setPrivacyModeArgsForCall []struct {
|
||||
|
@ -38,6 +63,70 @@ type FakeRoomConfig struct {
|
|||
invocationsMutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (fake *FakeRoomConfig) GetDefaultLanguage(arg1 context.Context) (string, error) {
|
||||
fake.getDefaultLanguageMutex.Lock()
|
||||
ret, specificReturn := fake.getDefaultLanguageReturnsOnCall[len(fake.getDefaultLanguageArgsForCall)]
|
||||
fake.getDefaultLanguageArgsForCall = append(fake.getDefaultLanguageArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
}{arg1})
|
||||
stub := fake.GetDefaultLanguageStub
|
||||
fakeReturns := fake.getDefaultLanguageReturns
|
||||
fake.recordInvocation("GetDefaultLanguage", []interface{}{arg1})
|
||||
fake.getDefaultLanguageMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1, ret.result2
|
||||
}
|
||||
return fakeReturns.result1, fakeReturns.result2
|
||||
}
|
||||
|
||||
func (fake *FakeRoomConfig) GetDefaultLanguageCallCount() int {
|
||||
fake.getDefaultLanguageMutex.RLock()
|
||||
defer fake.getDefaultLanguageMutex.RUnlock()
|
||||
return len(fake.getDefaultLanguageArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeRoomConfig) GetDefaultLanguageCalls(stub func(context.Context) (string, error)) {
|
||||
fake.getDefaultLanguageMutex.Lock()
|
||||
defer fake.getDefaultLanguageMutex.Unlock()
|
||||
fake.GetDefaultLanguageStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeRoomConfig) GetDefaultLanguageArgsForCall(i int) context.Context {
|
||||
fake.getDefaultLanguageMutex.RLock()
|
||||
defer fake.getDefaultLanguageMutex.RUnlock()
|
||||
argsForCall := fake.getDefaultLanguageArgsForCall[i]
|
||||
return argsForCall.arg1
|
||||
}
|
||||
|
||||
func (fake *FakeRoomConfig) GetDefaultLanguageReturns(result1 string, result2 error) {
|
||||
fake.getDefaultLanguageMutex.Lock()
|
||||
defer fake.getDefaultLanguageMutex.Unlock()
|
||||
fake.GetDefaultLanguageStub = nil
|
||||
fake.getDefaultLanguageReturns = struct {
|
||||
result1 string
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeRoomConfig) GetDefaultLanguageReturnsOnCall(i int, result1 string, result2 error) {
|
||||
fake.getDefaultLanguageMutex.Lock()
|
||||
defer fake.getDefaultLanguageMutex.Unlock()
|
||||
fake.GetDefaultLanguageStub = nil
|
||||
if fake.getDefaultLanguageReturnsOnCall == nil {
|
||||
fake.getDefaultLanguageReturnsOnCall = make(map[int]struct {
|
||||
result1 string
|
||||
result2 error
|
||||
})
|
||||
}
|
||||
fake.getDefaultLanguageReturnsOnCall[i] = struct {
|
||||
result1 string
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeRoomConfig) GetPrivacyMode(arg1 context.Context) (roomdb.PrivacyMode, error) {
|
||||
fake.getPrivacyModeMutex.Lock()
|
||||
ret, specificReturn := fake.getPrivacyModeReturnsOnCall[len(fake.getPrivacyModeArgsForCall)]
|
||||
|
@ -102,6 +191,68 @@ func (fake *FakeRoomConfig) GetPrivacyModeReturnsOnCall(i int, result1 roomdb.Pr
|
|||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeRoomConfig) SetDefaultLanguage(arg1 context.Context, arg2 string) error {
|
||||
fake.setDefaultLanguageMutex.Lock()
|
||||
ret, specificReturn := fake.setDefaultLanguageReturnsOnCall[len(fake.setDefaultLanguageArgsForCall)]
|
||||
fake.setDefaultLanguageArgsForCall = append(fake.setDefaultLanguageArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 string
|
||||
}{arg1, arg2})
|
||||
stub := fake.SetDefaultLanguageStub
|
||||
fakeReturns := fake.setDefaultLanguageReturns
|
||||
fake.recordInvocation("SetDefaultLanguage", []interface{}{arg1, arg2})
|
||||
fake.setDefaultLanguageMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeRoomConfig) SetDefaultLanguageCallCount() int {
|
||||
fake.setDefaultLanguageMutex.RLock()
|
||||
defer fake.setDefaultLanguageMutex.RUnlock()
|
||||
return len(fake.setDefaultLanguageArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeRoomConfig) SetDefaultLanguageCalls(stub func(context.Context, string) error) {
|
||||
fake.setDefaultLanguageMutex.Lock()
|
||||
defer fake.setDefaultLanguageMutex.Unlock()
|
||||
fake.SetDefaultLanguageStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeRoomConfig) SetDefaultLanguageArgsForCall(i int) (context.Context, string) {
|
||||
fake.setDefaultLanguageMutex.RLock()
|
||||
defer fake.setDefaultLanguageMutex.RUnlock()
|
||||
argsForCall := fake.setDefaultLanguageArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeRoomConfig) SetDefaultLanguageReturns(result1 error) {
|
||||
fake.setDefaultLanguageMutex.Lock()
|
||||
defer fake.setDefaultLanguageMutex.Unlock()
|
||||
fake.SetDefaultLanguageStub = nil
|
||||
fake.setDefaultLanguageReturns = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeRoomConfig) SetDefaultLanguageReturnsOnCall(i int, result1 error) {
|
||||
fake.setDefaultLanguageMutex.Lock()
|
||||
defer fake.setDefaultLanguageMutex.Unlock()
|
||||
fake.SetDefaultLanguageStub = nil
|
||||
if fake.setDefaultLanguageReturnsOnCall == nil {
|
||||
fake.setDefaultLanguageReturnsOnCall = make(map[int]struct {
|
||||
result1 error
|
||||
})
|
||||
}
|
||||
fake.setDefaultLanguageReturnsOnCall[i] = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeRoomConfig) SetPrivacyMode(arg1 context.Context, arg2 roomdb.PrivacyMode) error {
|
||||
fake.setPrivacyModeMutex.Lock()
|
||||
ret, specificReturn := fake.setPrivacyModeReturnsOnCall[len(fake.setPrivacyModeArgsForCall)]
|
||||
|
@ -167,8 +318,12 @@ func (fake *FakeRoomConfig) SetPrivacyModeReturnsOnCall(i int, result1 error) {
|
|||
func (fake *FakeRoomConfig) Invocations() map[string][][]interface{} {
|
||||
fake.invocationsMutex.RLock()
|
||||
defer fake.invocationsMutex.RUnlock()
|
||||
fake.getDefaultLanguageMutex.RLock()
|
||||
defer fake.getDefaultLanguageMutex.RUnlock()
|
||||
fake.getPrivacyModeMutex.RLock()
|
||||
defer fake.getPrivacyModeMutex.RUnlock()
|
||||
fake.setDefaultLanguageMutex.RLock()
|
||||
defer fake.setDefaultLanguageMutex.RUnlock()
|
||||
fake.setPrivacyModeMutex.RLock()
|
||||
defer fake.setPrivacyModeMutex.RUnlock()
|
||||
copiedInvocations := map[string][][]interface{}{}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
-- +migrate Up
|
||||
-- the configuration settings for this room, currently only privacy modes
|
||||
-- the configuration settings for this room, currently privacy mode settings and the default translation for the room
|
||||
CREATE TABLE config (
|
||||
id integer NOT NULL PRIMARY KEY,
|
||||
privacyMode integer NOT NULL, -- open, community, restricted
|
||||
defaultLanguage TEXT NOT NULL, -- a language tag, e.g. en, sv, de
|
||||
|
||||
CHECK (id == 0) -- should only ever store one row
|
||||
);
|
||||
|
@ -10,9 +11,10 @@ CREATE TABLE config (
|
|||
-- the config table will only ever contain one row: the rooms current settings
|
||||
-- we update that row whenever the config changes.
|
||||
-- to have something to update, we insert the first and only row at id 0
|
||||
INSERT INTO config (id, privacyMode) VALUES (
|
||||
0, -- the constant id we will query
|
||||
2 -- community is the default mode unless overridden
|
||||
INSERT INTO config (id, privacyMode, defaultLanguage) VALUES (
|
||||
0, -- the constant id we will query
|
||||
2, -- community is the default mode unless overridden
|
||||
"en" -- english is the default language for all installs
|
||||
);
|
||||
|
||||
-- +migrate Down
|
||||
|
|
|
@ -23,19 +23,22 @@ import (
|
|||
|
||||
// Config is an object representing the database table.
|
||||
type Config struct {
|
||||
ID int64 `boil:"id" json:"id" toml:"id" yaml:"id"`
|
||||
PrivacyMode roomdb.PrivacyMode `boil:"privacyMode" json:"privacyMode" toml:"privacyMode" yaml:"privacyMode"`
|
||||
ID int64 `boil:"id" json:"id" toml:"id" yaml:"id"`
|
||||
PrivacyMode roomdb.PrivacyMode `boil:"privacyMode" json:"privacyMode" toml:"privacyMode" yaml:"privacyMode"`
|
||||
DefaultLanguage string `boil:"defaultLanguage" json:"defaultLanguage" toml:"defaultLanguage" yaml:"defaultLanguage"`
|
||||
|
||||
R *configR `boil:"-" json:"-" toml:"-" yaml:"-"`
|
||||
L configL `boil:"-" json:"-" toml:"-" yaml:"-"`
|
||||
}
|
||||
|
||||
var ConfigColumns = struct {
|
||||
ID string
|
||||
PrivacyMode string
|
||||
ID string
|
||||
PrivacyMode string
|
||||
DefaultLanguage string
|
||||
}{
|
||||
ID: "id",
|
||||
PrivacyMode: "privacyMode",
|
||||
ID: "id",
|
||||
PrivacyMode: "privacyMode",
|
||||
DefaultLanguage: "defaultLanguage",
|
||||
}
|
||||
|
||||
// Generated where
|
||||
|
@ -62,11 +65,13 @@ func (w whereHelperroomdb_PrivacyMode) GTE(x roomdb.PrivacyMode) qm.QueryMod {
|
|||
}
|
||||
|
||||
var ConfigWhere = struct {
|
||||
ID whereHelperint64
|
||||
PrivacyMode whereHelperroomdb_PrivacyMode
|
||||
ID whereHelperint64
|
||||
PrivacyMode whereHelperroomdb_PrivacyMode
|
||||
DefaultLanguage whereHelperstring
|
||||
}{
|
||||
ID: whereHelperint64{field: "\"config\".\"id\""},
|
||||
PrivacyMode: whereHelperroomdb_PrivacyMode{field: "\"config\".\"privacyMode\""},
|
||||
ID: whereHelperint64{field: "\"config\".\"id\""},
|
||||
PrivacyMode: whereHelperroomdb_PrivacyMode{field: "\"config\".\"privacyMode\""},
|
||||
DefaultLanguage: whereHelperstring{field: "\"config\".\"defaultLanguage\""},
|
||||
}
|
||||
|
||||
// ConfigRels is where relationship names are stored.
|
||||
|
@ -86,8 +91,8 @@ func (*configR) NewStruct() *configR {
|
|||
type configL struct{}
|
||||
|
||||
var (
|
||||
configAllColumns = []string{"id", "privacyMode"}
|
||||
configColumnsWithoutDefault = []string{"privacyMode"}
|
||||
configAllColumns = []string{"id", "privacyMode", "defaultLanguage"}
|
||||
configColumnsWithoutDefault = []string{"privacyMode", "defaultLanguage"}
|
||||
configColumnsWithDefault = []string{"id"}
|
||||
configPrimaryKeyColumns = []string{"id"}
|
||||
)
|
||||
|
|
|
@ -40,7 +40,6 @@ func (c Config) SetPrivacyMode(ctx context.Context, pm roomdb.PrivacyMode) error
|
|||
return err
|
||||
}
|
||||
|
||||
// cblgh: a walkthrough of this step (again, now that i have some actual context) would be real good :)
|
||||
err = transact(c.db, func(tx *sql.Tx) error {
|
||||
// get the settings row
|
||||
config, err := models.FindConfig(ctx, tx, configRowID)
|
||||
|
@ -68,3 +67,46 @@ func (c Config) SetPrivacyMode(ctx context.Context, pm roomdb.PrivacyMode) error
|
|||
|
||||
return nil // alles gut!!
|
||||
}
|
||||
|
||||
// TODO: use proper language tag from "golang.org/x/text/language"?
|
||||
func (c Config) GetDefaultLanguage(ctx context.Context) (string, error) {
|
||||
config, err := models.FindConfig(ctx, c.db, configRowID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return config.DefaultLanguage, nil
|
||||
}
|
||||
|
||||
func (c Config) SetDefaultLanguage(ctx context.Context, langTag string) error {
|
||||
if len(langTag) == 0 {
|
||||
return fmt.Errorf("language tag cannot be empty")
|
||||
}
|
||||
|
||||
err := transact(c.db, func(tx *sql.Tx) error {
|
||||
// get the settings row
|
||||
config, err := models.FindConfig(ctx, tx, configRowID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// set the new language tag
|
||||
config.DefaultLanguage = langTag
|
||||
// issue update stmt
|
||||
rowsAffected, err := config.Update(ctx, tx, boil.Infer())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
return fmt.Errorf("setting default language should have update the settings row, instead 0 rows were updated")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil // alles gut!!
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
"github.com/ssb-ngi-pointer/go-ssb-room/web"
|
||||
weberrors "github.com/ssb-ngi-pointer/go-ssb-room/web/errors"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web/i18n"
|
||||
)
|
||||
|
||||
// HTMLTemplates define the list of files the template system should load.
|
||||
|
@ -65,6 +66,7 @@ func Handler(
|
|||
r *render.Renderer,
|
||||
roomState *roomstate.Manager,
|
||||
fh *weberrors.FlashHelper,
|
||||
locHelper *i18n.Helper,
|
||||
dbs Databases,
|
||||
) http.Handler {
|
||||
mux := &http.ServeMux{}
|
||||
|
@ -84,11 +86,12 @@ func Handler(
|
|||
var sh = settingsHandler{
|
||||
r: r,
|
||||
urlTo: urlTo,
|
||||
|
||||
db: dbs.Config,
|
||||
loc: locHelper,
|
||||
}
|
||||
mux.HandleFunc("/settings", r.HTML("admin/settings.tmpl", sh.overview))
|
||||
mux.HandleFunc("/settings/set-privacy", sh.setPrivacy)
|
||||
mux.HandleFunc("/settings/set-language", sh.setLanguage)
|
||||
|
||||
mux.HandleFunc("/menu", r.HTML("admin/menu.tmpl", func(w http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
return map[string]interface{}{}, nil
|
||||
|
|
|
@ -13,56 +13,68 @@ import (
|
|||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web"
|
||||
weberrors "github.com/ssb-ngi-pointer/go-ssb-room/web/errors"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web/i18n"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web/members"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
|
||||
)
|
||||
|
||||
type settingsHandler struct {
|
||||
r *render.Renderer
|
||||
r *render.Renderer
|
||||
urlTo web.URLMaker
|
||||
|
||||
db roomdb.RoomConfig
|
||||
db roomdb.RoomConfig
|
||||
loc *i18n.Helper
|
||||
}
|
||||
|
||||
func (h settingsHandler) overview(w http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
privacyModes := []roomdb.PrivacyMode{roomdb.ModeOpen, roomdb.ModeCommunity, roomdb.ModeRestricted}
|
||||
|
||||
currentMode, err := h.db.GetPrivacyMode(req.Context())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve current privacy mode: %w", err)
|
||||
}
|
||||
|
||||
currentLanguage, err := h.db.GetDefaultLanguage(req.Context())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve current privacy mode: %w", err)
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"CurrentMode": currentMode,
|
||||
"PrivacyModes": privacyModes,
|
||||
csrf.TemplateTag: csrf.TemplateField(req),
|
||||
"CurrentMode": currentMode,
|
||||
"CurrentLanguage": h.loc.ChooseTranslation(currentLanguage),
|
||||
"PrivacyModes": privacyModes,
|
||||
csrf.TemplateTag: csrf.TemplateField(req),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h settingsHandler) setPrivacy(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != "POST" {
|
||||
// TODO: proper error type
|
||||
h.r.Error(w, req, http.StatusBadRequest, fmt.Errorf("bad request"))
|
||||
func (h settingsHandler) setLanguage(w http.ResponseWriter, req *http.Request) {
|
||||
if !h.verifyPostRequirements(w, req) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := req.ParseForm(); err != nil {
|
||||
// TODO: proper error type
|
||||
h.r.Error(w, req, http.StatusBadRequest, fmt.Errorf("bad request: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
// get the member behind the POST
|
||||
currentMember := members.FromContext(req.Context())
|
||||
// handles error cases & make sures the member is an admin
|
||||
currentMember := h.getMember(w, req)
|
||||
if currentMember == nil {
|
||||
err := weberrors.ErrForbidden{Details: fmt.Errorf("not a registered member")}
|
||||
h.r.Error(w, req, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
// make sure the member is an admin
|
||||
if currentMember.Role != roomdb.RoleAdmin {
|
||||
err := weberrors.ErrForbidden{Details: fmt.Errorf("yr not an admin! naughty naughty")}
|
||||
h.r.Error(w, req, http.StatusInternalServerError, err)
|
||||
langTag := req.Form.Get("lang")
|
||||
|
||||
err := h.db.SetDefaultLanguage(req.Context(), langTag)
|
||||
if err != nil {
|
||||
h.r.Error(w, req, http.StatusBadRequest, fmt.Errorf("something went wrong when setting the default language: %w", err))
|
||||
}
|
||||
|
||||
// we successfully set the default language! time to redirect to the updated settings overview
|
||||
h.redirect(router.AdminSettings, w, req)
|
||||
}
|
||||
|
||||
func (h settingsHandler) setPrivacy(w http.ResponseWriter, req *http.Request) {
|
||||
if !h.verifyPostRequirements(w, req) {
|
||||
return
|
||||
}
|
||||
// handles error cases & make sures the member is an admin
|
||||
currentMember := h.getMember(w, req)
|
||||
if currentMember == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -78,6 +90,44 @@ func (h settingsHandler) setPrivacy(w http.ResponseWriter, req *http.Request) {
|
|||
}
|
||||
|
||||
// we successfully set the privacy mode! time to redirect to the updated settings overview
|
||||
overview := h.urlTo(router.AdminSettings).String()
|
||||
http.Redirect(w, req, overview, http.StatusFound)
|
||||
h.redirect(router.AdminSettings, w, req)
|
||||
}
|
||||
|
||||
/* common-use functions */
|
||||
|
||||
func (h settingsHandler) getMember(w http.ResponseWriter, req *http.Request) *roomdb.Member {
|
||||
// get the member behind the POST
|
||||
currentMember := members.FromContext(req.Context())
|
||||
if currentMember == nil {
|
||||
err := weberrors.ErrForbidden{Details: fmt.Errorf("not a registered member")}
|
||||
h.r.Error(w, req, http.StatusInternalServerError, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// make sure the member is an admin
|
||||
if currentMember.Role != roomdb.RoleAdmin {
|
||||
err := weberrors.ErrForbidden{Details: fmt.Errorf("yr not an admin! naughty naughty")}
|
||||
h.r.Error(w, req, http.StatusInternalServerError, err)
|
||||
return nil
|
||||
}
|
||||
return currentMember
|
||||
}
|
||||
|
||||
func (h settingsHandler) verifyPostRequirements(w http.ResponseWriter, req *http.Request) bool {
|
||||
if req.Method != "POST" {
|
||||
// TODO: proper error type
|
||||
h.r.Error(w, req, http.StatusBadRequest, fmt.Errorf("bad request"))
|
||||
return false
|
||||
}
|
||||
if err := req.ParseForm(); err != nil {
|
||||
// TODO: proper error type
|
||||
h.r.Error(w, req, http.StatusBadRequest, fmt.Errorf("bad request: %w", err))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (h settingsHandler) redirect(route string, w http.ResponseWriter, req *http.Request) {
|
||||
overview := h.urlTo(route).String()
|
||||
http.Redirect(w, req, overview, http.StatusSeeOther)
|
||||
}
|
||||
|
|
|
@ -64,6 +64,7 @@ func newSession(t *testing.T) *testSession {
|
|||
ts.ConfigDB = new(mockdb.FakeRoomConfig)
|
||||
// default mode for all tests
|
||||
ts.ConfigDB.GetPrivacyModeReturns(roomdb.ModeCommunity, nil)
|
||||
ts.ConfigDB.GetDefaultLanguageReturns("en", nil)
|
||||
ts.DeniedKeysDB = new(mockdb.FakeDeniedKeysService)
|
||||
ts.MembersDB = new(mockdb.FakeMembersService)
|
||||
ts.PinnedDB = new(mockdb.FakePinnedNoticesService)
|
||||
|
@ -107,7 +108,7 @@ func newSession(t *testing.T) *testSession {
|
|||
i18ntesting.WriteReplacement(t)
|
||||
|
||||
testRepo := repo.New(testPath)
|
||||
locHelper, err := i18n.New(testRepo)
|
||||
locHelper, err := i18n.New(testRepo, ts.ConfigDB)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -131,7 +132,7 @@ func newSession(t *testing.T) *testSession {
|
|||
testFuncs["is_logged_in"] = func() *roomdb.Member { return &ts.User }
|
||||
testFuncs["urlToNotice"] = func(name string) string { return "" }
|
||||
testFuncs["language_count"] = func() int { return 1 }
|
||||
testFuncs["list_languages"] = func() string { return "" }
|
||||
testFuncs["list_languages"] = func(*url.URL, string) string { return "" }
|
||||
testFuncs["relative_time"] = func(when time.Time) string { return humanize.Time(when) }
|
||||
|
||||
renderOpts := []render.Option{
|
||||
|
@ -153,6 +154,7 @@ func newSession(t *testing.T) *testSession {
|
|||
r,
|
||||
ts.RoomState,
|
||||
flashHelper,
|
||||
locHelper,
|
||||
Databases{
|
||||
Aliases: ts.AliasesDB,
|
||||
Config: ts.ConfigDB,
|
||||
|
|
|
@ -74,7 +74,7 @@ func New(
|
|||
m := router.CompleteApp()
|
||||
urlTo := web.NewURLTo(m, netInfo)
|
||||
|
||||
locHelper, err := i18n.New(repo)
|
||||
locHelper, err := i18n.New(repo, dbs.Config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -130,11 +130,9 @@ func New(
|
|||
}),
|
||||
|
||||
render.InjectTemplateFunc("list_languages", func(r *http.Request) interface{} {
|
||||
urlTo := web.NewURLTo(m)
|
||||
route := urlTo(router.CompleteSetLanguage).String()
|
||||
csrfElement := csrf.TemplateField(r)
|
||||
|
||||
createFormElement := func(tag, translation string) string {
|
||||
createFormElement := func(postRoute, tag, translation, classList string) string {
|
||||
return fmt.Sprintf(`
|
||||
<form
|
||||
action="%s"
|
||||
|
@ -146,16 +144,16 @@ func New(
|
|||
<input
|
||||
type="submit"
|
||||
value="%s"
|
||||
class="text-gray-500 hover:underline cursor-pointer"
|
||||
class="%s"
|
||||
/>
|
||||
</form>
|
||||
`, route, csrfElement, tag, r.RequestURI, translation)
|
||||
`, postRoute, csrfElement, tag, r.RequestURI, translation, classList)
|
||||
}
|
||||
return func() template.HTML {
|
||||
return func(postRoute *url.URL, classList string) template.HTML {
|
||||
languages := locHelper.ListLanguages()
|
||||
languageOptions := make([]string, len(languages))
|
||||
for tag, translation := range languages {
|
||||
languageOptions = append(languageOptions, createFormElement(tag, translation))
|
||||
languageOptions = append(languageOptions, createFormElement(postRoute.String(), tag, translation, classList))
|
||||
}
|
||||
return (template.HTML)(strings.Join(languageOptions, "\n"))
|
||||
}
|
||||
|
@ -264,6 +262,7 @@ func New(
|
|||
r,
|
||||
roomState,
|
||||
flashHelper,
|
||||
locHelper,
|
||||
admin.Databases{
|
||||
Aliases: dbs.Aliases,
|
||||
Config: dbs.Config,
|
||||
|
@ -283,14 +282,14 @@ func New(
|
|||
|
||||
session, err := cookieStore.Get(req, i18n.LanguageCookieName)
|
||||
if err != nil {
|
||||
fmt.Printf("cookie error? %w\n", err)
|
||||
fmt.Errorf("cookie error? %w\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
session.Values["lang"] = lang
|
||||
err = session.Save(req, w)
|
||||
if err != nil {
|
||||
fmt.Printf("we failed to save the language session cookie %w\n", err)
|
||||
fmt.Errorf("we failed to save the language session cookie %w\n", err)
|
||||
}
|
||||
|
||||
http.Redirect(w, req, previousRoute, http.StatusSeeOther)
|
||||
|
|
|
@ -83,6 +83,10 @@ ExplanationOpen = "Open invite codes, anyone may connect"
|
|||
ExplanationCommunity = "Members can create invites, anyone may connect"
|
||||
ExplanationRestricted = "Only admins/mods can create invites, only members may connect"
|
||||
|
||||
DefaultLanguageTitle = "Default Language"
|
||||
ExplanationDefaultLanguage = "The default language option controls the room web interface language displayed for first time visitors. The available languages options are defined by the installed translation files."
|
||||
SetDefaultLanguageTitle = "Set Default Language"
|
||||
|
||||
Settings = "Settings"
|
||||
|
||||
# banned dashboard
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/BurntSushi/toml"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web"
|
||||
"go.mindeco.de/http/render"
|
||||
"golang.org/x/text/language"
|
||||
|
@ -29,9 +30,10 @@ type Helper struct {
|
|||
bundle *i18n.Bundle
|
||||
languages map[string]string
|
||||
cookieStore *sessions.CookieStore
|
||||
config roomdb.RoomConfig
|
||||
}
|
||||
|
||||
func New(r repo.Interface) (*Helper, error) {
|
||||
func New(r repo.Interface, config roomdb.RoomConfig) (*Helper, error) {
|
||||
bundle := i18n.NewBundle(language.English)
|
||||
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
|
||||
|
||||
|
@ -134,7 +136,7 @@ func New(r repo.Interface) (*Helper, error) {
|
|||
|
||||
// create a mapping of language tags to the translated language names
|
||||
langmap := listLanguages(bundle)
|
||||
return &Helper{bundle: bundle, languages: langmap, cookieStore: cookieStore}, nil
|
||||
return &Helper{bundle: bundle, languages: langmap, cookieStore: cookieStore, config: config}, nil
|
||||
}
|
||||
|
||||
func listLanguages(bundle *i18n.Bundle) map[string]string {
|
||||
|
@ -164,6 +166,13 @@ func (h Helper) ListLanguages() map[string]string {
|
|||
return h.languages
|
||||
}
|
||||
|
||||
func (h Helper) ChooseTranslation(tag string) string {
|
||||
if translation, ok := h.languages[tag]; ok {
|
||||
return translation
|
||||
}
|
||||
return tag
|
||||
}
|
||||
|
||||
type Localizer struct {
|
||||
loc *i18n.Localizer
|
||||
}
|
||||
|
@ -193,7 +202,15 @@ func (h Helper) FromRequest(r *http.Request) *Localizer {
|
|||
return h.newLocalizer(prevCookie.(string), lang, accept)
|
||||
}
|
||||
|
||||
return h.newLocalizer(lang, accept)
|
||||
defaultLang, err := h.config.GetDefaultLanguage(r.Context())
|
||||
|
||||
// if we don't have a default language set, then fallback to whatever we have left :^)
|
||||
if err != nil {
|
||||
return h.newLocalizer(lang, accept)
|
||||
}
|
||||
|
||||
// if we don't have a user cookie set, then use the room's default language setting
|
||||
return h.newLocalizer(defaultLang, accept)
|
||||
}
|
||||
|
||||
func (h Helper) GetRenderFuncs() []render.Option {
|
||||
|
|
|
@ -6,13 +6,16 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb/mockdb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web/i18n"
|
||||
)
|
||||
|
||||
func TestListAllLanguages(t *testing.T) {
|
||||
configDB := new(mockdb.FakeRoomConfig)
|
||||
configDB.GetDefaultLanguageReturns("en", nil)
|
||||
r := repo.New(filepath.Join("testrun", t.Name()))
|
||||
a := assert.New(t)
|
||||
helper, err := i18n.New(r)
|
||||
helper, err := i18n.New(r, configDB)
|
||||
a.NoError(err)
|
||||
t.Log(helper)
|
||||
langmap := helper.ListLanguages()
|
||||
|
|
|
@ -9,8 +9,9 @@ const (
|
|||
AdminDashboard = "admin:dashboard"
|
||||
AdminMenu = "admin:menu"
|
||||
|
||||
AdminSettings = "admin:settings:overview"
|
||||
AdminSettingsSetPrivacy = "admin:settings:set-privacy"
|
||||
AdminSettings = "admin:settings:overview"
|
||||
AdminSettingsSetPrivacy = "admin:settings:set-privacy"
|
||||
AdminSettingsSetLanguage = "admin:settings:set-language"
|
||||
|
||||
AdminAliasesRevokeConfirm = "admin:aliases:revoke:confirm"
|
||||
AdminAliasesRevoke = "admin:aliases:revoke"
|
||||
|
@ -49,6 +50,7 @@ func Admin(m *mux.Router) *mux.Router {
|
|||
|
||||
m.Path("/settings").Methods("GET").Name(AdminSettings)
|
||||
m.Path("/settings/set-privacy").Methods("POST").Name(AdminSettingsSetPrivacy)
|
||||
m.Path("/settings/set-language").Methods("POST").Name(AdminSettingsSetLanguage)
|
||||
|
||||
m.Path("/menu").Methods("GET").Name(AdminMenu)
|
||||
|
||||
|
|
|
@ -4,55 +4,71 @@
|
|||
class="text-3xl tracking-tight font-black text-black mt-2 mb-0"
|
||||
>{{ i18n "Settings" }}</h1>
|
||||
|
||||
<div class="flex flex-col-reverse sm:flex-row justify-start items-stretch ">
|
||||
<div class="max-w-lg">
|
||||
<h2 class="text-xl tracking-tight font-bold text-black mt-2 mb-2">{{ i18n "PrivacyModesTitle" }}</h2>
|
||||
<p class="mb-4">
|
||||
{{ i18n "ExplanationPrivacyModes" }}
|
||||
<a class="text-pink-600 underline" href="https://ssb-ngi-pointer.github.io/rooms2/#privacy-modes">{{ i18n "RoomsSpecification" }}</a>.
|
||||
</p>
|
||||
<h3 class="text-gray-400 text-sm font-bold mb-2">{{ i18n "SetPrivacyModeTitle" }}</h3>
|
||||
<details class="mb-8 self-start w-96" id="change-privacy">
|
||||
<summary class="px-3 py-1 w-96 rounded shadow bg-white ring-1 ring-gray-300 hover:bg-gray-100 cursor-pointer">
|
||||
{{ i18n .CurrentMode.String }}
|
||||
</summary>
|
||||
<div class="max-w-2xl">
|
||||
<h2 class="text-xl tracking-tight font-bold text-black mt-2 mb-2">{{ i18n "PrivacyModesTitle" }}</h2>
|
||||
<p class="mb-4">
|
||||
{{ i18n "ExplanationPrivacyModes" }}
|
||||
<a class="text-pink-600 underline" href="https://ssb-ngi-pointer.github.io/rooms2/#privacy-modes">{{ i18n "RoomsSpecification" }}</a>.
|
||||
</p>
|
||||
<h3 class="text-gray-400 text-sm font-bold mb-2">{{ i18n "SetPrivacyModeTitle" }}</h3>
|
||||
<details class="mb-8 self-start max-w-sm" id="change-privacy">
|
||||
<summary class="px-3 py-1 max-w-sm rounded shadow bg-white ring-1 ring-gray-300 hover:bg-gray-100 cursor-pointer">
|
||||
{{ i18n .CurrentMode.String }}
|
||||
</summary>
|
||||
|
||||
<div class="absolute w-96 z-10 bg-white mt-2 shadow-xl ring-1 ring-gray-200 rounded divide-y flex flex-col items-stretch overflow-hidden">
|
||||
{{ range .PrivacyModes }}
|
||||
{{ if ne . $.CurrentMode }}
|
||||
<form
|
||||
action="{{ urlTo "admin:settings:set-privacy" }}"
|
||||
method="POST"
|
||||
>
|
||||
{{ $.csrfField }}
|
||||
<input type="hidden" name="privacy_mode" value="{{.}}">
|
||||
<input
|
||||
type="submit"
|
||||
value="{{ i18n .String }}"
|
||||
class="pl-10 pr-3 py-2 w-full text-left bg-white text-gray-700 hover:text-gray-900 hover:bg-gray-50 cursor-pointer"
|
||||
/>
|
||||
</form>
|
||||
{{ else }}
|
||||
<div class="pr-3 py-2 text-gray-600 flex flex-row items-center cursor-default">
|
||||
<div class="w-10 flex flex-row items-center justify-center">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span>{{ i18n .String }}</span>
|
||||
<div class="absolute max-w-sm z-10 bg-white mt-2 shadow-xl ring-1 ring-gray-200 rounded divide-y flex flex-col items-stretch overflow-hidden">
|
||||
{{ range .PrivacyModes }}
|
||||
{{ if ne . $.CurrentMode }}
|
||||
<form
|
||||
action="{{ urlTo "admin:settings:set-privacy" }}"
|
||||
method="POST"
|
||||
>
|
||||
{{ $.csrfField }}
|
||||
<input type="hidden" name="privacy_mode" value="{{.}}">
|
||||
<input
|
||||
type="submit"
|
||||
value="{{ i18n .String }}"
|
||||
class="pl-10 pr-3 py-2 w-full text-left bg-white text-gray-700 hover:text-gray-900 hover:bg-gray-50 cursor-pointer"
|
||||
/>
|
||||
</form>
|
||||
{{ else }}
|
||||
<div class="pr-3 py-2 text-gray-600 flex flex-row items-center cursor-default">
|
||||
<div class="w-10 flex flex-row items-center justify-center">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z" />
|
||||
</svg>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</details>
|
||||
<div class="grid max-w-lg grid-cols-3 gap-y-2 mb-8">
|
||||
<div class="text-xl text-gray-500 font-bold">{{ i18n "ModeOpen" }}</div>
|
||||
<div class="text-md col-span-2 italic">{{ i18n "ExplanationOpen" }}</div>
|
||||
<div class="text-xl text-gray-500 font-bold">{{ i18n "ModeCommunity" }}</div>
|
||||
<div class="text-md col-span-2 italic">{{ i18n "ExplanationCommunity" }}</div>
|
||||
<div class="text-xl text-gray-500 font-bold">{{ i18n "ModeRestricted" }}</div>
|
||||
<div class="text-md col-span-2 italic">{{ i18n "ExplanationRestricted" }}</div>
|
||||
</div>
|
||||
<span>{{ i18n .String }}</span>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</details>
|
||||
<div class="grid max-w-lg grid-cols-3 gap-y-2 mb-8">
|
||||
<div class="text-xl text-gray-500 font-bold">{{ i18n "ModeOpen" }}</div>
|
||||
<div class="text-md col-span-2 italic">{{ i18n "ExplanationOpen" }}</div>
|
||||
<div class="text-xl text-gray-500 font-bold">{{ i18n "ModeCommunity" }}</div>
|
||||
<div class="text-md col-span-2 italic">{{ i18n "ExplanationCommunity" }}</div>
|
||||
<div class="text-xl text-gray-500 font-bold">{{ i18n "ModeRestricted" }}</div>
|
||||
<div class="text-md col-span-2 italic">{{ i18n "ExplanationRestricted" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="max-w-2xl">
|
||||
<h2 class="text-xl tracking-tight font-bold text-black mt-2 mb-2">{{ i18n "DefaultLanguageTitle" }}</h2>
|
||||
<p class="mb-4">
|
||||
{{ i18n "ExplanationDefaultLanguage" }}
|
||||
</p>
|
||||
<h3 class="text-gray-400 text-sm font-bold mb-2">{{ i18n "SetDefaultLanguageTitle" }}</h3>
|
||||
<details class="mb-8 self-start max-w-sm">
|
||||
<summary class="px-3 py-1 max-w-sm rounded shadow bg-white ring-1 ring-gray-300 hover:bg-gray-100 cursor-pointer">
|
||||
{{ $.CurrentLanguage }}
|
||||
</summary>
|
||||
<div class="mt-2 bg-gray bg-white px-3 rounded ring-1 ring-gray-300">
|
||||
{{ $adminSetLanguageUrl := urlTo "admin:settings:set-language" }}
|
||||
{{ list_languages $adminSetLanguageUrl "pl-10 pr-3 py-2 w-full text-left bg-white text-gray-700 hover:text-gray-900 hover:bg-gray-50 cursor-pointer" }}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
|
@ -70,6 +70,7 @@
|
|||
{{block "footer" .}}
|
||||
{{$cocUrl := urlToNotice "NoticeCodeOfConduct"}}
|
||||
{{$ppUrl := urlToNotice "NoticePrivacyPolicy"}}
|
||||
{{$setLanguageUrl := urlTo "complete:set-language"}}
|
||||
<footer class="grid auto-rows-min mb-12">
|
||||
<div class="mb-4 flex flex-row items-center justify-center divide-x divide-gray-300">
|
||||
<a
|
||||
|
@ -98,7 +99,7 @@
|
|||
Language
|
||||
</summary>
|
||||
<div class="grid grid-cols-2 justify-items-center gap-x-1">
|
||||
{{ list_languages }}
|
||||
{{ list_languages $setLanguageUrl "text-gray-500 hover:underline cursor-pointer" }}
|
||||
</div>
|
||||
</details>
|
||||
{{ end }}
|
||||
|
|
Loading…
Reference in New Issue