diff options
author | guillep2k <18600385+guillep2k@users.noreply.github.com> | 2019-11-07 10:34:28 -0300 |
---|---|---|
committer | Lunny Xiao <xiaolunwen@gmail.com> | 2019-11-07 21:34:28 +0800 |
commit | 1f90147f3942065e2a7f564e8a3c97d23d41e6c0 (patch) | |
tree | fd5fb0bda2d84deb179bf99f9172b28f736ba9fe /modules/templates | |
parent | d5b1e6bc51f87eb1be07a4682798428bf4bbb9ce (diff) | |
download | gitea-1f90147f3942065e2a7f564e8a3c97d23d41e6c0.tar.gz gitea-1f90147f3942065e2a7f564e8a3c97d23d41e6c0.zip |
Use templates for issue e-mail subject and body (#8329)
* Add template capability for issue mail subject
* Remove test string
* Fix trim subject length
* Add comment to template and run make fmt
* Add information for the template
* Rename defaultMailSubject() to fallbackMailSubject()
* General rewrite of the mail template code
* Fix .Doer name
* Use text/template for subject instead of html
* Fix subject Re: prefix
* Fix mail tests
* Fix static templates
* [skip ci] Updated translations via Crowdin
* Expose db.SetMaxOpenConns and allow non MySQL dbs to set conn pool params (#8528)
* Expose db.SetMaxOpenConns and allow other dbs to set their connection params
* Add note about port exhaustion
Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>
* Prevent .code-view from overriding font on icon fonts (#8614)
* Correct some outdated statements in the contributing guidelines (#8612)
* More information for drone-cli in CONTRIBUTING.md
* Increases the version of drone-cli to 1.2.0
* Adds a note for the Docker Toolbox on Windows
Signed-off-by: LukBukkit <luk.bukkit@gmail.com>
* Fix the url for the blog repository (now on gitea.com)
Signed-off-by: LukBukkit <luk.bukkit@gmail.com>
* Remove TrN due to lack of lang context
* Redo templates to match previous code
* Fix extra character in template
* Unify PR & Issue tempaltes, fix format
* Remove default subject
* Add template tests
* Fix template
* Remove replaced function
* Provide User as models.User for better consistency
* Add docs
* Fix doc inaccuracies, improve examples
* Change mail footer to math AppName
* Add test for mail subject/body template separation
* Add support for code review comments
* Update docs/content/doc/advanced/mail-templates-us.md
Co-Authored-By: 6543 <24977596+6543@users.noreply.github.com>
Diffstat (limited to 'modules/templates')
-rw-r--r-- | modules/templates/dynamic.go | 33 | ||||
-rw-r--r-- | modules/templates/helper.go | 130 | ||||
-rw-r--r-- | modules/templates/helper_test.go | 55 | ||||
-rw-r--r-- | modules/templates/static.go | 23 |
4 files changed, 211 insertions, 30 deletions
diff --git a/modules/templates/dynamic.go b/modules/templates/dynamic.go index 6217f1c3b0..6153e8d027 100644 --- a/modules/templates/dynamic.go +++ b/modules/templates/dynamic.go @@ -11,6 +11,7 @@ import ( "io/ioutil" "path" "strings" + texttmpl "text/template" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -20,7 +21,8 @@ import ( ) var ( - templates = template.New("") + subjectTemplates = texttmpl.New("") + bodyTemplates = template.New("") ) // HTMLRenderer implements the macaron handler for serving HTML templates. @@ -59,9 +61,12 @@ func JSRenderer() macaron.Handler { } // Mailer provides the templates required for sending notification mails. -func Mailer() *template.Template { +func Mailer() (*texttmpl.Template, *template.Template) { + for _, funcs := range NewTextFuncMap() { + subjectTemplates.Funcs(funcs) + } for _, funcs := range NewFuncMap() { - templates.Funcs(funcs) + bodyTemplates.Funcs(funcs) } staticDir := path.Join(setting.StaticRootPath, "templates", "mail") @@ -84,15 +89,7 @@ func Mailer() *template.Template { continue } - _, err = templates.New( - strings.TrimSuffix( - filePath, - ".tmpl", - ), - ).Parse(string(content)) - if err != nil { - log.Warn("Failed to parse template %v", err) - } + buildSubjectBodyTemplate(subjectTemplates, bodyTemplates, strings.TrimSuffix(filePath, ".tmpl"), content) } } } @@ -117,18 +114,10 @@ func Mailer() *template.Template { continue } - _, err = templates.New( - strings.TrimSuffix( - filePath, - ".tmpl", - ), - ).Parse(string(content)) - if err != nil { - log.Warn("Failed to parse template %v", err) - } + buildSubjectBodyTemplate(subjectTemplates, bodyTemplates, strings.TrimSuffix(filePath, ".tmpl"), content) } } } - return templates + return subjectTemplates, bodyTemplates } diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 2d7a1aee9b..1347835b80 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -16,8 +16,10 @@ import ( "mime" "net/url" "path/filepath" + "regexp" "runtime" "strings" + texttmpl "text/template" "time" "unicode" @@ -34,6 +36,9 @@ import ( "github.com/editorconfig/editorconfig-core-go/v2" ) +// Used from static.go && dynamic.go +var mailSubjectSplit = regexp.MustCompile(`(?m)^-{3,}[\s]*$`) + // NewFuncMap returns functions for injecting to templates func NewFuncMap() []template.FuncMap { return []template.FuncMap{map[string]interface{}{ @@ -261,6 +266,112 @@ func NewFuncMap() []template.FuncMap { }} } +// NewTextFuncMap returns functions for injecting to text templates +// It's a subset of those used for HTML and other templates +func NewTextFuncMap() []texttmpl.FuncMap { + return []texttmpl.FuncMap{map[string]interface{}{ + "GoVer": func() string { + return strings.Title(runtime.Version()) + }, + "AppName": func() string { + return setting.AppName + }, + "AppSubUrl": func() string { + return setting.AppSubURL + }, + "AppUrl": func() string { + return setting.AppURL + }, + "AppVer": func() string { + return setting.AppVer + }, + "AppBuiltWith": func() string { + return setting.AppBuiltWith + }, + "AppDomain": func() string { + return setting.Domain + }, + "TimeSince": timeutil.TimeSince, + "TimeSinceUnix": timeutil.TimeSinceUnix, + "RawTimeSince": timeutil.RawTimeSince, + "DateFmtLong": func(t time.Time) string { + return t.Format(time.RFC1123Z) + }, + "DateFmtShort": func(t time.Time) string { + return t.Format("Jan 02, 2006") + }, + "List": List, + "SubStr": func(str string, start, length int) string { + if len(str) == 0 { + return "" + } + end := start + length + if length == -1 { + end = len(str) + } + if len(str) < end { + return str + } + return str[start:end] + }, + "EllipsisString": base.EllipsisString, + "URLJoin": util.URLJoin, + "Dict": func(values ...interface{}) (map[string]interface{}, error) { + if len(values)%2 != 0 { + return nil, errors.New("invalid dict call") + } + dict := make(map[string]interface{}, len(values)/2) + for i := 0; i < len(values); i += 2 { + key, ok := values[i].(string) + if !ok { + return nil, errors.New("dict keys must be strings") + } + dict[key] = values[i+1] + } + return dict, nil + }, + "Printf": fmt.Sprintf, + "Escape": Escape, + "Sec2Time": models.SecToTime, + "ParseDeadline": func(deadline string) []string { + return strings.Split(deadline, "|") + }, + "dict": func(values ...interface{}) (map[string]interface{}, error) { + if len(values) == 0 { + return nil, errors.New("invalid dict call") + } + + dict := make(map[string]interface{}) + + for i := 0; i < len(values); i++ { + switch key := values[i].(type) { + case string: + i++ + if i == len(values) { + return nil, errors.New("specify the key for non array values") + } + dict[key] = values[i] + case map[string]interface{}: + m := values[i].(map[string]interface{}) + for i, v := range m { + dict[i] = v + } + default: + return nil, errors.New("dict values must be maps") + } + } + return dict, nil + }, + "percentage": func(n int, values ...int) float32 { + var sum = 0 + for i := 0; i < len(values); i++ { + sum += values[i] + } + return float32(n) * 100 / float32(sum) + }, + }} +} + // Safe render raw as HTML func Safe(raw string) template.HTML { return template.HTML(raw) @@ -551,3 +662,22 @@ func MigrationIcon(hostname string) string { return "fa-git-alt" } } + +func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template, name string, content []byte) { + // Split template into subject and body + var subjectContent []byte + bodyContent := content + loc := mailSubjectSplit.FindIndex(content) + if loc != nil { + subjectContent = content[0:loc[0]] + bodyContent = content[loc[1]:] + } + if _, err := stpl.New(name). + Parse(string(subjectContent)); err != nil { + log.Warn("Failed to parse template [%s/subject]: %v", name, err) + } + if _, err := btpl.New(name). + Parse(string(bodyContent)); err != nil { + log.Warn("Failed to parse template [%s/body]: %v", name, err) + } +} diff --git a/modules/templates/helper_test.go b/modules/templates/helper_test.go new file mode 100644 index 0000000000..e2997cb853 --- /dev/null +++ b/modules/templates/helper_test.go @@ -0,0 +1,55 @@ +// 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 templates + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSubjectBodySeparator(t *testing.T) { + test := func(input, subject, body string) { + loc := mailSubjectSplit.FindIndex([]byte(input)) + if loc == nil { + assert.Empty(t, subject, "no subject found, but one expected") + assert.Equal(t, body, input) + } else { + assert.Equal(t, subject, string(input[0:loc[0]])) + assert.Equal(t, body, string(input[loc[1]:])) + } + } + + test("Simple\n---------------\nCase", + "Simple\n", + "\nCase") + test("Only\nBody", + "", + "Only\nBody") + test("Minimal\n---\nseparator", + "Minimal\n", + "\nseparator") + test("False --- separator", + "", + "False --- separator") + test("False\n--- separator", + "", + "False\n--- separator") + test("False ---\nseparator", + "", + "False ---\nseparator") + test("With extra spaces\n----- \t \nBody", + "With extra spaces\n", + "\nBody") + test("With leading spaces\n -------\nOnly body", + "", + "With leading spaces\n -------\nOnly body") + test("Multiple\n---\n-------\n---\nSeparators", + "Multiple\n", + "\n-------\n---\nSeparators") + test("Insuficient\n--\nSeparators", + "", + "Insuficient\n--\nSeparators") +} diff --git a/modules/templates/static.go b/modules/templates/static.go index f7e53ce887..435ccb1f95 100644 --- a/modules/templates/static.go +++ b/modules/templates/static.go @@ -14,6 +14,7 @@ import ( "io/ioutil" "path" "strings" + texttmpl "text/template" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -23,7 +24,8 @@ import ( ) var ( - templates = template.New("") + subjectTemplates = texttmpl.New("") + bodyTemplates = template.New("") ) type templateFileSystem struct { @@ -140,9 +142,12 @@ func JSRenderer() macaron.Handler { } // Mailer provides the templates required for sending notification mails. -func Mailer() *template.Template { +func Mailer() (*texttmpl.Template, *template.Template) { + for _, funcs := range NewTextFuncMap() { + subjectTemplates.Funcs(funcs) + } for _, funcs := range NewFuncMap() { - templates.Funcs(funcs) + bodyTemplates.Funcs(funcs) } for _, assetPath := range AssetNames() { @@ -161,7 +166,8 @@ func Mailer() *template.Template { continue } - templates.New( + buildSubjectBodyTemplate(subjectTemplates, + bodyTemplates, strings.TrimPrefix( strings.TrimSuffix( assetPath, @@ -169,7 +175,7 @@ func Mailer() *template.Template { ), "mail/", ), - ).Parse(string(content)) + content) } customDir := path.Join(setting.CustomPath, "templates", "mail") @@ -192,17 +198,18 @@ func Mailer() *template.Template { continue } - templates.New( + buildSubjectBodyTemplate(subjectTemplates, + bodyTemplates, strings.TrimSuffix( filePath, ".tmpl", ), - ).Parse(string(content)) + content) } } } - return templates + return subjectTemplates, bodyTemplates } func Asset(name string) ([]byte, error) { |