diff --git a/api/admin.go b/api/admin.go index bdacb3afb9..b19772fdff 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 f7b6a7eebc..c2f4e9c764 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 f537bbfdca..d202172d09 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 f47ed1c306..41a52fa0cf 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 af46bf113e..5c602a68ed 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 bb6bcb3374..d3807653d1 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 57a0e0bd29..779a6affe9 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 7349aee6f3..4f28584c42 100644 --- a/api/templates/email_change_body.html +++ b/api/templates/email_change_body.html @@ -17,8 +17,8 @@
- You updated your email-You email address for {{.Props.TeamDisplayName}} has been changed to {{.Props.NewEmail}}. {{.Props.Title}}+{{.Props.Info}} |
|
- You updated your email-To finish updating your email address for {{.Props.TeamDisplayName}}, please click the link below to confirm this is the right address. +{{.Props.Title}}+{{.Props.Info}} |
- Any questions at all, mail us any time: {{.ClientCfg.FeedbackEmail}}. - Best wishes, - The {{.ClientCfg.SiteName}} Team + {{.Html.EmailInfo}} |
{{end}}
diff --git a/api/templates/error.html b/api/templates/error.html
index 9fb2da1bae..2b6211be26 100644
--- a/api/templates/error.html
+++ b/api/templates/error.html
@@ -22,9 +22,9 @@