WIP forum action code. Currently disabled.

Add Http Conn Count tracking.
Move more panel phrases into the panel namespace.
Use a string builder in hookgen.
Use Countf() in a couple of places to eliminate boilerplate.
Reduce prepared stmt boilerplate in forum store with a lambda.
Reduce prepared stmt boilerplate in topic.go with a lambda.
Reduce prepared stmt boilerplate in group.go with a lambda.
Add TestSetCreatedAt method to *Topic.
Add DateOlderThanQ method to *accDeleteBuilder and *accUpdateBuilder.
Add Stmt method to *accUpdateBuilder and *AccSelectBuilder.
Add AccBuilder interface.
Shorten variable names.
Shorten extractPerm name to ep.
Add avatar_visibility setting stub. Implementation coming in a later commit.
Don't set an IP for installer generated posts.

Add counters_perf_tick_row hook.

Add avatar_visibility phrase.
Add avatar_visibility_label phrase.
Rename forums_no_description to forums_no_desc.
Rename panel.forums_create_description_label to panel.forums_create_desc_label.
Rename panel.forums_create_description to panel.forums_create_desc.
Rename panel_forum_description to panel.forum_desc.
Rename panel_forum_description_placeholder to panel.forum_desc_placeholder.
Add panel_debug_http_conns_label phrase.
Add panel.forum_actions_head phrase.
Add panel.forum_actions_create_head phrase.
Add panel.forum_action_run_on_topic_creation phrase.
Add panel.forum_action_run_days_after_topic_creation phrase.
Add panel.forum_action_run_days_after_topic_last_reply phrase.
Add panel.forum_action_action phrase.
Add panel.forum_action_action_delete phrase.
Add panel.forum_action_action_lock phrase.
Add panel.forum_action_action_unlock phrase.
Add panel.forum_action_action_move phrase.
Add panel.forum_action_extra phrase.
Add panel.forum_action_create_button phrase.

You will need to run the patcher / updater for this commit.
This commit is contained in:
Azareal 2021-04-08 00:23:11 +10:00
parent 943a684332
commit c60118e7c4
44 changed files with 1793 additions and 993 deletions

View File

@ -41,6 +41,7 @@ func AddHooks(add func(name, params, ret, htype string, multiHook, skip bool, de
}
vhooknoret("router_end", "w http.ResponseWriter,r *http.Request,u *User,prefix string,extraData string")
vhooknoret("topic_reply_row_assign", "r *ReplyUser")
vhooknoret("counters_perf_tick_row", "low int64,high int64,avg int64")
//forums_frow_assign
//Hook(name string, data interface{}) interface{}
/*hook := func(name, params, ret, pure string) {

View File

@ -5,12 +5,12 @@ package main // import "github.com/Azareal/Gosora/hook_gen"
import (
"fmt"
"log"
"strings"
"runtime/debug"
_ "github.com/Azareal/Gosora/extend"
c "github.com/Azareal/Gosora/common"
"strings"
h "github.com/Azareal/Gosora/cmd/common_hook_gen"
c "github.com/Azareal/Gosora/common"
_ "github.com/Azareal/Gosora/extend"
)
// TODO: Make sure all the errors in this file propagate upwards properly
@ -22,7 +22,7 @@ func main() {
debug.PrintStack()
}
}()
hooks := make(map[string]int)
for _, pl := range c.Plugins {
if len(pl.Meta.Hooks) > 0 {
@ -45,24 +45,25 @@ func main() {
}
}
log.Printf("hooks: %+v\n", hooks)
imports := []string{"net/http"}
hookVars := h.HookVars{imports,nil}
hookVars := h.HookVars{imports, nil}
var params2sb strings.Builder
add := func(name, params, ret, htype string, multiHook, skip bool, defaultRet, pure string) {
var params2 string
first := true
for _, param := range strings.Split(params,",") {
for _, param := range strings.Split(params, ",") {
if !first {
params2 += ","
params2sb.WriteRune(',')
}
pspl := strings.Split(strings.ReplaceAll(strings.TrimSpace(param)," "," ")," ")
params2 += pspl[0]
pspl := strings.Split(strings.ReplaceAll(strings.TrimSpace(param), " ", " "), " ")
params2sb.WriteString(pspl[0])
first = false
}
hookVars.Hooks = append(hookVars.Hooks, h.Hook{name, params, params2, ret, htype, hooks[name] > 0, multiHook, skip, defaultRet, pure})
hookVars.Hooks = append(hookVars.Hooks, h.Hook{name, params, params2sb.String(), ret, htype, hooks[name] > 0, multiHook, skip, defaultRet, pure})
params2sb.Reset()
}
h.AddHooks(add)
h.Write(hookVars)
log.Println("Successfully generated the hooks")
}
}

View File

@ -25,7 +25,7 @@ type DefaultActivityStream struct {
delete *sql.Stmt
deleteByParams *sql.Stmt
deleteByParamsExtra *sql.Stmt
aidsByParams *sql.Stmt
aidsByParams *sql.Stmt
aidsByParamsExtra *sql.Stmt
count *sql.Stmt
}
@ -38,7 +38,7 @@ func NewDefaultActivityStream(acc *qgen.Accumulator) (*DefaultActivityStream, er
delete: acc.Delete(as).Where("asid=?").Prepare(),
deleteByParams: acc.Delete(as).Where("event=? AND elementID=? AND elementType=?").Prepare(),
deleteByParamsExtra: acc.Delete(as).Where("event=? AND elementID=? AND elementType=? AND extra=?").Prepare(),
aidsByParams: acc.Select(as).Columns("asid").Where("event=? AND elementID=? AND elementType=?").Prepare(),
aidsByParams: acc.Select(as).Columns("asid").Where("event=? AND elementID=? AND elementType=?").Prepare(),
aidsByParamsExtra: acc.Select(as).Columns("asid").Where("event=? AND elementID=? AND elementType=? AND extra=?").Prepare(),
count: acc.Count(as).Prepare(),
}, acc.FirstError()
@ -90,16 +90,16 @@ func (s *DefaultActivityStream) AidsByParams(event string, elementID int, elemen
return aids, rows.Err()
}
func (s *DefaultActivityStream) AidsByParamsExtra(event string, elementID int, elementType, extra string) (aids []int, err error) {
rows, err := s.aidsByParamsExtra.Query(event, elementID, elementType, extra)
if err != nil {
return nil, err
func (s *DefaultActivityStream) AidsByParamsExtra(event string, elementID int, elementType, extra string) (aids []int, e error) {
rows, e := s.aidsByParamsExtra.Query(event, elementID, elementType, extra)
if e != nil {
return nil, e
}
defer rows.Close()
for rows.Next() {
var aid int
if err := rows.Scan(&aid); err != nil {
return nil, err
if e := rows.Scan(&aid); e != nil {
return nil, e
}
aids = append(aids, aid)
}
@ -109,9 +109,9 @@ func (s *DefaultActivityStream) AidsByParamsExtra(event string, elementID int, e
// TODO: Write a test for this
// Count returns the total number of activity stream items
func (s *DefaultActivityStream) Count() (count int) {
err := s.count.QueryRow().Scan(&count)
if err != nil {
LogError(err)
e := s.count.QueryRow().Scan(&count)
if e != nil {
LogError(e)
}
return count
}

View File

@ -10,6 +10,8 @@ import (
"database/sql"
"io"
"log"
"net"
"net/http"
"os"
"strconv"
"strings"
@ -291,3 +293,22 @@ func inqbuildstr(strs []string) ([]interface{}, string) {
}
return idList, sb.String()
}
var ConnWatch = &ConnWatcher{}
type ConnWatcher struct {
n int64
}
func (cw *ConnWatcher) StateChange(conn net.Conn, state http.ConnState) {
switch state {
case http.StateNew:
atomic.AddInt64(&cw.n, 1)
case http.StateHijacked, http.StateClosed:
atomic.AddInt64(&cw.n, -1)
}
}
func (cw *ConnWatcher) Count() int {
return int(atomic.LoadInt64(&cw.n))
}

View File

@ -85,25 +85,19 @@ func (co *Conversation) Posts(offset, itemsPerPage int) (posts []*ConversationPo
}
func (co *Conversation) PostsCount() (count int) {
err := convoStmts.countPosts.QueryRow(co.ID).Scan(&count)
if err != nil {
LogError(err)
}
return count
return Countf(convoStmts.countPosts, co.ID)
}
func (co *Conversation) Uids() (ids []int, err error) {
rows, err := convoStmts.getUsers.Query(co.ID)
if err != nil {
return nil, err
rows, e := convoStmts.getUsers.Query(co.ID)
if e != nil {
return nil, e
}
defer rows.Close()
for rows.Next() {
var id int
err := rows.Scan(&id)
if err != nil {
return nil, err
if e := rows.Scan(&id); e != nil {
return nil, e
}
ids = append(ids, id)
}
@ -111,12 +105,7 @@ func (co *Conversation) Uids() (ids []int, err error) {
}
func (co *Conversation) Has(uid int) (in bool) {
var count int
err := convoStmts.has.QueryRow(uid, co.ID).Scan(&count)
if err != nil {
LogError(err)
}
return count > 0
return Countf(convoStmts.has, uid, co.ID) > 0
}
func (co *Conversation) Update() error {
@ -247,7 +236,7 @@ func (s *DefaultConversationStore) GetUserExtra(uid, offset int) (cos []*Convers
cmap[co.ID] = &ConversationExtra{co, nil}
}
// TODO: Add a function for the q stuff
// TODO: Use inqbuild for this or a similar function
var q string
idList := make([]interface{}, len(raw))
for i, co := range raw {

View File

@ -51,21 +51,22 @@ func (co *DefaultPerfCounter) Tick() error {
b.Unlock()
return c
}
var low int64
hTbl := c.GetHookTable()
for _, b := range co.buckets {
var low int64
b.low.Lock()
low = b.low.counter
b.low.counter = math.MaxInt64
low, b.low.counter = b.low.counter, math.MaxInt64
b.low.Unlock()
if low == math.MaxInt64 {
low = 0
}
high := getCounter(b.high)
avg := getCounter(b.avg)
c.H_counters_perf_tick_row_hook(hTbl, low, high, avg)
err := co.insertChunk(low, high, avg) // TODO: Bulk insert for speed?
if err != nil {
return errors.Wrap(errors.WithStack(err), "perf counter")
e := co.insertChunk(low, high, avg) // TODO: Bulk insert for speed?
if e != nil {
return errors.Wrap(errors.WithStack(e), "perf counter")
}
}
return nil
@ -76,8 +77,8 @@ func (co *DefaultPerfCounter) insertChunk(low, high, avg int64) error {
return nil
}
c.DebugLogf("Inserting a pchunk with low %d, high %d, avg %d", low, high, avg)
_, err := co.insert.Exec(low, high, avg)
return err
_, e := co.insert.Exec(low, high, avg)
return e
}
func (co *DefaultPerfCounter) Push(dur time.Duration /*,_ bool*/) {

View File

@ -126,6 +126,8 @@ var hookTable = &HookTable{
"tasks_tick_topic_list": nil,
"tasks_tick_widget_wol": nil,
"counters_perf_tick_row": nil,
},
map[string][]func(string) string{
"preparse_preassign": nil,

399
common/forum_actions.go Normal file
View File

@ -0,0 +1,399 @@
package common
import (
"database/sql"
"fmt"
"strconv"
qgen "github.com/Azareal/Gosora/query_gen"
)
var ForumActionStore ForumActionStoreInt
//var ForumActionRunnableStore ForumActionRunnableStoreInt
const (
ForumActionDelete = iota
ForumActionLock
ForumActionUnlock
ForumActionMove
)
func ConvStringToAct(s string) int {
switch s {
case "delete":
return ForumActionDelete
case "lock":
return ForumActionLock
case "unlock":
return ForumActionUnlock
case "move":
return ForumActionMove
}
return -1
}
func ConvActToString(a int) string {
switch a {
case ForumActionDelete:
return "delete"
case ForumActionLock:
return "lock"
case ForumActionUnlock:
return "unlock"
case ForumActionMove:
return "move"
}
return ""
}
var forumActionStmts ForumActionStmts
type ForumActionStmts struct {
get1 *sql.Stmt
get2 *sql.Stmt
lock1 *sql.Stmt
lock2 *sql.Stmt
unlock1 *sql.Stmt
unlock2 *sql.Stmt
}
type ForumAction struct {
ID int
Forum int
RunOnTopicCreation bool
RunDaysAfterTopicCreation int
RunDaysAfterTopicLastReply int
Action int
Extra string
}
func init() {
DbInits.Add(func(acc *qgen.Accumulator) error {
t := "topics"
forumActionStmts = ForumActionStmts{
get1: acc.Select(t).Cols("tid,createdBy,poll").Where("parentID=?").DateOlderThanQ("createdAt", "day").Stmt(),
get2: acc.Select(t).Cols("tid,createdBy,poll").Where("parentID=?").DateOlderThanQ("lastReplyAt", "day").Stmt(),
/*lock1: acc.Update(t).Set("is_closed=1").Where("parentID=?").DateOlderThanQ("createdAt", "day").Stmt(),
lock2: acc.Update(t).Set("is_closed=1").Where("parentID=?").DateOlderThanQ("lastReplyAt", "day").Stmt(),
unlock1: acc.Update(t).Set("is_closed=0").Where("parentID=?").DateOlderThanQ("createdAt", "day").Stmt(),
unlock2: acc.Update(t).Set("is_closed=0").Where("parentID=?").DateOlderThanQ("lastReplyAt", "day").Stmt(),*/
}
return acc.FirstError()
})
}
func (a *ForumAction) Run() error {
if a.RunDaysAfterTopicCreation > 0 {
if e := a.runDaysAfterTopicCreation(); e != nil {
return e
}
}
if a.RunDaysAfterTopicLastReply > 0 {
if e := a.runDaysAfterTopicLastReply(); e != nil {
return e
}
}
return nil
}
func (a *ForumAction) runQ(stmt *sql.Stmt, days int, f func(t *Topic) error) error {
rows, e := stmt.Query(days, a.Forum)
if e != nil {
return e
}
defer rows.Close()
for rows.Next() {
// TODO: Decouple this
t := &Topic{ParentID: a.Forum}
if e := rows.Scan(&t.ID, &t.CreatedBy, &t.Poll); e != nil {
return e
}
if e = f(t); e != nil {
return e
}
}
return rows.Err()
}
func (a *ForumAction) runDaysAfterTopicCreation() (e error) {
switch a.Action {
case ForumActionDelete:
// TODO: Bulk delete?
e = a.runQ(forumActionStmts.get1, a.RunDaysAfterTopicCreation, func(t *Topic) error {
return t.Delete()
})
case ForumActionLock:
/*_, e := forumActionStmts.lock1.Exec(a.Forum)
if e != nil {
return e
}*/
// TODO: Bulk lock? Lock and get resultset of changed topics somehow?
fmt.Println("ForumActionLock")
e = a.runQ(forumActionStmts.get1, a.RunDaysAfterTopicCreation, func(t *Topic) error {
fmt.Printf("t: %+v\n", t)
return t.Lock()
})
case ForumActionUnlock:
// TODO: Bulk unlock? Unlock and get resultset of changed topics somehow?
e = a.runQ(forumActionStmts.get1, a.RunDaysAfterTopicCreation, func(t *Topic) error {
return t.Unlock()
})
case ForumActionMove:
destForum, e := strconv.Atoi(a.Extra)
if e != nil {
return e
}
e = a.runQ(forumActionStmts.get1, a.RunDaysAfterTopicCreation, func(t *Topic) error {
return t.MoveTo(destForum)
})
}
return e
}
func (a *ForumAction) runDaysAfterTopicLastReply() (e error) {
switch a.Action {
case ForumActionDelete:
e = a.runQ(forumActionStmts.get2, a.RunDaysAfterTopicLastReply, func(t *Topic) error {
return t.Delete()
})
case ForumActionLock:
// TODO: Bulk lock? Lock and get resultset of changed topics somehow?
e = a.runQ(forumActionStmts.get2, a.RunDaysAfterTopicLastReply, func(t *Topic) error {
return t.Lock()
})
case ForumActionUnlock:
// TODO: Bulk unlock? Unlock and get resultset of changed topics somehow?
e = a.runQ(forumActionStmts.get2, a.RunDaysAfterTopicLastReply, func(t *Topic) error {
return t.Unlock()
})
case ForumActionMove:
destForum, e := strconv.Atoi(a.Extra)
if e != nil {
return e
}
e = a.runQ(forumActionStmts.get2, a.RunDaysAfterTopicLastReply, func(t *Topic) error {
return t.MoveTo(destForum)
})
}
return nil
}
func (a *ForumAction) TopicCreation(tid int) error {
if !a.RunOnTopicCreation {
return nil
}
return nil
}
type ForumActionStoreInt interface {
Get(faid int) (*ForumAction, error)
GetInForum(fid int) ([]*ForumAction, error)
GetAll() ([]*ForumAction, error)
GetNewTopicActions(fid int) ([]*ForumAction, error)
Add(fa *ForumAction) (int, error)
Delete(faid int) error
Exists(faid int) bool
Count() int
CountInForum(fid int) int
DailyTick() error
}
type DefaultForumActionStore struct {
get *sql.Stmt
getInForum *sql.Stmt
getAll *sql.Stmt
getNewTopicActions *sql.Stmt
add *sql.Stmt
delete *sql.Stmt
exists *sql.Stmt
count *sql.Stmt
countInForum *sql.Stmt
}
func NewDefaultForumActionStore(acc *qgen.Accumulator) (*DefaultForumActionStore, error) {
fa := "forums_actions"
allCols := "faid,fid,runOnTopicCreation,runDaysAfterTopicCreation,runDaysAfterTopicLastReply,action,extra"
return &DefaultForumActionStore{
get: acc.Select(fa).Columns("fid,runOnTopicCreation,runDaysAfterTopicCreation,runDaysAfterTopicLastReply,action,extra").Where("faid=?").Prepare(),
getInForum: acc.Select(fa).Columns("faid,runOnTopicCreation,runDaysAfterTopicCreation,runDaysAfterTopicLastReply,action,extra").Where("fid=?").Prepare(),
getAll: acc.Select(fa).Columns(allCols).Prepare(),
getNewTopicActions: acc.Select(fa).Columns(allCols).Where("fid=? AND runOnTopicCreation=1").Prepare(),
add: acc.Insert(fa).Columns("fid,runOnTopicCreation,runDaysAfterTopicCreation,runDaysAfterTopicLastReply,action,extra").Fields("?,?,?,?,?,?").Prepare(),
delete: acc.Delete(fa).Where("faid=?").Prepare(),
exists: acc.Exists(fa, "faid").Prepare(),
count: acc.Count(fa).Prepare(),
countInForum: acc.Count(fa).Where("fid=?").Prepare(),
}, acc.FirstError()
}
func (s *DefaultForumActionStore) DailyTick() error {
fas, e := s.GetAll()
if e != nil {
return e
}
for _, fa := range fas {
if e := fa.Run(); e != nil {
return e
}
}
return nil
}
func (s *DefaultForumActionStore) Get(id int) (*ForumAction, error) {
fa := ForumAction{ID: id}
var str string
e := s.get.QueryRow(id).Scan(&fa.Forum, &fa.RunOnTopicCreation, &fa.RunDaysAfterTopicCreation, &fa.RunDaysAfterTopicLastReply, &str, &fa.Extra)
fa.Action = ConvStringToAct(str)
return &fa, e
}
func (s *DefaultForumActionStore) GetInForum(fid int) (fas []*ForumAction, e error) {
rows, e := s.getInForum.Query(fid)
if e != nil {
return nil, e
}
defer rows.Close()
var str string
for rows.Next() {
fa := ForumAction{Forum: fid}
if e := rows.Scan(&fa.ID, &fa.RunOnTopicCreation, &fa.RunDaysAfterTopicCreation, &fa.RunDaysAfterTopicLastReply, &str, &fa.Extra); e != nil {
return nil, e
}
fa.Action = ConvStringToAct(str)
fas = append(fas, &fa)
}
return fas, rows.Err()
}
func (s *DefaultForumActionStore) GetAll() (fas []*ForumAction, e error) {
rows, e := s.getAll.Query()
if e != nil {
return nil, e
}
defer rows.Close()
var str string
for rows.Next() {
fa := ForumAction{}
if e := rows.Scan(&fa.ID, &fa.Forum, &fa.RunOnTopicCreation, &fa.RunDaysAfterTopicCreation, &fa.RunDaysAfterTopicLastReply, &str, &fa.Extra); e != nil {
return nil, e
}
fa.Action = ConvStringToAct(str)
fas = append(fas, &fa)
}
return fas, rows.Err()
}
func (s *DefaultForumActionStore) GetNewTopicActions(fid int) (fas []*ForumAction, e error) {
rows, e := s.getNewTopicActions.Query(fid)
if e != nil {
return nil, e
}
defer rows.Close()
var str string
for rows.Next() {
fa := ForumAction{RunOnTopicCreation: true}
if e := rows.Scan(&fa.ID, &fa.Forum, &fa.RunDaysAfterTopicCreation, &fa.RunDaysAfterTopicLastReply, &str, &fa.Extra); e != nil {
return nil, e
}
fa.Action = ConvStringToAct(str)
fas = append(fas, &fa)
}
return fas, rows.Err()
}
func (s *DefaultForumActionStore) Add(fa *ForumAction) (int, error) {
res, e := s.add.Exec(fa.Forum, fa.RunOnTopicCreation, fa.RunDaysAfterTopicCreation, fa.RunDaysAfterTopicLastReply, ConvActToString(fa.Action), fa.Extra)
if e != nil {
return 0, e
}
lastID, e := res.LastInsertId()
return int(lastID), e
}
func (s *DefaultForumActionStore) Delete(id int) error {
_, e := s.delete.Exec(id)
return e
}
func (s *DefaultForumActionStore) Exists(id int) bool {
err := s.exists.QueryRow(id).Scan(&id)
if err != nil && err != ErrNoRows {
LogError(err)
}
return err != ErrNoRows
}
func (s *DefaultForumActionStore) Count() (count int) {
err := s.count.QueryRow().Scan(&count)
if err != nil {
LogError(err)
}
return count
}
func (s *DefaultForumActionStore) CountInForum(fid int) (count int) {
return Countf(s.countInForum, fid)
}
/*type ForumActionRunnable struct {
ID int
ActionID int
TargetID int
TargetType int // 0 = topic
RunAfter int //unixtime
}
type ForumActionRunnableStoreInt interface {
GetAfterTime(unix int) ([]*ForumActionRunnable, error)
GetInForum(fid int) ([]*ForumActionRunnable, error)
Delete(faid int) error
DeleteInForum(fid int) error
DeleteByActionID(faid int) error
Count() int
CountInForum(fid int) int
}
type DefaultForumActionRunnableStore struct {
delete *sql.Stmt
deleteInForum *sql.Stmt
count *sql.Stmt
countInForum *sql.Stmt
}
func NewDefaultForumActionRunnableStore(acc *qgen.Accumulator) (*DefaultForumActionRunnableStore, error) {
fa := "forums_actions"
return &DefaultForumActionRunnableStore{
delete: acc.Delete(fa).Where("faid=?").Prepare(),
deleteInForum: acc.Delete(fa).Where("fid=?").Prepare(),
count: acc.Count(fa).Prepare(),
countInForum: acc.Count(fa).Where("faid=?").Prepare(),
}, acc.FirstError()
}
func (s *DefaultForumActionRunnableStore) Delete(id int) error {
_, e := s.delete.Exec(id)
return e
}
func (s *DefaultForumActionRunnableStore) DeleteInForum(fid int) error {
_, e := s.deleteInForum.Exec(id)
return e
}
func (s *DefaultForumActionRunnableStore) Count() (count int) {
err := s.count.QueryRow().Scan(&count)
if err != nil {
LogError(err)
}
return count
}
func (s *DefaultForumActionRunnableStore) CountInForum(fid int) (count int) {
return Countf(s.countInForum, fid)
}
*/

View File

@ -52,9 +52,9 @@ func (s *MemoryForumPermsStore) Init() error {
// TODO: Optimise this?
func (s *MemoryForumPermsStore) ReloadAll() error {
DebugLog("Reloading the forum perms")
fids, err := Forums.GetAllIDs()
if err != nil {
return err
fids, e := Forums.GetAllIDs()
if e != nil {
return e
}
for _, fid := range fids {
if e := s.reload(fid); e != nil {
@ -68,13 +68,13 @@ func (s *MemoryForumPermsStore) ReloadAll() error {
return nil
}
func (s *MemoryForumPermsStore) parseForumPerm(perms []byte) (pperms *ForumPerms, err error) {
func (s *MemoryForumPermsStore) parseForumPerm(perms []byte) (pperms *ForumPerms, e error) {
DebugDetail("perms: ", string(perms))
pperms = BlankForumPerms()
err = json.Unmarshal(perms, &pperms)
e = json.Unmarshal(perms, &pperms)
pperms.ExtData = make(map[string]bool)
pperms.Overrides = true
return pperms, err
return pperms, e
}
func (s *MemoryForumPermsStore) Reload(fid int) error {
@ -246,10 +246,10 @@ func (s *MemoryForumPermsStore) Get(fid, gid int) (fp *ForumPerms, err error) {
// TODO: Check if the forum exists?
// TODO: Fix the races
func (s *MemoryForumPermsStore) GetCopy(fid, gid int) (fp ForumPerms, err error) {
fPermsPtr, err := s.Get(fid, gid)
if err != nil {
return fp, err
func (s *MemoryForumPermsStore) GetCopy(fid, gid int) (fp ForumPerms, e error) {
fPermsPtr, e := s.Get(fid, gid)
if e != nil {
return fp, e
}
return *fPermsPtr, nil
}

View File

@ -81,18 +81,21 @@ type MemoryForumStore struct {
func NewMemoryForumStore() (*MemoryForumStore, error) {
acc := qgen.NewAcc()
f := "forums"
set := func(s string) *sql.Stmt {
return acc.Update(f).Set(s).Where("fid=?").Prepare()
}
// TODO: Do a proper delete
return &MemoryForumStore{
get: acc.Select(f).Columns("name, desc, tmpl, active, order, preset, parentID, parentType, topicCount, lastTopicID, lastReplyerID").Where("fid=?").Prepare(),
getAll: acc.Select(f).Columns("fid, name, desc, tmpl, active, order, preset, parentID, parentType, topicCount, lastTopicID, lastReplyerID").Orderby("order ASC, fid ASC").Prepare(),
delete: acc.Update(f).Set("name='',active=0").Where("fid=?").Prepare(),
create: acc.Insert(f).Columns("name, desc, tmpl, active, preset").Fields("?,?,'',?,?").Prepare(),
delete: set("name='',active=0"),
create: acc.Insert(f).Columns("name,desc,tmpl,active,preset").Fields("?,?,'',?,?").Prepare(),
count: acc.Count(f).Where("name != ''").Prepare(),
updateCache: acc.Update(f).Set("lastTopicID=?, lastReplyerID=?").Where("fid=?").Prepare(),
addTopics: acc.Update(f).Set("topicCount=topicCount+?").Where("fid=?").Prepare(),
removeTopics: acc.Update(f).Set("topicCount=topicCount-?").Where("fid=?").Prepare(),
lastTopic: acc.Select("topics").Columns("tid").Where("parentID=?").Orderby("lastReplyAt DESC, createdAt DESC").Limit("1").Prepare(),
updateOrder: acc.Update(f).Set("order=?").Where("fid=?").Prepare(),
updateCache: set("lastTopicID=?,lastReplyerID=?"),
addTopics: set("topicCount=topicCount+?"),
removeTopics: set("topicCount=topicCount-?"),
lastTopic: acc.Select("topics").Columns("tid").Where("parentID=?").Orderby("lastReplyAt DESC,createdAt DESC").Limit("1").Prepare(),
updateOrder: set("order=?"),
}, acc.FirstError()
}

View File

@ -44,11 +44,13 @@ var groupStmts GroupStmts
func init() {
DbInits.Add(func(acc *qgen.Accumulator) error {
ug := "users_groups"
set := func(s string) *sql.Stmt {
return acc.Update("users_groups").Set(s).Where("gid=?").Prepare()
}
groupStmts = GroupStmts{
updateGroup: acc.Update(ug).Set("name=?,tag=?").Where("gid=?").Prepare(),
updateGroupRank: acc.Update(ug).Set("is_admin=?,is_mod=?,is_banned=?").Where("gid=?").Prepare(),
updateGroupPerms: acc.Update(ug).Set("permissions=?").Where("gid=?").Prepare(),
updateGroup: set("name=?,tag=?"),
updateGroupRank: set("is_admin=?,is_mod=?,is_banned=?"),
updateGroupPerms: set("permissions=?"),
}
return acc.FirstError()
})

View File

@ -141,22 +141,20 @@ func (s *MemoryGroupStore) GetCopy(id int) (Group, error) {
func (s *MemoryGroupStore) Reload(id int) error {
// TODO: Reload this data too
g, err := s.Get(id)
if err != nil {
g, e := s.Get(id)
if e != nil {
LogError(errors.New("can't get cansee data for group #" + strconv.Itoa(id)))
return nil
}
canSee := g.CanSee
g = &Group{ID: id, CanSee: canSee}
err = s.get.QueryRow(id).Scan(&g.Name, &g.PermissionsText, &g.PluginPermsText, &g.IsMod, &g.IsAdmin, &g.IsBanned, &g.Tag)
if err != nil {
return err
e = s.get.QueryRow(id).Scan(&g.Name, &g.PermissionsText, &g.PluginPermsText, &g.IsMod, &g.IsAdmin, &g.IsBanned, &g.Tag)
if e != nil {
return e
}
err = s.initGroup(g)
if err != nil {
LogError(err)
if e = s.initGroup(g); e != nil {
LogError(e)
return nil
}
@ -166,19 +164,19 @@ func (s *MemoryGroupStore) Reload(id int) error {
}
func (s *MemoryGroupStore) initGroup(g *Group) error {
err := json.Unmarshal(g.PermissionsText, &g.Perms)
if err != nil {
log.Printf("group: %+v\n", g)
e := json.Unmarshal(g.PermissionsText, &g.Perms)
if e != nil {
log.Printf("g: %+v\n", g)
log.Print("bad group perms: ", g.PermissionsText)
return err
return e
}
DebugLogf(g.Name+": %+v\n", g.Perms)
err = json.Unmarshal(g.PluginPermsText, &g.PluginPerms)
if err != nil {
log.Printf("group: %+v\n", g)
e = json.Unmarshal(g.PluginPermsText, &g.PluginPerms)
if e != nil {
log.Printf("g: %+v\n", g)
log.Print("bad group plugin perms: ", g.PluginPermsText)
return err
return e
}
DebugLogf(g.Name+": %+v\n", g.PluginPerms)
@ -188,9 +186,9 @@ func (s *MemoryGroupStore) initGroup(g *Group) error {
g.IsBanned = false
}
err = s.userCount.QueryRow(g.ID).Scan(&g.UserCount)
if err != sql.ErrNoRows {
return err
e = s.userCount.QueryRow(g.ID).Scan(&g.UserCount)
if e != sql.ErrNoRows {
return e
}
return nil
}

View File

@ -649,12 +649,18 @@ type GroupForumPermPreset struct {
type PanelEditForumPage struct {
*BasePanelPage
ID int
Name string
Desc string
Active bool
Preset string
Groups []GroupForumPermPreset
ID int
Name string
Desc string
Active bool
Preset string
Groups []GroupForumPermPreset
Actions []*ForumActionAction
}
type ForumActionAction struct {
*ForumAction
ActionName string
}
type NameLangToggle struct {
@ -793,11 +799,12 @@ type PanelDebugPage struct {
DBVersion string
Uptime string
OpenConns int
DBConns int
DBAdapter string
Goroutines int
CPUs int
HttpConns int
Tasks DebugPageTasks
MemStats runtime.MemStats

View File

@ -39,11 +39,11 @@ func NewDefaultBlockStore(acc *qgen.Accumulator) (*DefaultBlockStore, error) {
}
func (s *DefaultBlockStore) IsBlockedBy(blocker, blockee int) (bool, error) {
err := s.isBlocked.QueryRow(blocker, blockee).Scan(&blocker)
if err == ErrNoRows {
e := s.isBlocked.QueryRow(blocker, blockee).Scan(&blocker)
if e == ErrNoRows {
return false, nil
}
return err == nil, err
return e == nil, e
}
// TODO: Optimise the query to avoid preparing it on the spot? Maybe, use knowledge of the most common IN() parameter counts?
@ -55,34 +55,33 @@ func (s *DefaultBlockStore) BulkIsBlockedBy(blockers []int, blockee int) (bool,
return s.IsBlockedBy(blockers[0], blockee)
}
idList, q := inqbuild(blockers)
count, err := qgen.NewAcc().Count("users_blocks").Where("blocker IN(" + q + ") AND blockedUser=?").TotalP(idList...)
if err == ErrNoRows {
count, e := qgen.NewAcc().Count("users_blocks").Where("blocker IN(" + q + ") AND blockedUser=?").TotalP(idList...)
if e == ErrNoRows {
return false, nil
}
return count == 0, err
return count == 0, e
}
func (s *DefaultBlockStore) Add(blocker, blockee int) error {
_, err := s.add.Exec(blocker, blockee)
return err
_, e := s.add.Exec(blocker, blockee)
return e
}
func (s *DefaultBlockStore) Remove(blocker, blockee int) error {
_, err := s.remove.Exec(blocker, blockee)
return err
_, e := s.remove.Exec(blocker, blockee)
return e
}
func (s *DefaultBlockStore) BlockedByOffset(blocker, offset, perPage int) (uids []int, err error) {
rows, err := s.blockedBy.Query(blocker, offset, perPage)
if err != nil {
return nil, err
rows, e := s.blockedBy.Query(blocker, offset, perPage)
if e != nil {
return nil, e
}
defer rows.Close()
for rows.Next() {
var uid int
err := rows.Scan(&uid)
if err != nil {
return nil, err
if e := rows.Scan(&uid); e != nil {
return nil, e
}
uids = append(uids, uid)
}
@ -90,9 +89,9 @@ func (s *DefaultBlockStore) BlockedByOffset(blocker, offset, perPage int) (uids
}
func (s *DefaultBlockStore) BlockedByCount(blocker int) (count int) {
err := s.blockedByCount.QueryRow(blocker).Scan(&count)
if err != nil {
LogError(err)
e := s.blockedByCount.QueryRow(blocker).Scan(&count)
if e != nil {
LogError(e)
}
return count
}
@ -127,13 +126,13 @@ func NewDefaultFriendStore(acc *qgen.Accumulator) (*DefaultFriendStore, error) {
}
func (s *DefaultFriendStore) AddInvite(requester, target int) error {
_, err := s.addInvite.Exec(requester, target)
return err
_, e := s.addInvite.Exec(requester, target)
return e
}
func (s *DefaultFriendStore) Confirm(requester, target int) error {
_, err := s.confirm.Exec(requester, target)
return err
_, e := s.confirm.Exec(requester, target)
return e
}
func (s *DefaultFriendStore) GetOwnSentInvites(uid int) ([]FriendInvite, error) {

View File

@ -11,7 +11,7 @@ import (
"log"
"time"
"github.com/Azareal/Gosora/query_gen"
qgen "github.com/Azareal/Gosora/query_gen"
)
type TaskStmts struct {
@ -68,18 +68,22 @@ func AddShutdownTask(task func() error) {
func ScheduledHalfSecondTaskCount() int {
return len(ScheduledHalfSecondTasks)
}
// ScheduledSecondTaskCount is not concurrency safe
func ScheduledSecondTaskCount() int {
return len(ScheduledSecondTasks)
}
// ScheduledFifteenMinuteTaskCount is not concurrency safe
func ScheduledFifteenMinuteTaskCount() int {
return len(ScheduledFifteenMinuteTasks)
}
// ScheduledHourTaskCount is not concurrency safe
func ScheduledHourTaskCount() int {
return len(ScheduledHourTasks)
}
// ShutdownTaskCount is not concurrency safe
func ShutdownTaskCount() int {
return len(ShutdownTasks)
@ -87,25 +91,23 @@ func ShutdownTaskCount() int {
// TODO: Use AddScheduledSecondTask
func HandleExpiredScheduledGroups() error {
rows, err := taskStmts.getExpiredScheduledGroups.Query()
if err != nil {
return err
rows, e := taskStmts.getExpiredScheduledGroups.Query()
if e != nil {
return e
}
defer rows.Close()
var uid int
for rows.Next() {
err := rows.Scan(&uid)
if err != nil {
return err
if e := rows.Scan(&uid); e != nil {
return e
}
// Sneaky way of initialising a *User, please use the methods on the UserStore instead
user := BlankUser()
user.ID = uid
err = user.RevertGroupUpdate()
if err != nil {
return err
e = user.RevertGroupUpdate()
if e != nil {
return e
}
}
return rows.Err()
@ -122,28 +124,28 @@ func HandleServerSync() error {
}
var lastUpdate time.Time
err := taskStmts.getSync.QueryRow().Scan(&lastUpdate)
if err != nil {
return err
e := taskStmts.getSync.QueryRow().Scan(&lastUpdate)
if e != nil {
return e
}
if lastUpdate.After(lastSync) {
err = Forums.LoadForums()
if err != nil {
e = Forums.LoadForums()
if e != nil {
log.Print("Unable to reload the forums")
return err
return e
}
// TODO: Resync the groups
// TODO: Resync the permissions
err = LoadSettings()
if err != nil {
e = LoadSettings()
if e != nil {
log.Print("Unable to reload the settings")
return err
return e
}
err = WordFilters.ReloadAll()
if err != nil {
e = WordFilters.ReloadAll()
if e != nil {
log.Print("Unable to reload the word filters")
return err
return e
}
}
return nil

View File

@ -385,7 +385,7 @@ func compileTemplates(wg *sync.WaitGroup, c *tmpl.CTemplateSet, themeName string
debugCache := DebugPageCache{1, 1, 1, 2, 2, 2, true}
debugDatabase := DebugPageDatabase{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}
debugDisk := DebugPageDisk{1, 1, 1, 1, 1, 1}
dpage := PanelDebugPage{basePage, goVersion, dbVersion, "0s", 1, qgen.Builder.GetAdapter().GetName(), 1, 1, debugTasks, memStats, debugCache, debugDatabase, debugDisk}
dpage := PanelDebugPage{basePage, goVersion, dbVersion, "0s", 1, qgen.Builder.GetAdapter().GetName(), 1, 1, 1, debugTasks, memStats, debugCache, debugDatabase, debugDisk}
t.AddStd("panel_debug", "c.PanelDebugPage", dpage)
//t.AddStd("panel_analytics", "c.PanelAnalytics", Panel{basePage, "panel_dashboard_right","panel_dashboard", inter})
@ -395,9 +395,9 @@ func compileTemplates(wg *sync.WaitGroup, c *tmpl.CTemplateSet, themeName string
if content == "" {
return //log.Fatal("No content body for " + name)
}
err := writeFile("./tmpl_"+name+".go", content)
if err != nil {
log.Fatal(err)
e := writeFile("./tmpl_"+name+".go", content)
if e != nil {
log.Fatal(e)
}
}
wg.Add(1)
@ -595,9 +595,9 @@ func compileJSTemplates(wg *sync.WaitGroup, c *tmpl.CTemplateSet, themeName stri
if tname != "" {
tname = "_" + tname
}
err := writeFile(dirPrefix+"tmpl_"+name+tname+".jgo", content)
if err != nil {
log.Fatal(err)
e := writeFile(dirPrefix+"tmpl_"+name+tname+".jgo", content)
if e != nil {
log.Fatal(e)
}
wg.Done()
}()
@ -737,9 +737,9 @@ func writeTemplateList(c *tmpl.CTemplateSet, wg *sync.WaitGroup, prefix string)
log.Print("Writing template list")
wg.Add(1)
go func() {
err := writeFile(prefix+"tmpl_list.go", getTemplateList(c, wg, prefix))
if err != nil {
log.Fatal(err)
e := writeFile(prefix+"tmpl_list.go", getTemplateList(c, wg, prefix))
if e != nil {
log.Fatal(e)
}
wg.Done()
}()

View File

@ -199,6 +199,7 @@ type TopicStmts struct {
deleteActivity *sql.Stmt
edit *sql.Stmt
setPoll *sql.Stmt
testSetCreatedAt *sql.Stmt
createAction *sql.Stmt
getTopicUser *sql.Stmt // TODO: Can we get rid of this?
@ -209,31 +210,35 @@ var topicStmts TopicStmts
func init() {
DbInits.Add(func(acc *qgen.Accumulator) error {
t := "topics"
t, w := "topics", "tid=?"
set := func(s string) *sql.Stmt {
return acc.Update(t).Set(s).Where(w).Prepare()
}
topicStmts = TopicStmts{
getRids: acc.Select("replies").Columns("rid").Where("tid=?").Orderby("rid ASC").Limit("?,?").Prepare(),
getRids: acc.Select("replies").Columns("rid").Where(w).Orderby("rid ASC").Limit("?,?").Prepare(),
getReplies: acc.SimpleLeftJoin("replies AS r", "users AS u", "r.rid, r.content, r.createdBy, r.createdAt, r.lastEdit, r.lastEditBy, u.avatar, u.name, u.group, u.level, r.ip, r.likeCount, r.attachCount, r.actionType", "r.createdBy=u.uid", "r.tid=?", "r.rid ASC", "?,?"),
getReplies2: acc.SimpleLeftJoin("replies AS r", "users AS u", "r.rid, r.content, r.createdBy, r.createdAt, r.lastEdit, r.lastEditBy, u.avatar, u.name, u.group, u.level, r.likeCount, r.attachCount, r.actionType", "r.createdBy=u.uid", "r.tid=?", "r.rid ASC", "?,?"),
getReplies3: acc.Select("replies").Columns("rid, content, createdBy, createdAt, lastEdit, lastEditBy, likeCount, attachCount, actionType").Where("tid=?").Orderby("rid ASC").Limit("?,?").Prepare(),
addReplies: acc.Update(t).Set("postCount=postCount+?, lastReplyBy=?, lastReplyAt=UTC_TIMESTAMP()").Where("tid=?").Prepare(),
updateLastReply: acc.Update(t).Set("lastReplyID=?").Where("lastReplyID < ? AND tid=?").Prepare(),
lock: acc.Update(t).Set("is_closed=1").Where("tid=?").Prepare(),
unlock: acc.Update(t).Set("is_closed=0").Where("tid=?").Prepare(),
moveTo: acc.Update(t).Set("parentID=?").Where("tid=?").Prepare(),
stick: acc.Update(t).Set("sticky=1").Where("tid=?").Prepare(),
unstick: acc.Update(t).Set("sticky=0").Where("tid=?").Prepare(),
getReplies3: acc.Select("replies").Columns("rid,content,createdBy,createdAt,lastEdit,lastEditBy,likeCount,attachCount,actionType").Where(w).Orderby("rid ASC").Limit("?,?").Prepare(),
addReplies: set("postCount=postCount+?,lastReplyBy=?,lastReplyAt=UTC_TIMESTAMP()"),
updateLastReply: acc.Update(t).Set("lastReplyID=?").Where("lastReplyID<? AND tid=?").Prepare(),
lock: set("is_closed=1"),
unlock: set("is_closed=0"),
moveTo: set("parentID=?"),
stick: set("sticky=1"),
unstick: set("sticky=0"),
hasLikedTopic: acc.Select("likes").Columns("targetItem").Where("sentBy=? and targetItem=? and targetType='topics'").Prepare(),
createLike: acc.Insert("likes").Columns("weight, targetItem, targetType, sentBy, createdAt").Fields("?,?,?,?,UTC_TIMESTAMP()").Prepare(),
addLikesToTopic: acc.Update(t).Set("likeCount=likeCount+?").Where("tid=?").Prepare(),
delete: acc.Delete(t).Where("tid=?").Prepare(),
deleteReplies: acc.Delete("replies").Where("tid=?").Prepare(),
createLike: acc.Insert("likes").Columns("weight,targetItem,targetType,sentBy,createdAt").Fields("?,?,?,?,UTC_TIMESTAMP()").Prepare(),
addLikesToTopic: set("likeCount=likeCount+?"),
delete: acc.Delete(t).Where(w).Prepare(),
deleteReplies: acc.Delete("replies").Where(w).Prepare(),
deleteLikesForTopic: acc.Delete("likes").Where("targetItem=? AND targetType='topics'").Prepare(),
deleteActivity: acc.Delete("activity_stream").Where("elementID=? AND elementType='topic'").Prepare(),
edit: acc.Update(t).Set("title=?,content=?,parsed_content=?").Where("tid=?").Prepare(), // TODO: Only run the content update bits on non-polls, does this matter?
edit: set("title=?,content=?,parsed_content=?"), // TODO: Only run the content update bits on non-polls, does this matter?
setPoll: acc.Update(t).Set("poll=?").Where("tid=? AND poll=0").Prepare(),
createAction: acc.Insert("replies").Columns("tid, actionType, ip, createdBy, createdAt, lastUpdated, content, parsed_content").Fields("?,?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),'',''").Prepare(),
testSetCreatedAt: set("createdAt=?"),
createAction: acc.Insert("replies").Columns("tid,actionType,ip,createdBy,createdAt,lastUpdated,content,parsed_content").Fields("?,?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),'',''").Prepare(),
getTopicUser: acc.SimpleLeftJoin("topics AS t", "users AS u", "t.title, t.content, t.createdBy, t.createdAt, t.lastReplyAt, t.lastReplyBy, t.lastReplyID, t.is_closed, t.sticky, t.parentID, t.ip, t.views, t.postCount, t.likeCount, t.attachCount,t.poll, u.name, u.avatar, u.group, u.level", "t.createdBy=u.uid", "tid=?", "", ""),
getTopicUser: acc.SimpleLeftJoin("topics AS t", "users AS u", "t.title, t.content, t.createdBy, t.createdAt, t.lastReplyAt, t.lastReplyBy, t.lastReplyID, t.is_closed, t.sticky, t.parentID, t.ip, t.views, t.postCount, t.likeCount, t.attachCount,t.poll, u.name, u.avatar, u.group, u.level", "t.createdBy=u.uid", w, "", ""),
getByReplyID: acc.SimpleLeftJoin("replies AS r", "topics AS t", "t.tid, t.title, t.content, t.createdBy, t.createdAt, t.is_closed, t.sticky, t.parentID, t.ip, t.views, t.postCount, t.likeCount, t.poll, t.data", "r.tid=t.tid", "rid=?", "", ""),
}
return acc.FirstError()
@ -250,52 +255,58 @@ func (t *Topic) cacheRemove() {
}
// TODO: Write a test for this
func (t *Topic) AddReply(rid, uid int) (err error) {
_, err = topicStmts.addReplies.Exec(1, uid, t.ID)
if err != nil {
return err
func (t *Topic) AddReply(rid, uid int) (e error) {
_, e = topicStmts.addReplies.Exec(1, uid, t.ID)
if e != nil {
return e
}
_, err = topicStmts.updateLastReply.Exec(rid, rid, t.ID)
_, e = topicStmts.updateLastReply.Exec(rid, rid, t.ID)
t.cacheRemove()
return err
return e
}
func (t *Topic) Lock() (err error) {
_, err = topicStmts.lock.Exec(t.ID)
func (t *Topic) Lock() (e error) {
_, e = topicStmts.lock.Exec(t.ID)
t.cacheRemove()
return err
return e
}
func (t *Topic) Unlock() (err error) {
_, err = topicStmts.unlock.Exec(t.ID)
func (t *Topic) Unlock() (e error) {
_, e = topicStmts.unlock.Exec(t.ID)
t.cacheRemove()
return err
return e
}
func (t *Topic) MoveTo(destForum int) (err error) {
_, err = topicStmts.moveTo.Exec(destForum, t.ID)
func (t *Topic) MoveTo(destForum int) (e error) {
_, e = topicStmts.moveTo.Exec(destForum, t.ID)
t.cacheRemove()
if err != nil {
return err
if e != nil {
return e
}
err = Attachments.MoveTo(destForum, t.ID, "topics")
if err != nil {
return err
e = Attachments.MoveTo(destForum, t.ID, "topics")
if e != nil {
return e
}
return Attachments.MoveToByExtra(destForum, "replies", strconv.Itoa(t.ID))
}
// TODO: We might want more consistent terminology rather than using stick in some places and pin in others. If you don't understand the difference, there is none, they are one and the same.
func (t *Topic) Stick() (err error) {
_, err = topicStmts.stick.Exec(t.ID)
func (t *Topic) TestSetCreatedAt(s time.Time) (e error) {
_, e = topicStmts.testSetCreatedAt.Exec(s, t.ID)
t.cacheRemove()
return err
return e
}
func (t *Topic) Unstick() (err error) {
_, err = topicStmts.unstick.Exec(t.ID)
// TODO: We might want more consistent terminology rather than using stick in some places and pin in others. If you don't understand the difference, there is none, they are one and the same.
func (t *Topic) Stick() (e error) {
_, e = topicStmts.stick.Exec(t.ID)
t.cacheRemove()
return err
return e
}
func (t *Topic) Unstick() (e error) {
_, e = topicStmts.unstick.Exec(t.ID)
t.cacheRemove()
return e
}
// TODO: Test this
@ -323,49 +334,46 @@ func (t *Topic) Like(score, uid int) (err error) {
// TODO: Use a transaction
func (t *Topic) Unlike(uid int) error {
err := Likes.Delete(t.ID, "topics")
if err != nil {
return err
e := Likes.Delete(t.ID, "topics")
if e != nil {
return e
}
_, err = topicStmts.addLikesToTopic.Exec(-1, t.ID)
if err != nil {
return err
_, e = topicStmts.addLikesToTopic.Exec(-1, t.ID)
if e != nil {
return e
}
_, err = userStmts.decLiked.Exec(1, uid)
_, e = userStmts.decLiked.Exec(1, uid)
t.cacheRemove()
return err
return e
}
func handleLikedTopicReplies(tid int) error {
rows, err := userStmts.getLikedRepliesOfTopic.Query(tid)
if err != nil {
return err
rows, e := userStmts.getLikedRepliesOfTopic.Query(tid)
if e != nil {
return e
}
defer rows.Close()
for rows.Next() {
var rid int
err := rows.Scan(&rid)
if err != nil {
return err
if e := rows.Scan(&rid); e != nil {
return e
}
_, err = replyStmts.deleteLikesForReply.Exec(rid)
if err != nil {
return err
_, e = replyStmts.deleteLikesForReply.Exec(rid)
if e != nil {
return e
}
err = Activity.DeleteByParams("like", rid, "post")
if err != nil {
return err
e = Activity.DeleteByParams("like", rid, "post")
if e != nil {
return e
}
}
return rows.Err()
}
func handleTopicAttachments(tid int) error {
err := handleAttachments(userStmts.getAttachmentsOfTopic, tid)
if err != nil {
return err
e := handleAttachments(userStmts.getAttachmentsOfTopic, tid)
if e != nil {
return e
}
return handleAttachments(userStmts.getAttachmentsOfTopic2, tid)
}
@ -375,120 +383,113 @@ func handleReplyAttachments(rid int) error {
}
func handleAttachments(stmt *sql.Stmt, id int) error {
rows, err := stmt.Query(id)
if err != nil {
return err
rows, e := stmt.Query(id)
if e != nil {
return e
}
defer rows.Close()
for rows.Next() {
var aid int
err := rows.Scan(&aid)
if err != nil {
return err
if e := rows.Scan(&aid); e != nil {
return e
}
a, err := Attachments.FGet(aid)
if err != nil {
return err
a, e := Attachments.FGet(aid)
if e != nil {
return e
}
err = deleteAttachment(a)
if err != nil && err != sql.ErrNoRows {
return err
e = deleteAttachment(a)
if e != nil && e != sql.ErrNoRows {
return e
}
}
return rows.Err()
}
// TODO: Only load a row per createdBy, maybe with group by?
func handleTopicReplies(umap map[int]struct{}, uid, tid int) error {
rows, err := userStmts.getRepliesOfTopic.Query(uid, tid)
if err != nil {
return err
rows, e := userStmts.getRepliesOfTopic.Query(uid, tid)
if e != nil {
return e
}
defer rows.Close()
var createdBy int
for rows.Next() {
err := rows.Scan(&createdBy)
if err != nil {
return err
if e := rows.Scan(&createdBy); e != nil {
return e
}
umap[createdBy] = struct{}{}
}
return rows.Err()
}
// TODO: Use a transaction here
func (t *Topic) Delete() error {
/*creator, err := Users.Get(t.CreatedBy)
if err == nil {
err = creator.DecreasePostStats(WordCount(t.Content), true)
if err != nil {
return err
/*creator, e := Users.Get(t.CreatedBy)
if e == nil {
e = creator.DecreasePostStats(WordCount(t.Content), true)
if e != nil {
return e
}
} else if err != ErrNoRows {
return err
} else if e != ErrNoRows {
return e
}*/
// TODO: Clear reply cache too
_, err := topicStmts.delete.Exec(t.ID)
_, e := topicStmts.delete.Exec(t.ID)
t.cacheRemove()
if err != nil {
return err
if e != nil {
return e
}
err = Forums.RemoveTopic(t.ParentID)
if err != nil && err != ErrNoRows {
return err
e = Forums.RemoveTopic(t.ParentID)
if e != nil && e != ErrNoRows {
return e
}
_, err = topicStmts.deleteLikesForTopic.Exec(t.ID)
if err != nil {
return err
_, e = topicStmts.deleteLikesForTopic.Exec(t.ID)
if e != nil {
return e
}
if t.PostCount > 1 {
err = handleLikedTopicReplies(t.ID)
if err != nil {
return err
if e = handleLikedTopicReplies(t.ID); e != nil {
return e
}
umap := make(map[int]struct{})
err = handleTopicReplies(umap, t.CreatedBy, t.ID)
if err != nil {
return err
e = handleTopicReplies(umap, t.CreatedBy, t.ID)
if e != nil {
return e
}
_, err = topicStmts.deleteReplies.Exec(t.ID)
if err != nil {
return err
_, e = topicStmts.deleteReplies.Exec(t.ID)
if e != nil {
return e
}
for uid := range umap {
err = (&User{ID: uid}).RecalcPostStats()
if err != nil {
//log.Printf("err: %+v\n", err)
return err
e = (&User{ID: uid}).RecalcPostStats()
if e != nil {
//log.Printf("e: %+v\n", e)
return e
}
}
}
err = (&User{ID: t.CreatedBy}).RecalcPostStats()
if err != nil {
return err
e = (&User{ID: t.CreatedBy}).RecalcPostStats()
if e != nil {
return e
}
err = handleTopicAttachments(t.ID)
if err != nil {
return err
e = handleTopicAttachments(t.ID)
if e != nil {
return e
}
err = Subscriptions.DeleteResource(t.ID, "topic")
if err != nil {
return err
e = Subscriptions.DeleteResource(t.ID, "topic")
if e != nil {
return e
}
_, err = topicStmts.deleteActivity.Exec(t.ID)
if err != nil {
return err
_, e = topicStmts.deleteActivity.Exec(t.ID)
if e != nil {
return e
}
if t.Poll > 0 {
err = (&Poll{ID: t.Poll}).Delete()
if err != nil {
return err
e = (&Poll{ID: t.Poll}).Delete()
if e != nil {
return e
}
}
return nil
@ -513,9 +514,9 @@ func (t *Topic) Update(name, content string) error {
}
func (t *Topic) SetPoll(pollID int) error {
_, err := topicStmts.setPoll.Exec(pollID, t.ID) // TODO: Sniff if this changed anything to see if we hit an existing poll
_, e := topicStmts.setPoll.Exec(pollID, t.ID) // TODO: Sniff if this changed anything to see if we hit an existing poll
t.cacheRemove()
return err
return e
}
// TODO: Have this go through the ReplyStore?
@ -543,22 +544,19 @@ func (t *Topic) CreateActionReply(action, ip string, uid int) (err error) {
return err
}
func GetRidsForTopic(tid, offset int) (rids []int, err error) {
rows, err := topicStmts.getRids.Query(tid, offset, Config.ItemsPerPage)
if err != nil {
return nil, err
func GetRidsForTopic(tid, offset int) (rids []int, e error) {
rows, e := topicStmts.getRids.Query(tid, offset, Config.ItemsPerPage)
if e != nil {
return nil, e
}
defer rows.Close()
var rid int
for rows.Next() {
err := rows.Scan(&rid)
if err != nil {
return nil, err
if e := rows.Scan(&rid); e != nil {
return nil, e
}
rids = append(rids, rid)
}
return rids, rows.Err()
}

View File

@ -27,9 +27,9 @@ type weakpassHolder struct {
func InitWeakPasswords() error {
var weakpass weakpassHolder
err := unmarshalJsonFile("./config/weakpass_default.json", &weakpass)
if err != nil {
return err
e := unmarshalJsonFile("./config/weakpass_default.json", &weakpass)
if e != nil {
return e
}
wcon := make(map[string]struct{})
@ -41,9 +41,9 @@ func InitWeakPasswords() error {
}
weakpass = weakpassHolder{}
err = unmarshalJsonFileIgnore404("./config/weakpass.json", &weakpass)
if err != nil {
return err
e = unmarshalJsonFileIgnore404("./config/weakpass.json", &weakpass)
if e != nil {
return e
}
for _, item := range weakpass.Contains {

View File

@ -3,7 +3,7 @@
/*
*
* Gosora WebSocket Subsystem
* Copyright Azareal 2017 - 2020
* Copyright Azareal 2017 - 2021
*
*/
package common
@ -149,12 +149,12 @@ func wsPageResponses(wsUser *WSUser, conn *websocket.Conn, page string) {
if wsUser.User.ID == 0 {
return
}
_, tid, err := ParseSEOURL(page)
if err != nil {
_, tid, e := ParseSEOURL(page)
if e != nil {
return
}
topic, err := Topics.Get(tid)
if err != nil {
topic, e := Topics.Get(tid)
if e != nil {
return
}
if !Forums.Exists(topic.ParentID) {
@ -169,10 +169,10 @@ func wsPageResponses(wsUser *WSUser, conn *websocket.Conn, page string) {
return
}*/
fperms, err := FPStore.Get(topic.ParentID, usercpy.Group)
if err == ErrNoRows {
fperms, e := FPStore.Get(topic.ParentID, usercpy.Group)
if e == ErrNoRows {
fperms = BlankForumPerms()
} else if err != nil {
} else if e != nil {
return
}
cascadeForumPerms(fperms, usercpy)
@ -202,9 +202,9 @@ func wsPageResponses(wsUser *WSUser, conn *websocket.Conn, page string) {
default:
return
}
err := wsUser.SetPageForSocket(conn, page)
if err != nil {
LogError(err)
e := wsUser.SetPageForSocket(conn, page)
if e != nil {
LogError(e)
}
}
@ -221,7 +221,7 @@ func wsPageResume(wsUser *WSUser, conn *websocket.Conn, page string, resume int6
/*if resume >= hub.lastTick.Unix() {
conn.Write([]byte("resume tooslow"))
} else {
conn.Write([]byte("resume success"))
conn.Write([]byte("resume success"))
}*/
default:
return
@ -248,8 +248,8 @@ func wsLeavePage(wsUser *WSUser, conn *websocket.Conn, page string) {
return
}
wsUser.FinalizePage(page, func() {
_, tid, err := ParseSEOURL(page)
if err != nil {
_, tid, e := ParseSEOURL(page)
if e != nil {
return
}
topicMutex.Lock()
@ -258,8 +258,7 @@ func wsLeavePage(wsUser *WSUser, conn *websocket.Conn, page string) {
if !ok {
return
}
_, ok = topic[wsUser]
if !ok {
if _, ok = topic[wsUser]; !ok {
return
}
delete(topic, wsUser)
@ -272,9 +271,9 @@ func wsLeavePage(wsUser *WSUser, conn *websocket.Conn, page string) {
delete(adminStatsWatchers, conn)
adminStatsMutex.Unlock()
}
err := wsUser.SetPageForSocket(conn, "")
if err != nil {
LogError(err)
e := wsUser.SetPageForSocket(conn, "")
if e != nil {
LogError(e)
}
}
@ -290,9 +289,7 @@ var adminStatsMutex sync.RWMutex
func adminStatsTicker() {
time.Sleep(time.Second)
lastUonline := -1
lastGonline := -1
lastTotonline := -1
lastUonline, lastGonline, lastTotonline := -1, -1, -1
lastCPUPerc := -1
var lastAvailableRAM int64 = -1
var noStatUpdates, noRAMUpdates bool

View File

@ -26,11 +26,11 @@ func init() {
DbInits.Add(func(acc *qgen.Accumulator) error {
w := "widgets"
widgetStmts = WidgetStmts{
//getList: acc.Select(w).Columns("wid, position, side, type, active, location, data").Orderby("position ASC").Prepare(),
getDockList: acc.Select(w).Columns("wid, position, type, active, location, data").Where("side=?").Orderby("position ASC").Prepare(),
//getList: acc.Select(w).Columns("wid,position,side,type,active,location,data").Orderby("position ASC").Prepare(),
getDockList: acc.Select(w).Columns("wid,position,type,active,location,data").Where("side=?").Orderby("position ASC").Prepare(),
//model: acc.SimpleModel(w,"position,type,active,location,data","wid"),
delete: acc.Delete(w).Where("wid=?").Prepare(),
create: acc.Insert(w).Columns("position, side, type, active, location, data").Fields("?,?,?,?,?,?").Prepare(),
create: acc.Insert(w).Columns("position,side,type,active,location,data").Fields("?,?,?,?,?,?").Prepare(),
update: acc.Update(w).Set("position=?,side=?,type=?,active=?,location=?,data=?").Where("wid=?").Prepare(),
}
return acc.FirstError()
@ -73,10 +73,10 @@ func (w *Widget) Delete() error {
return nil
}
func (w *Widget) Copy() (owidget *Widget) {
owidget = &Widget{}
*owidget = *w
return owidget
func (w *Widget) Copy() (ow *Widget) {
ow = &Widget{}
*ow = *w
return ow
}
// TODO: Test this

View File

@ -28,8 +28,8 @@ func (u *WSUser) Ping() error {
continue
}
socket.conn.SetWriteDeadline(time.Now().Add(time.Minute))
err := socket.conn.WriteMessage(websocket.PingMessage, nil)
if err != nil {
e := socket.conn.WriteMessage(websocket.PingMessage, nil)
if e != nil {
socket.conn.Close()
}
}
@ -42,9 +42,9 @@ func (u *WSUser) WriteAll(msg string) error {
if socket == nil {
continue
}
w, err := socket.conn.NextWriter(websocket.TextMessage)
if err != nil {
return err
w, e := socket.conn.NextWriter(websocket.TextMessage)
if e != nil {
return e
}
_, _ = w.Write(msgbytes)
w.Close()
@ -66,8 +66,8 @@ func (u *WSUser) WriteToPageBytes(msg []byte, page string) error {
if socket.Page != page {
continue
}
w, err := socket.conn.NextWriter(websocket.TextMessage)
if err != nil {
w, e := socket.conn.NextWriter(websocket.TextMessage)
if e != nil {
continue // Skip dead sockets, a dedicated goroutine handles those
}
_, _ = w.Write(msg)
@ -90,8 +90,8 @@ func (u *WSUser) WriteToPageBytesMulti(msgs [][]byte, page string) error {
if socket.Page != page {
continue
}
w, err := socket.conn.NextWriter(websocket.TextMessage)
if err != nil {
w, e := socket.conn.NextWriter(websocket.TextMessage)
if e != nil {
continue // Skip dead sockets, a dedicated goroutine handles those
}
for _, msg := range msgs {
@ -184,7 +184,7 @@ func (u *WSUser) InPage(page string) bool {
return false
}
func (u *WSUser) FinalizePage(page string, handle func()) {
func (u *WSUser) FinalizePage(page string, h func()) {
u.Lock()
defer u.Unlock()
for _, socket := range u.Sockets {
@ -195,5 +195,5 @@ func (u *WSUser) FinalizePage(page string, handle func()) {
return
}
}
handle()
h()
}

File diff suppressed because it is too large Load Diff

View File

@ -31,18 +31,18 @@ func Lookup(name string) (InstallAdapter, bool) {
func createAdmin() error {
fmt.Println("Creating the admin user")
hashedPassword, salt, err := BcryptGeneratePassword("password")
if err != nil {
return err
hashedPassword, salt, e := BcryptGeneratePassword("password")
if e != nil {
return e
}
// Build the admin user query
adminUserStmt, err := qgen.Builder.SimpleInsert("users", "name, password, salt, email, group, is_super_admin, active, createdAt, lastActiveAt, lastLiked, oldestItemLikedCreatedAt, message, last_ip", "'Admin',?,?,'admin@localhost',1,1,1,UTC_TIMESTAMP(),UTC_TIMESTAMP(),UTC_TIMESTAMP(),UTC_TIMESTAMP(),'',''")
if err != nil {
return err
adminUserStmt, e := qgen.Builder.SimpleInsert("users", "name, password, salt, email, group, is_super_admin, active, createdAt, lastActiveAt, lastLiked, oldestItemLikedCreatedAt, message, last_ip", "'Admin',?,?,'admin@localhost',1,1,1,UTC_TIMESTAMP(),UTC_TIMESTAMP(),UTC_TIMESTAMP(),UTC_TIMESTAMP(),'',''")
if e != nil {
return e
}
// Run the admin user query
_, err = adminUserStmt.Exec(hashedPassword, salt)
return err
_, e = adminUserStmt.Exec(hashedPassword, salt)
return e
}

View File

@ -56,7 +56,9 @@
"megapost_min_words":"Mega Post Minimum Words",
"meta_desc":"Meta Description",
"rapid_loading":"Rapid Loaded?",
"google_site_verify":"Google Site Verify"
"google_site_verify":"Google Site Verify",
"avatar_visibility":"Avatar Visibility",
"avatar_visibility_label":"Everyone,Member Only"
},
"PermPresets": {
@ -479,14 +481,14 @@
"panel_rank_guests":"Guests",
"panel_rank_members":"Members",
"panel_preset_everyone":"Everyone",
"panel_preset_announcements":"Announcements",
"panel_preset_member_only":"Member Only",
"panel_preset_staff_only":"Staff Only",
"panel_preset_admin_only":"Admin Only",
"panel_preset_archive":"Archive",
"panel_preset_custom":"Custom",
"panel_preset_public":"Public",
"panel.preset_everyone":"Everyone",
"panel.preset_announcements":"Announcements",
"panel.preset_member_only":"Member Only",
"panel.preset_staff_only":"Staff Only",
"panel.preset_admin_only":"Admin Only",
"panel.preset_archive":"Archive",
"panel.preset_custom":"Custom",
"panel.preset_public":"Public",
"panel_active_hidden":"Hidden",
"panel_perms_no_access":"No Access",
@ -693,7 +695,7 @@
"forum_start_one":"Start one?",
"forums_head":"Forums",
"forums_no_description":"No description",
"forums_no_desc":"No description",
"forums_none":"None",
"forums_no_forums":"You don't have access to any forums.",
@ -944,26 +946,40 @@
"panel.forums_create_head":"Add Forum",
"panel.forums_create_name_label":"Name",
"panel.forums_create_name":"Super Secret Forum",
"panel.forums_create_description_label":"Description",
"panel.forums_create_description":"Where all the super secret stuff happens",
"panel.forums_create_desc_label":"Description",
"panel.forums_create_desc":"Where all the super secret stuff happens",
"panel.forums_active_label":"Active",
"panel.forums_preset_label":"Preset",
"panel.forums_create_button":"Add Forum",
"panel.forums_update_order_button":"Update Order",
"panel.forums_order_updated":"The forums have been successfully updated",
"panel_forum_head_suffix":" Forum",
"panel_forum_name":"Name",
"panel_forum_name_placeholder":"General Forum",
"panel_forum_description":"Description",
"panel_forum_description_placeholder":"Where the general stuff happens",
"panel_forum_active":"Active",
"panel_forum_preset":"Preset",
"panel_forum_update_button":"Update Forum",
"panel_forum_permissions_head":"Forum Permissions",
"panel_forum_edit_button":"Edit",
"panel_forum_short_update_button":"Update",
"panel_forum_full_edit_button":"Full Edit",
"panel.forum_head_suffix":" Forum",
"panel.forum_name":"Name",
"panel.forum_name_placeholder":"General Forum",
"panel.forum_desc":"Description",
"panel.forum_desc_placeholder":"Where the general stuff happens",
"panel.forum_active":"Active",
"panel.forum_preset":"Preset",
"panel.forum_update_button":"Update Forum",
"panel.forum_permissions_head":"Forum Permissions",
"panel.forum_edit_button":"Edit",
"panel.forum_short_update_button":"Update",
"panel.forum_full_edit_button":"Full Edit",
"panel.forum_actions_head":"Actions",
"panel.forum_actions_create_head":"Create Action",
"panel.forum_action_run_on_topic_creation":"Run on Topic Creation",
"panel.forum_action_run_days_after_topic_creation":"Run Days After Topic Creation",
"panel.forum_action_run_days_after_topic_last_reply":"Run Days After Topic Last Reply",
"panel.forum_action_action":"Action",
"panel.forum_action_action_delete":"Delete",
"panel.forum_action_action_lock":"Lock",
"panel.forum_action_action_unlock":"Unlock",
"panel.forum_action_action_move":"Move",
"panel.forum_action_extra":"Extra",
"panel.forum_action_create_button":"Create Action",
"panel_forum_delete_are_you_sure":"Are you sure you want to delete the '%s' forum?",
"panel_groups_head":"Groups",
@ -1244,6 +1260,7 @@
"panel_debug_goroutine_count_label":"Goroutines",
"panel_debug_cpu_count_label":"CPUs",
"panel_debug_http_conns_label":"HTTP Conns",
"panel_debug_tasks":"Tasks",
"panel_debug_tasks_half_second":"Half Second",

14
main.go
View File

@ -143,10 +143,10 @@ func storeInit() (e error) {
if e != nil {
return ws(e)
}
/*c.ForumActionStore, e = c.NewDefaultForumActionStore(acc)
c.ForumActionStore, e = c.NewDefaultForumActionStore(acc)
if e != nil {
return ws(e)
}*/
}
c.Convos, e = c.NewDefaultConversationStore(acc)
if e != nil {
return ws(e)
@ -549,8 +549,7 @@ func main() {
// Resource Management Goroutine
go func() {
uc := c.Users.GetCache()
tc := c.Topics.GetCache()
uc, tc := c.Users.GetCache(), c.Topics.GetCache()
if uc == nil && tc == nil {
return
}
@ -630,7 +629,7 @@ func main() {
func startServer() {
// We might not need the timeouts, if we're behind a reverse-proxy like Nginx
newServer := func(addr string, handler http.Handler) *http.Server {
newServer := func(addr string, h http.Handler) *http.Server {
rtime := c.Config.ReadTimeout
if rtime == 0 {
rtime = 8
@ -650,8 +649,9 @@ func startServer() {
itime = 0
}
return &http.Server{
Addr: addr,
Handler: handler,
Addr: addr,
Handler: h,
ConnState: c.ConnWatch.StateChange,
ReadTimeout: time.Duration(rtime) * time.Second,
WriteTimeout: time.Duration(wtime) * time.Second,

View File

@ -1128,8 +1128,8 @@ func TestReplyStore(t *testing.T) {
func testReplyStore(t *testing.T, newID int, ip string) {
ex, exf := exp(t), expf(t)
replyTest2 := func(r *c.Reply, err error, rid, parentID, createdBy int, content, ip string) {
expectNilErr(t, err)
replyTest2 := func(r *c.Reply, e error, rid, parentID, createdBy int, content, ip string) {
expectNilErr(t, e)
exf(r.ID == rid, "RID #%d has the wrong ID. It should be %d not %d", rid, rid, r.ID)
exf(r.ParentID == parentID, "The parent topic of RID #%d should be %d not %d", rid, parentID, r.ParentID)
exf(r.CreatedBy == createdBy, "The creator of RID #%d should be %d not %d", rid, createdBy, r.CreatedBy)
@ -1138,12 +1138,12 @@ func testReplyStore(t *testing.T, newID int, ip string) {
}
replyTest := func(rid, parentID, createdBy int, content, ip string) {
r, err := c.Rstore.Get(rid)
replyTest2(r, err, rid, parentID, createdBy, content, ip)
r, err = c.Rstore.GetCache().Get(rid)
replyTest2(r, err, rid, parentID, createdBy, content, ip)
r, e := c.Rstore.Get(rid)
replyTest2(r, e, rid, parentID, createdBy, content, ip)
r, e = c.Rstore.GetCache().Get(rid)
replyTest2(r, e, rid, parentID, createdBy, content, ip)
}
replyTest(1, 1, 1, "A reply!", "::1")
replyTest(1, 1, 1, "A reply!", "")
// ! This is hard to do deterministically as the system may pre-load certain items but let's give it a try:
//_, err = c.Rstore.GetCache().Get(1)
@ -2279,6 +2279,108 @@ func TestWidgets(t *testing.T) {
exf(len(widgets) == 0, "RightSidebar should have 0 items, not %d", len(widgets))
}
/*type ForumActionStoreInt interface {
Get(faid int) (*ForumAction, error)
GetInForum(fid int) ([]*ForumAction, error)
GetAll() ([]*ForumAction, error)
GetNewTopicActions(fid int) ([]*ForumAction, error)
Add(fa *ForumAction) (int, error)
Delete(faid int) error
Exists(faid int) bool
Count() int
CountInForum(fid int) int
DailyTick() error
}*/
func TestForumActions(t *testing.T) {
ex, exf, s := exp(t), expf(t), c.ForumActionStore
count := s.CountInForum(-1)
exf(count == 0, "count should be %d not %d", 0, count)
count = s.CountInForum(0)
exf(count == 0, "count in 0 should be %d not %d", 0, count)
ex(!s.Exists(-1), "faid -1 should not exist")
ex(!s.Exists(0), "faid 0 should not exist")
_, e := s.Get(-1)
recordMustNotExist(t, e, "faid -1 should not exist")
_, e = s.Get(0)
recordMustNotExist(t, e, "faid 0 should not exist")
noActions := func(fid, faid int) {
/*sfid, */ sfaid := /*strconv.Itoa(fid), */ strconv.Itoa(faid)
count := s.Count()
exf(count == 0, "count should be %d not %d", 0, count)
count = s.CountInForum(fid)
exf(count == 0, "count in %d should be %d not %d", fid, 0, count)
exf(!s.Exists(faid), "faid %d should not exist", faid)
_, e = s.Get(faid)
recordMustNotExist(t, e, "faid "+sfaid+" should not exist")
fas, e := s.GetInForum(fid)
//recordMustNotExist(t, e, "fid "+sfid+" should not have any actions")
expectNilErr(t, e) // TODO: Why does this not return ErrNoRows?
exf(len(fas) == 0, "len(fas) should be %d not %d", 0, len(fas))
fas, e = s.GetAll()
//recordMustNotExist(t, e, "there should not be any actions")
expectNilErr(t, e) // TODO: Why does this not return ErrNoRows?
exf(len(fas) == 0, "len(fas) should be %d not %d", 0, len(fas))
fas, e = s.GetNewTopicActions(fid)
//recordMustNotExist(t, e, "fid "+sfid+" should not have any new topic actions")
expectNilErr(t, e) // TODO: Why does this not return ErrNoRows?
exf(len(fas) == 0, "len(fas) should be %d not %d", 0, len(fas))
}
noActions(1, 1)
fid, e := c.Forums.Create("Forum Action Test", "Forum Action Test", true, "")
expectNilErr(t, e)
noActions(fid, 1)
faid, e := c.ForumActionStore.Add(&c.ForumAction{
Forum: fid,
RunOnTopicCreation: false,
RunDaysAfterTopicCreation: 1,
RunDaysAfterTopicLastReply: 0,
Action: c.ForumActionLock,
Extra: "",
})
expectNilErr(t, e)
exf(faid == 1, "faid should be %d not %d", 1, faid)
count = s.Count()
exf(count == 1, "count should be %d not %d", 1, count)
count = s.CountInForum(fid)
exf(count == 1, "count in %d should be %d not %d", fid, 1, count)
exf(s.Exists(faid), "faid %d should exist", faid)
fa, e := s.Get(faid)
expectNilErr(t, e)
exf(fa.ID == faid, "fa.ID should be %d not %d", faid, fa.ID)
exf(fa.Forum == fid, "fa.Forum should be %d not %d", fid, fa.Forum)
exf(fa.RunOnTopicCreation == false, "fa.RunOnTopicCreation should be false")
exf(fa.RunDaysAfterTopicCreation == 1, "fa.RunDaysAfterTopicCreation should be %d not %d", 1, fa.RunDaysAfterTopicCreation)
exf(fa.RunDaysAfterTopicLastReply == 0, "fa.RunDaysAfterTopicLastReply should be %d not %d", 0, fa.RunDaysAfterTopicLastReply)
exf(fa.Action == c.ForumActionLock, "fa.Action should be %d not %d", c.ForumActionLock, fa.Action)
exf(fa.Extra == "", "fa.Extra should be '%s' not '%s'", "", fa.Extra)
tid, e := c.Topics.Create(fid, "Forum Action Topic", "Forum Action Topic", 1, "")
expectNilErr(t, e)
topic, e := c.Topics.Get(tid)
expectNilErr(t, e)
dayAgo := time.Now().AddDate(0, 0, -5)
expectNilErr(t, topic.TestSetCreatedAt(dayAgo))
expectNilErr(t, fa.Run())
topic, e = c.Topics.Get(tid)
expectNilErr(t, e)
ex(topic.IsClosed, "topic.IsClosed should be true")
/*_, e = c.Rstore.Create(topic, "Forum Action Reply", "", 1)
expectNilErr(t, e)*/
_ = tid
expectNilErr(t, s.Delete(faid))
noActions(fid, faid)
}
func TestTopicList(t *testing.T) {
ex, exf := exp(t), expf(t)
fid, err := c.Forums.Create("Test Forum", "Desc for test forum", true, "")

View File

@ -53,6 +53,7 @@ func init() {
addPatch(33, patch33)
addPatch(34, patch34)
addPatch(35, patch35)
addPatch(36, patch36)
}
func bcol(col string, val bool) qgen.DBTableColumn {
@ -168,18 +169,17 @@ func patch0(scanner *bufio.Scanner) (err error) {
}
func patch1(scanner *bufio.Scanner) error {
routes := map[string]string{
return renameRoutes(map[string]string{
"routeAccountEditCriticalSubmit": "routes.AccountEditCriticalSubmit",
"routeAccountEditAvatar": "routes.AccountEditAvatar",
"routeAccountEditAvatarSubmit": "routes.AccountEditAvatarSubmit",
"routeAccountEditUsername": "routes.AccountEditUsername",
"routeAccountEditUsernameSubmit": "routes.AccountEditUsernameSubmit",
}
return renameRoutes(routes)
})
}
func patch2(scanner *bufio.Scanner) error {
routes := map[string]string{
return renameRoutes(map[string]string{
"routeLogout": "routes.AccountLogout",
"routeShowAttachment": "routes.ShowAttachment",
"routeChangeTheme": "routes.ChangeTheme",
@ -189,8 +189,7 @@ func patch2(scanner *bufio.Scanner) error {
"routeDynamic": "routes.DynamicRoute",
"routeUploads": "routes.UploadedFile",
"BadRoute": "routes.BadRoute",
}
return renameRoutes(routes)
})
}
func patch3(scanner *bufio.Scanner) error {
@ -248,14 +247,14 @@ func patch4(scanner *bufio.Scanner) error {
"routePanelForumsEditPermsAdvanceSubmit": "panel.ForumsEditPermsAdvanceSubmit",
"routePanelBackups": "panel.Backups",
}
err := renameRoutes(routes)
if err != nil {
return err
e := renameRoutes(routes)
if e != nil {
return e
}
err = execStmt(qgen.Builder.SimpleDelete("settings", "name='url_tags'"))
if err != nil {
return err
e = execStmt(qgen.Builder.SimpleDelete("settings", "name='url_tags'"))
if e != nil {
return e
}
return createTable("pages", "utf8mb4", "utf8mb4_general_ci",
@ -281,14 +280,14 @@ func patch5(scanner *bufio.Scanner) error {
"routes.AccountEditCritical": "routes.AccountEditPassword",
"routes.AccountEditCriticalSubmit": "routes.AccountEditPasswordSubmit",
}
err := renameRoutes(routes)
if err != nil {
return err
e := renameRoutes(routes)
if e != nil {
return e
}
err = execStmt(qgen.Builder.SimpleUpdate("menu_items", "path='/user/edit/'", "path='/user/edit/critical/'"))
if err != nil {
return err
e = execStmt(qgen.Builder.SimpleUpdate("menu_items", "path='/user/edit/'", "path='/user/edit/critical/'"))
if e != nil {
return e
}
return createTable("users_2fa_keys", "utf8mb4", "utf8mb4_general_ci",
@ -333,9 +332,9 @@ func renameRoutes(routes map[string]string) error {
}
for key, value := range routes {
err := replaceTextWhere(key, value)
if err != nil {
return err
e := replaceTextWhere(key, value)
if e != nil {
return e
}
}
@ -370,9 +369,9 @@ func patch8(scanner *bufio.Scanner) error {
"routePanelThemesMenuItemOrderSubmit": "panel.ThemesMenuItemOrderSubmit",
"routePanelDashboard": "panel.Dashboard",
}
err := renameRoutes(routes)
if err != nil {
return err
e := renameRoutes(routes)
if e != nil {
return e
}
return createTable("updates", "", "",
@ -812,36 +811,36 @@ func patch29(scanner *bufio.Scanner) error {
}
func patch30(scanner *bufio.Scanner) error {
err := execStmt(qgen.Builder.AddColumn("users_groups_promotions", tC{"registeredFor", "int", 0, false, false, "0"}, nil))
if err != nil {
return err
e := execStmt(qgen.Builder.AddColumn("users_groups_promotions", tC{"registeredFor", "int", 0, false, false, "0"}, nil))
if e != nil {
return e
}
return execStmt(qgen.Builder.SetDefaultColumn("users", "last_ip", "varchar", ""))
}
func patch31(scanner *bufio.Scanner) (e error) {
addKey := func(tbl, col string, tk qgen.DBTableKey) error {
/*err := execStmt(qgen.Builder.RemoveIndex(tbl, col))
if err != nil {
return err
/*e := execStmt(qgen.Builder.RemoveIndex(tbl, col))
if e != nil {
return e
}*/
return execStmt(qgen.Builder.AddKey(tbl, col, tk))
}
err := addKey("topics", "title", tK{"title", "fulltext", "", false})
if err != nil {
return err
e = addKey("topics", "title", tK{"title", "fulltext", "", false})
if e != nil {
return e
}
err = addKey("topics", "content", tK{"content", "fulltext", "", false})
if err != nil {
return err
e = addKey("topics", "content", tK{"content", "fulltext", "", false})
if e != nil {
return e
}
return addKey("replies", "content", tK{"content", "fulltext", "", false})
}
func createTable(tbl, charset, collation string, cols []tC, keys []tK) error {
err := execStmt(qgen.Builder.DropTable(tbl))
if err != nil {
return err
e := execStmt(qgen.Builder.DropTable(tbl))
if e != nil {
return e
}
return execStmt(qgen.Builder.CreateTable(tbl, charset, collation, cols, keys))
}
@ -934,9 +933,31 @@ func patch34(scanner *bufio.Scanner) error {
}
func patch35(scanner *bufio.Scanner) error {
err := execStmt(qgen.Builder.AddColumn("topics", tC{"weekEvenViews", "int", 0, false, false, "0"}, nil))
if err != nil {
return err
e := execStmt(qgen.Builder.AddColumn("topics", tC{"weekEvenViews", "int", 0, false, false, "0"}, nil))
if e != nil {
return e
}
return execStmt(qgen.Builder.AddColumn("topics", tC{"weekOddViews", "int", 0, false, false, "0"}, nil))
}
func patch36(scanner *bufio.Scanner) error {
e := createTable("forums_actions", "utf8mb4", "utf8mb4_general_ci",
[]tC{
{"faid", "int", 0, false, true, ""},
{"fid", "int", 0, false, false, ""},
bcol("runOnTopicCreation", false),
{"runDaysAfterTopicCreation", "int", 0, false, false, "0"},
{"runDaysAfterTopicLastReply", "int", 0, false, false, "0"},
ccol("action", 50, ""),
ccol("extra", 200, "''"),
},
[]tK{
{"faid", "primary", "", false},
},
)
if e != nil {
return e
}
//qgen.Install.SimpleInsert("settings", "name, content, type, constraints", "'activation_type','1','list','1-3'")
return execStmt(qgen.Builder.SimpleInsert("settings", "name, content, type", "'avatar_visibility','0','list','0-1'"))
}

View File

@ -32,6 +32,11 @@ func (b *accDeleteBuilder) DateOlderThan(col string, quantity int, unit string)
return b
}
func (b *accDeleteBuilder) DateOlderThanQ(col, unit string) *accDeleteBuilder {
b.dateCutoff = &dateCutoff{col, 0, unit, 11}
return b
}
/*func (b *accDeleteBuilder) Prepare() *sql.Stmt {
return b.build.SimpleDelete(b.table, b.where)
}*/
@ -87,6 +92,11 @@ func (b *accUpdateBuilder) DateOlderThan(col string, quantity int, unit string)
return b
}
func (b *accUpdateBuilder) DateOlderThanQ(col, unit string) *accUpdateBuilder {
b.up.dateCutoff = &dateCutoff{col, 0, unit, 11}
return b
}
func (b *accUpdateBuilder) WhereQ(sel *selectPrebuilder) *accUpdateBuilder {
b.up.whereSubQuery = sel
return b
@ -98,14 +108,24 @@ func (b *accUpdateBuilder) Prepare() *sql.Stmt {
}
return b.build.prepare(b.build.adapter.SimpleUpdate(b.up))
}
func (b *accUpdateBuilder) Stmt() *sql.Stmt {
if b.up.whereSubQuery != nil {
return b.build.prepare(b.build.adapter.SimpleUpdateSelect(b.up))
}
return b.build.prepare(b.build.adapter.SimpleUpdate(b.up))
}
func (b *accUpdateBuilder) Exec(args ...interface{}) (res sql.Result, err error) {
query, err := b.build.adapter.SimpleUpdate(b.up)
q, err := b.build.adapter.SimpleUpdate(b.up)
if err != nil {
return res, err
}
//fmt.Println("query:", query)
return b.build.exec(query, args...)
//fmt.Println("q:", q)
return b.build.exec(q, args...)
}
type AccBuilder interface {
Prepare() *sql.Stmt
}
type AccSelectBuilder struct {
@ -147,8 +167,8 @@ func (b *AccSelectBuilder) In(col string, inList []int) *AccSelectBuilder {
// TODO: Optimise this
where := col + " IN("
for _, item := range inList {
where += strconv.Itoa(item) + ","
for _, it := range inList {
where += strconv.Itoa(it) + ","
}
where = where[:len(where)-1] + ")"
if b.where != "" {
@ -217,6 +237,15 @@ func (b *AccSelectBuilder) Prepare() *sql.Stmt {
return b.build.SimpleSelect(b.table, b.columns, b.where, b.orderby, b.limit)
}
func (b *AccSelectBuilder) Stmt() *sql.Stmt {
// TODO: Phase out the procedural API and use the adapter's OO API? The OO API might need a bit more work before we do that and it needs to be rolled out to MSSQL.
if b.dateCutoff != nil || b.inChain != nil {
selectBuilder := b.build.GetAdapter().Builder().Select().FromAcc(b)
return b.build.prepare(b.build.GetAdapter().ComplexSelect(selectBuilder))
}
return b.build.SimpleSelect(b.table, b.columns, b.where, b.orderby, b.limit)
}
func (b *AccSelectBuilder) ComplexPrepare() *sql.Stmt {
selectBuilder := b.build.GetAdapter().Builder().Select().FromAcc(b)
return b.build.prepare(b.build.GetAdapter().ComplexSelect(selectBuilder))

View File

@ -199,6 +199,8 @@ func panelRoutes() *RouteGroup {
Action("panel.ForumsEditPermsSubmit", "/panel/forums/edit/perms/submit/", "extraData"),
View("panel.ForumsEditPermsAdvance", "/panel/forums/edit/perms/", "extraData"),
Action("panel.ForumsEditPermsAdvanceSubmit", "/panel/forums/edit/perms/adv/submit/", "extraData"),
Action("panel.ForumsEditActionCreateSubmit", "/panel/forums/action/create/submit/", "extraData"),
Action("panel.ForumsEditActionDeleteSubmit", "/panel/forums/action/delete/submit/", "extraData"),
View("panel.Settings", "/panel/settings/"),
View("panel.SettingEdit", "/panel/settings/edit/", "extraData"),

View File

@ -18,10 +18,10 @@ func Debug(w http.ResponseWriter, r *http.Request, user *c.User) c.RouteError {
goVersion := runtime.Version()
dbVersion := qgen.Builder.DbVersion()
upDuration := time.Since(c.StartTime)
hours := int(upDuration.Hours())
mins := int(upDuration.Minutes())
secs := int(upDuration.Seconds())
upDur := time.Since(c.StartTime)
hours := int(upDur.Hours())
mins := int(upDur.Minutes())
secs := int(upDur.Seconds())
var uptime string
if hours > 24 {
days := hours / 24
@ -44,6 +44,7 @@ func Debug(w http.ResponseWriter, r *http.Request, user *c.User) c.RouteError {
// TODO: Fetch the adapter from Builder rather than getting it from a global?
goroutines := runtime.NumGoroutine()
cpus := runtime.NumCPU()
httpConns := c.ConnWatch.Count()
debugTasks := c.DebugPageTasks{c.ScheduledHalfSecondTaskCount(), c.ScheduledSecondTaskCount(), c.ScheduledFifteenMinuteTaskCount(), c.ScheduledHourTaskCount(), c.ShutdownTaskCount()}
var memStats runtime.MemStats
@ -51,20 +52,17 @@ func Debug(w http.ResponseWriter, r *http.Request, user *c.User) c.RouteError {
var tlen, ulen, rlen int
var tcap, ucap, rcap int
tcache := c.Topics.GetCache()
if tcache != nil {
tlen = tcache.Length()
tcap = tcache.GetCapacity()
tc := c.Topics.GetCache()
if tc != nil {
tlen, tcap = tc.Length(), tc.GetCapacity()
}
ucache := c.Users.GetCache()
if ucache != nil {
ulen = ucache.Length()
ucap = ucache.GetCapacity()
uc := c.Users.GetCache()
if uc != nil {
ulen, ucap = uc.Length(), uc.GetCapacity()
}
rcache := c.Rstore.GetCache()
if rcache != nil {
rlen = rcache.Length()
rcap = rcache.GetCapacity()
rc := c.Rstore.GetCache()
if rc != nil {
rlen, rcap = rc.Length(), rc.GetCapacity()
}
topicListThawed := c.TopicListThaw.Thawed()
@ -121,12 +119,13 @@ func Debug(w http.ResponseWriter, r *http.Request, user *c.User) c.RouteError {
if fErr != nil {
return c.InternalError(fErr, w, r)
}
// TODO: How can we measure this without freezing up the entire page?
//gitSize, _ := c.DirSize("./.git")
gitSize := 0
debugDisk := c.DebugPageDisk{staticSize, attachSize, uploadsSize, logsSize, backupsSize, gitSize}
pi := c.PanelDebugPage{basePage, goVersion, dbVersion, uptime, openConnCount, qgen.Builder.GetAdapter().GetName(), goroutines, cpus, debugTasks, memStats, debugCache, debugDatabase, debugDisk}
pi := c.PanelDebugPage{basePage, goVersion, dbVersion, uptime, openConnCount, qgen.Builder.GetAdapter().GetName(), goroutines, cpus, httpConns, debugTasks, memStats, debugCache, debugDatabase, debugDisk}
return renderTemplate("panel", w, r, basePage.Header, c.Panel{basePage, "panel_dashboard_right", "debug_page", "panel_debug", pi})
}

View File

@ -144,6 +144,7 @@ func ForumsOrderSubmit(w http.ResponseWriter, r *http.Request, u *c.User) c.Rout
if ferr != nil {
return ferr
}
// TODO: Move this even earlier?
js := r.PostFormValue("js") == "1"
if !u.Perms.ManageForums {
return c.NoPermissionsJSQ(w, r, u, js)
@ -187,14 +188,14 @@ func ForumsEdit(w http.ResponseWriter, r *http.Request, u *c.User, sfid string)
}
basePage.Header.AddScriptAsync("panel_forum_edit.js")
forum, err := c.Forums.Get(fid)
f, err := c.Forums.Get(fid)
if err == sql.ErrNoRows {
return c.LocalError("The forum you're trying to edit doesn't exist.", w, r, u)
} else if err != nil {
return c.InternalError(err, w, r)
}
if forum.Preset == "" {
forum.Preset = "custom"
if f.Preset == "" {
f.Preset = "custom"
}
glist, err := c.Groups.GetAll()
@ -221,7 +222,16 @@ func ForumsEdit(w http.ResponseWriter, r *http.Request, u *c.User, sfid string)
basePage.AddNotice("panel_forum_updated")
}
pi := c.PanelEditForumPage{basePage, forum.ID, forum.Name, forum.Desc, forum.Active, forum.Preset, gplist}
falist, e := c.ForumActionStore.GetInForum(f.ID)
if err != sql.ErrNoRows && e != nil {
return c.InternalError(e, w, r)
}
afalist := make([]*c.ForumActionAction, len(falist))
for i, faitem := range falist {
afalist[i] = &c.ForumActionAction{faitem, c.ConvActToString(faitem.Action)}
}
pi := c.PanelEditForumPage{basePage, f.ID, f.Name, f.Desc, f.Active, f.Preset, gplist, afalist}
return renderTemplate("panel", w, r, basePage.Header, c.Panel{basePage, "", "", "panel_forum_edit", &pi})
}
@ -290,7 +300,7 @@ func ForumsEditPermsSubmit(w http.ResponseWriter, r *http.Request, u *c.User, sf
return c.LocalErrorJSQ("Invalid Group ID", w, r, u, js)
}
forum, err := c.Forums.Get(fid)
f, err := c.Forums.Get(fid)
if err == sql.ErrNoRows {
return c.LocalErrorJSQ("This forum doesn't exist", w, r, u, js)
} else if err != nil {
@ -298,7 +308,7 @@ func ForumsEditPermsSubmit(w http.ResponseWriter, r *http.Request, u *c.User, sf
}
permPreset := c.StripInvalidGroupForumPreset(r.PostFormValue("perm_preset"))
err = forum.SetPreset(permPreset, gid)
err = f.SetPreset(permPreset, gid)
if err != nil {
return c.LocalErrorJSQ(err.Error(), w, r, u, js)
}
@ -311,23 +321,20 @@ func ForumsEditPermsSubmit(w http.ResponseWriter, r *http.Request, u *c.User, sf
}
// A helper function for the Advanced portion of the Forum Perms Editor
func forumPermsExtractDash(paramList string) (fid, gid int, err error) {
func forumPermsExtractDash(paramList string) (fid, gid int, e error) {
params := strings.Split(paramList, "-")
if len(params) != 2 {
return fid, gid, errors.New("Parameter count mismatch")
}
fid, err = strconv.Atoi(params[0])
if err != nil {
fid, e = strconv.Atoi(params[0])
if e != nil {
return fid, gid, errors.New("The provided Forum ID is not a valid number.")
}
gid, err = strconv.Atoi(params[1])
if err != nil {
err = errors.New("The provided Group ID is not a valid number.")
gid, e = strconv.Atoi(params[1])
if e != nil {
e = errors.New("The provided Group ID is not a valid number.")
}
return fid, gid, err
return fid, gid, e
}
func ForumsEditPermsAdvance(w http.ResponseWriter, r *http.Request, u *c.User, paramList string) c.RouteError {
@ -403,7 +410,7 @@ func ForumsEditPermsAdvanceSubmit(w http.ResponseWriter, r *http.Request, u *c.U
return c.LocalError(err.Error(), w, r, u)
}
forum, err := c.Forums.Get(fid)
f, err := c.Forums.Get(fid)
if err == sql.ErrNoRows {
return c.LocalError("The forum you're trying to edit doesn't exist.", w, r, u)
} else if err != nil {
@ -417,25 +424,24 @@ func ForumsEditPermsAdvanceSubmit(w http.ResponseWriter, r *http.Request, u *c.U
return c.InternalError(err, w, r)
}
extractPerm := func(name string) bool {
ep := func(name string) bool {
pvalue := r.PostFormValue("perm-" + name)
return (pvalue == "1")
}
// TODO: Generate this code?
fp.ViewTopic = extractPerm("ViewTopic")
fp.LikeItem = extractPerm("LikeItem")
fp.CreateTopic = extractPerm("CreateTopic")
fp.EditTopic = extractPerm("EditTopic")
fp.DeleteTopic = extractPerm("DeleteTopic")
fp.CreateReply = extractPerm("CreateReply")
fp.EditReply = extractPerm("EditReply")
fp.DeleteReply = extractPerm("DeleteReply")
fp.PinTopic = extractPerm("PinTopic")
fp.CloseTopic = extractPerm("CloseTopic")
fp.MoveTopic = extractPerm("MoveTopic")
fp.ViewTopic = ep("ViewTopic")
fp.LikeItem = ep("LikeItem")
fp.CreateTopic = ep("CreateTopic")
fp.EditTopic = ep("EditTopic")
fp.DeleteTopic = ep("DeleteTopic")
fp.CreateReply = ep("CreateReply")
fp.EditReply = ep("EditReply")
fp.DeleteReply = ep("DeleteReply")
fp.PinTopic = ep("PinTopic")
fp.CloseTopic = ep("CloseTopic")
fp.MoveTopic = ep("MoveTopic")
err = forum.SetPerms(&fp, "custom", gid)
err = f.SetPerms(&fp, "custom", gid)
if err != nil {
return c.LocalErrorJSQ(err.Error(), w, r, u, js)
}
@ -446,3 +452,107 @@ func ForumsEditPermsAdvanceSubmit(w http.ResponseWriter, r *http.Request, u *c.U
return successRedirect("/panel/forums/edit/perms/"+strconv.Itoa(fid)+"-"+strconv.Itoa(gid)+"?updated=1", w, r, js)
}
func ForumsEditActionDeleteSubmit(w http.ResponseWriter, r *http.Request, u *c.User, sfaid string) c.RouteError {
_, ferr := c.SimplePanelUserCheck(w, r, u)
if ferr != nil {
return ferr
}
// TODO: Should we split this permission?
if !u.Perms.ManageForums {
return c.NoPermissions(w, r, u)
}
js := r.PostFormValue("js") == "1"
faid, e := strconv.Atoi(sfaid)
if e != nil {
return c.LocalError("The forum action ID is not a valid integer.", w, r, u)
}
e = c.ForumActionStore.Delete(faid)
if e != nil {
return c.InternalError(e, w, r)
}
fid, e := strconv.Atoi(r.FormValue("ret"))
if e != nil {
return c.LocalError("The forum action ID is not a valid integer.", w, r, u)
}
if !c.Forums.Exists(fid) {
return c.LocalError("The target forum doesn't exist.", w, r, u)
}
return successRedirect("/panel/forums/edit/"+strconv.Itoa(fid)+"?updated=1", w, r, js)
}
func ForumsEditActionCreateSubmit(w http.ResponseWriter, r *http.Request, u *c.User, sfid string) c.RouteError {
_, ferr := c.SimplePanelUserCheck(w, r, u)
if ferr != nil {
return ferr
}
// TODO: Should we split this permission?
if !u.Perms.ManageForums {
return c.NoPermissions(w, r, u)
}
js := r.PostFormValue("js") == "1"
fid, e := strconv.Atoi(sfid)
if e != nil {
return c.LocalError("The provided Forum ID is not a valid number.", w, r, u)
}
if !c.Forums.Exists(fid) {
return c.LocalError("This forum does not exist", w, r, u)
}
runOnTopicCreation := r.PostFormValue("action_run_on_topic_creation") == "1"
f := func(s string) (int, c.RouteError) {
i, e := strconv.Atoi(r.PostFormValue(s))
if e != nil {
return i, c.LocalError(s+" is not a valid integer.", w, r, u)
}
if i < 0 {
return i, c.LocalError(s+" cannot be less than 0", w, r, u)
}
return i, nil
}
runDaysAfterTopicCreation, re := f("action_run_days_after_topic_creation")
if re != nil {
return re
}
runDaysAfterTopicLastReply, re := f("action_run_days_after_topic_last_reply")
if re != nil {
return re
}
action := r.PostFormValue("action_action")
aint := c.ConvStringToAct(action)
if aint == -1 {
return c.LocalError("invalid action", w, r, u)
}
extra := r.PostFormValue("action_extra")
switch aint {
case c.ForumActionMove:
conv, e := strconv.Atoi(extra)
if e != nil {
return c.LocalError("action_extra is not a valid integer.", w, r, u)
}
extra = strconv.Itoa(conv)
default:
extra = ""
}
_, e = c.ForumActionStore.Add(&c.ForumAction{
Forum: fid,
RunOnTopicCreation: runOnTopicCreation,
RunDaysAfterTopicCreation: runDaysAfterTopicCreation,
RunDaysAfterTopicLastReply: runDaysAfterTopicLastReply,
Action: aint,
Extra: extra,
})
if e != nil {
return c.InternalError(e, w, r)
}
return successRedirect("/panel/forums/edit/"+strconv.Itoa(fid)+"?updated=1", w, r, js)
}

View File

@ -20,9 +20,9 @@ func WordFilters(w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError
}
// TODO: What if this list gets too long?
filters, err := c.WordFilters.GetAll()
if err != nil {
return c.InternalError(err, w, r)
filters, e := c.WordFilters.GetAll()
if e != nil {
return c.InternalError(e, w, r)
}
pi := c.PanelPage{basePage, tList, filters}
@ -48,13 +48,13 @@ func WordFiltersCreateSubmit(w http.ResponseWriter, r *http.Request, u *c.User)
// Unlike with find, it's okay if we leave this blank, as this means that the admin wants to remove the word entirely with no replacement
replace := strings.TrimSpace(r.PostFormValue("replace"))
wfid, err := c.WordFilters.Create(find, replace)
if err != nil {
return c.InternalErrorJSQ(err, w, r, js)
wfid, e := c.WordFilters.Create(find, replace)
if e != nil {
return c.InternalErrorJSQ(e, w, r, js)
}
err = c.AdminLogs.Create("create", wfid, "word_filter", u.GetIP(), u.ID)
if err != nil {
return c.InternalError(err, w, r)
e = c.AdminLogs.Create("create", wfid, "word_filter", u.GetIP(), u.ID)
if e != nil {
return c.InternalError(e, w, r)
}
return successRedirect("/panel/settings/word-filters/", w, r, js)

View File

@ -13,8 +13,8 @@ import (
func wsTopicList(topicList []*c.TopicsRow, lastPage int) *c.WsTopicList {
wsTopicList := make([]*c.WsTopicsRow, len(topicList))
for i, topicRow := range topicList {
wsTopicList[i] = topicRow.WebSockets()
for i, tr := range topicList {
wsTopicList[i] = tr.WebSockets()
}
return &c.WsTopicList{wsTopicList, lastPage, 0}
}

View File

@ -5,6 +5,7 @@ INSERT INTO [settings] ([name],[content],[type]) VALUES ('megapost_min_words','1
INSERT INTO [settings] ([name],[content],[type]) VALUES ('meta_desc','','html-attribute');
INSERT INTO [settings] ([name],[content],[type]) VALUES ('rapid_loading','1','bool');
INSERT INTO [settings] ([name],[content],[type]) VALUES ('google_site_verify','','html-attribute');
INSERT INTO [settings] ([name],[content],[type],[constraints]) VALUES ('avatar_visibility','0','list','0-1');
INSERT INTO [themes] ([uname],[default]) VALUES ('cosora',1);
INSERT INTO [emails] ([email],[uid],[validated]) VALUES ('admin@localhost',1,1);
INSERT INTO [users_groups] ([name],[permissions],[plugin_perms],[is_mod],[is_admin],[is_banned],[tag]) VALUES ('Administrator','{"BanUsers":true,"ActivateUsers":true,"EditUser":true,"EditUserEmail":true,"EditUserPassword":true,"EditUserGroup":true,"EditUserGroupSuperMod":true,"EditGroup":true,"EditGroupLocalPerms":true,"EditGroupGlobalPerms":true,"EditGroupSuperMod":true,"ManageForums":true,"EditSettings":true,"ManageThemes":true,"ManagePlugins":true,"ViewAdminLogs":true,"ViewIPs":true,"UploadFiles":true,"UploadAvatars":true,"UseConvos":true,"UseConvosOnlyWithMod":true,"CreateProfileReply":true,"AutoEmbed":true,"AutoLink":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true,"MoveTopic":true}','{}',1,1,0,'Admin');
@ -27,8 +28,8 @@ INSERT INTO [forums_permissions] ([gid],[fid],[permissions]) VALUES (3,2,'{"View
INSERT INTO [forums_permissions] ([gid],[fid],[permissions]) VALUES (4,2,'{"ViewTopic":true}');
INSERT INTO [forums_permissions] ([gid],[fid],[permissions]) VALUES (5,2,'{"ViewTopic":true}');
INSERT INTO [forums_permissions] ([gid],[fid],[permissions]) VALUES (6,2,'{"ViewTopic":true}');
INSERT INTO [topics] ([title],[content],[parsed_content],[createdAt],[lastReplyAt],[lastReplyBy],[createdBy],[parentID],[ip]) VALUES ('Test Topic','A topic automatically generated by the software.','A topic automatically generated by the software.',GETUTCDATE(),GETUTCDATE(),1,1,2,'::1');
INSERT INTO [replies] ([tid],[content],[parsed_content],[createdAt],[createdBy],[lastUpdated],[lastEdit],[lastEditBy],[ip]) VALUES (1,'A reply!','A reply!',GETUTCDATE(),1,GETUTCDATE(),0,0,'::1');
INSERT INTO [topics] ([title],[content],[parsed_content],[createdAt],[lastReplyAt],[lastReplyBy],[createdBy],[parentID],[ip]) VALUES ('Test Topic','A topic automatically generated by the software.','A topic automatically generated by the software.',GETUTCDATE(),GETUTCDATE(),1,1,2,'');
INSERT INTO [replies] ([tid],[content],[parsed_content],[createdAt],[createdBy],[lastUpdated],[lastEdit],[lastEditBy],[ip]) VALUES (1,'A reply!','A reply!',GETUTCDATE(),1,GETUTCDATE(),0,0,'');
INSERT INTO [menus] () VALUES ();
INSERT INTO [menu_items] ([mid],[name],[htmlID],[position],[path],[aria],[tooltip],[order]) VALUES (1,'{lang.menu_forums}','menu_forums','left','/forums/','{lang.menu_forums_aria}','{lang.menu_forums_tooltip}',0);
INSERT INTO [menu_items] ([mid],[name],[htmlID],[cssClass],[position],[path],[aria],[tooltip],[order]) VALUES (1,'{lang.menu_topics}','menu_topics','menu_topics','left','/topics/','{lang.menu_topics_aria}','{lang.menu_topics_tooltip}',1);

View File

@ -13,6 +13,7 @@ INSERT INTO `settings`(`name`,`content`,`type`) VALUES ('megapost_min_words','10
INSERT INTO `settings`(`name`,`content`,`type`) VALUES ('meta_desc','','html-attribute');
INSERT INTO `settings`(`name`,`content`,`type`) VALUES ('rapid_loading','1','bool');
INSERT INTO `settings`(`name`,`content`,`type`) VALUES ('google_site_verify','','html-attribute');
INSERT INTO `settings`(`name`,`content`,`type`,`constraints`) VALUES ('avatar_visibility','0','list','0-1');
INSERT INTO `themes`(`uname`,`default`) VALUES ('cosora',1);
INSERT INTO `emails`(`email`,`uid`,`validated`) VALUES ('admin@localhost',1,1);
INSERT INTO `users_groups`(`name`,`permissions`,`plugin_perms`,`is_mod`,`is_admin`,`is_banned`,`tag`) VALUES ('Administrator','{"BanUsers":true,"ActivateUsers":true,"EditUser":true,"EditUserEmail":true,"EditUserPassword":true,"EditUserGroup":true,"EditUserGroupSuperMod":true,"EditGroup":true,"EditGroupLocalPerms":true,"EditGroupGlobalPerms":true,"EditGroupSuperMod":true,"ManageForums":true,"EditSettings":true,"ManageThemes":true,"ManagePlugins":true,"ViewAdminLogs":true,"ViewIPs":true,"UploadFiles":true,"UploadAvatars":true,"UseConvos":true,"UseConvosOnlyWithMod":true,"CreateProfileReply":true,"AutoEmbed":true,"AutoLink":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true,"MoveTopic":true}','{}',1,1,0,'Admin');
@ -35,8 +36,8 @@ INSERT INTO `forums_permissions`(`gid`,`fid`,`permissions`) VALUES (3,2,'{"ViewT
INSERT INTO `forums_permissions`(`gid`,`fid`,`permissions`) VALUES (4,2,'{"ViewTopic":true}');
INSERT INTO `forums_permissions`(`gid`,`fid`,`permissions`) VALUES (5,2,'{"ViewTopic":true}');
INSERT INTO `forums_permissions`(`gid`,`fid`,`permissions`) VALUES (6,2,'{"ViewTopic":true}');
INSERT INTO `topics`(`title`,`content`,`parsed_content`,`createdAt`,`lastReplyAt`,`lastReplyBy`,`createdBy`,`parentID`,`ip`) VALUES ('Test Topic','A topic automatically generated by the software.','A topic automatically generated by the software.',UTC_TIMESTAMP(),UTC_TIMESTAMP(),1,1,2,'::1');
INSERT INTO `replies`(`tid`,`content`,`parsed_content`,`createdAt`,`createdBy`,`lastUpdated`,`lastEdit`,`lastEditBy`,`ip`) VALUES (1,'A reply!','A reply!',UTC_TIMESTAMP(),1,UTC_TIMESTAMP(),0,0,'::1');
INSERT INTO `topics`(`title`,`content`,`parsed_content`,`createdAt`,`lastReplyAt`,`lastReplyBy`,`createdBy`,`parentID`,`ip`) VALUES ('Test Topic','A topic automatically generated by the software.','A topic automatically generated by the software.',UTC_TIMESTAMP(),UTC_TIMESTAMP(),1,1,2,'');
INSERT INTO `replies`(`tid`,`content`,`parsed_content`,`createdAt`,`createdBy`,`lastUpdated`,`lastEdit`,`lastEditBy`,`ip`) VALUES (1,'A reply!','A reply!',UTC_TIMESTAMP(),1,UTC_TIMESTAMP(),0,0,'');
INSERT INTO `menus`() VALUES ();
INSERT INTO `menu_items`(`mid`,`name`,`htmlID`,`position`,`path`,`aria`,`tooltip`,`order`) VALUES (1,'{lang.menu_forums}','menu_forums','left','/forums/','{lang.menu_forums_aria}','{lang.menu_forums_tooltip}',0);
INSERT INTO `menu_items`(`mid`,`name`,`htmlID`,`cssClass`,`position`,`path`,`aria`,`tooltip`,`order`) VALUES (1,'{lang.menu_topics}','menu_topics','menu_topics','left','/topics/','{lang.menu_topics_aria}','{lang.menu_topics_tooltip}',1);

View File

@ -0,0 +1,10 @@
CREATE TABLE `forums_actions` (
`faid` int not null AUTO_INCREMENT,
`fid` int not null,
`runOnTopicCreation` boolean DEFAULT 0 not null,
`runDaysAfterTopicCreation` int DEFAULT 0 not null,
`runDaysAfterTopicLastReply` int DEFAULT 0 not null,
`action` varchar(50) not null,
`extra` varchar(200) DEFAULT '' not null,
primary key(`faid`)
) CHARSET=utf8mb4 COLLATE utf8mb4_general_ci;

View File

@ -5,6 +5,7 @@ INSERT INTO "settings"("name","content","type") VALUES ('megapost_min_words','10
INSERT INTO "settings"("name","content","type") VALUES ('meta_desc','','html-attribute');
INSERT INTO "settings"("name","content","type") VALUES ('rapid_loading','1','bool');
INSERT INTO "settings"("name","content","type") VALUES ('google_site_verify','','html-attribute');
INSERT INTO "settings"("name","content","type","constraints") VALUES ('avatar_visibility','0','list','0-1');
INSERT INTO "themes"("uname","default") VALUES ('cosora',1);
INSERT INTO "emails"("email","uid","validated") VALUES ('admin@localhost',1,1);
INSERT INTO "users_groups"("name","permissions","plugin_perms","is_mod","is_admin","is_banned","tag") VALUES ('Administrator','{"BanUsers":true,"ActivateUsers":true,"EditUser":true,"EditUserEmail":true,"EditUserPassword":true,"EditUserGroup":true,"EditUserGroupSuperMod":true,"EditGroup":true,"EditGroupLocalPerms":true,"EditGroupGlobalPerms":true,"EditGroupSuperMod":true,"ManageForums":true,"EditSettings":true,"ManageThemes":true,"ManagePlugins":true,"ViewAdminLogs":true,"ViewIPs":true,"UploadFiles":true,"UploadAvatars":true,"UseConvos":true,"UseConvosOnlyWithMod":true,"CreateProfileReply":true,"AutoEmbed":true,"AutoLink":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true,"MoveTopic":true}','{}',1,1,0,'Admin');
@ -27,8 +28,8 @@ INSERT INTO "forums_permissions"("gid","fid","permissions") VALUES (3,2,'{"ViewT
INSERT INTO "forums_permissions"("gid","fid","permissions") VALUES (4,2,'{"ViewTopic":true}');
INSERT INTO "forums_permissions"("gid","fid","permissions") VALUES (5,2,'{"ViewTopic":true}');
INSERT INTO "forums_permissions"("gid","fid","permissions") VALUES (6,2,'{"ViewTopic":true}');
INSERT INTO "topics"("title","content","parsed_content","createdAt","lastReplyAt","lastReplyBy","createdBy","parentID","ip") VALUES ('Test Topic','A topic automatically generated by the software.','A topic automatically generated by the software.',UTC_TIMESTAMP(),UTC_TIMESTAMP(),1,1,2,'::1');
INSERT INTO "replies"("tid","content","parsed_content","createdAt","createdBy","lastUpdated","lastEdit","lastEditBy","ip") VALUES (1,'A reply!','A reply!',UTC_TIMESTAMP(),1,UTC_TIMESTAMP(),0,0,'::1');
INSERT INTO "topics"("title","content","parsed_content","createdAt","lastReplyAt","lastReplyBy","createdBy","parentID","ip") VALUES ('Test Topic','A topic automatically generated by the software.','A topic automatically generated by the software.',UTC_TIMESTAMP(),UTC_TIMESTAMP(),1,1,2,'');
INSERT INTO "replies"("tid","content","parsed_content","createdAt","createdBy","lastUpdated","lastEdit","lastEditBy","ip") VALUES (1,'A reply!','A reply!',UTC_TIMESTAMP(),1,UTC_TIMESTAMP(),0,0,'');
INSERT INTO "menus"() VALUES ();
INSERT INTO "menu_items"("mid","name","htmlID","position","path","aria","tooltip","order") VALUES (1,'{lang.menu_forums}','menu_forums','left','/forums/','{lang.menu_forums_aria}','{lang.menu_forums_tooltip}',0);
INSERT INTO "menu_items"("mid","name","htmlID","cssClass","position","path","aria","tooltip","order") VALUES (1,'{lang.menu_topics}','menu_topics','menu_topics','left','/topics/','{lang.menu_topics_aria}','{lang.menu_topics_tooltip}',1);

View File

@ -10,10 +10,12 @@ CREATE TABLE "users" (
`lastActiveAt` timestamp not null,
`session` varchar (200) DEFAULT '' not null,
`last_ip` varchar (200) DEFAULT '' not null,
`profile_comments` int DEFAULT 0 not null,
`who_can_convo` int DEFAULT 0 not null,
`enable_embeds` int DEFAULT -1 not null,
`email` varchar (200) DEFAULT '' not null,
`avatar` varchar (100) DEFAULT '' not null,
`message` text DEFAULT '' not null,
`message` text not null,
`url_prefix` varchar (20) DEFAULT '' not null,
`url_name` varchar (100) DEFAULT '' not null,
`level` smallint DEFAULT 0 not null,

View File

@ -5,6 +5,6 @@ CREATE TABLE "widgets" (
`type` varchar (100) not null,
`active` boolean DEFAULT 0 not null,
`location` varchar (100) not null,
`data` text DEFAULT '' not null,
`data` text not null,
primary key(`wid`)
);

View File

@ -12,7 +12,7 @@
{{if .Desc}}
<span class="rowsmall"itemprop="description">{{.Desc}}</span>
{{else}}
<span class="rowsmall forum_nodesc">{{lang "forums_no_description"}}</span>
<span class="rowsmall forum_nodesc">{{lang "forums_no_desc"}}</span>
{{end}}
</span>
<span class="forum_right shift_right">

View File

@ -13,16 +13,16 @@
{{template "panel_debug_stat_head.html" "panel_debug_adapter_label"}}
{{/** TODO: Use this for active database connections when Go 1.11 lands **/}}
{{template "panel_debug_stat_head_q.html"}}
{{template "panel_debug_stat.html" .OpenConns}}
{{template "panel_debug_stat.html" .DBConns}}
{{template "panel_debug_stat.html" .DBAdapter}}
{{template "panel_debug_stat_q.html"}}
{{template "panel_debug_stat_head.html" "panel_debug_goroutine_count_label"}}
{{template "panel_debug_stat_head.html" "panel_debug_cpu_count_label"}}
{{template "panel_debug_stat_head_q.html"}}
{{template "panel_debug_stat_head.html" "panel_debug_http_conns_label"}}
{{template "panel_debug_stat.html" .Goroutines}}
{{template "panel_debug_stat.html" .CPUs}}
{{template "panel_debug_stat_q.html"}}
{{template "panel_debug_stat.html" .HttpConns}}
</div>
{{template "panel_debug_subhead.html" "panel_debug_tasks"}}
<div id="panel_debug"class="colstack_grid">

View File

@ -1,45 +1,45 @@
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{.Name}}{{lang "panel_forum_head_suffix"}}</h1></div>
<div class="rowitem"><h1>{{.Name}}{{lang "panel.forum_head_suffix"}}</h1></div>
</div>
<div id="panel_forum"class="colstack_item the_form">
<form action="/panel/forums/edit/submit/{{.ID}}?s={{.CurrentUser.Session}}"method="post">
<div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel_forum_name"}}</a></div>
<div class="formitem"><input name="forum_name"type="text"value="{{.Name}}"placeholder="{{lang "panel_forum_name_placeholder"}}"></div>
<div class="formitem formlabel"><a>{{lang "panel.forum_name"}}</a></div>
<div class="formitem"><input name="forum_name"type="text"value="{{.Name}}"placeholder="{{lang "panel.forum_name_placeholder"}}"></div>
</div>
<div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel_forum_description"}}</a></div>
<div class="formitem"><input name="forum_desc"type="text"value="{{.Desc}}"placeholder="{{lang "panel_forum_description_placeholder"}}"></div>
<div class="formitem formlabel"><a>{{lang "panel.forum_desc"}}</a></div>
<div class="formitem"><input name="forum_desc"type="text"value="{{.Desc}}"placeholder="{{lang "panel.forum_desc_placeholder"}}"></div>
</div>
<div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel_forum_active"}}</a></div>
<div class="formitem formlabel"><a>{{lang "panel.forum_active"}}</a></div>
<div class="formitem"><select name="forum_active">
<option{{if .Active}} selected{{end}} value=1>{{lang "option_yes"}}</option>
<option{{if not .Active}} selected{{end}} value=0>{{lang "option_no"}}</option>
</select></div>
</div>
<div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel_forum_preset"}}</a></div>
<div class="formitem formlabel"><a>{{lang "panel.forum_preset"}}</a></div>
<div class="formitem">
<select name="forum_preset">
<option{{if eq .Preset "all"}} selected{{end}} value="all">{{lang "panel_preset_everyone"}}</option>
<option{{if eq .Preset "announce"}} selected{{end}} value="announce">{{lang "panel_preset_announcements"}}</option>
<option{{if eq .Preset "members"}} selected{{end}} value="members">{{lang "panel_preset_member_only"}}</option>
<option{{if eq .Preset "staff"}} selected{{end}} value="staff">{{lang "panel_preset_staff_only"}}</option>
<option{{if eq .Preset "admins"}} selected{{end}} value="admins">{{lang "panel_preset_admin_only"}}</option>
<option{{if eq .Preset "archive"}} selected{{end}} value="archive">{{lang "panel_preset_archive"}}</option>
<option{{if eq .Preset "custom"}} selected{{end}} value="custom">{{lang "panel_preset_custom"}}</option>
<option{{if eq .Preset "all"}} selected{{end}} value="all">{{lang "panel.preset_everyone"}}</option>
<option{{if eq .Preset "announce"}} selected{{end}} value="announce">{{lang "panel.preset_announcements"}}</option>
<option{{if eq .Preset "members"}} selected{{end}} value="members">{{lang "panel.preset_member_only"}}</option>
<option{{if eq .Preset "staff"}} selected{{end}} value="staff">{{lang "panel.preset_staff_only"}}</option>
<option{{if eq .Preset "admins"}} selected{{end}} value="admins">{{lang "panel.preset_admin_only"}}</option>
<option{{if eq .Preset "archive"}} selected{{end}} value="archive">{{lang "panel.preset_archive"}}</option>
<option{{if eq .Preset "custom"}} selected{{end}} value="custom">{{lang "panel.preset_custom"}}</option>
</select>
</div>
</div>
<div class="formrow">
<div class="formitem"><button name="panel-button"class="formbutton form_middle_button">{{lang "panel_forum_update_button"}}</button></div>
<div class="formitem"><button name="panel-button"class="formbutton form_middle_button">{{lang "panel.forum_update_button"}}</button></div>
</div>
</form>
</div>
<div class="colstack_item colstack_head">
<div class="rowitem">
<h1>{{lang "panel_forum_permissions_head"}}</h1>
<h1>{{lang "panel.forum_permissions_head"}}</h1>
</div>
</div>
<div id="forum_quick_perms"class="colstack_item rowlist formlist the_form">
@ -48,13 +48,76 @@
<div class="formitem editable_parent">
<a>{{.Group.Name}}</a>
<input name="gid"value="{{.Group.ID}}"type="hidden"class="editable_block"data-field="gid"data-type="hidden"data-value="{{.Group.ID}}">
<span class="edit_fields hide_on_edit rowsmall">{{lang "panel_forum_edit_button"}}</span>
<span class="edit_fields hide_on_edit rowsmall">{{lang "panel.forum_edit_button"}}</span>
<div class="panel_floater">
<span data-field="perm_preset"data-type="list"data-value="{{.Preset}}" class="editable_block perm_preset perm_preset_{{.Preset}}"></span>
<a class="panel_right_button has_inner_button show_on_edit"href="/panel/forums/edit/perms/submit/{{$.ID}}"><button class='panel_tag submit_edit'type='submit'>{{lang "panel_forum_short_update_button"}}</button></a>
<a class="panel_right_button has_inner_button show_on_edit"href="/panel/forums/edit/perms/{{$.ID}}-{{.Group.ID}}"><button class='panel_tag'type='submit'>{{lang "panel_forum_full_edit_button"}}</button></a>
<span data-field="perm_preset"data-type="list"data-value="{{.Preset}}"class="editable_block perm_preset perm_preset_{{.Preset}}"></span>
<a class="panel_right_button has_inner_button show_on_edit"href="/panel/forums/edit/perms/submit/{{$.ID}}"><button class='panel_tag submit_edit'type='submit'>{{lang "panel.forum_short_update_button"}}</button></a>
<a class="panel_right_button has_inner_button show_on_edit"href="/panel/forums/edit/perms/{{$.ID}}-{{.Group.ID}}"><button class='panel_tag'type='submit'>{{lang "panel.forum_full_edit_button"}}</button></a>
</div>
</div>
</div>
{{end}}
</div>
{{if .Actions}}
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel.forum_actions_head"}}</h1></div>
</div>
<div id="panel_forum_actions"class="colstack_item rowlist">
{{range .Actions}}
<div class="rowitem panel_compactrow editable_parent">
<a class="panel_upshift">{{.ActionName}}{{if .RunDaysAfterTopicCreation}} - {{.RunDaysAfterTopicCreation}} days after topic creation{{end}}{{if .RunDaysAfterTopicLastReply}} - {{.RunDaysAfterTopicLastReply}} days after topic last reply{{end}}</a>
<span class="panel_floater">
<a href="/panel/forums/action/delete/submit/{{.ID}}?s={{$.CurrentUser.Session}}&ret={{$.ID}}"class="panel_tag panel_right_button delete_button"></a>
</span>
</div>
{{end}}
</div>
{{end}}
<div class="colstack_item colstack_head">
<div class="rowitem">
<h1>{{lang "panel.forum_actions_create_head"}}</h1>
</div>
</div>
<div id="panel_forum_action_create"class="colstack_item the_form">
<form action="/panel/forums/action/create/submit/{{.ID}}?s={{.CurrentUser.Session}}"method="post">
<!--<div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel.forum_action_run_on_topic_creation"}}</a></div>
<div class="formitem"><select name="action_run_on_topic_creation">
<option value=1>{{lang "option_yes"}}</option>
<option selected value=0>{{lang "option_no"}}</option>
</select></div>
</div>-->
<div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel.forum_action_run_days_after_topic_creation"}}</a></div>
<div class="formitem">
<input name="action_run_days_after_topic_creation"value="0"type="number">
</div>
</div>
<div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel.forum_action_run_days_after_topic_last_reply"}}</a></div>
<div class="formitem">
<input name="action_run_days_after_topic_last_reply"value="0"type="number">
</div>
</div>
<div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel.forum_action_action"}}</a></div>
<div class="formitem">
<select name="action_action">
<option value="delete"selected>{{lang "panel.forum_action_action_delete"}}</option>
<option value="lock">{{lang "panel.forum_action_action_lock"}}</option>
<option value="unlock">{{lang "panel.forum_action_action_unlock"}}</option>
<option value="move">{{lang "panel.forum_action_action_move"}}</option>
</select>
</div>
</div>
<div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel.forum_action_extra"}}</a></div>
<div class="formitem">
<input name="action_extra"type="text">
</div>
</div>
<div class="formrow">
<div class="formitem"><button name="panel-button"class="formbutton form_middle_button">{{lang "panel.forum_action_create_button"}}</button></div>
</div>
</form>
</div>

View File

@ -1,5 +1,5 @@
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{.Name}}{{lang "panel_forum_head_suffix"}}</h1></div>
<div class="rowitem"><h1>{{.Name}}{{lang "panel.forum_head_suffix"}}</h1></div>
</div>
<form action="/panel/forums/edit/perms/adv/submit/{{.ForumID}}-{{.GroupID}}?s={{.CurrentUser.Session}}"method="post">
<div class="colstack_item rowlist formlist the_form panel_forum_perms">
@ -16,7 +16,7 @@
</div>
</div>{{end}}
<div class="formrow">
<div class="formitem"><button name="panel-button"class="formbutton form_middle_button">{{lang "panel_forum_update_button"}}</button></div>
<div class="formitem"><button name="panel-button"class="formbutton form_middle_button">{{lang "panel.forum_update_button"}}</button></div>
</div>
</div>
</form>

View File

@ -40,8 +40,8 @@
<div class="formitem"><input name="name" type="text" placeholder="{{lang "panel.forums_create_name"}}"></div>
</div>
<div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel.forums_create_description_label"}}</a></div>
<div class="formitem"><input name="desc" type="text" placeholder="{{lang "panel.forums_create_description"}}"></div>
<div class="formitem formlabel"><a>{{lang "panel.forums_create_desc_label"}}</a></div>
<div class="formitem"><input name="desc" type="text" placeholder="{{lang "panel.forums_create_desc"}}"></div>
</div>
<div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel.forums_active_label"}}</a></div>
@ -53,13 +53,13 @@
<div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel.forums_preset_label"}}</a></div>
<div class="formitem"><select name="preset">
<option selected value="all">{{lang "panel_preset_everyone"}}</option>
<option value="announce">{{lang "panel_preset_announcements"}}</option>
<option value="members">{{lang "panel_preset_member_only"}}</option>
<option value="staff">{{lang "panel_preset_staff_only"}}</option>
<option value="admins">{{lang "panel_preset_admin_only"}}</option>
<option value="archive">{{lang "panel_preset_archive"}}</option>
<option value="custom">{{lang "panel_preset_custom"}}</option>
<option selected value="all">{{lang "panel.preset_everyone"}}</option>
<option value="announce">{{lang "panel.preset_announcements"}}</option>
<option value="members">{{lang "panel.preset_member_only"}}</option>
<option value="staff">{{lang "panel.preset_staff_only"}}</option>
<option value="admins">{{lang "panel.preset_admin_only"}}</option>
<option value="archive">{{lang "panel.preset_archive"}}</option>
<option value="custom">{{lang "panel.preset_custom"}}</option>
</select></div>
</div>
<div class="formrow">