summaryrefslogtreecommitdiffstats
path: root/modules/templates
diff options
context:
space:
mode:
authorguillep2k <18600385+guillep2k@users.noreply.github.com>2019-11-07 10:34:28 -0300
committerLunny Xiao <xiaolunwen@gmail.com>2019-11-07 21:34:28 +0800
commit1f90147f3942065e2a7f564e8a3c97d23d41e6c0 (patch)
treefd5fb0bda2d84deb179bf99f9172b28f736ba9fe /modules/templates
parentd5b1e6bc51f87eb1be07a4682798428bf4bbb9ce (diff)
downloadgitea-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.go33
-rw-r--r--modules/templates/helper.go130
-rw-r--r--modules/templates/helper_test.go55
-rw-r--r--modules/templates/static.go23
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) {