Reduce bandwidth usage for client templates.

Add js and ptmpl (stub) template functions.
Simple constant folding for true / false values in templates.
Use empty string instead of 0 for poll vote ips when DisablePollIP is enabled.

Shorten some things.
This commit is contained in:
Azareal 2020-02-19 09:04:14 +10:00
parent 7e3cd48284
commit 363826624f
17 changed files with 135 additions and 97 deletions

View File

@ -126,10 +126,10 @@ func LogWarning(err error, extra ...string) {
}
func errorHeader(w http.ResponseWriter, user User, title string) *Header {
header := DefaultHeader(w, user)
header.Title = title
header.Zone = "error"
return header
h := DefaultHeader(w, user)
h.Title = title
h.Zone = "error"
return h
}
// TODO: Dump the request?
@ -400,4 +400,4 @@ func handleErrorTemplate(w http.ResponseWriter, r *http.Request, pi ErrorPage) {
}
// Alias of routes.renderTemplate
var RenderTemplateAlias func(tmplName string, hookName string, w http.ResponseWriter, r *http.Request, header *Header, pi interface{}) error
var RenderTemplateAlias func(tmplName, hookName string, w http.ResponseWriter, r *http.Request, header *Header, pi interface{}) error

View File

@ -16,7 +16,7 @@ import (
"strings"
"sync"
"github.com/Azareal/Gosora/tmpl_client"
tmpl "github.com/Azareal/Gosora/tmpl_client"
)
type SFileList map[string]SFile
@ -70,7 +70,7 @@ func (list SFileList) JSTmplInit() error {
data = data[startIndex-len([]byte("if(tmplInits===undefined)")):]
data = replace(data, "// nolint", "")
data = replace(data, "func ", "function ")
data = replace(data, " error {\n", " {\nlet out = \"\"\n")
data = replace(data, " error {\n", " {\nlet o = \"\"\n")
funcIndex, hasFunc := skipAllUntilCharsExist(data, 0, []byte("function Template_"))
if !hasFunc {
return errors.New("no template function found")
@ -175,9 +175,9 @@ func (list SFileList) JSTmplInit() error {
}
})
data = replace(data, "for _, item := range ", "for(item of ")
data = replace(data, "w.Write([]byte(", "out += ")
data = replace(data, "w.Write(StringToBytes(", "out += ")
data = replace(data, "w.Write(", "out += ")
data = replace(data, "w.Write([]byte(", "o += ")
data = replace(data, "w.Write(StringToBytes(", "o += ")
data = replace(data, "w.Write(", "o += ")
data = replace(data, "+= c.", "+= ")
data = replace(data, "strconv.Itoa(", "")
data = replace(data, "strconv.FormatInt(", "")
@ -186,7 +186,7 @@ func (list SFileList) JSTmplInit() error {
data = replace(data, ", 10;", "")
data = replace(data, "var plist = GetTmplPhrasesBytes("+shortName+"_tmpl_phrase_id)", "const plist = tmplPhrases[\""+tmplName+"\"];")
data = replace(data, "var cached_var_", "let cached_var_")
data = replace(data, `tmpl_`+shortName+`_vars, ok := tmpl_`+shortName+`_i.`, `/*`)
data = replace(data, `tmpl_vars, ok := tmpl_i.`, `/*`)
data = replace(data, "[]byte(", "")
data = replace(data, "StringToBytes(", "")
data = replace(data, "RelativeTime(tmpl_"+shortName+"_vars.", "tmpl_"+shortName+"_vars.Relative")
@ -194,7 +194,7 @@ func (list SFileList) JSTmplInit() error {
data = replace(data, ".Format(\"2006-01-02 15:04:05\"", "")
data = replace(data, ", 10", "")
data = replace(data, "if ", "if(")
data = replace(data, "return nil", "return out")
data = replace(data, "return nil", "return o")
data = replace(data, " )", ")")
data = replace(data, " \n", "\n")
data = replace(data, "\n", ";\n")
@ -212,10 +212,12 @@ func (list SFileList) JSTmplInit() error {
fragset := tmpl.GetFrag(shortName)
if fragset != nil {
sfrags := []byte("let " + shortName + "_frags = [];\n")
sfrags := []byte("let " + shortName + "_frags = [\n")
for _, frags := range fragset {
sfrags = append(sfrags, []byte(shortName+"_frags.push(`"+string(frags)+"`);\n")...)
//sfrags = append(sfrags, []byte(shortName+"_frags.push(`"+string(frags)+"`);\n")...)
sfrags = append(sfrags, []byte("\t`"+string(frags)+"`,\n")...)
}
sfrags = append(sfrags, []byte("];\n")...)
data = append(sfrags, data...)
}
data = replace(data, "\n;", "\n")

View File

@ -13,8 +13,8 @@ var FPStore ForumPermsStore
type ForumPermsStore interface {
Init() error
GetAllMap() (bigMap map[int]map[int]*ForumPerms)
Get(fid, gid int) (fperms *ForumPerms, err error)
GetCopy(fid, gid int) (fperms ForumPerms, err error)
Get(fid, gid int) (fp *ForumPerms, err error)
GetCopy(fid, gid int) (fp ForumPerms, err error)
ReloadAll() error
Reload(id int) error
}
@ -193,7 +193,7 @@ func (s *MemoryForumPermsStore) GetAllMap() (bigMap map[int]map[int]*ForumPerms)
// TODO: Add a hook here and have plugin_guilds use it
// TODO: Check if the forum exists?
// TODO: Fix the races
func (s *MemoryForumPermsStore) Get(fid, gid int) (fperms *ForumPerms, err error) {
func (s *MemoryForumPermsStore) Get(fid, gid int) (fp *ForumPerms, err error) {
var fmap map[int]*ForumPerms
var ok bool
if fid%2 == 0 {
@ -206,22 +206,22 @@ func (s *MemoryForumPermsStore) Get(fid, gid int) (fperms *ForumPerms, err error
s.oddLock.RUnlock()
}
if !ok {
return fperms, ErrNoRows
return fp, ErrNoRows
}
fperms, ok = fmap[gid]
fp, ok = fmap[gid]
if !ok {
return fperms, ErrNoRows
return fp, ErrNoRows
}
return fperms, nil
return fp, nil
}
// TODO: Check if the forum exists?
// TODO: Fix the races
func (s *MemoryForumPermsStore) GetCopy(fid, gid int) (fperms ForumPerms, err error) {
func (s *MemoryForumPermsStore) GetCopy(fid, gid int) (fp ForumPerms, err error) {
fPermsPtr, err := s.Get(fid, gid)
if err != nil {
return fperms, err
return fp, err
}
return *fPermsPtr, nil
}

View File

@ -74,17 +74,17 @@ func InitPhrases(lang string) error {
return err
}
data, err := ioutil.ReadFile(path)
if err != nil {
return err
}
var ext = filepath.Ext("/langs/" + path)
ext := filepath.Ext("/langs/" + path)
if ext != ".json" {
log.Printf("Found a '%s' in /langs/", ext)
return nil
}
data, err := ioutil.ReadFile(path)
if err != nil {
return err
}
var langPack LanguagePack
err = json.Unmarshal(data, &langPack)
if err != nil {
@ -98,7 +98,7 @@ func InitPhrases(lang string) error {
// [prefix][name]phrase
langPack.TmplPhrasesPrefixes = make(map[string]map[string]string)
var conMap = make(map[string]string) // Cache phrase strings so we can de-dupe items to reduce memory use. There appear to be some minor improvements with this, although we would need a more thorough check to be sure.
conMap := make(map[string]string) // Cache phrase strings so we can de-dupe items to reduce memory use. There appear to be some minor improvements with this, although we would need a more thorough check to be sure.
for name, phrase := range langPack.TmplPhrases {
_, ok := conMap[phrase]
if !ok {
@ -304,10 +304,10 @@ func GetTmplPhrasesByPrefix(prefix string) (phrases map[string]string, ok bool)
return res, ok
}
func getPlaceholder(prefix string, suffix string) string {
func getPlaceholder(prefix, suffix string) string {
return "{lang." + prefix + "[" + suffix + "]}"
}
func getPlaceholderBytes(prefix string, suffix string) []byte {
func getPlaceholderBytes(prefix, suffix string) []byte {
return []byte("{lang." + prefix + "[" + suffix + "]}")
}

View File

@ -27,7 +27,7 @@ type PollStore interface {
Get(id int) (*Poll, error)
Exists(id int) bool
Create(parent Pollable, pollType int, pollOptions map[int]string) (int, error)
CastVote(optionIndex int, pollID int, uid int, ip string) error
CastVote(optionIndex, pollID, uid int, ip string) error
Reload(id int) error
//Count() int
@ -77,24 +77,24 @@ func (s *DefaultPollStore) Exists(id int) bool {
}
func (s *DefaultPollStore) Get(id int) (*Poll, error) {
poll, err := s.cache.Get(id)
p, err := s.cache.Get(id)
if err == nil {
return poll, nil
return p, nil
}
poll = &Poll{ID: id}
p = &Poll{ID: id}
var optionTxt []byte
err = s.get.QueryRow(id).Scan(&poll.ParentID, &poll.ParentTable, &poll.Type, &optionTxt, &poll.VoteCount)
err = s.get.QueryRow(id).Scan(&p.ParentID, &p.ParentTable, &p.Type, &optionTxt, &p.VoteCount)
if err != nil {
return nil, err
}
err = json.Unmarshal(optionTxt, &poll.Options)
err = json.Unmarshal(optionTxt, &p.Options)
if err == nil {
poll.QuickOptions = s.unpackOptionsMap(poll.Options)
s.cache.Set(poll)
p.QuickOptions = s.unpackOptionsMap(p.Options)
s.cache.Set(p)
}
return poll, err
return p, err
}
// TODO: Optimise the query to avoid preparing it on the spot? Maybe, use knowledge of the most common IN() parameter counts?
@ -212,9 +212,9 @@ func (s *DefaultPollStore) unpackOptionsMap(rawOptions map[int]string) []PollOpt
}
// TODO: Use a transaction for this?
func (s *DefaultPollStore) CastVote(optionIndex int, pollID int, uid int, ip string) error {
func (s *DefaultPollStore) CastVote(optionIndex, pollID, uid int, ip string) error {
if Config.DisablePollIP {
ip = "0"
ip = ""
}
_, err := s.addVote.Exec(pollID, uid, optionIndex, ip)
if err != nil {

View File

@ -526,7 +526,7 @@ func compileJSTemplates(wg *sync.WaitGroup, c *tmpl.CTemplateSet, themeName stri
t := TItemHold(make(map[string]TItem))
topicsRow := &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, now, now, user3.ID, 1, 1, "", "127.0.0.1", 1, 0, 1, 0, 1, "classname", 0, "", &user2, "", 0, &user3, "General", "/forum/general.2", nil}
topicsRow := &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, now, now, user3.ID, 1, 1, "", "::1", 1, 0, 1, 0, 1, "classname", 0, "", &user2, "", 0, &user3, "General", "/forum/general.2", nil}
t.AddStd("topics_topic", "c.TopicsRow", topicsRow)
poll := Poll{ID: 1, Type: 0, Options: map[int]string{0: "Nothing", 1: "Something"}, Results: map[int]int{0: 5, 1: 2}, QuickOptions: []PollOption{
@ -535,7 +535,7 @@ func compileJSTemplates(wg *sync.WaitGroup, c *tmpl.CTemplateSet, themeName stri
}, VoteCount: 7}
avatar, microAvatar := BuildAvatar(62, "")
miniAttach := []*MiniAttachment{&MiniAttachment{Path: "/"}}
topic := TopicUser{1, "blah", "Blah", "Hey there!", 62, false, false, now, now, 1, 1, 0, "", "127.0.0.1", 1, 0, 1, 0, "classname", poll.ID, "weird-data", BuildProfileURL("fake-user", 62), "Fake User", Config.DefaultGroup, avatar, microAvatar, 0, "", "", "", 58, false, miniAttach, nil, false}
topic := TopicUser{1, "blah", "Blah", "Hey there!", 62, false, false, now, now, 1, 1, 0, "", "::1", 1, 0, 1, 0, "classname", poll.ID, "weird-data", BuildProfileURL("fake-user", 62), "Fake User", Config.DefaultGroup, avatar, microAvatar, 0, "", "", "", 58, false, miniAttach, nil, false}
var replyList []*ReplyUser
// TODO: Do we really want the UID here to be zero?
avatar, microAvatar = BuildAvatar(0, "")
@ -816,6 +816,19 @@ func initDefaultTmplFuncMap() {
return ""
}
fmap["ptmpl"] = func(nameInt, pageInt, headerInt interface{}) interface{} {
header := headerInt.(*Header)
err := header.Theme.RunTmpl(nameInt.(string), pageInt, header.Writer)
if err != nil {
return err
}
return ""
}
fmap["js"] = func() interface{} {
return false
}
fmap["flush"] = func() interface{} {
return nil
}

View File

@ -114,6 +114,8 @@ func NewCTemplateSet(in string) *CTemplateSet {
"reltime": true,
"scope": true,
"dyntmpl": true,
"ptmpl": true,
"js": true,
"index": true,
"flush": true,
},
@ -183,7 +185,7 @@ type OutFrag struct {
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) {
func (c *CTemplateSet) CompileByLoggedin(name, fileDir, expects string, expectsInt interface{}, varList map[string]VarItem, imports ...string) (stub, gout, mout string, err error) {
c.importMap = map[string]string{}
for index, item := range c.baseImportMap {
c.importMap[index] = item
@ -233,15 +235,15 @@ import "errors"
// TODO: Try to remove this redundant interface cast
stub += `
// nolint
func Template_` + fname + `(tmpl_` + fname + `_i interface{}, w io.Writer) error {
tmpl_` + fname + `_vars, ok := tmpl_` + fname + `_i.(` + expects + `)
func Template_` + fname + `(tmpl_i interface{}, w io.Writer) error {
tmpl_vars, ok := tmpl_i.(` + expects + `)
if !ok {
return errors.New("invalid page struct value")
}
if tmpl_` + fname + `_vars.CurrentUser.Loggedin {
return Template_` + fname + `_member(tmpl_` + fname + `_i, w)
if tmpl_vars.CurrentUser.Loggedin {
return Template_` + fname + `_member(tmpl_i, w)
}
return Template_` + fname + `_guest(tmpl_` + fname + `_i, w)
return Template_` + fname + `_guest(tmpl_i, w)
}`
c.fileDir = fileDir
@ -265,7 +267,7 @@ func Template_` + fname + `(tmpl_` + fname + `_i interface{}, w io.Writer) error
return stub, gout, mout, err
}
func (c *CTemplateSet) Compile(name string, fileDir string, expects string, expectsInt interface{}, varList map[string]VarItem, imports ...string) (out string, err error) {
func (c *CTemplateSet) Compile(name, fileDir, expects string, expectsInt interface{}, varList map[string]VarItem, imports ...string) (out string, err error) {
if c.config.Debug {
c.logger.Println("Compiling template '" + name + "'")
}
@ -279,7 +281,7 @@ func (c *CTemplateSet) Compile(name string, fileDir string, expects string, expe
return c.compile(name, content, expects, expectsInt, varList, imports...)
}
func (c *CTemplateSet) compile(name string, content string, expects string, expectsInt interface{}, varList map[string]VarItem, imports ...string) (out string, err error) {
func (c *CTemplateSet) compile(name, content, expects string, expectsInt interface{}, varList map[string]VarItem, imports ...string) (out string, err error) {
defer func() {
if r := recover(); r != nil {
fmt.Println(r)
@ -363,6 +365,7 @@ func (c *CTemplateSet) compile(name string, content string, expects string, expe
var outBuf []OutBufferFrame
rootHold := "tmpl_" + fname + "_vars"
//rootHold := "tmpl_vars"
con := CContext{
RootHolder: rootHold,
VarHolder: rootHold,
@ -486,12 +489,17 @@ func (c *CTemplateSet) compile(name string, content string, expects string, expe
}
if c.lang == "normal" {
fout += "// nolint\nfunc Template_" + fname + "(tmpl_" + fname + "_i interface{}, w io.Writer) error {\n"
fout += `tmpl_` + fname + `_vars, ok := tmpl_` + fname + `_i.(` + expects + `)
fout += "// nolint\nfunc Template_" + fname + "(tmpl_i interface{}, w io.Writer) error {\n"
fout += `tmpl_` + fname + `_vars, ok := tmpl_i.(` + expects + `)
if !ok {
return errors.New("invalid page struct value")
}
`
/*fout += `tmpl_vars, ok := tmpl_i.(` + expects + `)
if !ok {
return errors.New("invalid page struct value")
}
`*/
fout += `var iw http.ResponseWriter
gzw, ok := w.(c.GzipResponseWriter)
if ok {
@ -501,6 +509,7 @@ func (c *CTemplateSet) compile(name string, content string, expects string, expe
`
} else {
fout += "// nolint\nfunc Template_" + fname + "(tmpl_" + fname + "_vars interface{}, w io.Writer) error {\n"
//fout += "// nolint\nfunc Template_" + fname + "(tmpl_vars interface{}, w io.Writer) error {\n"
}
if len(c.langIndexToName) > 0 {
@ -678,6 +687,15 @@ func (c *CTemplateSet) compileSwitch(con CContext, node parse.Node) {
}
}
// simple constant folding
if expr == "true" {
c.compileSwitch(con, node.List)
return
} else if expr == "false" {
c.compileSwitch(con, node.ElseList)
return
}
con.Push("startif", "if "+expr+" {\n")
c.compileSwitch(con, node.List)
if node.ElseList == nil {
@ -980,16 +998,16 @@ func (c *CTemplateSet) compileExprSwitch(con CContext, node *parse.CommandNode)
return out
}
func (c *CTemplateSet) unknownNode(node parse.Node) {
elem := reflect.ValueOf(node).Elem()
func (c *CTemplateSet) unknownNode(n parse.Node) {
elem := reflect.ValueOf(n).Elem()
c.logger.Println("Unknown Kind:", elem.Kind())
c.logger.Println("Unknown Type:", elem.Type().Name())
panic("I don't know what node this is! Grr...")
}
func (c *CTemplateSet) compileIdentSwitchN(con CContext, node *parse.CommandNode) (out string) {
func (c *CTemplateSet) compileIdentSwitchN(con CContext, n *parse.CommandNode) (out string) {
c.detail("in compileIdentSwitchN")
out, _, _, _ = c.compileIdentSwitch(con, node)
out, _, _, _ = c.compileIdentSwitch(con, n)
return out
}
@ -1052,7 +1070,7 @@ func (c *CTemplateSet) compareJoin(con CContext, pos int, node *parse.CommandNod
return pos, out
}
func (c *CTemplateSet) compileIdentSwitch(con CContext, node *parse.CommandNode) (out string, val reflect.Value, literal bool, notident bool) {
func (c *CTemplateSet) compileIdentSwitch(con CContext, node *parse.CommandNode) (out string, val reflect.Value, literal, notident bool) {
c.dumpCall("compileIdentSwitch", con, node)
litString := func(inner string, bytes bool) {
if !bytes {
@ -1136,6 +1154,14 @@ ArgLoop:
out = "c.HasWidgets(" + leftParam + "," + rightParam + ")"
literal = true
break ArgLoop
case "js":
if c.lang == "js" {
out = "true"
} else {
out = "false"
}
literal = true
break ArgLoop
case "lang":
// TODO: Implement string literals properly
leftOp := node.Args[pos+1].String()
@ -1262,7 +1288,8 @@ ArgLoop:
case "scope":
literal = true
break ArgLoop
case "dyntmpl":
// TODO: Optimise ptmpl
case "dyntmpl", "ptmpl":
var pageParam, headParam string
// TODO: Implement string literals properly
// TODO: Should we check to see if pos+3 is within the bounds of the slice?

View File

@ -16,8 +16,8 @@ import (
"os"
"path/filepath"
"reflect"
"strings"
"strconv"
"strings"
"text/template"
p "github.com/Azareal/Gosora/common/phrases"
@ -91,7 +91,7 @@ type ThemeMapTmplToDock struct {
func (t *Theme) LoadStaticFiles() error {
t.ResourceTemplates = template.New("")
fmap := make(map[string]interface{})
fmap["lang"] = func(phraseNameInt interface{}, tmplInt interface{}) interface{} {
fmap["lang"] = func(phraseNameInt, tmplInt interface{}) interface{} {
phraseName, ok := phraseNameInt.(string)
if !ok {
panic("phraseNameInt is not a string")
@ -252,7 +252,6 @@ func UpdateDefaultTheme(t *Theme) error {
if !ok {
return ErrNoDefaultTheme
}
err = dtheme.setActive(false)
if err != nil {
return err

View File

@ -298,8 +298,8 @@ var imageExts = ["png", "jpg", "jpe","jpeg","jif","jfi","jfif", "svg", "bmp", "g
}
var pollInputIndex = 1;
$("#add_poll_button").click((event) => {
event.preventDefault();
$("#add_poll_button").click((ev) => {
ev.preventDefault();
$(".poll_content_row").removeClass("auto_hide");
$("#has_poll_input").val("1");
$(".pollinputinput").click(addPollInput);

View File

@ -1,13 +1,13 @@
(() => {
addInitHook("end_init", () => {
fetch("/api/watches/")
.then(response => {
if(response.status!==200) {
.then(resp => {
if(resp.status!==200) {
console.log("error");
console.log("response:", response);
console.log("response:", resp);
return;
}
response.text().then(data => eval(data));
resp.text().then(data => eval(data));
})
.catch(err => console.log("err:", err));
});

View File

@ -43,7 +43,7 @@ func renderTemplate(tmplName string, w http.ResponseWriter, r *http.Request, hea
return nil
}
func buildBasePage(w http.ResponseWriter, r *http.Request, user *c.User, titlePhrase string, zone string) (*c.BasePanelPage, c.RouteError) {
func buildBasePage(w http.ResponseWriter, r *http.Request, user *c.User, titlePhrase, zone string) (*c.BasePanelPage, c.RouteError) {
header, stats, ferr := c.PanelUserCheck(w, r, user)
if ferr != nil {
return nil, ferr

View File

@ -214,19 +214,19 @@ func GroupsPromotionsCreateSubmit(w http.ResponseWriter, r *http.Request, user c
return c.LocalError("posts must be integer", w, r, user)
}
registeredHours, err := strconv.Atoi(r.FormValue("registered_hours"))
regHours, err := strconv.Atoi(r.FormValue("registered_hours"))
if err != nil {
return c.LocalError("registered_hours must be integer", w, r, user)
}
registeredDays, err := strconv.Atoi(r.FormValue("registered_days"))
regDays, err := strconv.Atoi(r.FormValue("registered_days"))
if err != nil {
return c.LocalError("registered_days must be integer", w, r, user)
}
registeredMonths, err := strconv.Atoi(r.FormValue("registered_months"))
regMonths, err := strconv.Atoi(r.FormValue("registered_months"))
if err != nil {
return c.LocalError("registered_months must be integer", w, r, user)
}
registeredMinutes := (registeredHours * 60) + (registeredDays * 24 * 60) + (registeredMonths * 30 * 24 * 60)
regMinutes := (regHours * 60) + (regDays * 24 * 60) + (regMonths * 30 * 24 * 60)
g, err := c.Groups.Get(from)
ferr := groupCheck(w, r, user, g, err)
@ -238,7 +238,7 @@ func GroupsPromotionsCreateSubmit(w http.ResponseWriter, r *http.Request, user c
if err != nil {
return ferr
}
pid, err := c.GroupPromotions.Create(from, to, twoWay, level, posts, registeredMinutes)
pid, err := c.GroupPromotions.Create(from, to, twoWay, level, posts, regMinutes)
if err != nil {
return c.InternalError(err, w, r)
}

View File

@ -17,7 +17,6 @@ func LogsRegs(w http.ResponseWriter, r *http.Request, user c.User) c.RouteError
if ferr != nil {
return ferr
}
logCount := c.RegLogs.Count()
page, _ := strconv.Atoi(r.FormValue("page"))
perPage := 12
@ -174,7 +173,6 @@ func LogsMod(w http.ResponseWriter, r *http.Request, user c.User) c.RouteError {
if ferr != nil {
return ferr
}
page, _ := strconv.Atoi(r.FormValue("page"))
perPage := 12
offset, page, lastPage := c.PageOffset(c.ModLogs.Count(), page, perPage)

View File

@ -117,7 +117,6 @@ func UnbanUser(w http.ResponseWriter, r *http.Request, user c.User, suid string)
} else if err != nil {
return c.InternalError(err, w, r)
}
if !targetUser.IsBanned {
return c.LocalError("The user you're trying to unban isn't banned.", w, r, user)
}

View File

@ -1,5 +1,5 @@
{{range .ItemList}}<article {{scope "post"}} id="post-{{.ID}}" itemscope itemtype="http://schema.org/CreativeWork" class="rowitem passive deletable_block editable_parent post_item{{if .ActionType}} action_item{{end}}{{if .Attachments}} has_attachs{{end}}">
{{template "topic_alt_userinfo.html" . }}
{{if js}}js{{/**{{ptmpl "topic_alt_userinfo" .}}**/}}{{else}}{{template "topic_alt_userinfo.html" . }}{{end}}
<div class="content_container">
{{if .ActionType}}
<span class="action_icon" aria-hidden="true">{{.ActionIcon}}</span>
@ -14,7 +14,7 @@
{{range .Attachments}}
<div class="attach_item attach_item_item{{if .Image}} attach_image_holder{{end}}">
{{if .Image}}<img src="//{{$.Header.Site.URL}}/attachs/{{.Path}}?sid={{.SectionID}}&amp;stype=forums" height=24 width=24 />{{end}}
<span class="attach_item_path" aid="{{.ID}}" fullPath="//{{$.Header.Site.URL}}/attachs/{{.Path}}">{{.Path}}</span>
<span class="attach_item_path" aid={{.ID}} fullPath="//{{$.Header.Site.URL}}/attachs/{{.Path}}">{{.Path}}</span>
<button class="attach_item_select">{{lang "topic.select_button_text"}}</button>
<button class="attach_item_copy">{{lang "topic.copy_button_text"}}</button>
</div>