From 8d2c24f7f9b9bce3a806e4748623bd3b2742025b Mon Sep 17 00:00:00 2001 From: Lanre Adelowo Date: Wed, 9 Jan 2019 18:22:57 +0100 Subject: [PATCH] Allow for user specific themes (#5668) * add migration and basic UI for changing a user's theme * update user themem * use right text on button * load theme based on users' selection * load theme based on users' selection in pwa too * update sample config * delete older theme loading * implement AfterLoad to set users' theme properly * set up default theme when creating a user. This uses the installation wide theme * use flash messages for error * set default theme when creating a user from the cli * fix @lunny review --- cmd/admin.go | 1 + custom/conf/app.ini.sample | 2 ++ models/migrations/migrations.go | 4 ++- models/migrations/v77.go | 17 +++++++++++++ models/user.go | 15 +++++++++++ modules/auth/user_form.go | 26 ++++++++++++++++++- modules/setting/defaults.go | 1 + modules/setting/setting.go | 8 +++--- options/locale/locale_en-US.ini | 6 +++++ routers/routes/routes.go | 4 ++- routers/user/setting/account.go | 28 ++++++++++++++++++++ templates/base/head.tmpl | 8 ++++-- templates/pwa/serviceworker_js.tmpl | 10 +++++--- templates/user/settings/account.tmpl | 38 ++++++++++++++++++++++++++++ 14 files changed, 157 insertions(+), 11 deletions(-) create mode 100644 models/migrations/v77.go diff --git a/cmd/admin.go b/cmd/admin.go index ac911f184b..0934a7364b 100644 --- a/cmd/admin.go +++ b/cmd/admin.go @@ -340,6 +340,7 @@ func runCreateUser(c *cli.Context) error { IsActive: true, IsAdmin: c.Bool("admin"), MustChangePassword: changePassword, + Theme: setting.UI.DefaultTheme, }); err != nil { return fmt.Errorf("CreateUser: %v", err) } diff --git a/custom/conf/app.ini.sample b/custom/conf/app.ini.sample index 0b24bf3414..31048f7857 100644 --- a/custom/conf/app.ini.sample +++ b/custom/conf/app.ini.sample @@ -85,6 +85,8 @@ MAX_DISPLAY_FILE_SIZE = 8388608 SHOW_USER_EMAIL = true ; Set the default theme for the Gitea install DEFAULT_THEME = gitea +; All available themes +THEMES = gitea,arc-green [ui.admin] ; Number of users that are displayed on one page diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 1fa94c42aa..4c6b6d64bc 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -18,7 +18,7 @@ import ( "github.com/Unknwon/com" "github.com/go-xorm/xorm" gouuid "github.com/satori/go.uuid" - "gopkg.in/ini.v1" + ini "gopkg.in/ini.v1" "code.gitea.io/gitea/modules/generate" "code.gitea.io/gitea/modules/log" @@ -206,6 +206,8 @@ var migrations = []Migration{ NewMigration("clear nonused data which not deleted when user was deleted", clearNonusedData), // v76 -> v77 NewMigration("add pull request rebase with merge commit", addPullRequestRebaseWithMerge), + // v77 -> v78 + NewMigration("add theme to users", addUserDefaultTheme), } // Migrate database to current version diff --git a/models/migrations/v77.go b/models/migrations/v77.go new file mode 100644 index 0000000000..12e7456642 --- /dev/null +++ b/models/migrations/v77.go @@ -0,0 +1,17 @@ +// 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 migrations + +import ( + "github.com/go-xorm/xorm" +) + +func addUserDefaultTheme(x *xorm.Engine) error { + type User struct { + Theme string `xorm:"VARCHAR(30)"` + } + + return x.Sync2(new(User)) +} diff --git a/models/user.go b/models/user.go index 3a47346e34..764c2280d7 100644 --- a/models/user.go +++ b/models/user.go @@ -140,6 +140,7 @@ type User struct { // Preferences DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"` + Theme string `xorm:"NOT NULL DEFAULT ''"` } // BeforeUpdate is invoked from XORM before updating this object. @@ -165,6 +166,13 @@ func (u *User) BeforeUpdate() { u.Description = base.TruncateString(u.Description, 255) } +// AfterLoad is invoked from XORM after filling all the fields of this object. +func (u *User) AfterLoad() { + if u.Theme == "" { + u.Theme = setting.UI.DefaultTheme + } +} + // SetLastLogin set time to last login func (u *User) SetLastLogin() { u.LastLoginUnix = util.TimeStampNow() @@ -176,6 +184,12 @@ func (u *User) UpdateDiffViewStyle(style string) error { return UpdateUserCols(u, "diff_view_style") } +// UpdateTheme updates a users' theme irrespective of the site wide theme +func (u *User) UpdateTheme(themeName string) error { + u.Theme = themeName + return UpdateUserCols(u, "theme") +} + // getEmail returns an noreply email, if the user has set to keep his // email address private, otherwise the primary email address. func (u *User) getEmail() string { @@ -777,6 +791,7 @@ func CreateUser(u *User) (err error) { u.HashPassword(u.Passwd) u.AllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization u.MaxRepoCreation = -1 + u.Theme = setting.UI.DefaultTheme if _, err = sess.Insert(u); err != nil { return err diff --git a/modules/auth/user_form.go b/modules/auth/user_form.go index c281672fe1..f59b795b4e 100644 --- a/modules/auth/user_form.go +++ b/modules/auth/user_form.go @@ -12,7 +12,7 @@ import ( "code.gitea.io/gitea/modules/setting" "github.com/go-macaron/binding" - "gopkg.in/macaron.v1" + macaron "gopkg.in/macaron.v1" ) // InstallForm form for installation page @@ -189,6 +189,30 @@ func (f *AddEmailForm) Validate(ctx *macaron.Context, errs binding.Errors) bindi return validate(errs, ctx.Data, f, ctx.Locale) } +// UpdateThemeForm form for updating a users' theme +type UpdateThemeForm struct { + Theme string `binding:"Required;MaxSize(30)"` +} + +// Validate validates the field +func (f *UpdateThemeForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { + return validate(errs, ctx.Data, f, ctx.Locale) +} + +// IsThemeExists checks if the theme is a theme available in the config. +func (f UpdateThemeForm) IsThemeExists() bool { + var exists bool + + for _, v := range setting.UI.Themes { + if strings.ToLower(v) == strings.ToLower(f.Theme) { + exists = true + break + } + } + + return exists +} + // ChangePasswordForm form for changing password type ChangePasswordForm struct { OldPassword string `form:"old_password" binding:"MaxSize(255)"` diff --git a/modules/setting/defaults.go b/modules/setting/defaults.go index 7630d10090..48257284c7 100644 --- a/modules/setting/defaults.go +++ b/modules/setting/defaults.go @@ -8,4 +8,5 @@ var ( defaultLangs = strings.Split("en-US,zh-CN,zh-HK,zh-TW,de-DE,fr-FR,nl-NL,lv-LV,ru-RU,uk-UA,ja-JP,es-ES,pt-BR,pl-PL,bg-BG,it-IT,fi-FI,tr-TR,cs-CZ,sr-SP,sv-SE,ko-KR", ",") defaultLangNames = strings.Split("English,简体中文,繁體中文(香港),繁體中文(台灣),Deutsch,français,Nederlands,latviešu,русский,Українська,日本語,español,português do Brasil,polski,български,italiano,suomi,Türkçe,čeština,српски,svenska,한국어", ",") defaultPullRequestWorkInProgressPrefixes = strings.Split("WIP:,[WIP]", ",") + defaultThemes = strings.Split("gitea", "arc-green") ) diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 23304453d2..88101937e9 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -33,9 +33,9 @@ import ( "github.com/go-macaron/session" _ "github.com/go-macaron/session/redis" // redis plugin for store session "github.com/go-xorm/core" - "github.com/kballard/go-shellquote" - "github.com/mcuadros/go-version" - "gopkg.in/ini.v1" + shellquote "github.com/kballard/go-shellquote" + version "github.com/mcuadros/go-version" + ini "gopkg.in/ini.v1" "strk.kbt.io/projects/go/libravatar" ) @@ -303,6 +303,7 @@ var ( MaxDisplayFileSize int64 ShowUserEmail bool DefaultTheme string + Themes []string Admin struct { UserPagingNum int @@ -329,6 +330,7 @@ var ( ThemeColorMetaTag: `#6cc644`, MaxDisplayFileSize: 8388608, DefaultTheme: `gitea`, + Themes: []string{`gitea`, `arc-green`}, Admin: struct { UserPagingNum int RepoPagingNum int diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 0835175f16..06fe12d10b 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -355,6 +355,7 @@ password_username_disabled = Non-local users are not allowed to change their use full_name = Full Name website = Website location = Location +update_theme = Update Theme update_profile = Update Profile update_profile_success = Your profile has been updated. change_username = Your username has been changed. @@ -362,6 +363,7 @@ change_username_prompt = Note: username changes also change your account URL. continue = Continue cancel = Cancel language = Language +ui = Theme lookup_avatar_by_mail = Look Up Avatar by Email Address federated_avatar_lookup = Federated Avatar Lookup @@ -382,14 +384,18 @@ password_change_disabled = Non-local users can not update their password through emails = Email Addresses manage_emails = Manage Email Addresses +manage_themes = Select default theme manage_openid = Manage OpenID Addresses email_desc = Your primary email address will be used for notifications and other operations. +theme_desc = This will be your default theme across the site. primary = Primary primary_email = Make Primary delete_email = Remove email_deletion = Remove Email Address email_deletion_desc = The email address and related information will be removed from your account. Git commits by this email address will remain unchanged. Continue? email_deletion_success = The email address has been removed. +theme_update_success = Your theme was updated. +theme_update_error = The selected theme does not exist. openid_deletion = Remove OpenID Address openid_deletion_desc = Removing this OpenID address from your account will prevent you from signing in with it. Continue? openid_deletion_success = The OpenID address has been removed. diff --git a/routers/routes/routes.go b/routers/routes/routes.go index b776d0c201..6011427321 100644 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -42,7 +42,7 @@ import ( "github.com/go-macaron/toolbox" "github.com/prometheus/client_golang/prometheus" "github.com/tstranex/u2f" - "gopkg.in/macaron.v1" + macaron "gopkg.in/macaron.v1" ) // NewMacaron initializes Macaron instance. @@ -243,6 +243,7 @@ func RegisterRoutes(m *macaron.Macaron) { m.Post("/email", bindIgnErr(auth.AddEmailForm{}), userSetting.EmailPost) m.Post("/email/delete", userSetting.DeleteEmail) m.Post("/delete", userSetting.DeleteAccount) + m.Post("/theme", bindIgnErr(auth.UpdateThemeForm{}), userSetting.UpdateUIThemePost) }) m.Group("/security", func() { m.Get("", userSetting.Security) @@ -292,6 +293,7 @@ func RegisterRoutes(m *macaron.Macaron) { }) }, reqSignIn, func(ctx *context.Context) { ctx.Data["PageIsUserSettings"] = true + ctx.Data["AllThemes"] = setting.UI.Themes }) m.Group("/user", func() { diff --git a/routers/user/setting/account.go b/routers/user/setting/account.go index bcf602c5ed..ff8631bf5b 100644 --- a/routers/user/setting/account.go +++ b/routers/user/setting/account.go @@ -168,6 +168,34 @@ func DeleteAccount(ctx *context.Context) { } } +// UpdateUIThemePost is used to update users' specific theme +func UpdateUIThemePost(ctx *context.Context, form auth.UpdateThemeForm) { + + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsAccount"] = true + + if ctx.HasError() { + ctx.Redirect(setting.AppSubURL + "/user/settings/account") + return + } + + if !form.IsThemeExists() { + ctx.Flash.Error(ctx.Tr("settings.theme_update_error")) + ctx.Redirect(setting.AppSubURL + "/user/settings/account") + return + } + + if err := ctx.User.UpdateTheme(form.Theme); err != nil { + ctx.Flash.Error(ctx.Tr("settings.theme_update_error")) + ctx.Redirect(setting.AppSubURL + "/user/settings/account") + return + } + + log.Trace("Update user theme: %s", ctx.User.Name) + ctx.Flash.Success(ctx.Tr("settings.theme_update_success")) + ctx.Redirect(setting.AppSubURL + "/user/settings/account") +} + func loadAccountData(ctx *context.Context) { emails, err := models.GetEmailAddresses(ctx.User.ID) if err != nil { diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl index 47c0d6b473..b6afe94bb4 100644 --- a/templates/base/head.tmpl +++ b/templates/base/head.tmpl @@ -6,7 +6,7 @@ {{if .Title}}{{.Title}} - {{end}}{{AppName}} - +