summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/admin.go12
-rw-r--r--api/admin_test.go70
-rw-r--r--api/api.go12
-rw-r--r--api/context.go7
-rw-r--r--api/license.go13
-rw-r--r--api/post.go35
-rw-r--r--api/team.go48
-rw-r--r--api/templates/email_change_body.html4
-rw-r--r--api/templates/email_change_subject.html2
-rw-r--r--api/templates/email_change_verify_body.html6
-rw-r--r--api/templates/email_change_verify_subject.html2
-rw-r--r--api/templates/email_footer.html2
-rw-r--r--api/templates/email_info.html4
-rw-r--r--api/templates/error.html8
-rw-r--r--api/templates/find_teams_body.html10
-rw-r--r--api/templates/find_teams_subject.html2
-rw-r--r--api/templates/invite_body.html8
-rw-r--r--api/templates/invite_subject.html2
-rw-r--r--api/templates/password_change_body.html4
-rw-r--r--api/templates/password_change_subject.html2
-rw-r--r--api/templates/post_body.html6
-rw-r--r--api/templates/post_subject.html2
-rw-r--r--api/templates/reset_body.html6
-rw-r--r--api/templates/reset_subject.html2
-rw-r--r--api/templates/signin_change_body.html4
-rw-r--r--api/templates/signin_change_subject.html2
-rw-r--r--api/templates/signup_team_body.html6
-rw-r--r--api/templates/signup_team_subject.html2
-rw-r--r--api/templates/verify_body.html6
-rw-r--r--api/templates/verify_subject.html2
-rw-r--r--api/templates/welcome_body.html10
-rw-r--r--api/templates/welcome_subject.html2
-rw-r--r--api/user.go121
-rw-r--r--api/web_conn.go7
-rw-r--r--api/web_hub.go3
-rw-r--r--api/web_socket.go7
-rw-r--r--api/web_team_hub.go3
-rw-r--r--api/webhook.go24
-rw-r--r--i18n/en.json450
-rw-r--r--i18n/es.json450
-rw-r--r--manualtesting/manual_testing.go12
-rw-r--r--manualtesting/test_autolink.go5
-rw-r--r--mattermost.go4
-rw-r--r--model/client.go11
-rw-r--r--model/user.go1
-rw-r--r--store/sql_channel_store.go16
-rw-r--r--store/sql_post_store.go52
-rw-r--r--store/sql_user_store.go27
-rw-r--r--store/store.go1
-rw-r--r--utils/config.go18
-rw-r--r--utils/config_test.go1
-rw-r--r--utils/license.go17
-rw-r--r--utils/lru.go2
-rw-r--r--utils/mail.go28
-rw-r--r--web/react/components/admin_console/admin_controller.jsx7
-rw-r--r--web/react/components/admin_console/admin_sidebar.jsx19
-rw-r--r--web/react/components/admin_console/analytics.jsx279
-rw-r--r--web/react/components/admin_console/system_analytics.jsx161
-rw-r--r--web/react/components/admin_console/team_analytics.jsx260
-rw-r--r--web/react/components/sidebar.jsx4
-rw-r--r--web/react/pages/admin_console.jsx77
-rw-r--r--web/react/pages/authorize.jsx75
-rw-r--r--web/react/pages/claim_account.jsx71
-rw-r--r--web/react/pages/docs.jsx64
-rw-r--r--web/react/pages/password_reset.jsx71
-rw-r--r--web/react/pages/signup_team.jsx63
-rw-r--r--web/react/pages/signup_team_complete.jsx67
-rw-r--r--web/react/pages/signup_user_complete.jsx73
-rw-r--r--web/react/pages/verify.jsx65
-rw-r--r--web/react/utils/client.jsx18
-rw-r--r--web/static/images/favicon/redfavicon-16x16.pngbin0 -> 18484 bytes
-rw-r--r--web/templates/admin_console.html6
-rw-r--r--web/web.go4
73 files changed, 2379 insertions, 568 deletions
diff --git a/api/admin.go b/api/admin.go
index bdacb3afb..b19772fdf 100644
--- a/api/admin.go
+++ b/api/admin.go
@@ -27,6 +27,7 @@ func InitAdmin(r *mux.Router) {
sr.Handle("/client_props", ApiAppHandler(getClientConfig)).Methods("GET")
sr.Handle("/log_client", ApiAppHandler(logClient)).Methods("POST")
sr.Handle("/analytics/{id:[A-Za-z0-9]+}/{name:[A-Za-z0-9_]+}", ApiAppHandler(getAnalytics)).Methods("GET")
+ sr.Handle("/analytics/{name:[A-Za-z0-9_]+}", ApiAppHandler(getAnalytics)).Methods("GET")
}
func getLogs(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -153,13 +154,15 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) {
name := params["name"]
if name == "standard" {
- var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 3)
+ var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 4)
rows[0] = &model.AnalyticsRow{"channel_open_count", 0}
rows[1] = &model.AnalyticsRow{"channel_private_count", 0}
rows[2] = &model.AnalyticsRow{"post_count", 0}
+ rows[3] = &model.AnalyticsRow{"unique_user_count", 0}
openChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_OPEN)
privateChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_PRIVATE)
postChan := Srv.Store.Post().AnalyticsPostCount(teamId)
+ userChan := Srv.Store.User().AnalyticsUniqueUserCount(teamId)
if r := <-openChan; r.Err != nil {
c.Err = r.Err
@@ -182,6 +185,13 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) {
rows[2].Value = float64(r.Data.(int64))
}
+ if r := <-userChan; r.Err != nil {
+ c.Err = r.Err
+ return
+ } else {
+ rows[3].Value = float64(r.Data.(int64))
+ }
+
w.Write([]byte(rows.ToJson()))
} else if name == "post_counts_day" {
if r := <-Srv.Store.Post().AnalyticsPostCountsByDay(teamId); r.Err != nil {
diff --git a/api/admin_test.go b/api/admin_test.go
index f7b6a7eeb..c2f4e9c76 100644
--- a/api/admin_test.go
+++ b/api/admin_test.go
@@ -151,7 +151,7 @@ func TestEmailTest(t *testing.T) {
}
}
-func TestGetAnalyticsStandard(t *testing.T) {
+func TestGetTeamAnalyticsStandard(t *testing.T) {
Setup()
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
@@ -169,7 +169,7 @@ func TestGetAnalyticsStandard(t *testing.T) {
post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post)
- if _, err := Client.GetAnalytics(team.Id, "standard"); err == nil {
+ if _, err := Client.GetTeamAnalytics(team.Id, "standard"); err == nil {
t.Fatal("Shouldn't have permissions")
}
@@ -180,7 +180,7 @@ func TestGetAnalyticsStandard(t *testing.T) {
Client.LoginByEmail(team.Name, user.Email, "pwd")
- if result, err := Client.GetAnalytics(team.Id, "standard"); err != nil {
+ if result, err := Client.GetTeamAnalytics(team.Id, "standard"); err != nil {
t.Fatal(err)
} else {
rows := result.Data.(model.AnalyticsRows)
@@ -214,6 +214,62 @@ func TestGetAnalyticsStandard(t *testing.T) {
t.Log(rows.ToJson())
t.Fatal()
}
+
+ if rows[3].Name != "unique_user_count" {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+
+ if rows[3].Value != 1 {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+ }
+
+ if result, err := Client.GetSystemAnalytics("standard"); err != nil {
+ t.Fatal(err)
+ } else {
+ rows := result.Data.(model.AnalyticsRows)
+
+ if rows[0].Name != "channel_open_count" {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+
+ if rows[0].Value < 2 {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+
+ if rows[1].Name != "channel_private_count" {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+
+ if rows[1].Value == 0 {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+
+ if rows[2].Name != "post_count" {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+
+ if rows[2].Value == 0 {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+
+ if rows[3].Name != "unique_user_count" {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+
+ if rows[3].Value == 0 {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
}
}
@@ -239,7 +295,7 @@ func TestGetPostCount(t *testing.T) {
Srv.Store.(*store.SqlStore).GetMaster().Exec("UPDATE Posts SET CreateAt = :CreateAt WHERE ChannelId = :ChannelId",
map[string]interface{}{"ChannelId": channel1.Id, "CreateAt": utils.MillisFromTime(utils.Yesterday())})
- if _, err := Client.GetAnalytics(team.Id, "post_counts_day"); err == nil {
+ if _, err := Client.GetTeamAnalytics(team.Id, "post_counts_day"); err == nil {
t.Fatal("Shouldn't have permissions")
}
@@ -250,7 +306,7 @@ func TestGetPostCount(t *testing.T) {
Client.LoginByEmail(team.Name, user.Email, "pwd")
- if result, err := Client.GetAnalytics(team.Id, "post_counts_day"); err != nil {
+ if result, err := Client.GetTeamAnalytics(team.Id, "post_counts_day"); err != nil {
t.Fatal(err)
} else {
rows := result.Data.(model.AnalyticsRows)
@@ -284,7 +340,7 @@ func TestUserCountsWithPostsByDay(t *testing.T) {
Srv.Store.(*store.SqlStore).GetMaster().Exec("UPDATE Posts SET CreateAt = :CreateAt WHERE ChannelId = :ChannelId",
map[string]interface{}{"ChannelId": channel1.Id, "CreateAt": utils.MillisFromTime(utils.Yesterday())})
- if _, err := Client.GetAnalytics(team.Id, "user_counts_with_posts_day"); err == nil {
+ if _, err := Client.GetTeamAnalytics(team.Id, "user_counts_with_posts_day"); err == nil {
t.Fatal("Shouldn't have permissions")
}
@@ -295,7 +351,7 @@ func TestUserCountsWithPostsByDay(t *testing.T) {
Client.LoginByEmail(team.Name, user.Email, "pwd")
- if result, err := Client.GetAnalytics(team.Id, "user_counts_with_posts_day"); err != nil {
+ if result, err := Client.GetTeamAnalytics(team.Id, "user_counts_with_posts_day"); err != nil {
t.Fatal(err)
} else {
rows := result.Data.(model.AnalyticsRows)
diff --git a/api/api.go b/api/api.go
index f537bbfdc..d202172d0 100644
--- a/api/api.go
+++ b/api/api.go
@@ -19,17 +19,25 @@ var ServerTemplates *template.Template
type ServerTemplatePage Page
-func NewServerTemplatePage(templateName string) *ServerTemplatePage {
+func NewServerTemplatePage(templateName, locale string) *ServerTemplatePage {
return &ServerTemplatePage{
TemplateName: templateName,
Props: make(map[string]string),
+ Extra: make(map[string]string),
+ Html: make(map[string]template.HTML),
ClientCfg: utils.ClientCfg,
- Locale: model.DEFAULT_LOCALE,
+ Locale: locale,
}
}
func (me *ServerTemplatePage) Render() string {
var text bytes.Buffer
+
+ T := utils.GetUserTranslations(me.Locale)
+ me.Props["Footer"] = T("api.templates.email_footer")
+ me.Html["EmailInfo"] = template.HTML(T("api.templates.email_info",
+ map[string]interface{}{"FeedbackEmail": me.ClientCfg["FeedbackEmail"], "SiteName": me.ClientCfg["SiteName"]}))
+
if err := ServerTemplates.ExecuteTemplate(&text, me.TemplateName, me); err != nil {
l4g.Error(utils.T("api.api.render.error"), me.TemplateName, err)
}
diff --git a/api/context.go b/api/context.go
index f47ed1c30..41a52fa0c 100644
--- a/api/context.go
+++ b/api/context.go
@@ -5,6 +5,7 @@ package api
import (
"fmt"
+ "html/template"
"net"
"net/http"
"net/url"
@@ -37,6 +38,8 @@ type Context struct {
type Page struct {
TemplateName string
Props map[string]string
+ Extra map[string]string
+ Html map[string]template.HTML
ClientCfg map[string]string
ClientLicense map[string]string
User *model.User
@@ -507,6 +510,10 @@ func RenderWebError(err *model.AppError, w http.ResponseWriter, r *http.Request)
props["SiteURL"] = GetProtocol(r) + "://" + r.Host
}
+ T, _ := utils.GetTranslationsAndLocale(w, r)
+ props["Title"] = T("api.templates.error.title", map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]})
+ props["Link"] = T("api.templates.error.link")
+
w.WriteHeader(err.StatusCode)
ServerTemplates.ExecuteTemplate(w, "error.html", Page{Props: props, ClientCfg: utils.ClientCfg})
}
diff --git a/api/license.go b/api/license.go
index af46bf113..5c602a68e 100644
--- a/api/license.go
+++ b/api/license.go
@@ -5,6 +5,7 @@ package api
import (
"bytes"
+ "fmt"
l4g "github.com/alecthomas/log4go"
"github.com/gorilla/mux"
"github.com/mattermost/platform/model"
@@ -63,6 +64,18 @@ func addLicense(c *Context, w http.ResponseWriter, r *http.Request) {
if success, licenseStr := utils.ValidateLicense(data); success {
license = model.LicenseFromJson(strings.NewReader(licenseStr))
+ if result := <-Srv.Store.User().AnalyticsUniqueUserCount(""); result.Err != nil {
+ c.Err = model.NewAppError("addLicense", "Unable to count total unique users.", fmt.Sprintf("err=%v", result.Err.Error()))
+ return
+ } else {
+ uniqueUserCount := result.Data.(int64)
+
+ if uniqueUserCount > int64(*license.Features.Users) {
+ c.Err = model.NewAppError("addLicense", fmt.Sprintf("This license only supports %d users, when your system has %d unique users. Unique users are counted distinctly by email address. You can see total user count under Site Reports -> View Statistics.", *license.Features.Users, uniqueUserCount), "")
+ return
+ }
+ }
+
if ok := utils.SetLicense(license); !ok {
c.LogAudit("failed - expired or non-started license")
c.Err = model.NewLocAppError("addLicense", "api.license.add_license.expired.app_error", nil, "")
diff --git a/api/post.go b/api/post.go
index bb6bcb337..d3807653d 100644
--- a/api/post.go
+++ b/api/post.go
@@ -10,6 +10,7 @@ import (
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
+ "html/template"
"net/http"
"net/url"
"path/filepath"
@@ -593,28 +594,26 @@ func sendNotificationsAndForget(c *Context, post *model.Post, team *model.Team,
channelName = channel.DisplayName
}
- subjectPage := NewServerTemplatePage("post_subject")
- subjectPage.Props["SiteURL"] = c.GetSiteURL()
- subjectPage.Props["TeamDisplayName"] = team.DisplayName
- subjectPage.Props["SubjectText"] = subjectText
- subjectPage.Props["Month"] = tm.Month().String()[:3]
- subjectPage.Props["Day"] = fmt.Sprintf("%d", tm.Day())
- subjectPage.Props["Year"] = fmt.Sprintf("%d", tm.Year())
+ month := userLocale(tm.Month().String())
+ day := fmt.Sprintf("%d", tm.Day())
+ year := fmt.Sprintf("%d", tm.Year())
+ zone, _ := tm.Zone()
- bodyPage := NewServerTemplatePage("post_body")
+ subjectPage := NewServerTemplatePage("post_subject", c.Locale)
+ subjectPage.Props["Subject"] = userLocale("api.templates.post_subject",
+ map[string]interface{}{"SubjectText": subjectText, "TeamDisplayName": team.DisplayName,
+ "Month": month[:3], "Day": day, "Year": year})
+
+ bodyPage := NewServerTemplatePage("post_body", c.Locale)
bodyPage.Props["SiteURL"] = c.GetSiteURL()
- bodyPage.Props["Nickname"] = profileMap[id].FirstName
- bodyPage.Props["TeamDisplayName"] = team.DisplayName
- bodyPage.Props["ChannelName"] = channelName
- bodyPage.Props["BodyText"] = bodyText
- bodyPage.Props["SenderName"] = senderName
- bodyPage.Props["Hour"] = fmt.Sprintf("%02d", tm.Hour())
- bodyPage.Props["Minute"] = fmt.Sprintf("%02d", tm.Minute())
- bodyPage.Props["Month"] = tm.Month().String()[:3]
- bodyPage.Props["Day"] = fmt.Sprintf("%d", tm.Day())
- bodyPage.Props["TimeZone"], _ = tm.Zone()
bodyPage.Props["PostMessage"] = model.ClearMentionTags(post.Message)
bodyPage.Props["TeamLink"] = teamURL + "/channels/" + channel.Name
+ bodyPage.Props["BodyText"] = bodyText
+ bodyPage.Props["Button"] = userLocale("api.templates.post_body.button")
+ bodyPage.Html["Info"] = template.HTML(userLocale("api.templates.post_body.info",
+ map[string]interface{}{"ChannelName": channelName, "SenderName": senderName,
+ "Hour": fmt.Sprintf("%02d", tm.Hour()), "Minute": fmt.Sprintf("%02d", tm.Minute()),
+ "TimeZone": zone, "Month": month, "Day": day}))
// attempt to fill in a message body if the post doesn't have any text
if len(strings.TrimSpace(bodyPage.Props["PostMessage"])) == 0 && len(post.Filenames) > 0 {
diff --git a/api/team.go b/api/team.go
index 57a0e0bd2..779a6affe 100644
--- a/api/team.go
+++ b/api/team.go
@@ -11,6 +11,7 @@ import (
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
+ "html/template"
"net/http"
"net/url"
"strconv"
@@ -57,10 +58,16 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- subjectPage := NewServerTemplatePage("signup_team_subject")
- subjectPage.Props["SiteURL"] = c.GetSiteURL()
- bodyPage := NewServerTemplatePage("signup_team_body")
+ subjectPage := NewServerTemplatePage("signup_team_subject", c.Locale)
+ subjectPage.Props["Subject"] = c.T("api.templates.signup_team_subject",
+ map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]})
+
+ bodyPage := NewServerTemplatePage("signup_team_body", c.Locale)
bodyPage.Props["SiteURL"] = c.GetSiteURL()
+ bodyPage.Props["Title"] = c.T("api.templates.signup_team_body.title")
+ bodyPage.Props["Button"] = c.T("api.templates.signup_team_body.button")
+ bodyPage.Html["Info"] = template.HTML(c.T("api.templates.signup_team_body.button",
+ map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]}))
props := make(map[string]string)
props["email"] = email
@@ -427,10 +434,16 @@ func emailTeams(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- subjectPage := NewServerTemplatePage("find_teams_subject")
- subjectPage.ClientCfg["SiteURL"] = c.GetSiteURL()
- bodyPage := NewServerTemplatePage("find_teams_body")
- bodyPage.ClientCfg["SiteURL"] = c.GetSiteURL()
+ siteURL := c.GetSiteURL()
+ subjectPage := NewServerTemplatePage("find_teams_subject", c.Locale)
+ subjectPage.Props["Subject"] = c.T("api.templates.find_teams_subject",
+ map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]})
+
+ bodyPage := NewServerTemplatePage("find_teams_body", c.Locale)
+ bodyPage.Props["SiteURL"] = siteURL
+ bodyPage.Props["Title"] = c.T("api.templates.find_teams_body.title")
+ bodyPage.Props["Found"] = c.T("api.templates.find_teams_body.found")
+ bodyPage.Props["NotFound"] = c.T("api.templates.find_teams_body.not_found")
if result := <-Srv.Store.Team().GetTeamsForEmail(email); result.Err != nil {
c.Err = result.Err
@@ -442,7 +455,7 @@ func emailTeams(c *Context, w http.ResponseWriter, r *http.Request) {
for _, team := range teams {
props[team.Name] = c.GetTeamURLFromTeam(team)
}
- bodyPage.Props = props
+ bodyPage.Extra = props
if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil {
l4g.Error(utils.T("api.team.email_teams.sending.error"), err)
@@ -511,16 +524,19 @@ func InviteMembers(c *Context, team *model.Team, user *model.User, invites []str
senderRole = c.T("api.team.invite_members.member")
}
- subjectPage := NewServerTemplatePage("invite_subject")
- subjectPage.Props["SenderName"] = sender
- subjectPage.Props["TeamDisplayName"] = team.DisplayName
+ subjectPage := NewServerTemplatePage("invite_subject", c.Locale)
+ subjectPage.Props["Subject"] = c.T("api.templates.invite_subject",
+ map[string]interface{}{"SenderName": sender, "TeamDisplayName": team.DisplayName, "SiteName": utils.ClientCfg["SiteName"]})
- bodyPage := NewServerTemplatePage("invite_body")
+ bodyPage := NewServerTemplatePage("invite_body", c.Locale)
bodyPage.Props["SiteURL"] = c.GetSiteURL()
- bodyPage.Props["TeamURL"] = c.GetTeamURL()
- bodyPage.Props["TeamDisplayName"] = team.DisplayName
- bodyPage.Props["SenderName"] = sender
- bodyPage.Props["SenderStatus"] = senderRole
+ bodyPage.Props["Title"] = c.T("api.templates.invite_body.title")
+ bodyPage.Html["Info"] = template.HTML(c.T("api.templates.invite_body.info",
+ map[string]interface{}{"SenderStatus": senderRole, "SenderName": sender, "TeamDisplayName": team.DisplayName}))
+ bodyPage.Props["Button"] = c.T("api.templates.invite_body.button")
+ bodyPage.Html["ExtraInfo"] = template.HTML(c.T("api.templates.invite_body.extra_info",
+ map[string]interface{}{"TeamDisplayName": team.DisplayName, "TeamURL": c.GetTeamURL()}))
+
props := make(map[string]string)
props["email"] = invite
props["id"] = team.Id
diff --git a/api/templates/email_change_body.html b/api/templates/email_change_body.html
index 7349aee6f..4f28584c4 100644
--- a/api/templates/email_change_body.html
+++ b/api/templates/email_change_body.html
@@ -17,8 +17,8 @@
<table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto">
<tr>
<td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;">
- <h2 style="font-weight: normal; margin-top: 10px;">You updated your email</h2>
- <p>You email address for {{.Props.TeamDisplayName}} has been changed to {{.Props.NewEmail}}.<br>If you did not make this change, please contact the system administrator.</p>
+ <h2 style="font-weight: normal; margin-top: 10px;">{{.Props.Title}}</h2>
+ <p>{{.Props.Info}}</p>
</td>
</tr>
<tr>
diff --git a/api/templates/email_change_subject.html b/api/templates/email_change_subject.html
index 4ff8026f1..afabc2191 100644
--- a/api/templates/email_change_subject.html
+++ b/api/templates/email_change_subject.html
@@ -1 +1 @@
-{{define "email_change_subject"}}[{{.ClientCfg.SiteName}}] Your email address has changed for {{.Props.TeamDisplayName}}{{end}}
+{{define "email_change_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.Subject}}{{end}}
diff --git a/api/templates/email_change_verify_body.html b/api/templates/email_change_verify_body.html
index 9d2c559b3..0d0c0aaba 100644
--- a/api/templates/email_change_verify_body.html
+++ b/api/templates/email_change_verify_body.html
@@ -17,10 +17,10 @@
<table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto">
<tr>
<td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;">
- <h2 style="font-weight: normal; margin-top: 10px;">You updated your email</h2>
- <p>To finish updating your email address for {{.Props.TeamDisplayName}}, please click the link below to confirm this is the right address.</p>
+ <h2 style="font-weight: normal; margin-top: 10px;">{{.Props.Title}}</h2>
+ <p>{{.Props.Info}}</p>
<p style="margin: 20px 0 15px">
- <a href="{{.Props.VerifyUrl}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Verify Email</a>
+ <a href="{{.Props.VerifyUrl}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">{{.Props.VerifyButton}}</a>
</p>
</td>
</tr>
diff --git a/api/templates/email_change_verify_subject.html b/api/templates/email_change_verify_subject.html
index 744aaccfc..4fc4f4846 100644
--- a/api/templates/email_change_verify_subject.html
+++ b/api/templates/email_change_verify_subject.html
@@ -1 +1 @@
-{{define "email_change_verify_subject"}}[{{.ClientCfg.SiteName}}] Verify new email address for {{.Props.TeamDisplayName}}{{end}}
+{{define "email_change_verify_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.Subject}}{{end}}
diff --git a/api/templates/email_footer.html b/api/templates/email_footer.html
index e3ff9c584..6dc7fa483 100644
--- a/api/templates/email_footer.html
+++ b/api/templates/email_footer.html
@@ -6,7 +6,7 @@
</p>
<p style="padding: 0 50px;">
(c) 2015 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br>
- To change your notification preferences, log in to your team site and go to Account Settings > Notifications.
+ {{.Props.Footer}}
</p>
</td>
diff --git a/api/templates/email_info.html b/api/templates/email_info.html
index 48725d144..0a34f18a0 100644
--- a/api/templates/email_info.html
+++ b/api/templates/email_info.html
@@ -1,9 +1,7 @@
{{define "email_info"}}
<td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;">
- Any questions at all, mail us any time: <a href="mailto:{{.ClientCfg.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientCfg.FeedbackEmail}}</a>.<br>
- Best wishes,<br>
- The {{.ClientCfg.SiteName}} Team<br>
+ {{.Html.EmailInfo}}
</td>
{{end}}
diff --git a/api/templates/error.html b/api/templates/error.html
index 6944f6c68..2b6211be2 100644
--- a/api/templates/error.html
+++ b/api/templates/error.html
@@ -11,8 +11,8 @@
<script src="/static/js/bootstrap-3.3.5.min.js"></script>
<script src="/static/js/react-bootstrap-0.25.1.min.js"></script>
- <link id="favicon" rel="icon" href="/static/images/favicon.ico" type="image/x-icon">
- <link rel="shortcut icon" href="/static/images/favicon.ico" type="image/x-icon">
+ <link id="favicon" rel="icon" href="/static/images/favicon/favicon-16x16.png" type="image/x-icon">
+ <link rel="shortcut icon" href="/static/images/favicon/favicon-16x16.png" type="image/x-icon">
<link href='/static/css/google-fonts.css' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="/static/css/styles.css">
@@ -22,9 +22,9 @@
<div class="container-fluid">
<div class="error__container">
<div class="error__icon"><i class="fa fa-exclamation-triangle"></i></div>
- <h2>{{ .ClientCfg.SiteName }} needs your help:</h2>
+ <h2>{{.Props.Title}}</h2>
<p>{{ .Props.Message }}</p>
- <a href="{{.Props.SiteURL}}">Go back to team site</a>
+ <a href="{{.Props.SiteURL}}">{{.Props.Link}}</a>
</div>
</div>
</body>
diff --git a/api/templates/find_teams_body.html b/api/templates/find_teams_body.html
index 0b52af033..1324091aa 100644
--- a/api/templates/find_teams_body.html
+++ b/api/templates/find_teams_body.html
@@ -17,14 +17,14 @@
<table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto">
<tr>
<td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;">
- <h2 style="font-weight: normal; margin-top: 10px;">Finding teams</h2>
- <p>{{ if .Props }}
- Your request to find teams associated with your email found the following:<br>
- {{range $index, $element := .Props}}
+ <h2 style="font-weight: normal; margin-top: 10px;">{{.Props.Title}}</h2>
+ <p>{{ if .Extra }}
+ {{.Props.Found}}<br>
+ {{range $index, $element := .Extra}}
<a href="{{ $element }}" style="text-decoration: none; color:#2389D7;">{{ $index }}</a><br>
{{ end }}
{{ else }}
- We could not find any teams for the given email.
+ {{.Props.NotFound}}
{{ end }}
</p>
</td>
diff --git a/api/templates/find_teams_subject.html b/api/templates/find_teams_subject.html
index f3a1437b3..ebc339562 100644
--- a/api/templates/find_teams_subject.html
+++ b/api/templates/find_teams_subject.html
@@ -1 +1 @@
-{{define "find_teams_subject"}}Your {{ .ClientCfg.SiteName }} Teams{{end}}
+{{define "find_teams_subject"}}{{.Props.Subject}}{{end}}
diff --git a/api/templates/invite_body.html b/api/templates/invite_body.html
index a81d0c3d5..2b6bde6d3 100644
--- a/api/templates/invite_body.html
+++ b/api/templates/invite_body.html
@@ -17,13 +17,13 @@
<table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto">
<tr>
<td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;">
- <h2 style="font-weight: normal; margin-top: 10px;">You've been invited</h2>
- <p>The team {{.Props.SenderStatus}} <strong>{{.Props.SenderName}}</strong>, has invited you to join <strong>{{.Props.TeamDisplayName}}</strong>.</p>
+ <h2 style="font-weight: normal; margin-top: 10px;">{{.Props.Title}}</h2>
+ <p>{{.Html.Info}}</p>
<p style="margin: 30px 0 15px">
- <a href="{{.Props.Link}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Join Team</a>
+ <a href="{{.Props.Link}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">{{.Props.Button}}</a>
</p>
<br/>
- <p>Mattermost lets you share messages and files from your PC or phone, with instant search and archiving. After you’ve joined <strong>{{.Props.TeamDisplayName}}</strong>, you can sign-in to your new team and access these features anytime from the web address:<br/><br/><a href="{{.Props.TeamURL}}">{{.Props.TeamURL}}</a></p>
+ <p>{{.Html.ExtraInfo}}</p>
</td>
</tr>
<tr>
diff --git a/api/templates/invite_subject.html b/api/templates/invite_subject.html
index 10f68969f..504915d50 100644
--- a/api/templates/invite_subject.html
+++ b/api/templates/invite_subject.html
@@ -1 +1 @@
-{{define "invite_subject"}}{{ .Props.SenderName }} invited you to join {{ .Props.TeamDisplayName }} Team on {{.ClientCfg.SiteName}}{{end}}
+{{define "invite_subject"}}{{.Props.Subject}}{{end}}
diff --git a/api/templates/password_change_body.html b/api/templates/password_change_body.html
index 6199a3423..2c4ba10ca 100644
--- a/api/templates/password_change_body.html
+++ b/api/templates/password_change_body.html
@@ -17,8 +17,8 @@
<table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto">
<tr>
<td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;">
- <h2 style="font-weight: normal; margin-top: 10px;">You updated your password</h2>
- <p>Your password has been updated for {{.Props.TeamDisplayName}} on {{ .Props.TeamURL }} by {{.Props.Method}}.<br>If this change wasn't initiated by you, please contact your system administrator.</p>
+ <h2 style="font-weight: normal; margin-top: 10px;">{{.Props.Title}}</h2>
+ <p>{{.Html.Info}}</p>
</td>
</tr>
<tr>
diff --git a/api/templates/password_change_subject.html b/api/templates/password_change_subject.html
index 0cbf052c1..897f1210d 100644
--- a/api/templates/password_change_subject.html
+++ b/api/templates/password_change_subject.html
@@ -1 +1 @@
-{{define "password_change_subject"}}Your password has been updated for {{.Props.TeamDisplayName}} on {{ .ClientCfg.SiteName }}{{end}}
+{{define "password_change_subject"}}{{.Props.Subject}}{{end}}
diff --git a/api/templates/post_body.html b/api/templates/post_body.html
index 468d5e205..54f34d1dd 100644
--- a/api/templates/post_body.html
+++ b/api/templates/post_body.html
@@ -17,10 +17,10 @@
<table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto">
<tr>
<td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;">
- <h2 style="font-weight: normal; margin-top: 10px;">You were mentioned</h2>
- <p>CHANNEL: {{.Props.ChannelName}}<br>{{.Props.SenderName}} - {{.Props.Hour}}:{{.Props.Minute}} {{.Props.TimeZone}}, {{.Props.Month}} {{.Props.Day}}<br><pre style="text-align:left;font-family: 'Lato', sans-serif; white-space: pre-wrap; white-space: -moz-pre-wrap; white-space: -pre-wrap; white-space: -o-pre-wrap; word-wrap: break-word;">{{.Props.PostMessage}}</pre></p>
+ <h2 style="font-weight: normal; margin-top: 10px;">{{.Props.BodyText}}</h2>
+ <p>{{.Html.Info}}<br><pre style="text-align:left;font-family: 'Lato', sans-serif; white-space: pre-wrap; white-space: -moz-pre-wrap; white-space: -pre-wrap; white-space: -o-pre-wrap; word-wrap: break-word;">{{.Props.PostMessage}}</pre></p>
<p style="margin: 20px 0 15px">
- <a href="{{.Props.TeamLink}}" style="background: #2389D7; display: inline-block; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 170px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Go To Channel</a>
+ <a href="{{.Props.TeamLink}}" style="background: #2389D7; display: inline-block; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 170px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">{{.Props.Button}}</a>
</p>
</td>
</tr>
diff --git a/api/templates/post_subject.html b/api/templates/post_subject.html
index f53353d85..60daaa432 100644
--- a/api/templates/post_subject.html
+++ b/api/templates/post_subject.html
@@ -1 +1 @@
-{{define "post_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.TeamDisplayName}} Team Notifications for {{.Props.Month}} {{.Props.Day}}, {{.Props.Year}}{{end}}
+{{define "post_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.Subject}}{{end}}
diff --git a/api/templates/reset_body.html b/api/templates/reset_body.html
index a608c804a..69cd44957 100644
--- a/api/templates/reset_body.html
+++ b/api/templates/reset_body.html
@@ -17,10 +17,10 @@
<table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto">
<tr>
<td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;">
- <h2 style="font-weight: normal; margin-top: 10px;">You requested a password reset</h2>
- <p>To change your password, click "Reset Password" below.<br>If you did not mean to reset your password, please ignore this email and your password will remain the same.</p>
+ <h2 style="font-weight: normal; margin-top: 10px;">{{.Props.Title}}</h2>
+ <p>{{.Html.Info}}</p>
<p style="margin: 20px 0 15px">
- <a href="{{.Props.ResetUrl}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Reset Password</a>
+ <a href="{{.Props.ResetUrl}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">{{.Props.Button}}</a>
</p>
</td>
</tr>
diff --git a/api/templates/reset_subject.html b/api/templates/reset_subject.html
index 87ad7bc38..a2852d332 100644
--- a/api/templates/reset_subject.html
+++ b/api/templates/reset_subject.html
@@ -1 +1 @@
-{{define "reset_subject"}}Reset your password{{end}}
+{{define "reset_subject"}}{{.Props.Subject}}{{end}}
diff --git a/api/templates/signin_change_body.html b/api/templates/signin_change_body.html
index 5b96df944..af8577f0f 100644
--- a/api/templates/signin_change_body.html
+++ b/api/templates/signin_change_body.html
@@ -17,8 +17,8 @@
<table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto">
<tr>
<td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;">
- <h2 style="font-weight: normal; margin-top: 10px;">You updated your sign-in method</h2>
- <p>You updated your sign-in method for {{.Props.TeamDisplayName}} on {{ .Props.TeamURL }} to {{.Props.Method}}.<br>If this change wasn't initiated by you, please contact your system administrator.</p>
+ <h2 style="font-weight: normal; margin-top: 10px;">{{.Props.Title}}</h2>
+ <p>{{.Html.Info}}</p>
</td>
</tr>
<tr>
diff --git a/api/templates/signin_change_subject.html b/api/templates/signin_change_subject.html
index b1d644a28..606dc4df3 100644
--- a/api/templates/signin_change_subject.html
+++ b/api/templates/signin_change_subject.html
@@ -1 +1 @@
-{{define "signin_change_subject"}}You updated your sign-in method for {{.Props.TeamDisplayName}} on {{ .ClientCfg.SiteName }}{{end}}
+{{define "signin_change_subject"}}{{.Props.Subject}}{{end}}
diff --git a/api/templates/signup_team_body.html b/api/templates/signup_team_body.html
index 2f384ac43..683a9891e 100644
--- a/api/templates/signup_team_body.html
+++ b/api/templates/signup_team_body.html
@@ -17,11 +17,11 @@
<table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto">
<tr>
<td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;">
- <h2 style="font-weight: normal; margin-top: 10px;">Thanks for creating a team!</h2>
+ <h2 style="font-weight: normal; margin-top: 10px;">{{.Props.Title}}</h2>
<p style="margin: 20px 0 25px">
- <a href="{{.Props.Link}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Set up your team</a>
+ <a href="{{.Props.Link}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">{{.Props.Button}}</a>
</p>
- {{ .ClientCfg.SiteName }} is one place for all your team communication, searchable and available anywhere.<br>You'll get more out of {{ .ClientCfg.SiteName }} when your team is in constant communication--let's get them on board.<br></p>
+ {{.Html.Info}}<br></p>
</td>
</tr>
<tr>
diff --git a/api/templates/signup_team_subject.html b/api/templates/signup_team_subject.html
index 4fc5b3d72..413a5c8c1 100644
--- a/api/templates/signup_team_subject.html
+++ b/api/templates/signup_team_subject.html
@@ -1 +1 @@
-{{define "signup_team_subject"}}{{ .ClientCfg.SiteName }} Team Setup{{end}} \ No newline at end of file
+{{define "signup_team_subject"}}{{.Props.Subject}}{{end}} \ No newline at end of file
diff --git a/api/templates/verify_body.html b/api/templates/verify_body.html
index c42b2a372..2b0d25f94 100644
--- a/api/templates/verify_body.html
+++ b/api/templates/verify_body.html
@@ -17,10 +17,10 @@
<table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto">
<tr>
<td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;">
- <h2 style="font-weight: normal; margin-top: 10px;">You've joined the {{ .Props.TeamDisplayName }} team</h2>
- <p>Please verify your email address by clicking below.</p>
+ <h2 style="font-weight: normal; margin-top: 10px;">{{.Props.Title}}</h2>
+ <p>{{.Props.Info}}</p>
<p style="margin: 20px 0 15px">
- <a href="{{.Props.VerifyUrl}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Verify Email</a>
+ <a href="{{.Props.VerifyUrl}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">{{.Props.Button}}</a>
</p>
</td>
</tr>
diff --git a/api/templates/verify_subject.html b/api/templates/verify_subject.html
index 9a3a11282..ad7fc2aaa 100644
--- a/api/templates/verify_subject.html
+++ b/api/templates/verify_subject.html
@@ -1 +1 @@
-{{define "verify_subject"}}[{{ .Props.TeamDisplayName }} {{ .ClientCfg.SiteName }}] Email Verification{{end}}
+{{define "verify_subject"}}{{.Props.Subject}}{{end}}
diff --git a/api/templates/welcome_body.html b/api/templates/welcome_body.html
index 71d838b08..b5ca9beb3 100644
--- a/api/templates/welcome_body.html
+++ b/api/templates/welcome_body.html
@@ -18,19 +18,19 @@
{{if .Props.VerifyUrl }}
<tr>
<td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;">
- <h2 style="font-weight: normal; margin-top: 10px;">You've joined the {{ .Props.TeamDisplayName }} team</h2>
- <p>Please verify your email address by clicking below.</p>
+ <h2 style="font-weight: normal; margin-top: 10px;">{{.Props.Title}}</h2>
+ <p>{{.Props.Info}}</p>
<p style="margin: 20px 0 15px">
- <a href="{{.Props.VerifyUrl}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Verify Email</a>
+ <a href="{{.Props.VerifyUrl}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">{{.Props.Button}}</a>
</p>
</td>
</tr>
{{end}}
<tr>
<td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;">
- <h2 style="font-weight: normal; margin-top: 25px; line-height: 1.5;">You can sign-in to your new team from the web address:</h2>
+ <h2 style="font-weight: normal; margin-top: 25px; line-height: 1.5;">{{.Props.Info2}}</h2>
<a href="{{.Props.TeamURL}}">{{.Props.TeamURL}}</a>
- <p>Mattermost lets you share messages and files from your PC or phone, with instant search and archiving.</p>
+ <p>{{.Props.Info3}}</p>
</td>
</tr>
</table>
diff --git a/api/templates/welcome_subject.html b/api/templates/welcome_subject.html
index c3b70ef20..95189b900 100644
--- a/api/templates/welcome_subject.html
+++ b/api/templates/welcome_subject.html
@@ -1 +1 @@
-{{define "welcome_subject"}}You joined {{ .Props.TeamDisplayName }}{{end}}
+{{define "welcome_subject"}}{{.Props.Subject}}{{end}}
diff --git a/api/user.go b/api/user.go
index 76ed66112..473f0da54 100644
--- a/api/user.go
+++ b/api/user.go
@@ -17,6 +17,7 @@ import (
"github.com/mattermost/platform/utils"
"github.com/mssola/user_agent"
"hash/fnv"
+ "html/template"
"image"
"image/color"
"image/draw"
@@ -134,7 +135,7 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
}
if sendWelcomeEmail {
- sendWelcomeEmailAndForget(ruser.Id, ruser.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team), ruser.EmailVerified)
+ sendWelcomeEmailAndForget(c, ruser.Id, ruser.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team), ruser.EmailVerified)
}
w.Write([]byte(ruser.ToJson()))
@@ -308,13 +309,19 @@ func CreateOAuthUser(c *Context, w http.ResponseWriter, r *http.Request, service
return ruser
}
-func sendWelcomeEmailAndForget(userId, email, teamName, teamDisplayName, siteURL, teamURL string, verified bool) {
+func sendWelcomeEmailAndForget(c *Context, userId, email, teamName, teamDisplayName, siteURL, teamURL string, verified bool) {
go func() {
- subjectPage := NewServerTemplatePage("welcome_subject")
- subjectPage.Props["TeamDisplayName"] = teamDisplayName
- bodyPage := NewServerTemplatePage("welcome_body")
+ subjectPage := NewServerTemplatePage("welcome_subject", c.Locale)
+ subjectPage.Props["Subject"] = c.T("api.templates.welcome_subject", map[string]interface{}{"TeamDisplayName": teamDisplayName})
+
+ bodyPage := NewServerTemplatePage("welcome_body", c.Locale)
bodyPage.Props["SiteURL"] = siteURL
+ bodyPage.Props["Title"] = c.T("api.templates.welcome_body.title", map[string]interface{}{"TeamDisplayName": teamDisplayName})
+ bodyPage.Props["Info"] = c.T("api.templates.welcome_body.info")
+ bodyPage.Props["Button"] = c.T("api.templates.welcome_body.button")
+ bodyPage.Props["Info2"] = c.T("api.templates.welcome_body.info2")
+ bodyPage.Props["Info3"] = c.T("api.templates.welcome_body.info3")
bodyPage.Props["TeamURL"] = teamURL
if !verified {
@@ -367,18 +374,21 @@ func addDirectChannelsAndForget(user *model.User) {
}()
}
-func SendVerifyEmailAndForget(userId, userEmail, teamName, teamDisplayName, siteURL, teamURL string) {
+func SendVerifyEmailAndForget(c *Context, userId, userEmail, teamName, teamDisplayName, siteURL, teamURL string) {
go func() {
link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, userEmail)
- subjectPage := NewServerTemplatePage("verify_subject")
- subjectPage.Props["SiteURL"] = siteURL
- subjectPage.Props["TeamDisplayName"] = teamDisplayName
- bodyPage := NewServerTemplatePage("verify_body")
+ subjectPage := NewServerTemplatePage("verify_subject", c.Locale)
+ subjectPage.Props["Subject"] = c.T("api.templates.verify_subject",
+ map[string]interface{}{"TeamDisplayName": teamDisplayName, "SiteName": utils.ClientCfg["SiteName"]})
+
+ bodyPage := NewServerTemplatePage("verify_body", c.Locale)
bodyPage.Props["SiteURL"] = siteURL
- bodyPage.Props["TeamDisplayName"] = teamDisplayName
+ bodyPage.Props["Title"] = c.T("api.templates.verify_body.title", map[string]interface{}{"TeamDisplayName": teamDisplayName})
+ bodyPage.Props["Info"] = c.T("api.templates.verify_body.info")
bodyPage.Props["VerifyUrl"] = link
+ bodyPage.Props["Button"] = c.T("api.templates.verify_body.button")
if err := utils.SendMail(userEmail, subjectPage.Render(), bodyPage.Render()); err != nil {
l4g.Error(utils.T("api.user.send_verify_email_and_forget.failed.error"), err)
@@ -1141,10 +1151,10 @@ func updateUser(c *Context, w http.ResponseWriter, r *http.Request) {
l4g.Error(tresult.Err.Message)
} else {
team := tresult.Data.(*model.Team)
- sendEmailChangeEmailAndForget(rusers[1].Email, rusers[0].Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL())
+ sendEmailChangeEmailAndForget(c, rusers[1].Email, rusers[0].Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL())
if utils.Cfg.EmailSettings.RequireEmailVerification {
- SendEmailChangeVerifyEmailAndForget(rusers[0].Id, rusers[0].Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team))
+ SendEmailChangeVerifyEmailAndForget(c, rusers[0].Id, rusers[0].Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team))
}
}
}
@@ -1224,7 +1234,7 @@ func updatePassword(c *Context, w http.ResponseWriter, r *http.Request) {
l4g.Error(tresult.Err.Message)
} else {
team := tresult.Data.(*model.Team)
- sendPasswordChangeEmailAndForget(user.Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL(), c.T("api.user.update_password.menu"))
+ sendPasswordChangeEmailAndForget(c, user.Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL(), c.T("api.user.update_password.menu"))
}
data := make(map[string]string)
@@ -1526,11 +1536,15 @@ func sendPasswordReset(c *Context, w http.ResponseWriter, r *http.Request) {
link := fmt.Sprintf("%s/reset_password?d=%s&h=%s", c.GetTeamURLFromTeam(team), url.QueryEscape(data), url.QueryEscape(hash))
- subjectPage := NewServerTemplatePage("reset_subject")
- subjectPage.Props["SiteURL"] = c.GetSiteURL()
- bodyPage := NewServerTemplatePage("reset_body")
+ subjectPage := NewServerTemplatePage("reset_subject", c.Locale)
+ subjectPage.Props["Subject"] = c.T("api.templates.reset_subject")
+
+ bodyPage := NewServerTemplatePage("reset_body", c.Locale)
bodyPage.Props["SiteURL"] = c.GetSiteURL()
+ bodyPage.Props["Title"] = c.T("api.templates.reset_body.title")
+ bodyPage.Html["Info"] = template.HTML(c.T("api.templates.reset_body.info"))
bodyPage.Props["ResetUrl"] = link
+ bodyPage.Props["Button"] = c.T("api.templates.reset_body.button")
if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil {
c.Err = model.NewLocAppError("sendPasswordReset", "api.user.send_password_reset.send.app_error", nil, "err="+err.Message)
@@ -1632,23 +1646,24 @@ func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) {
c.LogAuditWithUserId(userId, "success")
}
- sendPasswordChangeEmailAndForget(user.Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL(), "using a reset password link")
+ sendPasswordChangeEmailAndForget(c, user.Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL(), c.T("api.user.reset_password.method"))
props["new_password"] = ""
w.Write([]byte(model.MapToJson(props)))
}
-func sendPasswordChangeEmailAndForget(email, teamDisplayName, teamURL, siteURL, method string) {
+func sendPasswordChangeEmailAndForget(c *Context, email, teamDisplayName, teamURL, siteURL, method string) {
go func() {
- subjectPage := NewServerTemplatePage("password_change_subject")
- subjectPage.Props["SiteURL"] = siteURL
- subjectPage.Props["TeamDisplayName"] = teamDisplayName
- bodyPage := NewServerTemplatePage("password_change_body")
+ subjectPage := NewServerTemplatePage("password_change_subject", c.Locale)
+ subjectPage.Props["Subject"] = c.T("api.templates.password_change_subject",
+ map[string]interface{}{"TeamDisplayName": teamDisplayName, "SiteName": utils.ClientCfg["SiteName"]})
+
+ bodyPage := NewServerTemplatePage("password_change_body", c.Locale)
bodyPage.Props["SiteURL"] = siteURL
- bodyPage.Props["TeamDisplayName"] = teamDisplayName
- bodyPage.Props["TeamURL"] = teamURL
- bodyPage.Props["Method"] = method
+ bodyPage.Props["Title"] = c.T("api.templates.password_change_body.title")
+ bodyPage.Html["Info"] = template.HTML(c.T("api.templates.password_change_body.info",
+ map[string]interface{}{"TeamDisplayName": teamDisplayName, "TeamURL": teamURL, "Method": method}))
if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil {
l4g.Error(utils.T("api.user.send_password_change_email_and_forget.error"), err)
@@ -1657,17 +1672,18 @@ func sendPasswordChangeEmailAndForget(email, teamDisplayName, teamURL, siteURL,
}()
}
-func sendEmailChangeEmailAndForget(oldEmail, newEmail, teamDisplayName, teamURL, siteURL string) {
+func sendEmailChangeEmailAndForget(c *Context, oldEmail, newEmail, teamDisplayName, teamURL, siteURL string) {
go func() {
- subjectPage := NewServerTemplatePage("email_change_subject")
- subjectPage.Props["SiteURL"] = siteURL
- subjectPage.Props["TeamDisplayName"] = teamDisplayName
- bodyPage := NewServerTemplatePage("email_change_body")
+ subjectPage := NewServerTemplatePage("email_change_subject", c.Locale)
+ subjectPage.Props["Subject"] = c.T("api.templates.email_change_body",
+ map[string]interface{}{"TeamDisplayName": teamDisplayName})
+
+ bodyPage := NewServerTemplatePage("email_change_body", c.Locale)
bodyPage.Props["SiteURL"] = siteURL
- bodyPage.Props["TeamDisplayName"] = teamDisplayName
- bodyPage.Props["TeamURL"] = teamURL
- bodyPage.Props["NewEmail"] = newEmail
+ bodyPage.Props["Title"] = c.T("api.templates.email_change_body.title")
+ bodyPage.Props["Info"] = c.T("api.templates.email_change_body.info",
+ map[string]interface{}{"TeamDisplayName": teamDisplayName, "NewEmail": newEmail})
if err := utils.SendMail(oldEmail, subjectPage.Render(), bodyPage.Render()); err != nil {
l4g.Error(utils.T("api.user.send_email_change_email_and_forget.error"), err)
@@ -1676,18 +1692,22 @@ func sendEmailChangeEmailAndForget(oldEmail, newEmail, teamDisplayName, teamURL,
}()
}
-func SendEmailChangeVerifyEmailAndForget(userId, newUserEmail, teamName, teamDisplayName, siteURL, teamURL string) {
+func SendEmailChangeVerifyEmailAndForget(c *Context, userId, newUserEmail, teamName, teamDisplayName, siteURL, teamURL string) {
go func() {
link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, newUserEmail)
- subjectPage := NewServerTemplatePage("email_change_verify_subject")
- subjectPage.Props["SiteURL"] = siteURL
- subjectPage.Props["TeamDisplayName"] = teamDisplayName
- bodyPage := NewServerTemplatePage("email_change_verify_body")
+ subjectPage := NewServerTemplatePage("email_change_verify_subject", c.Locale)
+ subjectPage.Props["Subject"] = c.T("api.templates.email_change_verify_subject",
+ map[string]interface{}{"TeamDisplayName": teamDisplayName})
+
+ bodyPage := NewServerTemplatePage("email_change_verify_body", c.Locale)
bodyPage.Props["SiteURL"] = siteURL
- bodyPage.Props["TeamDisplayName"] = teamDisplayName
+ bodyPage.Props["Title"] = c.T("api.templates.email_change_verify_body.title")
+ bodyPage.Props["Info"] = c.T("api.templates.email_change_verify_body.info",
+ map[string]interface{}{"TeamDisplayName": teamDisplayName})
bodyPage.Props["VerifyUrl"] = link
+ bodyPage.Props["VerifyButton"] = c.T("api.templates.email_change_verify_body.button")
if err := utils.SendMail(newUserEmail, subjectPage.Render(), bodyPage.Render()); err != nil {
l4g.Error(utils.T("api.user.send_email_change_verify_email_and_forget.error"), err)
@@ -2028,7 +2048,7 @@ func CompleteSwitchWithOAuth(c *Context, w http.ResponseWriter, r *http.Request,
return
}
- sendSignInChangeEmailAndForget(user.Email, team.DisplayName, c.GetSiteURL()+"/"+team.Name, c.GetSiteURL(), strings.Title(service)+" SSO")
+ sendSignInChangeEmailAndForget(c, user.Email, team.DisplayName, c.GetSiteURL()+"/"+team.Name, c.GetSiteURL(), strings.Title(service)+" SSO")
}
func switchToEmail(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -2085,7 +2105,7 @@ func switchToEmail(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- sendSignInChangeEmailAndForget(user.Email, team.DisplayName, c.GetSiteURL()+"/"+team.Name, c.GetSiteURL(), "email and password")
+ sendSignInChangeEmailAndForget(c, user.Email, team.DisplayName, c.GetSiteURL()+"/"+team.Name, c.GetSiteURL(), c.T("api.templates.signin_change_email.body.method_email"))
RevokeAllSession(c, c.Session.UserId)
if c.Err != nil {
@@ -2099,17 +2119,18 @@ func switchToEmail(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(model.MapToJson(m)))
}
-func sendSignInChangeEmailAndForget(email, teamDisplayName, teamURL, siteURL, method string) {
+func sendSignInChangeEmailAndForget(c *Context, email, teamDisplayName, teamURL, siteURL, method string) {
go func() {
- subjectPage := NewServerTemplatePage("signin_change_subject")
- subjectPage.Props["SiteURL"] = siteURL
- subjectPage.Props["TeamDisplayName"] = teamDisplayName
- bodyPage := NewServerTemplatePage("signin_change_body")
+ subjectPage := NewServerTemplatePage("signin_change_subject", c.Locale)
+ subjectPage.Props["Subject"] = c.T("api.templates.singin_change_email.subject",
+ map[string]interface{}{"TeamDisplayName": teamDisplayName, "SiteName": utils.ClientCfg["SiteName"]})
+
+ bodyPage := NewServerTemplatePage("signin_change_body", c.Locale)
bodyPage.Props["SiteURL"] = siteURL
- bodyPage.Props["TeamDisplayName"] = teamDisplayName
- bodyPage.Props["TeamURL"] = teamURL
- bodyPage.Props["Method"] = method
+ bodyPage.Props["Title"] = c.T("api.templates.signin_change_email.body.title")
+ bodyPage.Html["Info"] = template.HTML(c.T("api.templates.singin_change_email.body.info",
+ map[string]interface{}{"TeamDisplayName": teamDisplayName, "TeamURL": teamURL, "Method": method}))
if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil {
l4g.Error(utils.T("api.user.send_sign_in_change_email_and_forget.error"), err)
diff --git a/api/web_conn.go b/api/web_conn.go
index 2b0e29038..515a8ab31 100644
--- a/api/web_conn.go
+++ b/api/web_conn.go
@@ -8,6 +8,7 @@ import (
"github.com/gorilla/websocket"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
+ "github.com/mattermost/platform/utils"
"time"
)
@@ -33,11 +34,11 @@ func NewWebConn(ws *websocket.Conn, teamId string, userId string, sessionId stri
pchan := Srv.Store.User().UpdateLastPingAt(userId, model.GetMillis())
if result := <-achan; result.Err != nil {
- l4g.Error("Failed to update LastActivityAt for user_id=%v and session_id=%v, err=%v", userId, sessionId, result.Err)
+ l4g.Error(utils.T("api.web_conn.new_web_conn.last_activity.error"), userId, sessionId, result.Err)
}
if result := <-pchan; result.Err != nil {
- l4g.Error("Failed to updated LastPingAt for user_id=%v, err=%v", userId, result.Err)
+ l4g.Error(utils.T("api.web_conn.new_web_conn.last_ping.error"), userId, result.Err)
}
}()
@@ -56,7 +57,7 @@ func (c *WebConn) readPump() {
go func() {
if result := <-Srv.Store.User().UpdateLastPingAt(c.UserId, model.GetMillis()); result.Err != nil {
- l4g.Error("Failed to updated LastPingAt for user_id=%v, err=%v", c.UserId, result.Err)
+ l4g.Error(utils.T("api.web_conn.new_web_conn.last_ping.error"), c.UserId, result.Err)
}
}()
diff --git a/api/web_hub.go b/api/web_hub.go
index 4361d1035..5fe9d6ae8 100644
--- a/api/web_hub.go
+++ b/api/web_hub.go
@@ -6,6 +6,7 @@ package api
import (
l4g "github.com/alecthomas/log4go"
"github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
)
type Hub struct {
@@ -86,7 +87,7 @@ func (h *Hub) Start() {
nh.broadcast <- msg
}
case s := <-h.stop:
- l4g.Debug("stopping %v connections", s)
+ l4g.Debug(utils.T("api.web_hub.start.stopping.debug"), s)
for _, v := range h.teamHubs {
v.Stop()
}
diff --git a/api/web_socket.go b/api/web_socket.go
index 995e2a677..7590e6646 100644
--- a/api/web_socket.go
+++ b/api/web_socket.go
@@ -8,11 +8,12 @@ import (
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
"github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
"net/http"
)
func InitWebSocket(r *mux.Router) {
- l4g.Debug("Initializing web socket api routes")
+ l4g.Debug(utils.T("api.web_socket.init.debug"))
r.Handle("/websocket", ApiUserRequired(connect)).Methods("GET")
hub.Start()
}
@@ -28,8 +29,8 @@ func connect(c *Context, w http.ResponseWriter, r *http.Request) {
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
- l4g.Error("websocket connect err: %v", err)
- c.Err = model.NewAppError("connect", "Failed to upgrade websocket connection", "")
+ l4g.Error(utils.T("api.web_socket.connect.error"), err)
+ c.Err = model.NewLocAppError("connect", "api.web_socket.connect.upgrade.app_error", nil, "")
return
}
diff --git a/api/web_team_hub.go b/api/web_team_hub.go
index bb9ed9526..55300c828 100644
--- a/api/web_team_hub.go
+++ b/api/web_team_hub.go
@@ -6,6 +6,7 @@ package api
import (
l4g "github.com/alecthomas/log4go"
"github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
)
type TeamHub struct {
@@ -65,7 +66,7 @@ func (h *TeamHub) Start() {
case s := <-h.stop:
if s {
- l4g.Debug("team hub stopping for teamId=%v", h.teamId)
+ l4g.Debug(utils.T("api.web_team_hun.start.debug"), h.teamId)
for webCon := range h.connections {
webCon.WebSocket.Close()
diff --git a/api/webhook.go b/api/webhook.go
index a9a88b7b8..1372fe335 100644
--- a/api/webhook.go
+++ b/api/webhook.go
@@ -12,7 +12,7 @@ import (
)
func InitWebhook(r *mux.Router) {
- l4g.Debug("Initializing webhook api routes")
+ l4g.Debug(utils.T("api.webhook.init.debug"))
sr := r.PathPrefix("/hooks").Subrouter()
sr.Handle("/incoming/create", ApiUserRequired(createIncomingHook)).Methods("POST")
@@ -27,7 +27,7 @@ func InitWebhook(r *mux.Router) {
func createIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks {
- c.Err = model.NewAppError("createIncomingHook", "Incoming webhooks have been disabled by the system admin.", "")
+ c.Err = model.NewLocAppError("createIncomingHook", "api.webhook.create_incoming.disabled.app_errror", nil, "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
@@ -74,7 +74,7 @@ func createIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
func deleteIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks {
- c.Err = model.NewAppError("deleteIncomingHook", "Incoming webhooks have been disabled by the system admin.", "")
+ c.Err = model.NewLocAppError("deleteIncomingHook", "api.webhook.delete_incoming.disabled.app_errror", nil, "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
@@ -95,7 +95,7 @@ func deleteIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
} else {
if c.Session.UserId != result.Data.(*model.IncomingWebhook).UserId && !c.IsTeamAdmin() {
c.LogAudit("fail - inappropriate permissions")
- c.Err = model.NewAppError("deleteIncomingHook", "Inappropriate permissions to delete incoming webhook", "user_id="+c.Session.UserId)
+ c.Err = model.NewLocAppError("deleteIncomingHook", "api.webhook.delete_incoming.permissions.app_errror", nil, "user_id="+c.Session.UserId)
return
}
}
@@ -111,7 +111,7 @@ func deleteIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
func getIncomingHooks(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks {
- c.Err = model.NewAppError("getIncomingHooks", "Incoming webhooks have been disabled by the system admin.", "")
+ c.Err = model.NewLocAppError("getIncomingHooks", "api.webhook.get_incoming.disabled.app_error", nil, "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
@@ -127,7 +127,7 @@ func getIncomingHooks(c *Context, w http.ResponseWriter, r *http.Request) {
func createOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.ServiceSettings.EnableOutgoingWebhooks {
- c.Err = model.NewAppError("createOutgoingHook", "Outgoing webhooks have been disabled by the system admin.", "")
+ c.Err = model.NewLocAppError("createOutgoingHook", "api.webhook.create_outgoing.disabled.app_error", nil, "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
@@ -167,7 +167,7 @@ func createOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
} else if len(hook.TriggerWords) == 0 {
- c.Err = model.NewAppError("createOutgoingHook", "Either trigger_words or channel_id must be set", "")
+ c.Err = model.NewLocAppError("createOutgoingHook", "api.webhook.create_outgoing.triggers.app_error", nil, "")
return
}
@@ -183,7 +183,7 @@ func createOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) {
func getOutgoingHooks(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.ServiceSettings.EnableOutgoingWebhooks {
- c.Err = model.NewAppError("getOutgoingHooks", "Outgoing webhooks have been disabled by the system admin.", "")
+ c.Err = model.NewLocAppError("getOutgoingHooks", "api.webhook.get_outgoing.disabled.app_error", nil, "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
@@ -199,7 +199,7 @@ func getOutgoingHooks(c *Context, w http.ResponseWriter, r *http.Request) {
func deleteOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks {
- c.Err = model.NewAppError("deleteOutgoingHook", "Outgoing webhooks have been disabled by the system admin.", "")
+ c.Err = model.NewLocAppError("deleteOutgoingHook", "api.webhook.delete_outgoing.disabled.app_error", nil, "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
@@ -220,7 +220,7 @@ func deleteOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) {
} else {
if c.Session.UserId != result.Data.(*model.OutgoingWebhook).CreatorId && !c.IsTeamAdmin() {
c.LogAudit("fail - inappropriate permissions")
- c.Err = model.NewAppError("deleteOutgoingHook", "Inappropriate permissions to delete outcoming webhook", "user_id="+c.Session.UserId)
+ c.Err = model.NewLocAppError("deleteOutgoingHook", "api.webhook.delete_outgoing.permissions.app_error", nil, "user_id="+c.Session.UserId)
return
}
}
@@ -236,7 +236,7 @@ func deleteOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) {
func regenOutgoingHookToken(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks {
- c.Err = model.NewAppError("regenOutgoingHookToken", "Outgoing webhooks have been disabled by the system admin.", "")
+ c.Err = model.NewLocAppError("regenOutgoingHookToken", "api.webhook.regen_outgoing_token.disabled.app_error", nil, "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
@@ -260,7 +260,7 @@ func regenOutgoingHookToken(c *Context, w http.ResponseWriter, r *http.Request)
if c.Session.UserId != hook.CreatorId && !c.IsTeamAdmin() {
c.LogAudit("fail - inappropriate permissions")
- c.Err = model.NewAppError("regenOutgoingHookToken", "Inappropriate permissions to regenerate outcoming webhook token", "user_id="+c.Session.UserId)
+ c.Err = model.NewLocAppError("regenOutgoingHookToken", "api.webhook.regen_outgoing_token.permissions.app_error", nil, "user_id="+c.Session.UserId)
return
}
}
diff --git a/i18n/en.json b/i18n/en.json
index 4edc176d6..3d92042bb 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -1,5 +1,53 @@
[
{
+ "id": "April",
+ "translation": "April"
+ },
+ {
+ "id": "August",
+ "translation": "August"
+ },
+ {
+ "id": "December",
+ "translation": "December"
+ },
+ {
+ "id": "February",
+ "translation": "February"
+ },
+ {
+ "id": "January",
+ "translation": "January"
+ },
+ {
+ "id": "July",
+ "translation": "July"
+ },
+ {
+ "id": "June",
+ "translation": "June"
+ },
+ {
+ "id": "March",
+ "translation": "March"
+ },
+ {
+ "id": "May",
+ "translation": "May"
+ },
+ {
+ "id": "November",
+ "translation": "November"
+ },
+ {
+ "id": "October",
+ "translation": "October"
+ },
+ {
+ "id": "September",
+ "translation": "September"
+ },
+ {
"id": "api.admin.file_read_error",
"translation": "Error reading log file"
},
@@ -749,7 +797,7 @@
},
{
"id": "api.post.update_post.permissions_details.app_error",
- "translation": "Already delted id={{.PostId}}"
+ "translation": "Already deleted id={{.PostId}}"
},
{
"id": "api.post_get_post_by_id.get.app_error",
@@ -992,6 +1040,198 @@
"translation": "You do not have the appropriate permissions"
},
{
+ "id": "api.templates.email_change_body.info",
+ "translation": "You email address for {{.TeamDisplayName}} has been changed to {{.NewEmail}}.<br>If you did not make this change, please contact the system administrator."
+ },
+ {
+ "id": "api.templates.email_change_body.title",
+ "translation": "You updated your email"
+ },
+ {
+ "id": "api.templates.email_change_subject",
+ "translation": "Your email address has changed for {{.TeamDisplayName}}"
+ },
+ {
+ "id": "api.templates.email_change_verify_body.button",
+ "translation": "Verify Email"
+ },
+ {
+ "id": "api.templates.email_change_verify_body.info",
+ "translation": "To finish updating your email address for {{.TeamDisplayName}}, please click the link below to confirm this is the right address."
+ },
+ {
+ "id": "api.templates.email_change_verify_body.title",
+ "translation": "You updated your email"
+ },
+ {
+ "id": "api.templates.email_change_verify_subject",
+ "translation": "Verify new email address for {{.TeamDisplayName}}"
+ },
+ {
+ "id": "api.templates.email_footer",
+ "translation": "To change your notification preferences, log in to your team site and go to Account Settings > Notifications."
+ },
+ {
+ "id": "api.templates.email_info",
+ "translation": "Any questions at all, mail us any time: <a href='mailto:{{.FeedbackEmail}}' style='text-decoration: none; color:#2389D7;'>{{.FeedbackEmail}}</a>.<br>Best wishes,<br>The {{.SiteName}} Team<br>"
+ },
+ {
+ "id": "api.templates.error.link",
+ "translation": "Go back to team site"
+ },
+ {
+ "id": "api.templates.error.title",
+ "translation": "{{ .SiteName }} needs your help:"
+ },
+ {
+ "id": "api.templates.find_teams_body.found",
+ "translation": "Your request to find teams associated with your email found the following:"
+ },
+ {
+ "id": "api.templates.find_teams_body.not_found",
+ "translation": "We could not find any teams for the given email."
+ },
+ {
+ "id": "api.templates.find_teams_body.title",
+ "translation": "Finding teams"
+ },
+ {
+ "id": "api.templates.find_teams_subject",
+ "translation": "Your {{ .SiteName }} Teams"
+ },
+ {
+ "id": "api.templates.invite_body.button",
+ "translation": "Join Team"
+ },
+ {
+ "id": "api.templates.invite_body.extra_info",
+ "translation": "Mattermost lets you share messages and files from your PC or phone, with instant search and archiving. After you’ve joined <strong>{{.TeamDisplayName}}</strong>, you can sign-in to your new team and access these features anytime from the web address:<br/><br/><a href='{{.TeamURL}}'>{{.TeamURL}}</a>"
+ },
+ {
+ "id": "api.templates.invite_body.info",
+ "translation": "The team {{.SenderStatus}} <strong>{{.SenderName}}</strong>, has invited you to join <strong>{{.TeamDisplayName}}</strong>."
+ },
+ {
+ "id": "api.templates.invite_body.title",
+ "translation": "You've been invited"
+ },
+ {
+ "id": "api.templates.invite_subject",
+ "translation": "{{ .SenderName }} invited you to join {{ .TeamDisplayName }} Team on {{.SiteName}}"
+ },
+ {
+ "id": "api.templates.password_change_body.info",
+ "translation": "Your password has been updated for {{.TeamDisplayName}} on {{ .TeamURL }} by {{.Method}}.<br>If this change wasn't initiated by you, please contact your system administrator."
+ },
+ {
+ "id": "api.templates.password_change_body.title",
+ "translation": "You updated your password"
+ },
+ {
+ "id": "api.templates.password_change_subject",
+ "translation": "Your password has been updated for {{.TeamDisplayName}} on {{ .SiteName }}"
+ },
+ {
+ "id": "api.templates.post_body.button",
+ "translation": "Go To Channel"
+ },
+ {
+ "id": "api.templates.post_body.info",
+ "translation": "CHANNEL: {{.ChannelName}}<br>{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}"
+ },
+ {
+ "id": "api.templates.post_subject",
+ "translation": "{{.SubjectText}} on {{.TeamDisplayName}} at {{.Month}} {{.Day}}, {{.Year}}"
+ },
+ {
+ "id": "api.templates.reset_body.button",
+ "translation": "Reset Password"
+ },
+ {
+ "id": "api.templates.reset_body.info",
+ "translation": "To change your password, click \"Reset Password\" below.<br>If you did not mean to reset your password, please ignore this email and your password will remain the same."
+ },
+ {
+ "id": "api.templates.reset_body.title",
+ "translation": "You requested a password reset"
+ },
+ {
+ "id": "api.templates.reset_subject",
+ "translation": "Reset your password"
+ },
+ {
+ "id": "api.templates.signin_change_email.body.method_email",
+ "translation": "email and password"
+ },
+ {
+ "id": "api.templates.signin_change_email.body.title",
+ "translation": "You updated your sign-in method"
+ },
+ {
+ "id": "api.templates.signup_team_body.button",
+ "translation": "Set up your team"
+ },
+ {
+ "id": "api.templates.signup_team_body.info",
+ "translation": "{{ .SiteName }} is one place for all your team communication, searchable and available anywhere.<br>You'll get more out of {{ .SiteName }} when your team is in constant communication--let's get them on board."
+ },
+ {
+ "id": "api.templates.signup_team_body.title",
+ "translation": "Thanks for creating a team!"
+ },
+ {
+ "id": "api.templates.signup_team_subject",
+ "translation": "{{ .SiteName }} Team Setup"
+ },
+ {
+ "id": "api.templates.singin_change_email.body.info",
+ "translation": "You updated your sign-in method for {{.TeamDisplayName}} on {{ .TeamURL }} to {{.Method}}.<br>If this change wasn't initiated by you, please contact your system administrator."
+ },
+ {
+ "id": "api.templates.singin_change_email.subject",
+ "translation": "You updated your sign-in method for {{.TeamDisplayName}} on {{ .SiteName }}"
+ },
+ {
+ "id": "api.templates.verify_body.button",
+ "translation": "Verify Email"
+ },
+ {
+ "id": "api.templates.verify_body.info",
+ "translation": "Please verify your email address by clicking below."
+ },
+ {
+ "id": "api.templates.verify_body.title",
+ "translation": "You've joined the {{ .TeamDisplayName }} team"
+ },
+ {
+ "id": "api.templates.verify_subject",
+ "translation": "[{{ .TeamDisplayName }} {{ .SiteName }}] Email Verification"
+ },
+ {
+ "id": "api.templates.welcome_body.button",
+ "translation": "Verify Email"
+ },
+ {
+ "id": "api.templates.welcome_body.info",
+ "translation": "Please verify your email address by clicking below."
+ },
+ {
+ "id": "api.templates.welcome_body.info2",
+ "translation": "You can sign-in to your new team from the web address:"
+ },
+ {
+ "id": "api.templates.welcome_body.info3",
+ "translation": "Mattermost lets you share messages and files from your PC or phone, with instant search and archiving."
+ },
+ {
+ "id": "api.templates.welcome_body.title",
+ "translation": "You've joined the {{ .TeamDisplayName }} team"
+ },
+ {
+ "id": "api.templates.welcome_subject",
+ "translation": "You joined {{ .TeamDisplayName }}"
+ },
+ {
"id": "api.user.add_direct_channels_and_forget.failed.error",
"translation": "Failed to add direct channel preferences for user user_id=%s, team_id=%s, err=%v"
},
@@ -1192,6 +1432,10 @@
"translation": "The reset link has expired"
},
{
+ "id": "api.user.reset_password.method",
+ "translation": "using a reset password link"
+ },
+ {
"id": "api.user.reset_password.sso.app_error",
"translation": "Cannot reset password for SSO accounts"
},
@@ -1320,11 +1564,215 @@
"translation": "Unable to upload profile image. File is too large."
},
{
+ "id": "api.web_conn.new_web_conn.last_activity.error",
+ "translation": "Failed to update LastActivityAt for user_id=%v and session_id=%v, err=%v"
+ },
+ {
+ "id": "api.web_conn.new_web_conn.last_ping.error",
+ "translation": "Failed to update LastPingAt for user_id=%v, err=%v"
+ },
+ {
+ "id": "api.web_hub.start.stopping.debug",
+ "translation": "stopping %v connections"
+ },
+ {
+ "id": "api.web_socket.connect.error",
+ "translation": "websocket connect err: %v"
+ },
+ {
+ "id": "api.web_socket.connect.upgrade.app_error",
+ "translation": "Failed to upgrade websocket connection"
+ },
+ {
+ "id": "api.web_socket.init.debug",
+ "translation": "Initializing web socket api routes"
+ },
+ {
+ "id": "api.web_team_hun.start.debug",
+ "translation": "team hub stopping for teamId=%v"
+ },
+ {
+ "id": "api.webhook.create_incoming.disabled.app_errror",
+ "translation": "Incoming webhooks have been disabled by the system admin."
+ },
+ {
+ "id": "api.webhook.create_outgoing.disabled.app_error",
+ "translation": "Outgoing webhooks have been disabled by the system admin."
+ },
+ {
+ "id": "api.webhook.create_outgoing.triggers.app_error",
+ "translation": "Either trigger_words or channel_id must be set"
+ },
+ {
+ "id": "api.webhook.delete_incoming.disabled.app_errror",
+ "translation": "Incoming webhooks have been disabled by the system admin."
+ },
+ {
+ "id": "api.webhook.delete_incoming.permissions.app_errror",
+ "translation": "Inappropriate permissions to delete incoming webhook"
+ },
+ {
+ "id": "api.webhook.delete_outgoing.disabled.app_error",
+ "translation": "Outgoing webhooks have been disabled by the system admin."
+ },
+ {
+ "id": "api.webhook.delete_outgoing.permissions.app_error",
+ "translation": "Inappropriate permissions to delete outcoming webhook"
+ },
+ {
+ "id": "api.webhook.get_incoming.disabled.app_error",
+ "translation": "Incoming webhooks have been disabled by the system admin."
+ },
+ {
+ "id": "api.webhook.get_outgoing.disabled.app_error",
+ "translation": "Outgoing webhooks have been disabled by the system admin."
+ },
+ {
+ "id": "api.webhook.init.debug",
+ "translation": "Initializing webhook api routes"
+ },
+ {
+ "id": "api.webhook.regen_outgoing_token.disabled.app_error",
+ "translation": "Outgoing webhooks have been disabled by the system admin."
+ },
+ {
+ "id": "api.webhook.regen_outgoing_token.permissions.app_error",
+ "translation": "Inappropriate permissions to regenerate outcoming webhook token"
+ },
+ {
+ "id": "manaultesting.get_channel_id.no_found.debug",
+ "translation": "Could not find channel: %v, %v possibilites searched"
+ },
+ {
+ "id": "manaultesting.get_channel_id.unable.debug",
+ "translation": "Unable to get channels"
+ },
+ {
+ "id": "manaultesting.manual_test.create.info",
+ "translation": "Creating user and team"
+ },
+ {
+ "id": "manaultesting.manual_test.parse.app_error",
+ "translation": "Unable to parse URL"
+ },
+ {
+ "id": "manaultesting.manual_test.setup.info",
+ "translation": "Setting up for manual test..."
+ },
+ {
+ "id": "manaultesting.manual_test.uid.debug",
+ "translation": "No uid in url"
+ },
+ {
+ "id": "manaultesting.test_autolink.info",
+ "translation": "Manual Auto Link Test"
+ },
+ {
+ "id": "manaultesting.test_autolink.unable.app_error",
+ "translation": "Unable to get channels"
+ },
+ {
"id": "mattermost.current_version",
"translation": "Current version is %v (%v/%v/%v)"
},
{
+ "id": "utils.config.load_config.decoding.panic",
+ "translation": "Error decoding config file={{.Filename}}, err={{.Error}}"
+ },
+ {
+ "id": "utils.config.load_config.getting.panic",
+ "translation": "Error getting config info file={{.Filename}}, err={{.Error}}"
+ },
+ {
+ "id": "utils.config.load_config.opening.panic",
+ "translation": "Error opening config file={{.Filename}}, err={{.Error}}"
+ },
+ {
+ "id": "utils.config.load_config.validating.panic",
+ "translation": "Error validating config file={{.Filename}}, err={{.Error}}"
+ },
+ {
+ "id": "utils.config.save_config.saving.app_error",
+ "translation": "An error occurred while saving the file to {{.Filename}}"
+ },
+ {
"id": "utils.i18n.loaded",
"translation": "Loaded system translations for '%v' from '%v'"
+ },
+ {
+ "id": "utils.iru.with_evict",
+ "translation": "Must provide a positive size"
+ },
+ {
+ "id": "utils.license.load_license.invalid.warn",
+ "translation": "No valid enterprise license found"
+ },
+ {
+ "id": "utils.license.load_license.open_find.warn",
+ "translation": "Unable to open/find license file"
+ },
+ {
+ "id": "utils.license.remove_license.unable.error",
+ "translation": "Unable to remove license file, err=%v"
+ },
+ {
+ "id": "utils.license.validate_license.decode.error",
+ "translation": "Encountered error decoding license, err=%v"
+ },
+ {
+ "id": "utils.license.validate_license.invalid.error",
+ "translation": "Invalid signature, err=%v"
+ },
+ {
+ "id": "utils.license.validate_license.not_long.error",
+ "translation": "Signed license not long enough"
+ },
+ {
+ "id": "utils.license.validate_license.signing.error",
+ "translation": "Encountered error signing license, err=%v"
+ },
+ {
+ "id": "utils.mail.connect_smtp.open.app_error",
+ "translation": "Failed to open connection"
+ },
+ {
+ "id": "utils.mail.connect_smtp.open_tls.app_error",
+ "translation": "Failed to open TLS connection"
+ },
+ {
+ "id": "utils.mail.new_client.auth.app_error",
+ "translation": "Failed to authenticate on SMTP server"
+ },
+ {
+ "id": "utils.mail.new_client.open.error",
+ "translation": "Failed to open a connection to SMTP server %v"
+ },
+ {
+ "id": "utils.mail.send_mail.close.app_error",
+ "translation": "Failed to close connection to SMTP server"
+ },
+ {
+ "id": "utils.mail.send_mail.from_address.app_error",
+ "translation": "Failed to add from email address"
+ },
+ {
+ "id": "utils.mail.send_mail.msg.app_error",
+ "translation": "Failed to write email message"
+ },
+ {
+ "id": "utils.mail.send_mail.msg_data.app_error",
+ "translation": "Failed to add email messsage data"
+ },
+ {
+ "id": "utils.mail.send_mail.sending.debug",
+ "translation": "sending mail to %v with subject of '%v'"
+ },
+ {
+ "id": "utils.mail.send_mail.to_address.app_error",
+ "translation": "Failed to add to email address"
+ },
+ {
+ "id": "utils.mail.test.configured.error",
+ "translation": "SMTP server settings do not appear to be configured properly err=%v details=%v"
}
] \ No newline at end of file
diff --git a/i18n/es.json b/i18n/es.json
index 480f23857..4ec3eb8d8 100644
--- a/i18n/es.json
+++ b/i18n/es.json
@@ -1,5 +1,53 @@
[
{
+ "id": "April",
+ "translation": "Abril"
+ },
+ {
+ "id": "August",
+ "translation": "Agosto"
+ },
+ {
+ "id": "December",
+ "translation": "Diciembre"
+ },
+ {
+ "id": "February",
+ "translation": "Febrero"
+ },
+ {
+ "id": "January",
+ "translation": "Enero"
+ },
+ {
+ "id": "July",
+ "translation": "Julio"
+ },
+ {
+ "id": "June",
+ "translation": "Junio"
+ },
+ {
+ "id": "March",
+ "translation": "Marzo"
+ },
+ {
+ "id": "May",
+ "translation": "Mayo"
+ },
+ {
+ "id": "November",
+ "translation": "Noviembre"
+ },
+ {
+ "id": "October",
+ "translation": "Octubre"
+ },
+ {
+ "id": "September",
+ "translation": "Septiembre"
+ },
+ {
"id": "api.admin.file_read_error",
"translation": "Error leyendo el archivo de registro"
},
@@ -749,7 +797,7 @@
},
{
"id": "api.post.update_post.permissions_details.app_error",
- "translation": "Already delted id={{.PostId}}"
+ "translation": "Ya fué elminado el id={{.PostId}}"
},
{
"id": "api.post_get_post_by_id.get.app_error",
@@ -992,6 +1040,198 @@
"translation": "No tienes los permisos apropiados"
},
{
+ "id": "api.templates.email_change_body.info",
+ "translation": "Tu dirección de correo electrónico para {{.TeamDisplayName}} ha sido cambiada por {{.NewEmail}}.<br>Si este cambio no fue realizado por ti, por favor contacta un administrador de sistema."
+ },
+ {
+ "id": "api.templates.email_change_body.title",
+ "translation": "Haz actualizado tu correo electrónico"
+ },
+ {
+ "id": "api.templates.email_change_subject",
+ "translation": "Tu dirección de correo para {{.TeamDisplayName}} ha cambiado"
+ },
+ {
+ "id": "api.templates.email_change_verify_body.button",
+ "translation": "Confirmar Correo"
+ },
+ {
+ "id": "api.templates.email_change_verify_body.info",
+ "translation": "Para terminar de actualizar tu dirección de correo para {{.TeamDisplayName}}, por favor pincha en botón de abajo para confirmar que está es la dirección correcta."
+ },
+ {
+ "id": "api.templates.email_change_verify_body.title",
+ "translation": "Haz actualizado tu correo electrónico"
+ },
+ {
+ "id": "api.templates.email_change_verify_subject",
+ "translation": "Verificación de la nueva dirección de correo electrónico para {{.TeamDisplayName}}"
+ },
+ {
+ "id": "api.templates.email_footer",
+ "translation": "Para cambiar tus preferencias de notificaciones, inicia sesión en tu equipo y dirigete a Configurar Cuenta > Notificaciones."
+ },
+ {
+ "id": "api.templates.email_info",
+ "translation": "Si tienes alguna pregunta, escribenos en cualquier momento a: <a href=\"mailto:{{.FeedbackEmail}}\" style=\"text-decoration: none; color:#2389D7;\">{{.FeedbackEmail}}</a>.<br>Los mejores deseos,<br>El Equipo {{.SiteName}}<br>"
+ },
+ {
+ "id": "api.templates.error.link",
+ "translation": "Volver al sitio del equipo"
+ },
+ {
+ "id": "api.templates.error.title",
+ "translation": "Necesitamos de tu ayuda en {{ .SiteName }}:"
+ },
+ {
+ "id": "api.templates.find_teams_body.found",
+ "translation": "Haz solicitado encontrar los equipos a los que tienes asociado tu correo electrónico y encontramos los siguientes:"
+ },
+ {
+ "id": "api.templates.find_teams_body.not_found",
+ "translation": "No hemos podido encontrar ningún equipo asociado al correo electrónico suministrado."
+ },
+ {
+ "id": "api.templates.find_teams_body.title",
+ "translation": "Tus Equipos"
+ },
+ {
+ "id": "api.templates.find_teams_subject",
+ "translation": "Tus equipos de {{ .SiteName }}"
+ },
+ {
+ "id": "api.templates.invite_body.button",
+ "translation": "Unirme al Equipo"
+ },
+ {
+ "id": "api.templates.invite_body.extra_info",
+ "translation": "Mattermost te permite compartir mensajes y archivos de tu Computador o teléfono, incluyendo busquedas instantáneas. Una vez que te hayas unido a <strong>{{.TeamDisplayName}}</strong>, Podrás iniciar sesión en tu nuevo equipo y utilizar todas las características en cualquier momento desde la dirección web:<br/><br/><a href=\"{{.TeamURL}}\">{{.TeamURL}}</a>"
+ },
+ {
+ "id": "api.templates.invite_body.info",
+ "translation": "<strong>{{.SenderName}}</strong> {{.SenderStatus}} del equipo <strong>{{.TeamDisplayName}}</strong>, te ha invitado a unirte."
+ },
+ {
+ "id": "api.templates.invite_body.title",
+ "translation": "Haz sido invitado"
+ },
+ {
+ "id": "api.templates.invite_subject",
+ "translation": "{{ .SenderName }} te ha invitado a unirte al equipo {{ .TeamDisplayName }} en {{.SiteName}}"
+ },
+ {
+ "id": "api.templates.password_change_body.info",
+ "translation": "Tu contraseña ha sido cambiada para {{.TeamDisplayName}} en {{ .TeamURL }} por el método de {{.Method}}.<br>Si este cambio no fue realizado por ti, por favor contacta a un administrador del sistema."
+ },
+ {
+ "id": "api.templates.password_change_body.title",
+ "translation": "Haz cambiado tu contraseña"
+ },
+ {
+ "id": "api.templates.password_change_subject",
+ "translation": "Tu contraseña ha sido cambiada para {{.TeamDisplayName}} en {{ .SiteName }}"
+ },
+ {
+ "id": "api.templates.post_body.button",
+ "translation": "Ir al Canal"
+ },
+ {
+ "id": "api.templates.post_body.info",
+ "translation": "Canal: {{.ChannelName}}<br>{{.SenderName}} - {{.Day}} {{.Month}}, {{.Hour}}:{{.Minute}} {{.TimeZone}}"
+ },
+ {
+ "id": "api.templates.post_subject",
+ "translation": "{{.SubjectText}} en {{.TeamDisplayName}} el {{.Day}} {{.Month}}, {{.Year}}"
+ },
+ {
+ "id": "api.templates.reset_body.button",
+ "translation": "Restablecer Contraseña"
+ },
+ {
+ "id": "api.templates.reset_body.info",
+ "translation": "Para cambiar tu contraseña, pincha el botón \"Restablecer Contraseña\" que se encuentra abajo.<br>Si no fue tu intención restablecer tu contraseña, por favor ignora este correo y tu contraseña seguirá siendo la misma."
+ },
+ {
+ "id": "api.templates.reset_body.title",
+ "translation": "Haz solicitado restablecer tu contraseña"
+ },
+ {
+ "id": "api.templates.reset_subject",
+ "translation": "Restablece tu contraseña"
+ },
+ {
+ "id": "api.templates.signin_change_email.body.method_email",
+ "translation": "correo electrónico y contraseña"
+ },
+ {
+ "id": "api.templates.signin_change_email.body.title",
+ "translation": "Haz actualizado el método con el que inicias sesión"
+ },
+ {
+ "id": "api.templates.signup_team_body.button",
+ "translation": "Configura tu equipo"
+ },
+ {
+ "id": "api.templates.signup_team_body.info",
+ "translation": "{{ .SiteName }} es el lugar para todas las comunicaciones de tu equipo, con capacidades de búsqueda y disponible desde cualquier parte.<br>Podrás aprovechar al máximo {{ .SiteName }} cuando tu equipo esté en constante comunicación--Traigamoslos a bordo."
+ },
+ {
+ "id": "api.templates.signup_team_body.title",
+ "translation": "¡Gracias por haber creado un equipo!"
+ },
+ {
+ "id": "api.templates.signup_team_subject",
+ "translation": "Configuración del equipo en {{ .SiteName }}"
+ },
+ {
+ "id": "api.templates.singin_change_email.body.info",
+ "translation": "Haz actualizado el método con el que inicias sesión en {{.TeamURL}} para el equipo {{.TeamDisplayName}} por {{.Method}}.<br>Si este cambio no fue realizado por ti, por favor contacta a un administrador del sistema."
+ },
+ {
+ "id": "api.templates.singin_change_email.subject",
+ "translation": "Cambio del método de inicio de sesión para {{.TeamDisplayName}} en {{ .SiteName }}"
+ },
+ {
+ "id": "api.templates.verify_body.button",
+ "translation": "Confirmar Correo"
+ },
+ {
+ "id": "api.templates.verify_body.info",
+ "translation": "Por favor verifica tu correo electrónico al pinchar el botón de abajo."
+ },
+ {
+ "id": "api.templates.verify_body.title",
+ "translation": "Te haz unido al equipo {{ .TeamDisplayName }}"
+ },
+ {
+ "id": "api.templates.verify_subject",
+ "translation": "[{{ .TeamDisplayName }} {{ .SiteName }}] Correo de Verificación"
+ },
+ {
+ "id": "api.templates.welcome_body.button",
+ "translation": "Confirmar Correo"
+ },
+ {
+ "id": "api.templates.welcome_body.info",
+ "translation": "Por favor verifica tu correo electrónico al pinchar el botón de abajo."
+ },
+ {
+ "id": "api.templates.welcome_body.info2",
+ "translation": "Puedes iniciar sesión en tu nuevo equipo desde la dirección web:"
+ },
+ {
+ "id": "api.templates.welcome_body.info3",
+ "translation": "Mattermost te permite compartir mensajes y archivos desde un computador o teléfono desde donde te encuentres."
+ },
+ {
+ "id": "api.templates.welcome_body.title",
+ "translation": "Te haz unido al equipo {{ .TeamDisplayName }}"
+ },
+ {
+ "id": "api.templates.welcome_subject",
+ "translation": "Te haz unido a {{ .TeamDisplayName }}"
+ },
+ {
"id": "api.user.add_direct_channels_and_forget.failed.error",
"translation": "Falla al agragar las preferencias del canal directo para el usuario user_id=%s, team_id=%s, err=%v"
},
@@ -1192,6 +1432,10 @@
"translation": "El enlace para restablecer la contraseña ha expirado"
},
{
+ "id": "api.user.reset_password.method",
+ "translation": "utilizando el enlace para restablecer contraseña"
+ },
+ {
"id": "api.user.reset_password.sso.app_error",
"translation": "No se puede restablecer la contraseña para cuentas SSO"
},
@@ -1320,11 +1564,215 @@
"translation": "No se pudo actualizar la imagen del perfil. El archivo es muy grande."
},
{
+ "id": "api.web_conn.new_web_conn.last_activity.error",
+ "translation": "Falla al actualizar LastActivityAt para user_id=%v and session_id=%v, err=%v"
+ },
+ {
+ "id": "api.web_conn.new_web_conn.last_ping.error",
+ "translation": "Falla al actualizar LastPingAt para el user_id=%v, err=%v"
+ },
+ {
+ "id": "api.web_hub.start.stopping.debug",
+ "translation": "deteniendo todas las conexiones"
+ },
+ {
+ "id": "api.web_socket.connect.error",
+ "translation": "conexión al websocket err: %v"
+ },
+ {
+ "id": "api.web_socket.connect.upgrade.app_error",
+ "translation": "Falla al actualizar la conexión del websocket"
+ },
+ {
+ "id": "api.web_socket.init.debug",
+ "translation": "Inicializando rutas del API para los web socket"
+ },
+ {
+ "id": "api.web_team_hun.start.debug",
+ "translation": "deteniendo el hub de equipo para teamId=%v"
+ },
+ {
+ "id": "api.webhook.create_incoming.disabled.app_errror",
+ "translation": "Webhooks entrantes han sido deshabilitados por el administrador del sistema."
+ },
+ {
+ "id": "api.webhook.create_outgoing.disabled.app_error",
+ "translation": "Webhooks de Salida han sido deshabilitados por el administrador del sistema."
+ },
+ {
+ "id": "api.webhook.create_outgoing.triggers.app_error",
+ "translation": "Debe establecerse palabras gatilladoras o un channel_id"
+ },
+ {
+ "id": "api.webhook.delete_incoming.disabled.app_errror",
+ "translation": "Webhooks entrantes han sido deshabilitados por el administrador del sistema."
+ },
+ {
+ "id": "api.webhook.delete_incoming.permissions.app_errror",
+ "translation": "Permisos inapropiados para eliminar un webhook entrante"
+ },
+ {
+ "id": "api.webhook.delete_outgoing.disabled.app_error",
+ "translation": "Webhooks de Salida han sido deshabilitados por el administrador del sistema."
+ },
+ {
+ "id": "api.webhook.delete_outgoing.permissions.app_error",
+ "translation": "Permisos inapropiados para eliminar el webhook saliente"
+ },
+ {
+ "id": "api.webhook.get_incoming.disabled.app_error",
+ "translation": "Webhooks entrantes han sido deshabilitados por el administrador del sistema."
+ },
+ {
+ "id": "api.webhook.get_outgoing.disabled.app_error",
+ "translation": "Webhooks de Salida han sido deshabilitados por el administrador del sistema."
+ },
+ {
+ "id": "api.webhook.init.debug",
+ "translation": "Inicializando rutas del API para los Webhooks"
+ },
+ {
+ "id": "api.webhook.regen_outgoing_token.disabled.app_error",
+ "translation": "Webhooks de Salida han sido deshabilitados por el administrador del sistema."
+ },
+ {
+ "id": "api.webhook.regen_outgoing_token.permissions.app_error",
+ "translation": "Permisos inapropiados para regenerar un token para el Webhook saliente"
+ },
+ {
+ "id": "manaultesting.get_channel_id.no_found.debug",
+ "translation": "No pudimos encontrar el canal: %v, búsqueda realizada con estas posibilidades %v"
+ },
+ {
+ "id": "manaultesting.get_channel_id.unable.debug",
+ "translation": "No se pudo obtener los canales"
+ },
+ {
+ "id": "manaultesting.manual_test.create.info",
+ "translation": "Creando usuario y equipo"
+ },
+ {
+ "id": "manaultesting.manual_test.parse.app_error",
+ "translation": "No se pudo analizar el URL"
+ },
+ {
+ "id": "manaultesting.manual_test.setup.info",
+ "translation": "Configurando para pruebas manuales..."
+ },
+ {
+ "id": "manaultesting.manual_test.uid.debug",
+ "translation": "No hay un uid en el url"
+ },
+ {
+ "id": "manaultesting.test_autolink.info",
+ "translation": "Prueba Manual de Enlaces Automáticos"
+ },
+ {
+ "id": "manaultesting.test_autolink.unable.app_error",
+ "translation": "No se pudo obtener los canales"
+ },
+ {
"id": "mattermost.current_version",
"translation": "La versión actual es %v (%v/%v/%v)"
},
{
+ "id": "utils.config.load_config.decoding.panic",
+ "translation": "Error decifrando la configuración del archivo={{.Filename}}, err={{.Error}}"
+ },
+ {
+ "id": "utils.config.load_config.getting.panic",
+ "translation": "Error obteniendo la iformación de configuración del archivo={{.Filename}}, err={{.Error}}"
+ },
+ {
+ "id": "utils.config.load_config.opening.panic",
+ "translation": "Error abriendo la configuración del archivo={{.Filename}}, err={{.Error}}"
+ },
+ {
+ "id": "utils.config.load_config.validating.panic",
+ "translation": "Error validando la configuración del archivo={{.Filename}}, err={{.Error}}"
+ },
+ {
+ "id": "utils.config.save_config.saving.app_error",
+ "translation": "Ocurrió un error mientras se guardaba el archivo en {{.Filename}}"
+ },
+ {
"id": "utils.i18n.loaded",
"translation": "Cargada traducciones del sistema para '%v' desde '%v'"
+ },
+ {
+ "id": "utils.iru.with_evict",
+ "translation": "Debe proporcionar un tamaño positivo"
+ },
+ {
+ "id": "utils.license.load_license.invalid.warn",
+ "translation": "No se encontró una licencia enterprise válida"
+ },
+ {
+ "id": "utils.license.load_license.open_find.warn",
+ "translation": "No pudimos encontrar/abrir el achivo de licencia"
+ },
+ {
+ "id": "utils.license.remove_license.unable.error",
+ "translation": "No se pudo remover el archivo de la licencia, err=%v"
+ },
+ {
+ "id": "utils.license.validate_license.decode.error",
+ "translation": "Encontramos un error decodificando la licencia, err=%v"
+ },
+ {
+ "id": "utils.license.validate_license.invalid.error",
+ "translation": "Firma inválida, err=%v"
+ },
+ {
+ "id": "utils.license.validate_license.not_long.error",
+ "translation": "La licencia firmada no es suficientemente larga"
+ },
+ {
+ "id": "utils.license.validate_license.signing.error",
+ "translation": "Encontramos un error al firmar la licencia, err=%v"
+ },
+ {
+ "id": "utils.mail.connect_smtp.open.app_error",
+ "translation": "Falla al abrir conexión"
+ },
+ {
+ "id": "utils.mail.connect_smtp.open_tls.app_error",
+ "translation": "Falla al abrir una conexión TLS"
+ },
+ {
+ "id": "utils.mail.new_client.auth.app_error",
+ "translation": "Falla autenticando contra el servidor SMTP"
+ },
+ {
+ "id": "utils.mail.new_client.open.error",
+ "translation": "Falla al abrir la conexión al servidor SMTP %v"
+ },
+ {
+ "id": "utils.mail.send_mail.close.app_error",
+ "translation": "Falla al cerrar la conexión al servidor SMTP"
+ },
+ {
+ "id": "utils.mail.send_mail.from_address.app_error",
+ "translation": "Falla al agregar el correo electrónico desde"
+ },
+ {
+ "id": "utils.mail.send_mail.msg.app_error",
+ "translation": "Falla al escribir el mensaje del correo electrónico"
+ },
+ {
+ "id": "utils.mail.send_mail.msg_data.app_error",
+ "translation": "Falla al agregar la data al mensaje del correo electrónico"
+ },
+ {
+ "id": "utils.mail.send_mail.sending.debug",
+ "translation": "enviano correo electrónico a %v con el asunto '%v'"
+ },
+ {
+ "id": "utils.mail.send_mail.to_address.app_error",
+ "translation": "Falla al agregar el correo electrónico para"
+ },
+ {
+ "id": "utils.mail.test.configured.error",
+ "translation": "El servidor SMTP parece no estar configurado apropiadamente err=%v details=%v"
}
] \ No newline at end of file
diff --git a/manualtesting/manual_testing.go b/manualtesting/manual_testing.go
index befc835fb..2f1096fd5 100644
--- a/manualtesting/manual_testing.go
+++ b/manualtesting/manual_testing.go
@@ -32,12 +32,12 @@ func InitManualTesting() {
func manualTest(c *api.Context, w http.ResponseWriter, r *http.Request) {
// Let the world know
- l4g.Info("Setting up for manual test...")
+ l4g.Info(utils.T("manaultesting.manual_test.setup.info"))
// URL Parameters
params, err := url.ParseQuery(r.URL.RawQuery)
if err != nil {
- c.Err = model.NewAppError("/manual", "Unable to parse URL", "")
+ c.Err = model.NewLocAppError("/manual", "manaultesting.manual_test.parse.app_error", nil, "")
return
}
@@ -49,7 +49,7 @@ func manualTest(c *api.Context, w http.ResponseWriter, r *http.Request) {
hash := hasher.Sum32()
rand.Seed(int64(hash))
} else {
- l4g.Debug("No uid in url")
+ l4g.Debug(utils.T("manaultesting.manual_test.uid.debug"))
}
// Create a client for tests to use
@@ -61,7 +61,7 @@ func manualTest(c *api.Context, w http.ResponseWriter, r *http.Request) {
var teamID string
var userID string
if ok1 && ok2 {
- l4g.Info("Creating user and team")
+ l4g.Info(utils.T("manaultesting.manual_test.create.info"))
// Create team for testing
team := &model.Team{
DisplayName: teamDisplayName[0],
@@ -153,7 +153,7 @@ func getChannelID(channelname string, teamid string, userid string) (id string,
// Grab all the channels
result := <-api.Srv.Store.Channel().GetChannels(teamid, userid)
if result.Err != nil {
- l4g.Debug("Unable to get channels")
+ l4g.Debug(utils.T("manaultesting.get_channel_id.unable.debug"))
return "", false
}
@@ -164,6 +164,6 @@ func getChannelID(channelname string, teamid string, userid string) (id string,
return channel.Id, true
}
}
- l4g.Debug("Could not find channel: " + channelname + ", " + strconv.Itoa(len(data.Channels)) + " possibilites searched")
+ l4g.Debug(utils.T("manaultesting.get_channel_id.no_found.debug"), channelname, strconv.Itoa(len(data.Channels)))
return "", false
}
diff --git a/manualtesting/test_autolink.go b/manualtesting/test_autolink.go
index 16d2d713a..f9f213da1 100644
--- a/manualtesting/test_autolink.go
+++ b/manualtesting/test_autolink.go
@@ -6,6 +6,7 @@ package manualtesting
import (
l4g "github.com/alecthomas/log4go"
"github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
)
const LINK_POST_TEXT = `
@@ -20,10 +21,10 @@ https://medium.com/@slackhq/11-useful-tips-for-getting-the-most-of-slack-5dfb3d1
`
func testAutoLink(env TestEnvironment) *model.AppError {
- l4g.Info("Manual Auto Link Test")
+ l4g.Info(utils.T("manaultesting.test_autolink.info"))
channelID, err := getChannelID(model.DEFAULT_CHANNEL, env.CreatedTeamId, env.CreatedUserId)
if err != true {
- return model.NewAppError("/manualtest", "Unable to get channels", "")
+ return model.NewLocAppError("/manualtest", "manaultesting.test_autolink.unable.app_error", nil, "")
}
post := &model.Post{
diff --git a/mattermost.go b/mattermost.go
index 9786a6abd..51a9591db 100644
--- a/mattermost.go
+++ b/mattermost.go
@@ -66,7 +66,9 @@ func main() {
api.InitApi()
web.InitWeb()
- utils.LoadLicense()
+ if model.BuildEnterpriseReady == "true" {
+ utils.LoadLicense()
+ }
if flagRunCmds {
runCmds()
diff --git a/model/client.go b/model/client.go
index 75b93c971..b8e7c4894 100644
--- a/model/client.go
+++ b/model/client.go
@@ -434,7 +434,7 @@ func (c *Client) TestEmail(config *Config) (*Result, *AppError) {
}
}
-func (c *Client) GetAnalytics(teamId, name string) (*Result, *AppError) {
+func (c *Client) GetTeamAnalytics(teamId, name string) (*Result, *AppError) {
if r, err := c.DoApiGet("/admin/analytics/"+teamId+"/"+name, "", ""); err != nil {
return nil, err
} else {
@@ -443,6 +443,15 @@ func (c *Client) GetAnalytics(teamId, name string) (*Result, *AppError) {
}
}
+func (c *Client) GetSystemAnalytics(name string) (*Result, *AppError) {
+ if r, err := c.DoApiGet("/admin/analytics/"+name, "", ""); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), AnalyticsRowsFromJson(r.Body)}, nil
+ }
+}
+
func (c *Client) CreateChannel(channel *Channel) (*Result, *AppError) {
if r, err := c.DoApiPost("/channels/create", channel.ToJson()); err != nil {
return nil, err
diff --git a/model/user.go b/model/user.go
index 7744b0073..44228d93f 100644
--- a/model/user.go
+++ b/model/user.go
@@ -236,7 +236,6 @@ func (u *User) Sanitize(options map[string]bool) {
}
func (u *User) ClearNonProfileFields() {
- u.CreateAt = 0
u.UpdateAt = 0
u.Password = ""
u.AuthData = ""
diff --git a/store/sql_channel_store.go b/store/sql_channel_store.go
index 4585647de..336398ae7 100644
--- a/store/sql_channel_store.go
+++ b/store/sql_channel_store.go
@@ -869,15 +869,13 @@ func (s SqlChannelStore) AnalyticsTypeCount(teamId string, channelType string) S
go func() {
result := StoreResult{}
- v, err := s.GetReplica().SelectInt(
- `SELECT
- COUNT(Id) AS Value
- FROM
- Channels
- WHERE
- TeamId = :TeamId
- AND Type = :ChannelType`,
- map[string]interface{}{"TeamId": teamId, "ChannelType": channelType})
+ query := "SELECT COUNT(Id) AS Value FROM Channels WHERE Type = :ChannelType"
+
+ if len(teamId) > 0 {
+ query += " AND TeamId = :TeamId"
+ }
+
+ v, err := s.GetReplica().SelectInt(query, map[string]interface{}{"TeamId": teamId, "ChannelType": channelType})
if err != nil {
result.Err = model.NewAppError("SqlChannelStore.AnalyticsTypeCount", "We couldn't get channel type counts", err.Error())
} else {
diff --git a/store/sql_post_store.go b/store/sql_post_store.go
index 40dca9930..e332858e4 100644
--- a/store/sql_post_store.go
+++ b/store/sql_post_store.go
@@ -805,9 +805,13 @@ func (s SqlPostStore) AnalyticsUserCountsWithPostsByDay(teamId string) StoreChan
FROM
Posts, Channels
WHERE
- Posts.ChannelId = Channels.Id
- AND Channels.TeamId = :TeamId
- AND Posts.CreateAt <= :EndTime
+ Posts.ChannelId = Channels.Id`
+
+ if len(teamId) > 0 {
+ query += " AND Channels.TeamId = :TeamId"
+ }
+
+ query += ` AND Posts.CreateAt <= :EndTime
ORDER BY Name DESC) AS t1
GROUP BY Name
ORDER BY Name DESC
@@ -824,9 +828,13 @@ func (s SqlPostStore) AnalyticsUserCountsWithPostsByDay(teamId string) StoreChan
FROM
Posts, Channels
WHERE
- Posts.ChannelId = Channels.Id
- AND Channels.TeamId = :TeamId
- AND Posts.CreateAt <= :EndTime
+ Posts.ChannelId = Channels.Id`
+
+ if len(teamId) > 0 {
+ query += " AND Channels.TeamId = :TeamId"
+ }
+
+ query += ` AND Posts.CreateAt <= :EndTime
ORDER BY Name DESC) AS t1
GROUP BY Name
ORDER BY Name DESC
@@ -869,9 +877,13 @@ func (s SqlPostStore) AnalyticsPostCountsByDay(teamId string) StoreChannel {
FROM
Posts, Channels
WHERE
- Posts.ChannelId = Channels.Id
- AND Channels.TeamId = :TeamId
- AND Posts.CreateAt <= :EndTime
+ Posts.ChannelId = Channels.Id`
+
+ if len(teamId) > 0 {
+ query += " AND Channels.TeamId = :TeamId"
+ }
+
+ query += ` AND Posts.CreateAt <= :EndTime
AND Posts.CreateAt >= :StartTime) AS t1
GROUP BY Name
ORDER BY Name DESC
@@ -888,9 +900,13 @@ func (s SqlPostStore) AnalyticsPostCountsByDay(teamId string) StoreChannel {
FROM
Posts, Channels
WHERE
- Posts.ChannelId = Channels.Id
- AND Channels.TeamId = :TeamId
- AND Posts.CreateAt <= :EndTime
+ Posts.ChannelId = Channels.Id`
+
+ if len(teamId) > 0 {
+ query += " AND Channels.TeamId = :TeamId"
+ }
+
+ query += ` AND Posts.CreateAt <= :EndTime
AND Posts.CreateAt >= :StartTime) AS t1
GROUP BY Name
ORDER BY Name DESC
@@ -924,16 +940,20 @@ func (s SqlPostStore) AnalyticsPostCount(teamId string) StoreChannel {
go func() {
result := StoreResult{}
- v, err := s.GetReplica().SelectInt(
+ query :=
`SELECT
COUNT(Posts.Id) AS Value
FROM
Posts,
Channels
WHERE
- Posts.ChannelId = Channels.Id
- AND Channels.TeamId = :TeamId`,
- map[string]interface{}{"TeamId": teamId})
+ Posts.ChannelId = Channels.Id`
+
+ if len(teamId) > 0 {
+ query += " AND Channels.TeamId = :TeamId"
+ }
+
+ v, err := s.GetReplica().SelectInt(query, map[string]interface{}{"TeamId": teamId})
if err != nil {
result.Err = model.NewAppError("SqlPostStore.AnalyticsPostCount", "We couldn't get post counts", err.Error())
} else {
diff --git a/store/sql_user_store.go b/store/sql_user_store.go
index 0f73f73c3..efd8b7f33 100644
--- a/store/sql_user_store.go
+++ b/store/sql_user_store.go
@@ -600,3 +600,30 @@ func (us SqlUserStore) PermanentDelete(userId string) StoreChannel {
return storeChannel
}
+
+func (us SqlUserStore) AnalyticsUniqueUserCount(teamId string) StoreChannel {
+
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ query := "SELECT COUNT(DISTINCT Email) FROM Users"
+
+ if len(teamId) > 0 {
+ query += " WHERE TeamId = :TeamId"
+ }
+
+ v, err := us.GetReplica().SelectInt(query, map[string]interface{}{"TeamId": teamId})
+ if err != nil {
+ result.Err = model.NewAppError("SqlUserStore.AnalyticsUniqueUserCount", "We couldn't get the unique user count", err.Error())
+ } else {
+ result.Data = v
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
diff --git a/store/store.go b/store/store.go
index 179cfecd7..b91b08f27 100644
--- a/store/store.go
+++ b/store/store.go
@@ -125,6 +125,7 @@ type UserStore interface {
GetTotalActiveUsersCount() StoreChannel
GetSystemAdminProfiles() StoreChannel
PermanentDelete(userId string) StoreChannel
+ AnalyticsUniqueUserCount(teamId string) StoreChannel
}
type SessionStore interface {
diff --git a/utils/config.go b/utils/config.go
index c2ae1f7a0..9d2c2f588 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -118,12 +118,14 @@ func GetLogFileLocation(fileLocation string) string {
func SaveConfig(fileName string, config *model.Config) *model.AppError {
b, err := json.MarshalIndent(config, "", " ")
if err != nil {
- return model.NewAppError("SaveConfig", "An error occurred while saving the file to "+fileName, err.Error())
+ return model.NewLocAppError("SaveConfig", "utils.config.save_config.saving.app_error",
+ map[string]interface{}{"Filename": fileName}, err.Error())
}
err = ioutil.WriteFile(fileName, b, 0644)
if err != nil {
- return model.NewAppError("SaveConfig", "An error occurred while saving the file to "+fileName, err.Error())
+ return model.NewLocAppError("SaveConfig", "utils.config.save_config.saving.app_error",
+ map[string]interface{}{"Filename": fileName}, err.Error())
}
return nil
@@ -138,18 +140,21 @@ func LoadConfig(fileName string) {
file, err := os.Open(fileName)
if err != nil {
- panic("Error opening config file=" + fileName + ", err=" + err.Error())
+ panic(T("utils.config.load_config.opening.panic",
+ map[string]interface{}{"Filename": fileName, "Error": err.Error()}))
}
decoder := json.NewDecoder(file)
config := model.Config{}
err = decoder.Decode(&config)
if err != nil {
- panic("Error decoding config file=" + fileName + ", err=" + err.Error())
+ panic(T("utils.config.load_config.decoding.panic",
+ map[string]interface{}{"Filename": fileName, "Error": err.Error()}))
}
if info, err := file.Stat(); err != nil {
- panic("Error getting config info file=" + fileName + ", err=" + err.Error())
+ panic(T("utils.config.load_config.getting.panic",
+ map[string]interface{}{"Filename": fileName, "Error": err.Error()}))
} else {
CfgLastModified = info.ModTime().Unix()
CfgFileName = fileName
@@ -158,7 +163,8 @@ func LoadConfig(fileName string) {
config.SetDefaults()
if err := config.IsValid(); err != nil {
- panic("Error validating config file=" + fileName + ", err=" + err.Message)
+ panic(T("utils.config.load_config.validating.panic",
+ map[string]interface{}{"Filename": fileName, "Error": err.Message}))
}
configureLog(&config.LogSettings)
diff --git a/utils/config_test.go b/utils/config_test.go
index 0b334d36c..6f36b30c3 100644
--- a/utils/config_test.go
+++ b/utils/config_test.go
@@ -9,4 +9,5 @@ import (
func TestConfig(t *testing.T) {
LoadConfig("config.json")
+ InitTranslations()
}
diff --git a/utils/license.go b/utils/license.go
index 7594e33af..0d1cd597c 100644
--- a/utils/license.go
+++ b/utils/license.go
@@ -44,7 +44,7 @@ NxpC+5KFhU+xSeeklNqwCgnlOyZ7qSTxmdJHb+60SwuYnnGIYzLJhY4LYDr4J+KR
func LoadLicense() {
file, err := os.Open(LicenseLocation())
if err != nil {
- l4g.Warn("Unable to open/find license file")
+ l4g.Warn(T("utils.license.load_license.open_find.warn"))
return
}
defer file.Close()
@@ -55,9 +55,10 @@ func LoadLicense() {
if success, licenseStr := ValidateLicense(buf.Bytes()); success {
license := model.LicenseFromJson(strings.NewReader(licenseStr))
SetLicense(license)
+ return
}
- l4g.Warn("No valid enterprise license found")
+ l4g.Warn(T("utils.license.load_license.invalid.warn"))
}
func SetLicense(license *model.License) bool {
@@ -83,7 +84,7 @@ func RemoveLicense() bool {
ClientLicense = getClientLicense(License)
if err := os.Remove(LicenseLocation()); err != nil {
- l4g.Error("Unable to remove license file, err=%v", err.Error())
+ l4g.Error(T("utils.license.remove_license.unable.error"), err.Error())
return false
}
@@ -95,17 +96,17 @@ func ValidateLicense(signed []byte) (bool, string) {
_, err := base64.StdEncoding.Decode(decoded, signed)
if err != nil {
- l4g.Error("Encountered error decoding license, err=%v", err.Error())
+ l4g.Error(T("utils.license.validate_license.decode.error"), err.Error())
return false, ""
}
if len(decoded) <= 256 {
- l4g.Error("Signed license not long enough")
+ l4g.Error(T("utils.license.validate_license.not_long.error"))
return false, ""
}
// remove null terminator
- if decoded[len(decoded)-1] == byte(0) {
+ for decoded[len(decoded)-1] == byte(0) {
decoded = decoded[:len(decoded)-1]
}
@@ -116,7 +117,7 @@ func ValidateLicense(signed []byte) (bool, string) {
public, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
- l4g.Error("Encountered error signing license, err=%v", err.Error())
+ l4g.Error(T("utils.license.validate_license.signing.error"), err.Error())
return false, ""
}
@@ -128,7 +129,7 @@ func ValidateLicense(signed []byte) (bool, string) {
err = rsa.VerifyPKCS1v15(rsaPublic, crypto.SHA512, d, signature)
if err != nil {
- l4g.Error("Invalid signature, err=%v", err.Error())
+ l4g.Error(T("utils.license.validate_license.invalid.error"), err.Error())
return false, ""
}
diff --git a/utils/lru.go b/utils/lru.go
index 61a515e14..f5f7959d8 100644
--- a/utils/lru.go
+++ b/utils/lru.go
@@ -38,7 +38,7 @@ func NewLru(size int) *Cache {
func NewLruWithEvict(size int, onEvicted func(key interface{}, value interface{})) (*Cache, error) {
if size <= 0 {
- return nil, errors.New("Must provide a positive size")
+ return nil, errors.New(T("utils.iru.with_evict"))
}
c := &Cache{
size: size,
diff --git a/utils/mail.go b/utils/mail.go
index 2f2c10b61..4a0b987e6 100644
--- a/utils/mail.go
+++ b/utils/mail.go
@@ -34,12 +34,12 @@ func connectToSMTPServer(config *model.Config) (net.Conn, *model.AppError) {
conn, err = tls.Dial("tcp", config.EmailSettings.SMTPServer+":"+config.EmailSettings.SMTPPort, tlsconfig)
if err != nil {
- return nil, model.NewAppError("SendMail", "Failed to open TLS connection", err.Error())
+ return nil, model.NewLocAppError("SendMail", "utils.mail.connect_smtp.open_tls.app_error", nil, err.Error())
}
} else {
conn, err = net.Dial("tcp", config.EmailSettings.SMTPServer+":"+config.EmailSettings.SMTPPort)
if err != nil {
- return nil, model.NewAppError("SendMail", "Failed to open connection", err.Error())
+ return nil, model.NewLocAppError("SendMail", "utils.mail.connect_smtp.open.app_error", nil, err.Error())
}
}
@@ -49,15 +49,15 @@ func connectToSMTPServer(config *model.Config) (net.Conn, *model.AppError) {
func newSMTPClient(conn net.Conn, config *model.Config) (*smtp.Client, *model.AppError) {
c, err := smtp.NewClient(conn, config.EmailSettings.SMTPServer+":"+config.EmailSettings.SMTPPort)
if err != nil {
- l4g.Error("Failed to open a connection to SMTP server %v", err)
- return nil, model.NewAppError("SendMail", "Failed to open TLS connection", err.Error())
+ l4g.Error(T("utils.mail.new_client.open.error"), err)
+ return nil, model.NewLocAppError("SendMail", "utils.mail.connect_smtp.open_tls.app_error", nil, err.Error())
}
// GO does not support plain auth over a non encrypted connection.
// so if not tls then no auth
auth := smtp.PlainAuth("", config.EmailSettings.SMTPUsername, config.EmailSettings.SMTPPassword, config.EmailSettings.SMTPServer+":"+config.EmailSettings.SMTPPort)
if config.EmailSettings.ConnectionSecurity == model.CONN_SECURITY_TLS {
if err = c.Auth(auth); err != nil {
- return nil, model.NewAppError("SendMail", "Failed to authenticate on SMTP server", err.Error())
+ return nil, model.NewLocAppError("SendMail", "utils.mail.new_client.auth.app_error", nil, err.Error())
}
} else if config.EmailSettings.ConnectionSecurity == model.CONN_SECURITY_STARTTLS {
tlsconfig := &tls.Config{
@@ -66,7 +66,7 @@ func newSMTPClient(conn net.Conn, config *model.Config) (*smtp.Client, *model.Ap
}
c.StartTLS(tlsconfig)
if err = c.Auth(auth); err != nil {
- return nil, model.NewAppError("SendMail", "Failed to authenticate on SMTP server", err.Error())
+ return nil, model.NewLocAppError("SendMail", "utils.mail.new_client.auth.app_error", nil, err.Error())
}
}
return c, nil
@@ -79,14 +79,14 @@ func TestConnection(config *model.Config) {
conn, err1 := connectToSMTPServer(config)
if err1 != nil {
- l4g.Error("SMTP server settings do not appear to be configured properly err=%v details=%v", err1.Message, err1.DetailedError)
+ l4g.Error(T("utils.mail.test.configured.error"), err1.Message, err1.DetailedError)
return
}
defer conn.Close()
c, err2 := newSMTPClient(conn, config)
if err2 != nil {
- l4g.Error("SMTP connection settings do not appear to be configured properly err=%v details=%v", err2.Message, err2.DetailedError)
+ l4g.Error(T("utils.mail.test.configured.error"), err2.Message, err2.DetailedError)
return
}
defer c.Quit()
@@ -102,7 +102,7 @@ func SendMailUsingConfig(to, subject, body string, config *model.Config) *model.
return nil
}
- l4g.Debug("sending mail to " + to + " with subject of '" + subject + "'")
+ l4g.Debug(T("utils.mail.send_mail.sending.debug"), to, subject)
fromMail := mail.Address{config.EmailSettings.FeedbackName, config.EmailSettings.FeedbackEmail}
toMail := mail.Address{"", to}
@@ -136,26 +136,26 @@ func SendMailUsingConfig(to, subject, body string, config *model.Config) *model.
defer c.Close()
if err := c.Mail(fromMail.Address); err != nil {
- return model.NewAppError("SendMail", "Failed to add from email address", err.Error())
+ return model.NewLocAppError("SendMail", "utils.mail.send_mail.from_address.app_error", nil, err.Error())
}
if err := c.Rcpt(toMail.Address); err != nil {
- return model.NewAppError("SendMail", "Failed to add to email address", err.Error())
+ return model.NewLocAppError("SendMail", "utils.mail.send_mail.to_address.app_error", nil, err.Error())
}
w, err := c.Data()
if err != nil {
- return model.NewAppError("SendMail", "Failed to add email messsage data", err.Error())
+ return model.NewLocAppError("SendMail", "utils.mail.send_mail.msg_data.app_error", nil, err.Error())
}
_, err = w.Write([]byte(message))
if err != nil {
- return model.NewAppError("SendMail", "Failed to write email message", err.Error())
+ return model.NewLocAppError("SendMail", "utils.mail.send_mail.msg.app_error", nil, err.Error())
}
err = w.Close()
if err != nil {
- return model.NewAppError("SendMail", "Failed to close connection to SMTP server", err.Error())
+ return model.NewLocAppError("SendMail", "utils.mail.send_mail.close.app_error", nil, err.Error())
}
return nil
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx
index 0f85c238d..efd163017 100644
--- a/web/react/components/admin_console/admin_controller.jsx
+++ b/web/react/components/admin_console/admin_controller.jsx
@@ -1,4 +1,4 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import AdminSidebar from './admin_sidebar.jsx';
@@ -23,6 +23,7 @@ import TeamUsersTab from './team_users.jsx';
import TeamAnalyticsTab from './team_analytics.jsx';
import LdapSettingsTab from './ldap_settings.jsx';
import LicenseSettingsTab from './license_settings.jsx';
+import SystemAnalyticsTab from './system_analytics.jsx';
export default class AdminController extends React.Component {
constructor(props) {
@@ -45,7 +46,7 @@ export default class AdminController extends React.Component {
config: AdminStore.getConfig(),
teams: AdminStore.getAllTeams(),
selectedTeams,
- selected: props.tab || 'service_settings',
+ selected: props.tab || 'system_analytics',
selectedTeam: props.teamId || null
};
@@ -165,6 +166,8 @@ export default class AdminController extends React.Component {
if (this.state.teams) {
tab = <TeamAnalyticsTab team={this.state.teams[this.state.selectedTeam]} />;
}
+ } else if (this.state.selected === 'system_analytics') {
+ tab = <SystemAnalyticsTab />;
}
}
diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx
index 5a5eaa055..66f82c55b 100644
--- a/web/react/components/admin_console/admin_sidebar.jsx
+++ b/web/react/components/admin_console/admin_sidebar.jsx
@@ -196,6 +196,25 @@ export default class AdminSidebar extends React.Component {
<li>
<h4>
<span className='icon fa fa-gear'></span>
+ <span>{'SITE REPORTS'}</span>
+ </h4>
+ </li>
+ </ul>
+ <ul className='nav nav__sub-menu padded'>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('system_analytics')}
+ onClick={this.handleClick.bind(this, 'system_analytics', null)}
+ >
+ {'View Statistics'}
+ </a>
+ </li>
+ </ul>
+ <ul className='nav nav__sub-menu'>
+ <li>
+ <h4>
+ <span className='icon fa fa-gear'></span>
<span>{'SETTINGS'}</span>
</h4>
</li>
diff --git a/web/react/components/admin_console/analytics.jsx b/web/react/components/admin_console/analytics.jsx
new file mode 100644
index 000000000..70ef1ecab
--- /dev/null
+++ b/web/react/components/admin_console/analytics.jsx
@@ -0,0 +1,279 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import * as Utils from '../../utils/utils.jsx';
+import Constants from '../../utils/constants.jsx';
+import LineChart from './line_chart.jsx';
+
+var Tooltip = ReactBootstrap.Tooltip;
+var OverlayTrigger = ReactBootstrap.OverlayTrigger;
+
+export default class Analytics extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {};
+ }
+
+ render() { // in the future, break down these into smaller components
+ var serverError = '';
+ if (this.props.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.props.serverError}</label></div>;
+ }
+
+ var totalCount = (
+ <div className='col-sm-3'>
+ <div className='total-count'>
+ <div className='title'>{'Total Users'}<i className='fa fa-users'/></div>
+ <div className='content'>{this.props.uniqueUserCount == null ? 'Loading...' : this.props.uniqueUserCount}</div>
+ </div>
+ </div>
+ );
+
+ var openChannelCount = (
+ <div className='col-sm-3'>
+ <div className='total-count'>
+ <div className='title'>{'Public Channels'}<i className='fa fa-globe'/></div>
+ <div className='content'>{this.props.channelOpenCount == null ? 'Loading...' : this.props.channelOpenCount}</div>
+ </div>
+ </div>
+ );
+
+ var openPrivateCount = (
+ <div className='col-sm-3'>
+ <div className='total-count'>
+ <div className='title'>{'Private Groups'}<i className='fa fa-lock'/></div>
+ <div className='content'>{this.props.channelPrivateCount == null ? 'Loading...' : this.props.channelPrivateCount}</div>
+ </div>
+ </div>
+ );
+
+ var postCount = (
+ <div className='col-sm-3'>
+ <div className='total-count'>
+ <div className='title'>{'Total Posts'}<i className='fa fa-comment'/></div>
+ <div className='content'>{this.props.postCount == null ? 'Loading...' : this.props.postCount}</div>
+ </div>
+ </div>
+ );
+
+ var postCountsByDay = (
+ <div className='col-sm-12'>
+ <div className='total-count by-day'>
+ <div className='title'>{'Total Posts'}</div>
+ <div className='content'>{'Loading...'}</div>
+ </div>
+ </div>
+ );
+
+ if (this.props.postCountsDay != null) {
+ let content;
+ if (this.props.postCountsDay.labels.length === 0) {
+ content = 'Not enough data for a meaningful representation.';
+ } else {
+ content = (
+ <LineChart
+ data={this.props.postCountsDay}
+ width='740'
+ height='225'
+ />
+ );
+ }
+ postCountsByDay = (
+ <div className='col-sm-12'>
+ <div className='total-count by-day'>
+ <div className='title'>{'Total Posts'}</div>
+ <div className='content'>
+ {content}
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ var usersWithPostsByDay = (
+ <div className='col-sm-12'>
+ <div className='total-count by-day'>
+ <div className='title'>{'Active Users With Posts'}</div>
+ <div className='content'>{'Loading...'}</div>
+ </div>
+ </div>
+ );
+
+ if (this.props.userCountsWithPostsDay != null) {
+ let content;
+ if (this.props.userCountsWithPostsDay.labels.length === 0) {
+ content = 'Not enough data for a meaningful representation.';
+ } else {
+ content = (
+ <LineChart
+ data={this.props.userCountsWithPostsDay}
+ width='740'
+ height='225'
+ />
+ );
+ }
+ usersWithPostsByDay = (
+ <div className='col-sm-12'>
+ <div className='total-count by-day'>
+ <div className='title'>{'Active Users With Posts'}</div>
+ <div className='content'>
+ {content}
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ let recentActiveUser;
+ if (this.props.recentActiveUsers != null) {
+ let content;
+ if (this.props.recentActiveUsers.length === 0) {
+ content = 'Loading...';
+ } else {
+ content = (
+ <table>
+ <tbody>
+ {
+ this.props.recentActiveUsers.map((user) => {
+ const tooltip = (
+ <Tooltip id={'recent-user-email-tooltip-' + user.id}>
+ {user.email}
+ </Tooltip>
+ );
+
+ return (
+ <tr key={'recent-user-table-entry-' + user.id}>
+ <td>
+ <OverlayTrigger
+ delayShow={Constants.OVERLAY_TIME_DELAY}
+ placement='top'
+ overlay={tooltip}
+ >
+ <time>
+ {user.username}
+ </time>
+ </OverlayTrigger>
+ </td>
+ <td>{Utils.displayDateTime(user.last_activity_at)}</td>
+ </tr>
+ );
+ })
+ }
+ </tbody>
+ </table>
+ );
+ }
+ recentActiveUser = (
+ <div className='col-sm-6'>
+ <div className='total-count recent-active-users'>
+ <div className='title'>{'Recent Active Users'}</div>
+ <div className='content'>
+ {content}
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ let newUsers;
+ if (this.props.newlyCreatedUsers != null) {
+ let content;
+ if (this.props.newlyCreatedUsers.length === 0) {
+ content = 'Loading...';
+ } else {
+ content = (
+ <table>
+ <tbody>
+ {
+ this.props.newlyCreatedUsers.map((user) => {
+ const tooltip = (
+ <Tooltip id={'new-user-email-tooltip-' + user.id}>
+ {user.email}
+ </Tooltip>
+ );
+
+ return (
+ <tr key={'new-user-table-entry-' + user.id}>
+ <td>
+ <OverlayTrigger
+ delayShow={Constants.OVERLAY_TIME_DELAY}
+ placement='top'
+ overlay={tooltip}
+ >
+ <time>
+ {user.username}
+ </time>
+ </OverlayTrigger>
+ </td>
+ <td>{Utils.displayDateTime(user.create_at)}</td>
+ </tr>
+ );
+ })
+ }
+ </tbody>
+ </table>
+ );
+ }
+ newUsers = (
+ <div className='col-sm-6'>
+ <div className='total-count recent-active-users'>
+ <div className='title'>{'Newly Created Users'}</div>
+ <div className='content'>
+ {content}
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ return (
+ <div className='wrapper--fixed team_statistics'>
+ <h3>{'Statistics for ' + this.props.title}</h3>
+ {serverError}
+ <div className='row'>
+ {totalCount}
+ {postCount}
+ {openChannelCount}
+ {openPrivateCount}
+ </div>
+ <div className='row'>
+ {postCountsByDay}
+ </div>
+ <div className='row'>
+ {usersWithPostsByDay}
+ </div>
+ <div className='row'>
+ {recentActiveUser}
+ {newUsers}
+ </div>
+ </div>
+ );
+ }
+}
+
+Analytics.defaultProps = {
+ title: null,
+ channelOpenCount: null,
+ channelPrivateCount: null,
+ postCount: null,
+ postCountsDay: null,
+ userCountsWithPostsDay: null,
+ recentActiveUsers: null,
+ newlyCreatedUsers: null,
+ uniqueUserCount: null,
+ serverError: null
+};
+
+Analytics.propTypes = {
+ title: React.PropTypes.string,
+ channelOpenCount: React.PropTypes.number,
+ channelPrivateCount: React.PropTypes.number,
+ postCount: React.PropTypes.number,
+ postCountsDay: React.PropTypes.object,
+ userCountsWithPostsDay: React.PropTypes.object,
+ recentActiveUsers: React.PropTypes.array,
+ newlyCreatedUsers: React.PropTypes.array,
+ uniqueUserCount: React.PropTypes.number,
+ serverError: React.PropTypes.string
+};
diff --git a/web/react/components/admin_console/system_analytics.jsx b/web/react/components/admin_console/system_analytics.jsx
new file mode 100644
index 000000000..f54813a94
--- /dev/null
+++ b/web/react/components/admin_console/system_analytics.jsx
@@ -0,0 +1,161 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import Analytics from './analytics.jsx';
+import * as Client from '../../utils/client.jsx';
+
+export default class SystemAnalytics extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.getData = this.getData.bind(this);
+
+ this.state = { // most of this state should be from a store in the future
+ users: null,
+ serverError: null,
+ channel_open_count: null,
+ channel_private_count: null,
+ post_count: null,
+ post_counts_day: null,
+ user_counts_with_posts_day: null,
+ recent_active_users: null,
+ newly_created_users: null,
+ unique_user_count: null
+ };
+ }
+
+ componentDidMount() {
+ this.getData();
+ }
+
+ getData() { // should be moved to an action creator eventually
+ Client.getSystemAnalytics(
+ 'standard',
+ (data) => {
+ for (var index in data) {
+ if (data[index].name === 'channel_open_count') {
+ this.setState({channel_open_count: data[index].value});
+ }
+
+ if (data[index].name === 'channel_private_count') {
+ this.setState({channel_private_count: data[index].value});
+ }
+
+ if (data[index].name === 'post_count') {
+ this.setState({post_count: data[index].value});
+ }
+
+ if (data[index].name === 'unique_user_count') {
+ this.setState({unique_user_count: data[index].value});
+ }
+ }
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+
+ Client.getSystemAnalytics(
+ 'post_counts_day',
+ (data) => {
+ data.reverse();
+
+ var chartData = {
+ labels: [],
+ datasets: [{
+ label: 'Total Posts',
+ fillColor: 'rgba(151,187,205,0.2)',
+ strokeColor: 'rgba(151,187,205,1)',
+ pointColor: 'rgba(151,187,205,1)',
+ pointStrokeColor: '#fff',
+ pointHighlightFill: '#fff',
+ pointHighlightStroke: 'rgba(151,187,205,1)',
+ data: []
+ }]
+ };
+
+ for (var index in data) {
+ if (data[index]) {
+ var row = data[index];
+ chartData.labels.push(row.name);
+ chartData.datasets[0].data.push(row.value);
+ }
+ }
+
+ this.setState({post_counts_day: chartData});
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+
+ Client.getSystemAnalytics(
+ 'user_counts_with_posts_day',
+ (data) => {
+ data.reverse();
+
+ var chartData = {
+ labels: [],
+ datasets: [{
+ label: 'Active Users With Posts',
+ fillColor: 'rgba(151,187,205,0.2)',
+ strokeColor: 'rgba(151,187,205,1)',
+ pointColor: 'rgba(151,187,205,1)',
+ pointStrokeColor: '#fff',
+ pointHighlightFill: '#fff',
+ pointHighlightStroke: 'rgba(151,187,205,1)',
+ data: []
+ }]
+ };
+
+ for (var index in data) {
+ if (data[index]) {
+ var row = data[index];
+ chartData.labels.push(row.name);
+ chartData.datasets[0].data.push(row.value);
+ }
+ }
+
+ this.setState({user_counts_with_posts_day: chartData});
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ componentWillReceiveProps() {
+ this.setState({
+ serverError: null,
+ channel_open_count: null,
+ channel_private_count: null,
+ post_count: null,
+ post_counts_day: null,
+ user_counts_with_posts_day: null,
+ unique_user_count: null
+ });
+
+ this.getData();
+ }
+
+ render() {
+ return (
+ <div>
+ <Analytics
+ title={'the System'}
+ channelOpenCount={this.state.channel_open_count}
+ channelPrivateCount={this.state.channel_private_count}
+ postCount={this.state.post_count}
+ postCountsDay={this.state.post_counts_day}
+ userCountsWithPostsDay={this.state.user_counts_with_posts_day}
+ uniqueUserCount={this.state.unique_user_count}
+ serverError={this.state.serverError}
+ />
+ </div>
+ );
+ }
+}
+
+SystemAnalytics.propTypes = {
+ team: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/team_analytics.jsx b/web/react/components/admin_console/team_analytics.jsx
index fe7230946..c164dd98c 100644
--- a/web/react/components/admin_console/team_analytics.jsx
+++ b/web/react/components/admin_console/team_analytics.jsx
@@ -1,13 +1,8 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+import Analytics from './analytics.jsx';
import * as Client from '../../utils/client.jsx';
-import * as Utils from '../../utils/utils.jsx';
-import Constants from '../../utils/constants.jsx';
-import LineChart from './line_chart.jsx';
-
-var Tooltip = ReactBootstrap.Tooltip;
-var OverlayTrigger = ReactBootstrap.OverlayTrigger;
export default class TeamAnalytics extends React.Component {
constructor(props) {
@@ -15,7 +10,7 @@ export default class TeamAnalytics extends React.Component {
this.getData = this.getData.bind(this);
- this.state = {
+ this.state = { // most of this state should be from a store in the future
users: null,
serverError: null,
channel_open_count: null,
@@ -24,7 +19,8 @@ export default class TeamAnalytics extends React.Component {
post_counts_day: null,
user_counts_with_posts_day: null,
recent_active_users: null,
- newly_created_users: null
+ newly_created_users: null,
+ unique_user_count: null
};
}
@@ -32,8 +28,8 @@ export default class TeamAnalytics extends React.Component {
this.getData(this.props.team.id);
}
- getData(teamId) {
- Client.getAnalytics(
+ getData(teamId) { // should be moved to an action creator eventually
+ Client.getTeamAnalytics(
teamId,
'standard',
(data) => {
@@ -49,6 +45,10 @@ export default class TeamAnalytics extends React.Component {
if (data[index].name === 'post_count') {
this.setState({post_count: data[index].value});
}
+
+ if (data[index].name === 'unique_user_count') {
+ this.setState({unique_user_count: data[index].value});
+ }
}
},
(err) => {
@@ -56,7 +56,7 @@ export default class TeamAnalytics extends React.Component {
}
);
- Client.getAnalytics(
+ Client.getTeamAnalytics(
teamId,
'post_counts_day',
(data) => {
@@ -91,7 +91,7 @@ export default class TeamAnalytics extends React.Component {
}
);
- Client.getAnalytics(
+ Client.getTeamAnalytics(
teamId,
'user_counts_with_posts_day',
(data) => {
@@ -152,6 +152,10 @@ export default class TeamAnalytics extends React.Component {
var recentActive = [];
for (let i = 0; i < usersList.length; i++) {
+ if (usersList[i].last_activity_at == null) {
+ continue;
+ }
+
recentActive.push(usersList[i]);
if (i > 19) {
break;
@@ -198,227 +202,29 @@ export default class TeamAnalytics extends React.Component {
post_counts_day: null,
user_counts_with_posts_day: null,
recent_active_users: null,
- newly_created_users: null
+ newly_created_users: null,
+ unique_user_count: null
});
this.getData(newProps.team.id);
}
- componentWillUnmount() {
- }
-
render() {
- var serverError = '';
- if (this.state.serverError) {
- serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
- }
-
- var totalCount = (
- <div className='col-sm-3'>
- <div className='total-count'>
- <div className='title'>{'Total Users'}<i className='fa fa-users'/></div>
- <div className='content'>{this.state.users == null ? 'Loading...' : Object.keys(this.state.users).length}</div>
- </div>
- </div>
- );
-
- var openChannelCount = (
- <div className='col-sm-3'>
- <div className='total-count'>
- <div className='title'>{'Public Channels'}<i className='fa fa-globe'/></div>
- <div className='content'>{this.state.channel_open_count == null ? 'Loading...' : this.state.channel_open_count}</div>
- </div>
- </div>
- );
-
- var openPrivateCount = (
- <div className='col-sm-3'>
- <div className='total-count'>
- <div className='title'>{'Private Groups'}<i className='fa fa-lock'/></div>
- <div className='content'>{this.state.channel_private_count == null ? 'Loading...' : this.state.channel_private_count}</div>
- </div>
- </div>
- );
-
- var postCount = (
- <div className='col-sm-3'>
- <div className='total-count'>
- <div className='title'>{'Total Posts'}<i className='fa fa-comment'/></div>
- <div className='content'>{this.state.post_count == null ? 'Loading...' : this.state.post_count}</div>
- </div>
- </div>
- );
-
- var postCountsByDay = (
- <div className='col-sm-12'>
- <div className='total-count by-day'>
- <div className='title'>{'Total Posts'}</div>
- <div className='content'>{'Loading...'}</div>
- </div>
- </div>
- );
-
- if (this.state.post_counts_day != null) {
- postCountsByDay = (
- <div className='col-sm-12'>
- <div className='total-count by-day'>
- <div className='title'>{'Total Posts'}</div>
- <div className='content'>
- <LineChart
- data={this.state.post_counts_day}
- width='740'
- height='225'
- />
- </div>
- </div>
- </div>
- );
- }
-
- var usersWithPostsByDay = (
- <div className='col-sm-12'>
- <div className='total-count by-day'>
- <div className='title'>{'Total Posts'}</div>
- <div>{'Loading...'}</div>
- </div>
- </div>
- );
-
- if (this.state.user_counts_with_posts_day != null) {
- usersWithPostsByDay = (
- <div className='col-sm-12'>
- <div className='total-count by-day'>
- <div className='title'>{'Active Users With Posts'}</div>
- <div className='content'>
- <LineChart
- data={this.state.user_counts_with_posts_day}
- width='740'
- height='225'
- />
- </div>
- </div>
- </div>
- );
- }
-
- var recentActiveUser = (
- <div className='recent-active-users'>
- <div>{'Recent Active Users'}</div>
- <div>{'Loading...'}</div>
- </div>
- );
-
- if (this.state.recent_active_users != null) {
- recentActiveUser = (
- <div className='col-sm-6'>
- <div className='total-count recent-active-users'>
- <div className='title'>{'Recent Active Users'}</div>
- <div className='content'>
- <table>
- <tbody>
- {
- this.state.recent_active_users.map((user) => {
- const tooltip = (
- <Tooltip id={'recent-user-email-tooltip-' + user.id}>
- {user.email}
- </Tooltip>
- );
-
- return (
- <tr key={'recent-user-table-entry-' + user.id}>
- <td>
- <OverlayTrigger
- delayShow={Constants.OVERLAY_TIME_DELAY}
- placement='top'
- overlay={tooltip}
- >
- <time>
- {user.username}
- </time>
- </OverlayTrigger>
- </td>
- <td>{Utils.displayDateTime(user.last_activity_at)}</td>
- </tr>
- );
- })
- }
- </tbody>
- </table>
- </div>
- </div>
- </div>
- );
- }
-
- var newUsers = (
- <div className='recent-active-users'>
- <div>{'Newly Created Users'}</div>
- <div>{'Loading...'}</div>
- </div>
- );
-
- if (this.state.newly_created_users != null) {
- newUsers = (
- <div className='col-sm-6'>
- <div className='total-count recent-active-users'>
- <div className='title'>{'Newly Created Users'}</div>
- <div className='content'>
- <table>
- <tbody>
- {
- this.state.newly_created_users.map((user) => {
- const tooltip = (
- <Tooltip id={'new-user-email-tooltip-' + user.id}>
- {user.email}
- </Tooltip>
- );
-
- return (
- <tr key={'new-user-table-entry-' + user.id}>
- <td>
- <OverlayTrigger
- delayShow={Constants.OVERLAY_TIME_DELAY}
- placement='top'
- overlay={tooltip}
- >
- <time>
- {user.username}
- </time>
- </OverlayTrigger>
- </td>
- <td>{Utils.displayDateTime(user.create_at)}</td>
- </tr>
- );
- })
- }
- </tbody>
- </table>
- </div>
- </div>
- </div>
- );
- }
-
return (
- <div className='wrapper--fixed team_statistics'>
- <h3>{'Statistics for ' + this.props.team.name}</h3>
- {serverError}
- <div className='row'>
- {totalCount}
- {postCount}
- {openChannelCount}
- {openPrivateCount}
- </div>
- <div className='row'>
- {postCountsByDay}
- </div>
- <div className='row'>
- {usersWithPostsByDay}
- </div>
- <div className='row'>
- {recentActiveUser}
- {newUsers}
- </div>
+ <div>
+ <Analytics
+ title={this.props.team.name}
+ users={this.state.users}
+ channelOpenCount={this.state.channel_open_count}
+ channelPrivateCount={this.state.channel_private_count}
+ postCount={this.state.post_count}
+ postCountsDay={this.state.post_counts_day}
+ userCountsWithPostsDay={this.state.user_counts_with_posts_day}
+ recentActiveUsers={this.state.recent_active_users}
+ newlyCreatedUsers={this.state.newly_created_users}
+ uniqueUserCount={this.state.unique_user_count}
+ serverError={this.state.serverError}
+ />
</div>
);
}
diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx
index eaeb7bb91..c902731c9 100644
--- a/web/react/components/sidebar.jsx
+++ b/web/react/components/sidebar.jsx
@@ -506,9 +506,9 @@ export default class Sidebar extends React.Component {
link.rel = 'shortcut icon';
link.id = 'favicon';
if (this.badgesActive) {
- link.href = '/static/images/redfavicon.ico';
+ link.href = '/static/images/favicon/redfavicon-16x16.png';
} else {
- link.href = '/static/images/favicon.ico';
+ link.href = '/static/images/favicon/favicon-16x16.png';
}
var head = document.getElementsByTagName('head')[0];
var oldLink = document.getElementById('favicon');
diff --git a/web/react/pages/admin_console.jsx b/web/react/pages/admin_console.jsx
index cbd2bd80d..3f4c39934 100644
--- a/web/react/pages/admin_console.jsx
+++ b/web/react/pages/admin_console.jsx
@@ -4,25 +4,68 @@
import ErrorBar from '../components/error_bar.jsx';
import SelectTeamModal from '../components/admin_console/select_team_modal.jsx';
import AdminController from '../components/admin_console/admin_controller.jsx';
+import * as Client from '../utils/client.jsx';
-export function setupAdminConsolePage(props) {
- ReactDOM.render(
- <AdminController
- tab={props.ActiveTab}
- teamId={props.TeamId}
- />,
- document.getElementById('admin_controller')
- );
+var IntlProvider = ReactIntl.IntlProvider;
- ReactDOM.render(
- <SelectTeamModal />,
- document.getElementById('select_team_modal')
- );
+class Root extends React.Component {
+ constructor() {
+ super();
+ this.state = {
+ translations: null,
+ loaded: false
+ };
+ }
- ReactDOM.render(
- <ErrorBar/>,
- document.getElementById('error_bar')
- );
+ static propTypes() {
+ return {
+ map: React.PropTypes.object.isRequired
+ };
+ }
+
+ componentWillMount() {
+ Client.getTranslations(
+ this.props.map.Locale,
+ (data) => {
+ this.setState({
+ translations: data,
+ loaded: true
+ });
+ },
+ () => {
+ this.setState({
+ loaded: true
+ });
+ }
+ );
+ }
+
+ render() {
+ if (!this.state.loaded) {
+ return <div></div>;
+ }
+
+ return (
+ <IntlProvider
+ locale={this.props.map.Locale}
+ messages={this.state.translations}
+ >
+ <div>
+ <ErrorBar/>
+ <AdminController
+ tab={this.props.map.ActiveTab}
+ teamId={this.props.map.TeamId}
+ />
+ <SelectTeamModal />
+ </div>
+ </IntlProvider>
+ );
+ }
}
-global.window.setup_admin_console_page = setupAdminConsolePage;
+global.window.setup_admin_console_page = function setup(props) {
+ ReactDOM.render(
+ <Root map={props} />,
+ document.getElementById('admin_controller')
+ );
+};
diff --git a/web/react/pages/authorize.jsx b/web/react/pages/authorize.jsx
index 71f17d007..7474332ce 100644
--- a/web/react/pages/authorize.jsx
+++ b/web/react/pages/authorize.jsx
@@ -2,20 +2,69 @@
// See License.txt for license information.
import Authorize from '../components/authorize.jsx';
+import * as Client from '../utils/client.jsx';
-function setupAuthorizePage(props) {
+var IntlProvider = ReactIntl.IntlProvider;
+
+class Root extends React.Component {
+ constructor() {
+ super();
+ this.state = {
+ translations: null,
+ loaded: false
+ };
+ }
+
+ static propTypes() {
+ return {
+ map: React.PropTypes.object.isRequired
+ };
+ }
+
+ componentWillMount() {
+ Client.getTranslations(
+ this.props.map.Locale,
+ (data) => {
+ this.setState({
+ translations: data,
+ loaded: true
+ });
+ },
+ () => {
+ this.setState({
+ loaded: true
+ });
+ }
+ );
+ }
+
+ render() {
+ if (!this.state.loaded) {
+ return <div></div>;
+ }
+
+ return (
+ <IntlProvider
+ locale={this.props.map.Locale}
+ messages={this.state.translations}
+ >
+ <Authorize
+ teamName={this.props.map.TeamName}
+ appName={this.props.map.AppName}
+ responseType={this.props.map.ResponseType}
+ clientId={this.props.map.ClientId}
+ redirectUri={this.props.map.RedirectUri}
+ scope={this.props.map.Scope}
+ state={this.props.map.State}
+ />
+ </IntlProvider>
+ );
+ }
+}
+
+global.window.setup_authorize_page = function setup(props) {
ReactDOM.render(
- <Authorize
- teamName={props.TeamName}
- appName={props.AppName}
- responseType={props.ResponseType}
- clientId={props.ClientId}
- redirectUri={props.RedirectUri}
- scope={props.Scope}
- state={props.State}
- />,
+ <Root map={props} />,
document.getElementById('authorize')
);
-}
-
-global.window.setup_authorize_page = setupAuthorizePage;
+};
diff --git a/web/react/pages/claim_account.jsx b/web/react/pages/claim_account.jsx
index bca203d96..7c6af73ca 100644
--- a/web/react/pages/claim_account.jsx
+++ b/web/react/pages/claim_account.jsx
@@ -2,18 +2,67 @@
// See License.txt for license information.
import ClaimAccount from '../components/claim/claim_account.jsx';
+import * as Client from '../utils/client.jsx';
-function setupClaimAccountPage(props) {
+var IntlProvider = ReactIntl.IntlProvider;
+
+class Root extends React.Component {
+ constructor() {
+ super();
+ this.state = {
+ translations: null,
+ loaded: false
+ };
+ }
+
+ static propTypes() {
+ return {
+ map: React.PropTypes.object.isRequired
+ };
+ }
+
+ componentWillMount() {
+ Client.getTranslations(
+ this.props.map.Locale,
+ (data) => {
+ this.setState({
+ translations: data,
+ loaded: true
+ });
+ },
+ () => {
+ this.setState({
+ loaded: true
+ });
+ }
+ );
+ }
+
+ render() {
+ if (!this.state.loaded) {
+ return <div></div>;
+ }
+
+ return (
+ <IntlProvider
+ locale={this.props.map.Locale}
+ messages={this.state.translations}
+ >
+ <ClaimAccount
+ email={this.props.map.Email}
+ currentType={this.props.map.CurrentType}
+ newType={this.props.map.NewType}
+ teamName={this.props.map.TeamName}
+ teamDisplayName={this.props.map.TeamDisplayName}
+ />
+ </IntlProvider>
+ );
+ }
+}
+
+global.window.setup_claim_account_page = function setup(props) {
ReactDOM.render(
- <ClaimAccount
- email={props.Email}
- currentType={props.CurrentType}
- newType={props.NewType}
- teamName={props.TeamName}
- teamDisplayName={props.TeamDisplayName}
- />,
+ <Root map={props} />,
document.getElementById('claim')
);
-}
-
-global.window.setup_claim_account_page = setupClaimAccountPage;
+}; \ No newline at end of file
diff --git a/web/react/pages/docs.jsx b/web/react/pages/docs.jsx
index 74d9c2d19..2f5d4db55 100644
--- a/web/react/pages/docs.jsx
+++ b/web/react/pages/docs.jsx
@@ -2,15 +2,63 @@
// See License.txt for license information.
import Docs from '../components/docs.jsx';
+import * as Client from '../utils/client.jsx';
-function setupDocumentationPage(props) {
- ReactDOM.render(
- <Docs
- site={props.Site}
- />,
- document.getElementById('docs')
- );
+var IntlProvider = ReactIntl.IntlProvider;
+
+class Root extends React.Component {
+ constructor() {
+ super();
+ this.state = {
+ translations: null,
+ loaded: false
+ };
+ }
+
+ static propTypes() {
+ return {
+ map: React.PropTypes.object.isRequired
+ };
+ }
+
+ componentWillMount() {
+ Client.getTranslations(
+ this.props.map.Locale,
+ (data) => {
+ this.setState({
+ translations: data,
+ loaded: true
+ });
+ },
+ () => {
+ this.setState({
+ loaded: true
+ });
+ }
+ );
+ }
+
+ render() {
+ if (!this.state.loaded) {
+ return <div></div>;
+ }
+
+ return (
+ <IntlProvider
+ locale={this.props.map.Locale}
+ messages={this.state.translations}
+ >
+ <Docs site={this.props.map.Site} />
+ </IntlProvider>
+ );
+ }
}
global.window.mm_user = global.window.mm_user || {};
-global.window.setup_documentation_page = setupDocumentationPage;
+
+global.window.setup_documentation_page = function setup(props) {
+ ReactDOM.render(
+ <Root map={props} />,
+ document.getElementById('docs')
+ );
+};
diff --git a/web/react/pages/password_reset.jsx b/web/react/pages/password_reset.jsx
index 4a6f1dcb0..23bbf2691 100644
--- a/web/react/pages/password_reset.jsx
+++ b/web/react/pages/password_reset.jsx
@@ -2,18 +2,67 @@
// See License.txt for license information.
import PasswordReset from '../components/password_reset.jsx';
+import * as Client from '../utils/client.jsx';
-function setupPasswordResetPage(props) {
+var IntlProvider = ReactIntl.IntlProvider;
+
+class Root extends React.Component {
+ constructor() {
+ super();
+ this.state = {
+ translations: null,
+ loaded: false
+ };
+ }
+
+ static propTypes() {
+ return {
+ map: React.PropTypes.object.isRequired
+ };
+ }
+
+ componentWillMount() {
+ Client.getTranslations(
+ this.props.map.Locale,
+ (data) => {
+ this.setState({
+ translations: data,
+ loaded: true
+ });
+ },
+ () => {
+ this.setState({
+ loaded: true
+ });
+ }
+ );
+ }
+
+ render() {
+ if (!this.state.loaded) {
+ return <div></div>;
+ }
+
+ return (
+ <IntlProvider
+ locale={this.props.map.Locale}
+ messages={this.state.translations}
+ >
+ <PasswordReset
+ isReset={this.props.map.IsReset}
+ teamDisplayName={this.props.map.TeamDisplayName}
+ teamName={this.props.map.TeamName}
+ hash={this.props.map.Hash}
+ data={this.props.map.Data}
+ />
+ </IntlProvider>
+ );
+ }
+}
+
+global.window.setup_password_reset_page = function setup(props) {
ReactDOM.render(
- <PasswordReset
- isReset={props.IsReset}
- teamDisplayName={props.TeamDisplayName}
- teamName={props.TeamName}
- hash={props.Hash}
- data={props.Data}
- />,
+ <Root map={props} />,
document.getElementById('reset')
);
-}
-
-global.window.setup_password_reset_page = setupPasswordResetPage;
+};
diff --git a/web/react/pages/signup_team.jsx b/web/react/pages/signup_team.jsx
index 08ea45000..8f4f86a7c 100644
--- a/web/react/pages/signup_team.jsx
+++ b/web/react/pages/signup_team.jsx
@@ -2,8 +2,60 @@
// See License.txt for license information.
import SignupTeam from '../components/signup_team.jsx';
+import * as Client from '../utils/client.jsx';
-function setupSignupTeamPage(props) {
+var IntlProvider = ReactIntl.IntlProvider;
+
+class Root extends React.Component {
+ constructor() {
+ super();
+ this.state = {
+ translations: null,
+ loaded: false
+ };
+ }
+
+ static propTypes() {
+ return {
+ map: React.PropTypes.object.isRequired,
+ teams: React.PropTypes.object.isRequired
+ };
+ }
+
+ componentWillMount() {
+ Client.getTranslations(
+ this.props.map.Locale,
+ (data) => {
+ this.setState({
+ translations: data,
+ loaded: true
+ });
+ },
+ () => {
+ this.setState({
+ loaded: true
+ });
+ }
+ );
+ }
+
+ render() {
+ if (!this.state.loaded) {
+ return <div></div>;
+ }
+
+ return (
+ <IntlProvider
+ locale={this.props.map.Locale}
+ messages={this.state.translations}
+ >
+ <SignupTeam teams={this.props.teams} />
+ </IntlProvider>
+ );
+ }
+}
+
+global.window.setup_signup_team_page = function setup(props) {
var teams = [];
for (var prop in props) {
@@ -15,9 +67,10 @@ function setupSignupTeamPage(props) {
}
ReactDOM.render(
- <SignupTeam teams={teams} />,
+ <Root
+ map={props}
+ teams={teams}
+ />,
document.getElementById('signup-team')
);
-}
-
-global.window.setup_signup_team_page = setupSignupTeamPage;
+}; \ No newline at end of file
diff --git a/web/react/pages/signup_team_complete.jsx b/web/react/pages/signup_team_complete.jsx
index d5ed144a1..1bee4e598 100644
--- a/web/react/pages/signup_team_complete.jsx
+++ b/web/react/pages/signup_team_complete.jsx
@@ -2,16 +2,65 @@
// See License.txt for license information.
import SignupTeamComplete from '../components/signup_team_complete.jsx';
+import * as Client from '../utils/client.jsx';
-function setupSignupTeamCompletePage(props) {
+var IntlProvider = ReactIntl.IntlProvider;
+
+class Root extends React.Component {
+ constructor() {
+ super();
+ this.state = {
+ translations: null,
+ loaded: false
+ };
+ }
+
+ static propTypes() {
+ return {
+ map: React.PropTypes.object.isRequired
+ };
+ }
+
+ componentWillMount() {
+ Client.getTranslations(
+ this.props.map.Locale,
+ (data) => {
+ this.setState({
+ translations: data,
+ loaded: true
+ });
+ },
+ () => {
+ this.setState({
+ loaded: true
+ });
+ }
+ );
+ }
+
+ render() {
+ if (!this.state.loaded) {
+ return <div></div>;
+ }
+
+ return (
+ <IntlProvider
+ locale={this.props.map.Locale}
+ messages={this.state.translations}
+ >
+ <SignupTeamComplete
+ email={this.props.map.Email}
+ hash={this.props.map.Hash}
+ data={this.props.map.Data}
+ />
+ </IntlProvider>
+ );
+ }
+}
+
+global.window.setup_signup_team_complete_page = function setup(props) {
ReactDOM.render(
- <SignupTeamComplete
- email={props.Email}
- hash={props.Hash}
- data={props.Data}
- />,
+ <Root map={props} />,
document.getElementById('signup-team-complete')
);
-}
-
-global.window.setup_signup_team_complete_page = setupSignupTeamCompletePage;
+}; \ No newline at end of file
diff --git a/web/react/pages/signup_user_complete.jsx b/web/react/pages/signup_user_complete.jsx
index de2c48443..6c761c1ee 100644
--- a/web/react/pages/signup_user_complete.jsx
+++ b/web/react/pages/signup_user_complete.jsx
@@ -2,19 +2,68 @@
// See License.txt for license information.
import SignupUserComplete from '../components/signup_user_complete.jsx';
+import * as Client from '../utils/client.jsx';
-function setupSignupUserCompletePage(props) {
+var IntlProvider = ReactIntl.IntlProvider;
+
+class Root extends React.Component {
+ constructor() {
+ super();
+ this.state = {
+ translations: null,
+ loaded: false
+ };
+ }
+
+ static propTypes() {
+ return {
+ map: React.PropTypes.object.isRequired
+ };
+ }
+
+ componentWillMount() {
+ Client.getTranslations(
+ this.props.map.Locale,
+ (data) => {
+ this.setState({
+ translations: data,
+ loaded: true
+ });
+ },
+ () => {
+ this.setState({
+ loaded: true
+ });
+ }
+ );
+ }
+
+ render() {
+ if (!this.state.loaded) {
+ return <div></div>;
+ }
+
+ return (
+ <IntlProvider
+ locale={this.props.map.Locale}
+ messages={this.state.translations}
+ >
+ <SignupUserComplete
+ teamId={this.props.map.TeamId}
+ teamName={this.props.map.TeamName}
+ teamDisplayName={this.props.map.TeamDisplayName}
+ email={this.props.map.Email}
+ hash={this.props.map.Hash}
+ data={this.props.map.Data}
+ />
+ </IntlProvider>
+ );
+ }
+}
+
+global.window.setup_signup_user_complete_page = function setup(props) {
ReactDOM.render(
- <SignupUserComplete
- teamId={props.TeamId}
- teamName={props.TeamName}
- teamDisplayName={props.TeamDisplayName}
- email={props.Email}
- hash={props.Hash}
- data={props.Data}
- />,
+ <Root map={props} />,
document.getElementById('signup-user-complete')
);
-}
-
-global.window.setup_signup_user_complete_page = setupSignupUserCompletePage;
+}; \ No newline at end of file
diff --git a/web/react/pages/verify.jsx b/web/react/pages/verify.jsx
index d4ce4844d..2fc619e58 100644
--- a/web/react/pages/verify.jsx
+++ b/web/react/pages/verify.jsx
@@ -2,15 +2,66 @@
// See License.txt for license information.
import EmailVerify from '../components/email_verify.jsx';
+import * as Client from '../utils/client.jsx';
-global.window.setupVerifyPage = function setupVerifyPage(props) {
+var IntlProvider = ReactIntl.IntlProvider;
+
+class Root extends React.Component {
+ constructor() {
+ super();
+ this.state = {
+ translations: null,
+ loaded: false
+ };
+ }
+
+ static propTypes() {
+ return {
+ map: React.PropTypes.object.isRequired
+ };
+ }
+
+ componentWillMount() {
+ Client.getTranslations(
+ this.props.map.Locale,
+ (data) => {
+ this.setState({
+ translations: data,
+ loaded: true
+ });
+ },
+ () => {
+ this.setState({
+ loaded: true
+ });
+ }
+ );
+ }
+
+ render() {
+ if (!this.state.loaded) {
+ return <div></div>;
+ }
+
+ return (
+ <IntlProvider
+ locale={this.props.map.Locale}
+ messages={this.state.translations}
+ >
+ <EmailVerify
+ isVerified={this.props.map.IsVerified}
+ teamURL={this.props.map.TeamURL}
+ userEmail={this.props.map.UserEmail}
+ resendSuccess={this.props.map.ResendSuccess}
+ />
+ </IntlProvider>
+ );
+ }
+}
+
+global.window.setupVerifyPage = function setup(props) {
ReactDOM.render(
- <EmailVerify
- isVerified={props.IsVerified}
- teamURL={props.TeamURL}
- userEmail={props.UserEmail}
- resendSuccess={props.ResendSuccess}
- />,
+ <Root map={props} />,
document.getElementById('verify')
);
};
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index 65353b70d..09cd4162a 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -399,7 +399,7 @@ export function getConfig(success, error) {
});
}
-export function getAnalytics(teamId, name, success, error) {
+export function getTeamAnalytics(teamId, name, success, error) {
$.ajax({
url: '/api/v1/admin/analytics/' + teamId + '/' + name,
dataType: 'json',
@@ -407,7 +407,21 @@ export function getAnalytics(teamId, name, success, error) {
type: 'GET',
success,
error: (xhr, status, err) => {
- var e = handleError('getAnalytics', xhr, status, err);
+ var e = handleError('getTeamAnalytics', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function getSystemAnalytics(name, success, error) {
+ $.ajax({
+ url: '/api/v1/admin/analytics/' + name,
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('getSystemAnalytics', xhr, status, err);
error(e);
}
});
diff --git a/web/static/images/favicon/redfavicon-16x16.png b/web/static/images/favicon/redfavicon-16x16.png
new file mode 100644
index 000000000..9645113ef
--- /dev/null
+++ b/web/static/images/favicon/redfavicon-16x16.png
Binary files differ
diff --git a/web/templates/admin_console.html b/web/templates/admin_console.html
index 0e37a4660..08c90493e 100644
--- a/web/templates/admin_console.html
+++ b/web/templates/admin_console.html
@@ -6,11 +6,7 @@
<body>
<script src="/static/js/Chart.min.js"></script>
-<div id='error_bar'></div>
-
-<div id='admin_controller' class='container-fluid'></div>
-
-<div id='select_team_modal'></div>
+<div id='admin_controller'></div>
<script>
window.setup_admin_console_page({{ .Props }});
diff --git a/web/web.go b/web/web.go
index f73860b67..48755f94f 100644
--- a/web/web.go
+++ b/web/web.go
@@ -565,9 +565,9 @@ func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) {
user := result.Data.(*model.User)
if user.LastActivityAt > 0 {
- api.SendEmailChangeVerifyEmailAndForget(user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team))
+ api.SendEmailChangeVerifyEmailAndForget(c, user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team))
} else {
- api.SendVerifyEmailAndForget(user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team))
+ api.SendVerifyEmailAndForget(c, user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team))
}
newAddress := strings.Replace(r.URL.String(), "&resend=true", "&resend_success=true", -1)