diff --git a/cmd/query_gen/main.go b/cmd/query_gen/main.go index 9c54cf95..9725dd0f 100644 --- a/cmd/query_gen/main.go +++ b/cmd/query_gen/main.go @@ -241,13 +241,14 @@ func seedTables(adapter qgen.Adapter) error { return nil } -func copyInsertMap(in map[string]interface{}) (out map[string]interface{}) { +// ? - What is this for? +/*func copyInsertMap(in map[string]interface{}) (out map[string]interface{}) { out = make(map[string]interface{}) for col, value := range in { out[col] = value } return out -} +}*/ type LitStr string diff --git a/cmd/query_gen/tables.go b/cmd/query_gen/tables.go index a6dcd2db..f01f46c8 100644 --- a/cmd/query_gen/tables.go +++ b/cmd/query_gen/tables.go @@ -493,6 +493,22 @@ func createTables(adapter qgen.Adapter) error { }, ) + // TODO: Implement this + /* + qgen.Install.CreateTable("login_logs", "", "", + []qgen.DBTableColumn{ + qgen.DBTableColumn{"lid", "int", 0, false, true, ""}, + qgen.DBTableColumn{"uid", "int", 0, false, false, ""}, + qgen.DBTableColumn{"success", "bool", 0, false, false, "0"}, // Did this attempt succeed? + qgen.DBTableColumn{"ipaddress", "varchar", 200, false, false, ""}, + qgen.DBTableColumn{"doneAt", "createdAt", 0, false, false, ""}, + }, + []qgen.DBTableKey{ + qgen.DBTableKey{"lid", "primary"}, + }, + ) + */ + qgen.Install.CreateTable("moderation_logs", "", "", []qgen.DBTableColumn{ qgen.DBTableColumn{"action", "varchar", 100, false, false, ""}, diff --git a/common/audit_logs.go b/common/audit_logs.go index f9fb881e..dda02669 100644 --- a/common/audit_logs.go +++ b/common/audit_logs.go @@ -2,6 +2,7 @@ package common import ( "database/sql" + "time" "github.com/Azareal/Gosora/query_gen" ) @@ -55,10 +56,12 @@ func (store *SQLModLogStore) GlobalCount() (logCount int) { func buildLogList(rows *sql.Rows) (logs []LogItem, err error) { for rows.Next() { var log LogItem - err := rows.Scan(&log.Action, &log.ElementID, &log.ElementType, &log.IPAddress, &log.ActorID, &log.DoneAt) + var doneAt time.Time + err := rows.Scan(&log.Action, &log.ElementID, &log.ElementType, &log.IPAddress, &log.ActorID, &doneAt) if err != nil { return logs, err } + log.DoneAt = doneAt.Format("2006-01-02 15:04:05") logs = append(logs, log) } return logs, rows.Err() diff --git a/common/misc_logs.go b/common/misc_logs.go index c34f4130..1d8e160f 100644 --- a/common/misc_logs.go +++ b/common/misc_logs.go @@ -1,7 +1,11 @@ package common -import "database/sql" -import "github.com/Azareal/Gosora/query_gen" +import ( + "database/sql" + "time" + + "github.com/Azareal/Gosora/query_gen" +) var RegLogs RegLogStore @@ -83,10 +87,12 @@ func (store *SQLRegLogStore) GetOffset(offset int, perPage int) (logs []RegLogIt for rows.Next() { var log RegLogItem - err := rows.Scan(&log.ID, &log.Username, &log.Email, &log.FailureReason, &log.Success, &log.IPAddress, &log.DoneAt) + var doneAt time.Time + err := rows.Scan(&log.ID, &log.Username, &log.Email, &log.FailureReason, &log.Success, &log.IPAddress, &doneAt) if err != nil { return logs, err } + log.DoneAt = doneAt.Format("2006-01-02 15:04:05") logs = append(logs, log) } return logs, rows.Err() diff --git a/common/phrases/phrases.go b/common/phrases/phrases.go index 39a808ba..134fd42c 100644 --- a/common/phrases/phrases.go +++ b/common/phrases/phrases.go @@ -108,7 +108,8 @@ func InitPhrases(lang string) error { for index, phraseName := range phraseNames { phrase, ok := langPack.TmplPhrases[phraseName] if !ok { - log.Print("Couldn't find template phrase '" + phraseName + "'") + log.Printf("langPack.TmplPhrases: %+v\n", langPack.TmplPhrases) + panic("Couldn't find template phrase '" + phraseName + "'") } phraseSet[index] = []byte(phrase) } diff --git a/common/template_init.go b/common/template_init.go index 91a6bb46..f71db65f 100644 --- a/common/template_init.go +++ b/common/template_init.go @@ -16,10 +16,19 @@ import ( "github.com/Azareal/Gosora/common/templates" ) -var Ctemplates []string +var Ctemplates []string // TODO: Use this to filter out top level templates we don't need var Templates = template.New("") var PrebuildTmplList []func(User, *Header) CTmpl +func skipCTmpl(key string) bool { + for _, tmpl := range Ctemplates { + if strings.HasSuffix(key, "/"+tmpl+".html") { + return true + } + } + return false +} + type CTmpl struct { Name string Filename string @@ -65,6 +74,8 @@ var Template_forum_handle = func(pi ForumPage, w io.Writer) error { } return Templates.ExecuteTemplate(w, mapping+".html", pi) } +var Template_forum_guest_handle = Template_forum_handle +var Template_forum_member_handle = Template_forum_handle // nolint var Template_forums_handle = func(pi ForumsPage, w io.Writer) error { @@ -83,6 +94,8 @@ var Template_profile_handle = func(pi ProfilePage, w io.Writer) error { } return Templates.ExecuteTemplate(w, mapping+".html", pi) } +var Template_profile_guest_handle = Template_profile_handle +var Template_profile_member_handle = Template_profile_handle // nolint var Template_create_topic_handle = func(pi CreateTopicPage, w io.Writer) error { @@ -245,7 +258,7 @@ func CompileTemplates() error { varList = make(map[string]tmpl.VarItem) header.Title = "User 526" ppage := ProfilePage{header, replyList, user, 0, 0} // TODO: Use the score from user to generate the currentScore and nextScore - profileTmpl, err := compile("profile", "common.ProfilePage", ppage) + profileTmpl, err := compileByLoggedin("profile", "common.ProfilePage", ppage) if err != nil { return err } @@ -284,7 +297,7 @@ func CompileTemplates() error { forumItem := BlankForum(1, "general-forum.1", "General Forum", "Where the general stuff happens", true, "all", 0, "", 0) header.Title = "General Forum" forumPage := ForumPage{header, topicsList, forumItem, Paginator{[]int{1}, 1, 1}} - forumTmpl, err := compile("forum", "common.ForumPage", forumPage) + forumTmpl, err := compileByLoggedin("forum", "common.ForumPage", forumPage) if err != nil { return err } @@ -495,7 +508,21 @@ func writeTemplateList(c *tmpl.CTemplateSet, wg *sync.WaitGroup, prefix string) getterstr += "\treturn " + templateName + "_frags\n" } getterstr += "}\nreturn nil\n}\n" - out += "\n// nolint\nfunc init() {\n" + c.FragOut + "\n" + getterstr + "}\n" + out += "\n// nolint\nfunc init() {\n" + var bodyMap = make(map[string]string) //map[body]fragmentPrefix + for _, frag := range c.FragOut { + var fragmentPrefix string + front := frag.TmplName + "_frags[" + strconv.Itoa(frag.Index) + "]" + fp, ok := bodyMap[frag.Body] + if !ok { + fragmentPrefix = front + " = []byte(`" + frag.Body + "`)\n" + bodyMap[frag.Body] = front + } else { + fragmentPrefix = front + " = " + fp + "\n" + } + out += fragmentPrefix + } + out += "\n" + getterstr + "}\n" err := writeFile(prefix+"template_list.go", out) if err != nil { log.Fatal(err) @@ -573,6 +600,17 @@ func InitTemplates() error { return template.HTML(phrases.GetTmplPhrase(phraseName)) } + // TODO: Implement this in the template generator too + fmap["langf"] = func(phraseNameInt interface{}, args ...interface{}) interface{} { + phraseName, ok := phraseNameInt.(string) + if !ok { + panic("phraseNameInt is not a string") + } + // TODO: Log non-existent phrases? + // TODO: Optimise TmplPhrasef so we don't use slow Sprintf there + return template.HTML(phrases.GetTmplPhrasef(phraseName, args...)) + } + fmap["level"] = func(levelInt interface{}) interface{} { level, ok := levelInt.(int) if !ok { @@ -581,6 +619,15 @@ func InitTemplates() error { return template.HTML(phrases.GetLevelPhrase(level)) } + fmap["abstime"] = func(timeInt interface{}) interface{} { + time, ok := timeInt.(time.Time) + if !ok { + panic("timeInt is not a time.Time") + } + //return time.String() + return time.Format("2006-01-02 15:04:05") + } + fmap["scope"] = func(name interface{}) interface{} { return "" } @@ -606,6 +653,10 @@ func InitTemplates() error { for index, path := range templateFiles { path = strings.Replace(path, "\\", "/", -1) log.Print("templateFile: ", path) + if skipCTmpl(path) { + log.Print("skipping") + continue + } templateFileMap[path] = index } @@ -616,6 +667,10 @@ func InitTemplates() error { for _, path := range overrideFiles { path = strings.Replace(path, "\\", "/", -1) log.Print("overrideFile: ", path) + if skipCTmpl(path) { + log.Print("skipping") + continue + } index, ok := templateFileMap["templates/"+strings.TrimPrefix(path, "templates/overrides/")] if !ok { log.Print("not ok: templates/" + strings.TrimPrefix(path, "templates/overrides/")) diff --git a/common/templates/templates.go b/common/templates/templates.go index 828780a9..ad843723 100644 --- a/common/templates/templates.go +++ b/common/templates/templates.go @@ -49,7 +49,7 @@ type CTemplateSet struct { TemplateFragmentCount map[string]int FragOnce map[string]bool fragmentCursor map[string]int - FragOut string + FragOut []OutFrag fragBuf []Fragment varList map[string]VarItem localVars map[string]map[string]VarItemReflect @@ -88,9 +88,11 @@ func NewCTemplateSet() *CTemplateSet { "dock": true, "elapsed": true, "lang": true, - "level": true, - "scope": true, - "dyntmpl": true, + //"langf":true, + "level": true, + "abstime": true, + "scope": true, + "dyntmpl": true, }, } } @@ -124,6 +126,12 @@ type Skipper struct { Index int } +type OutFrag struct { + TmplName string + Index int + Body string +} + func (c *CTemplateSet) CompileByLoggedin(name string, fileDir string, expects string, expectsInt interface{}, varList map[string]VarItem, imports ...string) (stub string, gout string, mout string, err error) { c.importMap = map[string]string{} for index, item := range c.baseImportMap { @@ -366,9 +374,8 @@ func (c *CTemplateSet) compile(name string, content, expects string, expectsInt fout += "return nil\n}\n" var writeFrag = func(tmplName string, index int, body string) { - fragmentPrefix := tmplName + "_frags[" + strconv.Itoa(index) + "]" + " = []byte(`" + body + "`)\n" - c.detail("writing ", fragmentPrefix) - c.FragOut += fragmentPrefix + //c.detail("writing ", fragmentPrefix) + c.FragOut = append(c.FragOut, OutFrag{tmplName, index, body}) } for _, frag := range c.fragBuf { @@ -925,6 +932,7 @@ ArgLoop: notident = true con.PushPhrase(len(c.langIndexToName) - 1) break ArgLoop + // TODO: Implement langf case "level": // TODO: Implement level literals leftOperand := node.Args[pos+1].String() @@ -936,6 +944,17 @@ ArgLoop: litString("phrases.GetLevelPhrase("+leftParam+")", false) c.importMap[langPkg] = langPkg break ArgLoop + case "abstime": + // TODO: Implement level literals + leftOperand := node.Args[pos+1].String() + if len(leftOperand) == 0 { + panic("The leftoperand for function abstime cannot be left blank") + } + leftParam, _ := c.compileIfVarSub(con, leftOperand) + // TODO: Refactor this + litString(leftParam+".Format(\"2006-01-02 15:04:05\")", false) + c.importMap["time"] = "time" + break ArgLoop case "scope": literal = true break ArgLoop @@ -1255,6 +1274,19 @@ func (c *CTemplateSet) compileVarSub(con CContext, varname string, val reflect.V case reflect.Int64: c.importMap["strconv"] = "strconv" base = "[]byte(strconv.FormatInt(" + varname + ", 10))" + case reflect.Struct: + // TODO: Avoid clashing with other packages which have structs named Time + if val.Type().Name() == "Time" { + c.importMap["time"] = "time" + base = "[]byte(" + varname + ".String())" + } else { + if !val.IsValid() { + panic(assLines + varname + "^\n" + "Invalid value. Maybe, it doesn't exist?") + } + fmt.Println("Unknown Struct Name:", varname) + fmt.Println("Unknown Struct:", val.Type().Name()) + panic("-- I don't know what this variable's type is o.o\n") + } default: if !val.IsValid() { panic(assLines + varname + "^\n" + "Invalid value. Maybe, it doesn't exist?") diff --git a/common/theme.go b/common/theme.go index 5e8c7365..272635f6 100644 --- a/common/theme.go +++ b/common/theme.go @@ -12,6 +12,7 @@ import ( "net/http" "os" "path/filepath" + "reflect" "strings" "text/template" @@ -50,6 +51,7 @@ type Theme struct { RunOnDock func(string) string //(dock string) (sbody string) // This variable should only be set and unset by the system, not the theme meta file + // TODO: Should we phase out Active and make the default theme store the primary source of truth? Active bool } @@ -95,6 +97,16 @@ func (theme *Theme) LoadStaticFiles() error { } return phrase } + fmap["toArr"] = func(args ...interface{}) []interface{} { + return args + } + fmap["concat"] = func(args ...interface{}) interface{} { + var out string + for _, arg := range args { + out += arg.(string) + } + return out + } theme.ResourceTemplates.Funcs(fmap) template.Must(theme.ResourceTemplates.ParseGlob("./themes/" + theme.Name + "/public/*.css")) @@ -128,6 +140,7 @@ func (theme *Theme) AddThemeStaticFiles() error { // TODO: Prepare resource templates for each loaded langpack? err = theme.ResourceTemplates.ExecuteTemplate(&b, filename, CSSData{Phrases: phraseMap}) if err != nil { + log.Print("Failed in adding static file '" + path + "' for default theme '" + theme.Name + "'") return err } data = b.Bytes() @@ -267,16 +280,16 @@ func (theme *Theme) MapTemplates() { func (theme *Theme) setActive(active bool) error { var sink bool - err := themeStmts.isThemeDefault.QueryRow(theme.Name).Scan(&sink) + err := themeStmts.isDefault.QueryRow(theme.Name).Scan(&sink) if err != nil && err != sql.ErrNoRows { return err } hasTheme := err != sql.ErrNoRows if hasTheme { - _, err = themeStmts.updateTheme.Exec(active, theme.Name) + _, err = themeStmts.update.Exec(active, theme.Name) } else { - _, err = themeStmts.addTheme.Exec(theme.Name, active) + _, err = themeStmts.add.Exec(theme.Name, active) } if err != nil { return err @@ -331,3 +344,120 @@ func (theme Theme) BuildDock(dock string) (sbody string) { } return "" } + +type GzipResponseWriter struct { + io.Writer + http.ResponseWriter +} + +func (w GzipResponseWriter) Write(b []byte) (int, error) { + return w.Writer.Write(b) +} + +// NEW method of doing theme templates to allow one user to have a different theme to another. Under construction. +// TODO: Generate the type switch instead of writing it by hand +// TODO: Cut the number of types in half +func (theme *Theme) RunTmpl(template string, pi interface{}, w io.Writer) error { + // Unpack this to avoid an indirect call + gzw, ok := w.(GzipResponseWriter) + if ok { + w = gzw.Writer + } + + var getTmpl = theme.GetTmpl(template) + switch tmplO := getTmpl.(type) { + case *func(CustomPagePage, io.Writer) error: + var tmpl = *tmplO + return tmpl(pi.(CustomPagePage), w) + case *func(TopicPage, io.Writer) error: + var tmpl = *tmplO + return tmpl(pi.(TopicPage), w) + case *func(TopicListPage, io.Writer) error: + var tmpl = *tmplO + return tmpl(pi.(TopicListPage), w) + case *func(ForumPage, io.Writer) error: + var tmpl = *tmplO + return tmpl(pi.(ForumPage), w) + case *func(ForumsPage, io.Writer) error: + var tmpl = *tmplO + return tmpl(pi.(ForumsPage), w) + case *func(ProfilePage, io.Writer) error: + var tmpl = *tmplO + return tmpl(pi.(ProfilePage), w) + case *func(CreateTopicPage, io.Writer) error: + var tmpl = *tmplO + return tmpl(pi.(CreateTopicPage), w) + case *func(IPSearchPage, io.Writer) error: + var tmpl = *tmplO + return tmpl(pi.(IPSearchPage), w) + case *func(AccountDashPage, io.Writer) error: + var tmpl = *tmplO + return tmpl(pi.(AccountDashPage), w) + case *func(ErrorPage, io.Writer) error: + var tmpl = *tmplO + return tmpl(pi.(ErrorPage), w) + case *func(Page, io.Writer) error: + var tmpl = *tmplO + return tmpl(pi.(Page), w) + case func(CustomPagePage, io.Writer) error: + return tmplO(pi.(CustomPagePage), w) + case func(TopicPage, io.Writer) error: + return tmplO(pi.(TopicPage), w) + case func(TopicListPage, io.Writer) error: + return tmplO(pi.(TopicListPage), w) + case func(ForumPage, io.Writer) error: + return tmplO(pi.(ForumPage), w) + case func(ForumsPage, io.Writer) error: + return tmplO(pi.(ForumsPage), w) + case func(ProfilePage, io.Writer) error: + return tmplO(pi.(ProfilePage), w) + case func(CreateTopicPage, io.Writer) error: + return tmplO(pi.(CreateTopicPage), w) + case func(IPSearchPage, io.Writer) error: + return tmplO(pi.(IPSearchPage), w) + case func(AccountDashPage, io.Writer) error: + return tmplO(pi.(AccountDashPage), w) + case func(ErrorPage, io.Writer) error: + return tmplO(pi.(ErrorPage), w) + case func(Page, io.Writer) error: + return tmplO(pi.(Page), w) + case nil, string: + mapping, ok := theme.TemplatesMap[template] + if !ok { + mapping = template + } + return Templates.ExecuteTemplate(w, mapping+".html", pi) + default: + log.Print("theme ", theme) + log.Print("template ", template) + log.Print("pi ", pi) + log.Print("tmplO ", tmplO) + log.Print("getTmpl ", getTmpl) + + valueOf := reflect.ValueOf(tmplO) + log.Print("initial valueOf.Type()", valueOf.Type()) + for valueOf.Kind() == reflect.Interface || valueOf.Kind() == reflect.Ptr { + valueOf = valueOf.Elem() + log.Print("valueOf.Elem().Type() ", valueOf.Type()) + } + log.Print("deferenced valueOf.Type() ", valueOf.Type()) + log.Print("valueOf.Kind() ", valueOf.Kind()) + + return errors.New("Unknown template type") + } +} + +// GetTmpl attempts to get the template for a specific theme, otherwise it falls back on the default template pointer, which if absent will fallback onto the template interpreter +func (theme *Theme) GetTmpl(template string) interface{} { + // TODO: Figure out why we're getting a nil pointer here when transpiled templates are disabled, I would have assumed that we would just fall back to !ok on this + // Might have something to do with it being the theme's TmplPtr map, investigate. + tmpl, ok := theme.TmplPtr[template] + if ok { + return tmpl + } + tmpl, ok = TmplPtrMap[template] + if ok { + return tmpl + } + return template +} diff --git a/common/theme_list.go b/common/theme_list.go index 52eb9616..a415dda2 100644 --- a/common/theme_list.go +++ b/common/theme_list.go @@ -9,7 +9,6 @@ import ( "log" "net/http" "os" - "reflect" "sync" "sync/atomic" @@ -29,22 +28,21 @@ var fallbackTheme = "cosora" var overridenTemplates = make(map[string]bool) // ? What is this used for? type ThemeStmts struct { - getThemes *sql.Stmt - isThemeDefault *sql.Stmt - updateTheme *sql.Stmt - addTheme *sql.Stmt + getAll *sql.Stmt + isDefault *sql.Stmt + update *sql.Stmt + add *sql.Stmt } var themeStmts ThemeStmts func init() { - DefaultThemeBox.Store(fallbackTheme) DbInits.Add(func(acc *qgen.Accumulator) error { themeStmts = ThemeStmts{ - getThemes: acc.Select("themes").Columns("uname, default").Prepare(), - isThemeDefault: acc.Select("themes").Columns("default").Where("uname = ?").Prepare(), - updateTheme: acc.Update("themes").Set("default = ?").Where("uname = ?").Prepare(), - addTheme: acc.Insert("themes").Columns("uname, default").Fields("?,?").Prepare(), + getAll: acc.Select("themes").Columns("uname, default").Prepare(), + isDefault: acc.Select("themes").Columns("default").Where("uname = ?").Prepare(), + update: acc.Update("themes").Set("default = ?").Where("uname = ?").Prepare(), + add: acc.Insert("themes").Columns("uname, default").Fields("?,?").Prepare(), } return acc.FirstError() }) @@ -57,7 +55,11 @@ func NewThemeList() (themes ThemeList, err error) { if err != nil { return themes, err } + if len(themeFiles) == 0 { + return themes, errors.New("You don't have any themes") + } + var lastTheme, defaultTheme string for _, themeFile := range themeFiles { if !themeFile.IsDir() { continue @@ -77,6 +79,14 @@ func NewThemeList() (themes ThemeList, err error) { return themes, err } + if theme.Name == "" { + return themes, errors.New("Theme " + themePath + " doesn't have a name set in theme.json") + } + if theme.Name == fallbackTheme { + defaultTheme = fallbackTheme + } + lastTheme = theme.Name + // TODO: Implement the static file part of this and fsnotify if theme.Path != "" { log.Print("Resolving redirect to " + theme.Path) @@ -130,6 +140,12 @@ func NewThemeList() (themes ThemeList, err error) { themes[theme.Name] = theme } + + if defaultTheme == "" { + defaultTheme = lastTheme + } + DefaultThemeBox.Store(defaultTheme) + return themes, nil } @@ -139,7 +155,7 @@ func (themes ThemeList) LoadActiveStatus() error { ChangeDefaultThemeMutex.Lock() defer ChangeDefaultThemeMutex.Unlock() - rows, err := themeStmts.getThemes.Query() + rows, err := themeStmts.getAll.Query() if err != nil { return err } @@ -290,123 +306,6 @@ func ResetTemplateOverrides() { log.Print("All of the template overrides have been reset") } -type GzipResponseWriter struct { - io.Writer - http.ResponseWriter -} - -func (w GzipResponseWriter) Write(b []byte) (int, error) { - return w.Writer.Write(b) -} - -// NEW method of doing theme templates to allow one user to have a different theme to another. Under construction. -// TODO: Generate the type switch instead of writing it by hand -// TODO: Cut the number of types in half -func (theme *Theme) RunTmpl(template string, pi interface{}, w io.Writer) error { - // Unpack this to avoid an indirect call - gzw, ok := w.(GzipResponseWriter) - if ok { - w = gzw.Writer - } - - var getTmpl = theme.GetTmpl(template) - switch tmplO := getTmpl.(type) { - case *func(CustomPagePage, io.Writer) error: - var tmpl = *tmplO - return tmpl(pi.(CustomPagePage), w) - case *func(TopicPage, io.Writer) error: - var tmpl = *tmplO - return tmpl(pi.(TopicPage), w) - case *func(TopicListPage, io.Writer) error: - var tmpl = *tmplO - return tmpl(pi.(TopicListPage), w) - case *func(ForumPage, io.Writer) error: - var tmpl = *tmplO - return tmpl(pi.(ForumPage), w) - case *func(ForumsPage, io.Writer) error: - var tmpl = *tmplO - return tmpl(pi.(ForumsPage), w) - case *func(ProfilePage, io.Writer) error: - var tmpl = *tmplO - return tmpl(pi.(ProfilePage), w) - case *func(CreateTopicPage, io.Writer) error: - var tmpl = *tmplO - return tmpl(pi.(CreateTopicPage), w) - case *func(IPSearchPage, io.Writer) error: - var tmpl = *tmplO - return tmpl(pi.(IPSearchPage), w) - case *func(AccountDashPage, io.Writer) error: - var tmpl = *tmplO - return tmpl(pi.(AccountDashPage), w) - case *func(ErrorPage, io.Writer) error: - var tmpl = *tmplO - return tmpl(pi.(ErrorPage), w) - case *func(Page, io.Writer) error: - var tmpl = *tmplO - return tmpl(pi.(Page), w) - case func(CustomPagePage, io.Writer) error: - return tmplO(pi.(CustomPagePage), w) - case func(TopicPage, io.Writer) error: - return tmplO(pi.(TopicPage), w) - case func(TopicListPage, io.Writer) error: - return tmplO(pi.(TopicListPage), w) - case func(ForumPage, io.Writer) error: - return tmplO(pi.(ForumPage), w) - case func(ForumsPage, io.Writer) error: - return tmplO(pi.(ForumsPage), w) - case func(ProfilePage, io.Writer) error: - return tmplO(pi.(ProfilePage), w) - case func(CreateTopicPage, io.Writer) error: - return tmplO(pi.(CreateTopicPage), w) - case func(IPSearchPage, io.Writer) error: - return tmplO(pi.(IPSearchPage), w) - case func(AccountDashPage, io.Writer) error: - return tmplO(pi.(AccountDashPage), w) - case func(ErrorPage, io.Writer) error: - return tmplO(pi.(ErrorPage), w) - case func(Page, io.Writer) error: - return tmplO(pi.(Page), w) - case nil, string: - mapping, ok := theme.TemplatesMap[template] - if !ok { - mapping = template - } - return Templates.ExecuteTemplate(w, mapping+".html", pi) - default: - log.Print("theme ", theme) - log.Print("template ", template) - log.Print("pi ", pi) - log.Print("tmplO ", tmplO) - log.Print("getTmpl ", getTmpl) - - valueOf := reflect.ValueOf(tmplO) - log.Print("initial valueOf.Type()", valueOf.Type()) - for valueOf.Kind() == reflect.Interface || valueOf.Kind() == reflect.Ptr { - valueOf = valueOf.Elem() - log.Print("valueOf.Elem().Type() ", valueOf.Type()) - } - log.Print("deferenced valueOf.Type() ", valueOf.Type()) - log.Print("valueOf.Kind() ", valueOf.Kind()) - - return errors.New("Unknown template type") - } -} - -// GetThemeTemplate attempts to get the template for a specific theme, otherwise it falls back on the default template pointer, which if absent will fallback onto the template interpreter -func (theme *Theme) GetTmpl(template string) interface{} { - // TODO: Figure out why we're getting a nil pointer here when transpiled templates are disabled, I would have assumed that we would just fall back to !ok on this - // Might have something to do with it being the theme's TmplPtr map, investigate. - tmpl, ok := theme.TmplPtr[template] - if ok { - return tmpl - } - tmpl, ok = TmplPtrMap[template] - if ok { - return tmpl - } - return template -} - // CreateThemeTemplate creates a theme template on the current default theme func CreateThemeTemplate(theme string, name string) { Themes[theme].TmplPtr[name] = func(pi Page, w http.ResponseWriter) error { diff --git a/common/utils.go b/common/utils.go index 8fbaa7f6..bd233a69 100644 --- a/common/utils.go +++ b/common/utils.go @@ -118,6 +118,49 @@ func RelativeTime(t time.Time) string { return fmt.Sprintf("%d hours ago", int(seconds/60/60)) } +// TODO: Finish this faster and more localised version of RelativeTime +/* +// TODO: Write a test for this +// ! Experimental +func RelativeTimeBytes(t time.Time, lang int) []byte { + diff := time.Since(t) + hours := diff.Hours() + seconds := diff.Seconds() + weeks := int(hours / 24 / 7) + months := int(hours / 24 / 31) + switch { + case months > 3: + if t.Year() != time.Now().Year() { + return []byte(t.Format(phrases.RTime.MultiYear(lang))) + } + return []byte(t.Format(phrases.RTime.SingleYear(lang))) + case months > 1: + return phrases.RTime.Months(lang, months) + case months == 1: + return phrases.RTime.Month(lang) + case weeks > 1: + return phrases.RTime.Weeks(lang, weeks) + case int(hours/24) == 7: + return phrases.RTime.Week(lang) + case int(hours/24) == 1: + return phrases.RTime.Day(lang) + case int(hours/24) > 1: + return phrases.RTime.Days(lang, int(hours/24)) + case seconds <= 1: + return phrases.RTime.Moment(lang) + case seconds < 60: + return phrases.RTime.Seconds(lang, int(seconds)) + case seconds < 120: + return phrases.RTime.Minute(lang) + case seconds < 3600: + return phrases.RTime.Minutes(lang, int(seconds/60)) + case seconds < 7200: + return phrases.RTime.Hour(lang) + } + return phrases.RTime.Hours(lang, int(seconds/60/60)) +} +*/ + // TODO: Write a test for this func ConvertByteUnit(bytes float64) (float64, string) { switch { diff --git a/langs/english.json b/langs/english.json index dda4308a..a42daf59 100644 --- a/langs/english.json +++ b/langs/english.json @@ -98,7 +98,6 @@ "register_username_too_long_prefix":"The username is too long, max: ", "register_email_fail":"We were unable to send the email for you to confirm that this email address belongs to you. You may not have access to some functionality until you do so. Please ask an administrator for assistance.", - "alerts_no_new_alerts":"No new alerts", "alerts_no_actor":"Unable to find the actor", "alerts_no_target_user":"Unable to find the target user", "alerts_no_linked_topic":"Unable to find the linked topic", @@ -356,7 +355,7 @@ "forum_locked":"Locked", "topics_moderate":"Moderate", "topics_replies_suffix":" replies", - "forums_topics_suffix":" topics", + "forums.topics_suffix":" topics", "topics_gap_likes_suffix":" likes", "topics_likes_suffix":"likes", "topics_last":"Last", diff --git a/templates/header.html b/templates/header.html index f1e87260..1c6fe5b0 100644 --- a/templates/header.html +++ b/templates/header.html @@ -45,7 +45,7 @@
{{.CurrentUser.Name}} - {{lang "alerts_no_new_alerts"}} + {{lang "alerts.no_alerts_short"}}
{{end}} diff --git a/templates/panel_adminlogs.html b/templates/panel_adminlogs.html index d276392b..e45febbe 100644 --- a/templates/panel_adminlogs.html +++ b/templates/panel_adminlogs.html @@ -13,7 +13,7 @@ {{if $.CurrentUser.Perms.ViewIPs}}
{{.IPAddress}}{{end}} - {{.DoneAt}} + {{.DoneAt}}
diff --git a/templates/panel_modlogs.html b/templates/panel_modlogs.html index 9224aa7d..ef5bbed2 100644 --- a/templates/panel_modlogs.html +++ b/templates/panel_modlogs.html @@ -13,7 +13,7 @@ {{if $.CurrentUser.Perms.ViewIPs}}
{{.IPAddress}}{{end}} - {{.DoneAt}} + {{.DoneAt}}
diff --git a/templates/panel_reglogs.html b/templates/panel_reglogs.html index f90fad41..679865a3 100644 --- a/templates/panel_reglogs.html +++ b/templates/panel_reglogs.html @@ -13,7 +13,7 @@ {{if $.CurrentUser.Perms.ViewIPs}}
{{.IPAddress}}{{end}} - {{.DoneAt}} + {{.DoneAt}}
diff --git a/templates/profile.html b/templates/profile.html index aa52a43e..eb30f7a3 100644 --- a/templates/profile.html +++ b/templates/profile.html @@ -44,6 +44,7 @@
+ {{if .CurrentUser.Loggedin}} {{if .CurrentUser.Perms.BanUsers}} {{end}} + {{end}}
diff --git a/templates/profile_comments_row.html b/templates/profile_comments_row.html index a7978039..db2a97e7 100644 --- a/templates/profile_comments_row.html +++ b/templates/profile_comments_row.html @@ -8,7 +8,7 @@ {{if $.CurrentUser.IsMod}} - {{end}} + {{end}} @@ -30,7 +30,7 @@ {{if $.CurrentUser.IsMod}} - + {{end}} diff --git a/templates/topic.html b/templates/topic.html index 6506ee42..5b585bfb 100644 --- a/templates/topic.html +++ b/templates/topic.html @@ -65,7 +65,7 @@ {{if .CurrentUser.Perms.EditTopic}}{{end}} {{end}} - {{if .CurrentUser.Perms.DeleteTopic}}{{end}} + {{if .CurrentUser.Perms.DeleteTopic}}{{end}} {{if .CurrentUser.Perms.CloseTopic}}{{if .Topic.IsClosed}}{{else}}{{end}}{{end}} diff --git a/templates/topic_alt.html b/templates/topic_alt.html index eb985fcf..b3d97d1a 100644 --- a/templates/topic_alt.html +++ b/templates/topic_alt.html @@ -95,7 +95,7 @@
- {{.Topic.RelativeCreatedAt}} + {{.Topic.RelativeCreatedAt}} {{if .CurrentUser.Perms.ViewIPs}}{{end}}
diff --git a/templates/topic_alt_posts.html b/templates/topic_alt_posts.html index e6c47cbc..1d7cdeeb 100644 --- a/templates/topic_alt_posts.html +++ b/templates/topic_alt_posts.html @@ -28,7 +28,7 @@
{{.LikeCount}} - {{.RelativeCreatedAt}} + {{.RelativeCreatedAt}} {{if $.CurrentUser.Loggedin}}{{if $.CurrentUser.Perms.ViewIPs}}{{end}}{{end}}
diff --git a/templates/topic_posts.html b/templates/topic_posts.html index 73c7b0bc..560c9760 100644 --- a/templates/topic_posts.html +++ b/templates/topic_posts.html @@ -18,7 +18,7 @@ {{if $.CurrentUser.Perms.EditReply}}{{end}} {{end}} - {{if $.CurrentUser.Perms.DeleteReply}}{{end}} + {{if $.CurrentUser.Perms.DeleteReply}}{{end}} {{if $.CurrentUser.Perms.ViewIPs}}{{end}} diff --git a/templates/topics_topic.html b/templates/topics_topic.html index 1cf3897d..050db1a7 100644 --- a/templates/topics_topic.html +++ b/templates/topics_topic.html @@ -27,7 +27,7 @@ {{.LastUser.Name}}'s Avatar {{.LastUser.Name}}
- {{.RelativeLastReplyAt}} + {{.RelativeLastReplyAt}}
diff --git a/themes/cosora/public/main.css b/themes/cosora/public/main.css index 8095c417..a01da68e 100644 --- a/themes/cosora/public/main.css +++ b/themes/cosora/public/main.css @@ -2,9 +2,9 @@ --header-border-color: hsl(0,0%,80%); --element-border-color: hsl(0,0%,85%); --element-background-color: white; - --replies-lang-string: "{{index .Phrases "topics_replies_suffix"}}"; - --topics-lang-string: "{{index .Phrases "forums_topics_suffix"}}"; - --likes-lang-string: "{{index .Phrases "topics_gap_likes_suffix"}}"; + --replies-lang-string: "{{lang "topics_replies_suffix" . }}"; + --topics-lang-string: "{{lang "forums.topics_suffix" . }}"; + --likes-lang-string: "{{lang "topics_gap_likes_suffix" . }}"; --primary-link-color: hsl(0,0%,40%); --primary-text-color: hsl(0,0%,20%); --lightened-primary-text-color: hsl(0,0%,30%); @@ -137,7 +137,7 @@ li { left: -1px; } .alert_aftercounter:before { - content: "{{index .Phrases "menu_alerts"}}"; + content: "{{lang "menu_alerts" . }}"; margin-left: 4px; } @@ -401,7 +401,7 @@ h1, h3 { margin-right: 9px; } .topic_list_title_block .pre_opt:before { - content: "{{index .Phrases "topics_click_topics_to_select"}}"; + content: "{{lang "topics_click_topics_to_select" . }}"; font-size: 14px; } .topic_list_title, .forum_title { @@ -982,7 +982,7 @@ textarea { content: "(" } .topic_view_count:after { - content: "{{index .Phrases "topic.view_count_suffix"}})"; + content: "{{lang "topic.view_count_suffix" . }})"; } .postImage { width: 100%; @@ -1074,7 +1074,7 @@ textarea { display: block; } .like_count:after { - content: "{{index .Phrases "topic.like_count_suffix"}}"; + content: "{{lang "topic.like_count_suffix" . }}"; margin-right: 6px; } @@ -1102,31 +1102,31 @@ textarea { } .add_like:before, .remove_like:before { - content: "{{index .Phrases "topic.plus_one"}}"; + content: "{{lang "topic.plus_one" . }}"; } .button_container .open_edit:after, .edit_item:after{ - content: "{{index .Phrases "topic.edit_button_text"}}"; + content: "{{lang "topic.edit_button_text" . }}"; } .delete_item:after { - content: "{{index .Phrases "topic.delete_button_text"}}"; + content: "{{lang "topic.delete_button_text" . }}"; } .ip_item_button:after { - content: "{{index .Phrases "topic.ip_button_text"}}"; + content: "{{lang "topic.ip_button_text" . }}"; } .lock_item:after { - content: "{{index .Phrases "topic.lock_button_text"}}"; + content: "{{lang "topic.lock_button_text" . }}"; } .unlock_item:after { - content: "{{index .Phrases "topic.unlock_button_text"}}"; + content: "{{lang "topic.unlock_button_text" . }}"; } .pin_item:after { - content: "{{index .Phrases "topic.pin_button_text"}}"; + content: "{{lang "topic.pin_button_text" . }}"; } .unpin_item:after { - content: "{{index .Phrases "topic.unpin_button_text"}}"; + content: "{{lang "topic.unpin_button_text" . }}"; } .report_item:after { - content: "{{index .Phrases "topic.report_button_text"}}"; + content: "{{lang "topic.report_button_text" .}}"; } #ip_search_container .rowlist .rowitem { @@ -1803,7 +1803,7 @@ textarea { content: ""; } .like_count:before { - content: "{{index .Phrases "topic.plus"}}"; + content: "{{lang "topic.plus" . }}"; font-weight: normal; } .created_at { diff --git a/themes/cosora/public/panel.css b/themes/cosora/public/panel.css index ddc42499..0b5a014d 100644 --- a/themes/cosora/public/panel.css +++ b/themes/cosora/public/panel.css @@ -214,31 +214,31 @@ margin-right: 6px; } .perm_preset_no_access:before { - content: "{{index .Phrases "panel_perms_no_access" }}"; + content: "{{lang "panel_perms_no_access" . }}"; color: hsl(0,100%,20%); } .perm_preset_read_only:before, .perm_preset_can_post:before { color: hsl(120,100%,20%); } .perm_preset_read_only:before { - content: "{{index .Phrases "panel_perms_read_only" }}"; + content: "{{lang "panel_perms_read_only" . }}"; } .perm_preset_can_post:before { - content: "{{index .Phrases "panel_perms_can_post" }}"; + content: "{{lang "panel_perms_can_post" . }}"; } .perm_preset_can_moderate:before { - content: "{{index .Phrases "panel_perms_can_moderate" }}"; + content: "{{lang "panel_perms_can_moderate" . }}"; color: hsl(240,100%,20%); } .perm_preset_quasi_mod:before { - content: "{{index .Phrases "panel_perms_quasi_mod" }}"; + content: "{{lang "panel_perms_quasi_mod" . }}"; } .perm_preset_custom:before { - content: "{{index .Phrases "panel_perms_custom" }}"; + content: "{{lang "panel_perms_custom" . }}"; color: hsl(0,0%,20%); } .perm_preset_default:before { - content: "{{index .Phrases "panel_perms_default" }}"; + content: "{{lang "panel_perms_default" . }}"; } .panel_submitrow .rowitem { diff --git a/themes/nox/public/main.css b/themes/nox/public/main.css index eaaca860..9a6cb694 100644 --- a/themes/nox/public/main.css +++ b/themes/nox/public/main.css @@ -777,27 +777,13 @@ input[type=checkbox]:checked + label .sel { .button_container .open_edit:after, .edit_item:after { content: "{{lang "topic.edit_button_text" . }}"; } -.delete_item:after { - content: "{{lang "topic.delete_button_text" . }}"; -} .ip_item_button:after { content: "{{lang "topic.ip_button_text" . }}"; -} -.lock_item:after { - content: "{{lang "topic.lock_button_text" . }}"; -} -.unlock_item:after { - content: "{{lang "topic.unlock_button_text" . }}"; -} -.pin_item:after { - content: "{{lang "topic.pin_button_text" . }}"; -} -.unpin_item:after { - content: "{{lang "topic.unpin_button_text" . }}"; -} -.report_item:after { - content: "{{lang "topic.report_button_text" . }}"; -} +}{{$p := .}} +{{range (toArr "delete" "lock" "unlock" "pin" "unpin" "report")}} +.{{.}}_item:after { + content: "{{lang (concat "topic." . "_button_text") ($p) }}"; +}{{end}} .like_count:after { content: "{{lang "topic.like_count_suffix" . }}"; } diff --git a/themes/shadow/public/main.css b/themes/shadow/public/main.css index 32f8b950..07fcb1d2 100644 --- a/themes/shadow/public/main.css +++ b/themes/shadow/public/main.css @@ -100,7 +100,7 @@ li { font-size: 14px; } .alert_aftercounter:before { - content: "{{index .Phrases "menu_alerts"}}"; + content: "{{lang "menu_alerts" . }}"; } .menu_alerts .alertList, .hide_on_big, .show_on_mobile { @@ -290,54 +290,30 @@ a { } .like_label:before { - content: "{{index .Phrases "topic.plus_one"}}"; -} -.edit_label:before { - content: "{{index .Phrases "topic.edit_button_text"}}"; -} -.trash_label:before { - content: "{{index .Phrases "topic.delete_button_text"}}"; -} -.pin_label:before { - content: "{{index .Phrases "topic.pin_button_text"}}"; -} -.lock_label:before { - content: "{{index .Phrases "topic.lock_button_text"}}"; -} -.unlock_label:before { - content: "{{index .Phrases "topic.unlock_button_text"}}"; -} -.unpin_label:before { - content: "{{index .Phrases "topic.unpin_button_text"}}"; -} -.ip_label:before { - content: "{{index .Phrases "topic.ip_button_text"}}"; -} -.flag_label:before { - content: "{{index .Phrases "topic.flag_button_text"}}"; -} + content: "{{lang "topic.plus_one" . }}"; +}{{$out := .}} +{{range (toArr "edit" "delete" "pin" "lock" "unlock" "unpin" "ip" "flag")}} +.{{.}}_label:before { + content: "{{lang (concat "topic." . "_button_text") ($out) }}"; +}{{end}} .like_count_label, .like_count { display: none; } .like_count_label:before { - content: "{{index .Phrases "topics_likes_suffix"}}"; + content: "{{lang "topics_likes_suffix" . }}"; } -.has_likes .like_count_label { +.has_likes .like_count_label, .has_likes .like_count { font-size: 12px; display: block; float: left; line-height: 19px; } .has_likes .like_count { - font-size: 12px; - display: block; - float: left; - line-height: 19px; margin-right: 2px; } .like_count:before { - content: "{{index .Phrases "pipe"}}"; + content: "{{lang "pipe" . }}"; margin-right: 5px; } @@ -695,21 +671,21 @@ input[type=checkbox]:checked + label.poll_option_label .sel { } .topic_list_title_block .pre_opt:before { - content: "{{index .Phrases "topics_click_topics_to_select"}}"; + content: "{{lang "topics_click_topics_to_select" . }}"; font-size: 14px; } .create_topic_opt a:before { - content: "{{index .Phrases "topics_new_topic"}}"; + content: "{{lang "topics_new_topic" . }}"; margin-left: 3px; } .locked_opt a:before { - content: "{{index .Phrases "forum_locked"}}"; + content: "{{lang "forum_locked" . }}"; } .mod_opt a { margin-left: 4px; } .mod_opt a:after { - content: "{{index .Phrases "topics_moderate"}}"; + content: "{{lang "topics_moderate" . }}"; padding-left: 1px; } .create_topic_opt { @@ -835,10 +811,10 @@ input[type=checkbox]:checked + label.poll_option_label .sel { white-space: nowrap; } .topic_list .lastReplyAt:before { - content: "{{index .Phrases "topics_last"}}: "; + content: "{{lang "topics_last" . }}: "; } .topic_list .starter:before { - content: "{{index .Phrases "topics_starter"}}: "; + content: "{{lang "topics_starter" . }}: "; } .topic_middle { display: none; @@ -906,7 +882,7 @@ input[type=checkbox]:checked + label.poll_option_label .sel { font-weight: normal; } #profile_left_pane .report_item:after { - content: "{{index .Phrases "topic.report_button_text"}}"; + content: "{{lang "topic.report_button_text" . }}"; } #profile_left_lane .profileName { font-size: 18px; diff --git a/themes/shadow/public/panel.css b/themes/shadow/public/panel.css index 524bcca4..b37931a5 100644 --- a/themes/shadow/public/panel.css +++ b/themes/shadow/public/panel.css @@ -31,10 +31,10 @@ } .edit_button:before { - content: "{{index .Phrases "panel_edit_button_text"}}"; + content: "{{lang "panel_edit_button_text" . }}"; } .delete_button:after { - content: "{{index .Phrases "panel_delete_button_text"}}"; + content: "{{lang "panel_delete_button_text" . }}"; } #panel_forums .rowitem { diff --git a/themes/tempra-simple/public/main.css b/themes/tempra-simple/public/main.css index d297bdb6..43fa8a37 100644 --- a/themes/tempra-simple/public/main.css +++ b/themes/tempra-simple/public/main.css @@ -469,10 +469,10 @@ input, select { white-space: nowrap; } .topic_list .lastReplyAt:before { - content: "{{index .Phrases "topics_last"}}: "; + content: "{{lang "topics_last" . }}: "; } .topic_list .starter:before { - content: "{{index .Phrases "topics_starter"}}: "; + content: "{{lang "topics_starter" . }}: "; } @supports not (display: grid) { @@ -682,7 +682,7 @@ button.username { .edit_label:before { content: "🖊️"; } -.trash_label:before { +.delete_label:before { content: "🗑️"; } .pin_label:before, .unpin_label:before { @@ -802,22 +802,20 @@ input[type=checkbox]:checked + label.poll_option_label .sel { display: none; } -.alert { +/* TODO: Can we just set .alert on the alert_success and .alert_error ones? */ +.alert, .alert_success, .alert_error { display: block; padding: 5px; margin-bottom: 10px; +} +.alert { border: 1px solid hsl(0, 0%, 80%); } .alert_success { - display: block; - padding: 5px; - border: 1px solid A2FC00; - margin-bottom: 10px; - background-color: DAF7A6; + border: 1px solid #A2FC00; + background-color: #DAF7A6; } .alert_error { - display: block; - padding: 5px; border: 1px solid #FF004B; margin-bottom: 8px; background-color: #FEB7CC; @@ -920,7 +918,7 @@ input[type=checkbox]:checked + label.poll_option_label .sel { font-size: 18px; } #profile_left_lane .report_item:after { - content: "{{index .Phrases "topic.report_button_text"}}"; + content: "{{lang "topic.report_button_text" . }}"; } #profile_right_lane { width: calc(100% - 245px); diff --git a/themes/tempra-simple/public/panel.css b/themes/tempra-simple/public/panel.css index 15925d8b..7d03acdf 100644 --- a/themes/tempra-simple/public/panel.css +++ b/themes/tempra-simple/public/panel.css @@ -9,10 +9,10 @@ } .edit_button:before { - content: "{{index .Phrases "panel_edit_button_text"}}"; + content: "{{lang "panel_edit_button_text" . }}"; } .delete_button:after { - content: "{{index .Phrases "panel_delete_button_text"}}"; + content: "{{lang "panel_delete_button_text" . }}"; } .tag-mini { @@ -104,32 +104,32 @@ } .perm_preset_no_access:before { - content: "{{index .Phrases "panel_perms_no_access" }}"; + content: "{{lang "panel_perms_no_access" . }}"; color: maroon; } .perm_preset_read_only:before, .perm_preset_can_post:before { color: green; } .perm_preset_read_only:before { - content: "{{index .Phrases "panel_perms_read_only" }}"; + content: "{{lang "panel_perms_read_only" . }}"; } .perm_preset_can_post:before { - content: "{{index .Phrases "panel_perms_can_post" }}"; + content: "{{lang "panel_perms_can_post" . }}"; } .perm_preset_can_moderate:before { - content: "{{index .Phrases "panel_perms_can_moderate" }}"; + content: "{{lang "panel_perms_can_moderate" . }}"; color: darkblue; } .perm_preset_quasi_mod:before { - content: "{{index .Phrases "panel_perms_quasi_mod" }}"; + content: "{{lang "panel_perms_quasi_mod" . }}"; color: darkblue; } .perm_preset_custom:before { - content: "{{index .Phrases "panel_perms_custom" }}"; + content: "{{lang "panel_perms_custom" . }}"; color: black; } .perm_preset_default:before { - content: "{{index .Phrases "panel_perms_default" }}"; + content: "{{lang "panel_perms_default" . }}"; } #panel_dashboard_right .colstack_head {