Add super experimental support for per-forum templates.

Add the ExtraTmpls developer setting to config.json
Call ForumStore.BypassGet in ForumStore.Reload instead of duplicating it.
Fix a potential deadlock in ForumStore.Create.
Attempt to fix a weird installation bug.
Add a few more forum store test cases.

You will need to run the updater / patcher for this commit.
This commit is contained in:
Azareal 2019-05-27 19:00:40 +10:00
parent 7e7f9e46bd
commit 0d1e4e5993
12 changed files with 143 additions and 77 deletions

View File

@ -181,6 +181,7 @@ func createTables(adapter qgen.Adapter) error {
tblColumn{"fid", "int", 0, false, true, ""},
tblColumn{"name", "varchar", 100, false, false, ""},
tblColumn{"desc", "varchar", 200, false, false, ""},
tblColumn{"tmpl", "varchar", 200, false, false, ""},
tblColumn{"active", "boolean", 0, false, false, "1"},
tblColumn{"order", "int", 0, false, false, "0"},
tblColumn{"topicCount", "int", 0, false, false, "0"},

View File

@ -27,6 +27,7 @@ type Forum struct {
Link string
Name string
Desc string
Tmpl string
Active bool
Order int
Preset string

View File

@ -10,6 +10,7 @@ import (
"database/sql"
"errors"
"log"
//"fmt"
"sort"
"sync"
"sync/atomic"
@ -77,8 +78,8 @@ func NewMemoryForumStore() (*MemoryForumStore, error) {
acc := qgen.NewAcc()
// TODO: Do a proper delete
return &MemoryForumStore{
get: acc.Select("forums").Columns("name, desc, active, order, preset, parentID, parentType, topicCount, lastTopicID, lastReplyerID").Where("fid = ?").Prepare(),
getAll: acc.Select("forums").Columns("fid, name, desc, active, order, preset, parentID, parentType, topicCount, lastTopicID, lastReplyerID").Orderby("order ASC, fid ASC").Prepare(),
get: acc.Select("forums").Columns("name, desc, tmpl, active, order, preset, parentID, parentType, topicCount, lastTopicID, lastReplyerID").Where("fid = ?").Prepare(),
getAll: acc.Select("forums").Columns("fid, name, desc, tmpl, active, order, preset, parentID, parentType, topicCount, lastTopicID, lastReplyerID").Orderby("order ASC, fid ASC").Prepare(),
delete: acc.Update("forums").Set("name= '', active = 0").Where("fid = ?").Prepare(),
create: acc.Insert("forums").Columns("name, desc, active, preset").Fields("?,?,?,?").Prepare(),
count: acc.Count("forums").Where("name != ''").Prepare(),
@ -109,7 +110,7 @@ func (mfs *MemoryForumStore) LoadForums() error {
var i = 0
for ; rows.Next(); i++ {
forum := &Forum{ID: 0, Active: true, Preset: "all"}
err = rows.Scan(&forum.ID, &forum.Name, &forum.Desc, &forum.Active, &forum.Order, &forum.Preset, &forum.ParentID, &forum.ParentType, &forum.TopicCount, &forum.LastTopicID, &forum.LastReplyerID)
err = rows.Scan(&forum.ID, &forum.Name, &forum.Desc, &forum.Tmpl, &forum.Active, &forum.Order, &forum.Preset, &forum.ParentID, &forum.ParentType, &forum.TopicCount, &forum.LastTopicID, &forum.LastReplyerID)
if err != nil {
return err
}
@ -133,9 +134,9 @@ func (mfs *MemoryForumStore) LoadForums() error {
// TODO: Hide social groups too
// ? - Will this be hit a lot by plugin_guilds?
func (mfs *MemoryForumStore) rebuildView() {
func (s *MemoryForumStore) rebuildView() {
var forumView []*Forum
mfs.forums.Range(func(_ interface{}, value interface{}) bool {
s.forums.Range(func(_ interface{}, value interface{}) bool {
forum := value.(*Forum)
// ? - ParentType blank means that it doesn't have a parent
if forum.Active && forum.Name != "" && forum.ParentType == "" {
@ -144,20 +145,20 @@ func (mfs *MemoryForumStore) rebuildView() {
return true
})
sort.Sort(SortForum(forumView))
mfs.forumView.Store(forumView)
s.forumView.Store(forumView)
TopicListThaw.Thaw()
}
func (mfs *MemoryForumStore) DirtyGet(id int) *Forum {
fint, ok := mfs.forums.Load(id)
func (s *MemoryForumStore) DirtyGet(id int) *Forum {
fint, ok := s.forums.Load(id)
if !ok || fint.(*Forum).Name == "" {
return &Forum{ID: -1, Name: ""}
}
return fint.(*Forum)
}
func (mfs *MemoryForumStore) CacheGet(id int) (*Forum, error) {
fint, ok := mfs.forums.Load(id)
func (s *MemoryForumStore) CacheGet(id int) (*Forum, error) {
fint, ok := s.forums.Load(id)
if !ok || fint.(*Forum).Name == "" {
return nil, ErrNoRows
}
@ -184,7 +185,7 @@ func (s *MemoryForumStore) Get(id int) (*Forum, error) {
func (s *MemoryForumStore) BypassGet(id int) (*Forum, error) {
var forum = &Forum{ID: id}
err := s.get.QueryRow(id).Scan(&forum.Name, &forum.Desc, &forum.Active, &forum.Order, &forum.Preset, &forum.ParentID, &forum.ParentType, &forum.TopicCount, &forum.LastTopicID, &forum.LastReplyerID)
err := s.get.QueryRow(id).Scan(&forum.Name, &forum.Desc, &forum.Tmpl,&forum.Active, &forum.Order, &forum.Preset, &forum.ParentID, &forum.ParentType, &forum.TopicCount, &forum.LastTopicID, &forum.LastReplyerID)
if err != nil {
return nil, err
}
@ -200,10 +201,10 @@ func (s *MemoryForumStore) BypassGet(id int) (*Forum, error) {
}
// TODO: Optimise this
func (mfs *MemoryForumStore) BulkGetCopy(ids []int) (forums []Forum, err error) {
func (s *MemoryForumStore) BulkGetCopy(ids []int) (forums []Forum, err error) {
forums = make([]Forum, len(ids))
for i, id := range ids {
forum, err := mfs.Get(id)
forum, err := s.Get(id)
if err != nil {
return nil, err
}
@ -212,29 +213,24 @@ func (mfs *MemoryForumStore) BulkGetCopy(ids []int) (forums []Forum, err error)
return forums, nil
}
func (mfs *MemoryForumStore) Reload(id int) error {
var forum = &Forum{ID: id}
err := mfs.get.QueryRow(id).Scan(&forum.Name, &forum.Desc, &forum.Active, &forum.Order, &forum.Preset, &forum.ParentID, &forum.ParentType, &forum.TopicCount, &forum.LastTopicID, &forum.LastReplyerID)
func (s *MemoryForumStore) Reload(id int) error {
forum, err := s.BypassGet(id)
if err != nil {
return err
}
forum.Link = BuildForumURL(NameToSlug(forum.Name), forum.ID)
forum.LastTopic = Topics.DirtyGet(forum.LastTopicID)
forum.LastReplyer = Users.DirtyGet(forum.LastReplyerID)
mfs.CacheSet(forum)
s.CacheSet(forum)
return nil
}
func (mfs *MemoryForumStore) CacheSet(forum *Forum) error {
mfs.forums.Store(forum.ID, forum)
mfs.rebuildView()
func (s *MemoryForumStore) CacheSet(forum *Forum) error {
s.forums.Store(forum.ID, forum)
s.rebuildView()
return nil
}
// ! Has a randomised order
func (mfs *MemoryForumStore) GetAll() (forumView []*Forum, err error) {
mfs.forums.Range(func(_ interface{}, value interface{}) bool {
func (s *MemoryForumStore) GetAll() (forumView []*Forum, err error) {
s.forums.Range(func(_ interface{}, value interface{}) bool {
forumView = append(forumView, value.(*Forum))
return true
})
@ -243,8 +239,8 @@ func (mfs *MemoryForumStore) GetAll() (forumView []*Forum, err error) {
}
// ? - Can we optimise the sorting?
func (mfs *MemoryForumStore) GetAllIDs() (ids []int, err error) {
mfs.forums.Range(func(_ interface{}, value interface{}) bool {
func (s *MemoryForumStore) GetAllIDs() (ids []int, err error) {
s.forums.Range(func(_ interface{}, value interface{}) bool {
ids = append(ids, value.(*Forum).ID)
return true
})
@ -252,13 +248,13 @@ func (mfs *MemoryForumStore) GetAllIDs() (ids []int, err error) {
return ids, nil
}
func (mfs *MemoryForumStore) GetAllVisible() (forumView []*Forum, err error) {
forumView = mfs.forumView.Load().([]*Forum)
func (s *MemoryForumStore) GetAllVisible() (forumView []*Forum, err error) {
forumView = s.forumView.Load().([]*Forum)
return forumView, nil
}
func (mfs *MemoryForumStore) GetAllVisibleIDs() ([]int, error) {
forumView := mfs.forumView.Load().([]*Forum)
func (s *MemoryForumStore) GetAllVisibleIDs() ([]int, error) {
forumView := s.forumView.Load().([]*Forum)
var ids = make([]int, len(forumView))
for i := 0; i < len(forumView); i++ {
ids[i] = forumView[i].ID
@ -275,8 +271,8 @@ func (mfs *MemoryForumStore) GetFirstChild(parentID int, parentType string) (*Fo
}*/
// TODO: Add a query for this rather than hitting cache
func (mfs *MemoryForumStore) Exists(id int) bool {
forum, ok := mfs.forums.Load(id)
func (s *MemoryForumStore) Exists(id int) bool {
forum, ok := s.forums.Load(id)
if !ok {
return false
}
@ -284,9 +280,9 @@ func (mfs *MemoryForumStore) Exists(id int) bool {
}
// TODO: Batch deletions with name blanking? Is this necessary?
func (mfs *MemoryForumStore) CacheDelete(id int) {
mfs.forums.Delete(id)
mfs.rebuildView()
func (s *MemoryForumStore) CacheDelete(id int) {
s.forums.Delete(id)
s.rebuildView()
}
// TODO: Add a hook to allow plugin_guilds to detect when one of it's forums has just been deleted?
@ -299,47 +295,49 @@ func (s *MemoryForumStore) Delete(id int) error {
return err
}
func (mfs *MemoryForumStore) AddTopic(tid int, uid int, fid int) error {
_, err := mfs.updateCache.Exec(tid, uid, fid)
func (s *MemoryForumStore) AddTopic(tid int, uid int, fid int) error {
_, err := s.updateCache.Exec(tid, uid, fid)
if err != nil {
return err
}
_, err = mfs.addTopics.Exec(1, fid)
_, err = s.addTopics.Exec(1, fid)
if err != nil {
return err
}
// TODO: Bypass the database and update this with a lock or an unsafe atomic swap
return mfs.Reload(fid)
return s.Reload(fid)
}
// TODO: Update the forum cache with the latest topic
func (mfs *MemoryForumStore) RemoveTopic(fid int) error {
_, err := mfs.removeTopics.Exec(1, fid)
func (s *MemoryForumStore) RemoveTopic(fid int) error {
_, err := s.removeTopics.Exec(1, fid)
if err != nil {
return err
}
// TODO: Bypass the database and update this with a lock or an unsafe atomic swap
mfs.Reload(fid)
s.Reload(fid)
return nil
}
// DEPRECATED. forum.Update() will be the way to do this in the future, once it's completed
// TODO: Have a pointer to the last topic rather than storing it on the forum itself
func (mfs *MemoryForumStore) UpdateLastTopic(tid int, uid int, fid int) error {
_, err := mfs.updateCache.Exec(tid, uid, fid)
func (s *MemoryForumStore) UpdateLastTopic(tid int, uid int, fid int) error {
_, err := s.updateCache.Exec(tid, uid, fid)
if err != nil {
return err
}
// TODO: Bypass the database and update this with a lock or an unsafe atomic swap
return mfs.Reload(fid)
return s.Reload(fid)
}
func (mfs *MemoryForumStore) Create(forumName string, forumDesc string, active bool, preset string) (int, error) {
func (s *MemoryForumStore) Create(forumName string, forumDesc string, active bool, preset string) (int, error) {
if forumName == "" {
return 0, ErrBlankName
}
forumCreateMutex.Lock()
res, err := mfs.create.Exec(forumName, forumDesc, active, preset)
defer forumCreateMutex.Unlock()
res, err := s.create.Exec(forumName, forumDesc, active, preset)
if err != nil {
return 0, err
}
@ -350,13 +348,12 @@ func (mfs *MemoryForumStore) Create(forumName string, forumDesc string, active b
}
fid := int(fid64)
err = mfs.Reload(fid)
err = s.Reload(fid)
if err != nil {
return 0, err
}
PermmapToQuery(PresetToPermmap(preset), fid)
forumCreateMutex.Unlock()
return fid, nil
}
@ -383,8 +380,8 @@ func (s *MemoryForumStore) Length() (length int) {
// TODO: Get the total count of forums in the forum store rather than doing a heavy query for this?
// GlobalCount returns the total number of forums
func (mfs *MemoryForumStore) GlobalCount() (fcount int) {
err := mfs.count.QueryRow().Scan(&fcount)
func (s *MemoryForumStore) GlobalCount() (fcount int) {
err := s.count.QueryRow().Scan(&fcount)
if err != nil {
LogError(err)
}

View File

@ -121,6 +121,7 @@ type devConfig struct {
NoFsnotify bool // Super Experimental!
FullReqLog bool
ExtraTmpls string // Experimental flag for adding compiled templates, we'll likely replace this with a better mechanism
}
// configHolder is purely for having a big struct to unmarshal data into

View File

@ -206,7 +206,7 @@ func CompileTemplates() error {
return nil
}
func compileCommons(c *tmpl.CTemplateSet, header *Header, header2 *Header, out TItemHold) error {
func compileCommons(c *tmpl.CTemplateSet, header *Header, header2 *Header, forumList []Forum, out TItemHold) error {
// TODO: Add support for interface{}s
_, user2, user3 := tmplInitUsers()
now := time.Now()
@ -221,16 +221,6 @@ func compileCommons(c *tmpl.CTemplateSet, header *Header, header2 *Header, out T
return header2
}*/
// TODO: Use a dummy forum list to avoid o(n) problems
var forumList []Forum
forums, err := Forums.GetAll()
if err != nil {
return err
}
for _, forum := range forums {
forumList = append(forumList, *forum)
}
var topicsList []*TopicsRow
topicsList = append(topicsList, &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, now, now, user3.ID, 1, 1, "", "127.0.0.1", 1, 0, 1, 1, 0, "classname", 0, "", &user2, "", 0, &user3, "General", "/forum/general.2", nil})
topicListPage := TopicListPage{htitle("Topic List"), topicsList, forumList, Config.DefaultForum, TopicListSort{"lastupdated", false}, Paginator{[]int{1}, 1, 1}}
@ -283,13 +273,23 @@ func compileTemplates(wg *sync.WaitGroup, c *tmpl.CTemplateSet, themeName string
ru.Init()
replyList = append(replyList, ru)
// TODO: Use a dummy forum list to avoid o(n) problems
var forumList []Forum
forums, err := Forums.GetAll()
if err != nil {
return err
}
for _, forum := range forums {
forumList = append(forumList, *forum)
}
// Convienience function to save a line here and there
var htitle = func(name string) *Header {
header.Title = name
return header
}
tmpls := TItemHold(make(map[string]TItem))
err := compileCommons(c, header, header2, tmpls)
err = compileCommons(c, header, header2, forumList, tmpls)
if err != nil {
return err
}
@ -297,6 +297,45 @@ func compileTemplates(wg *sync.WaitGroup, c *tmpl.CTemplateSet, themeName string
ppage := ProfilePage{htitle("User 526"), replyList, user, 0, 0} // TODO: Use the score from user to generate the currentScore and nextScore
tmpls.Add("profile", "common.ProfilePage", ppage)
var topicsList []*TopicsRow
topicsList = append(topicsList, &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, now, now, user3.ID, 1, 1, "", "127.0.0.1", 1, 0, 1, 1, 0, "classname", 0, "", &user2, "", 0, &user3, "General", "/forum/general.2", nil})
topicListPage := TopicListPage{htitle("Topic List"), topicsList, forumList, Config.DefaultForum, TopicListSort{"lastupdated", false}, Paginator{[]int{1}, 1, 1}}
forumItem := BlankForum(1, "general-forum.1", "General Forum", "Where the general stuff happens", true, "all", 0, "", 0)
forumPage := ForumPage{htitle("General Forum"), topicsList, forumItem, Paginator{[]int{1}, 1, 1}}
// Experimental!
for _, tmpl := range strings.Split(Dev.ExtraTmpls,",") {
sp := strings.Split(tmpl,":")
if len(sp) < 2 {
continue
}
typ := "0"
if len(sp) == 3 {
typ = sp[2]
}
var pi interface{}
switch sp[1] {
case "common.TopicListPage":
pi = topicListPage
case "common.ForumPage":
pi = forumPage
case "common.ProfilePage":
pi = ppage
case "common.Page":
pi = Page{htitle("Something"), tList, nil}
default:
continue
}
if typ == "1" {
tmpls.Add(sp[0], sp[1], pi)
} else {
tmpls.AddStd(sp[0], sp[1], pi)
}
}
tmpls.AddStd("login", "common.Page", Page{htitle("Login Page"), tList, nil})
tmpls.AddStd("register", "common.Page", Page{htitle("Registration Page"), tList, "nananana"})
tmpls.AddStd("error", "common.ErrorPage", ErrorPage{htitle("Error"), "A problem has occurred in the system."})
@ -374,10 +413,7 @@ func compileTemplates(wg *sync.WaitGroup, c *tmpl.CTemplateSet, themeName string
writeTemplate(name, tmpl)
}
}
/*writeTemplate("login", loginTmpl)
writeTemplate("register", registerTmpl)
writeTemplate("ip_search", ipSearchTmpl)
writeTemplate("error", errorTmpl)*/
return nil
}

View File

@ -113,11 +113,6 @@ func (ins *MysqlInstaller) InitDatabase() (err error) {
}
fmt.Println("Successfully connected to the database")
_, err = db.Exec("SET FOREIGN_KEY_CHECKS = 0;")
if err != nil {
return err
}
// Ready the query builder
ins.db = db
qgen.Builder.SetConn(db)
@ -147,9 +142,10 @@ func (ins *MysqlInstaller) createTable(f os.FileInfo) error {
}
data = bytes.TrimSpace(data)
_, err = ins.db.Exec(string(data))
q = string(data)
_, err = ins.db.Exec(q)
if err != nil {
fmt.Println("Failed query:", string(data))
fmt.Println("Failed query:", q)
fmt.Println("e:", err)
return err
}
@ -165,6 +161,11 @@ func (ins *MysqlInstaller) TableDefs() (err error) {
return err
}
_, err = ins.db.Exec("SET FOREIGN_KEY_CHECKS = 0;")
if err != nil {
return err
}
for _, f := range files {
if !strings.HasPrefix(f.Name(), "query_") {
continue

View File

@ -536,6 +536,7 @@ func TestForumStore(t *testing.T) {
if !c.PluginsInited {
c.InitPlugins()
}
// TODO: Test ForumStore.Reload
fcache, ok := c.Forums.(c.ForumCache)
expect(t, ok, "Unable to cast ForumStore to ForumCache")
@ -570,6 +571,22 @@ func TestForumStore(t *testing.T) {
expectDesc = "A place for general discussions which don't fit elsewhere"
expect(t, forum.Desc == expectDesc, fmt.Sprintf("The forum description should be '%s' not '%s'", expectDesc, forum.Desc))
// Forum reload test, kind of hacky but gets the job done
/*
CacheGet(id int) (*Forum, error)
CacheSet(forum *Forum) error
*/
expect(t,ok,"ForumCache should be available")
forum.Name = "nanana"
fcache.CacheSet(forum)
forum, err = c.Forums.Get(2)
recordMustExist(t, err, "Couldn't find FID #2")
expect(t, forum.Name == "nanana", fmt.Sprintf("The faux name should be nanana not %s", forum.Name))
expectNilErr(t,c.Forums.Reload(2))
forum, err = c.Forums.Get(2)
recordMustExist(t, err, "Couldn't find FID #2")
expect(t, forum.Name == "General", fmt.Sprintf("The proper name should be 2 not %s", forum.Name))
expect(t, !c.Forums.Exists(-1), "FID #-1 shouldn't exist")
expect(t, !c.Forums.Exists(0), "FID #0 shouldn't exist")
expect(t, c.Forums.Exists(1), "FID #1 should exist")

View File

@ -34,6 +34,7 @@ func init() {
addPatch(19, patch19)
addPatch(20, patch20)
addPatch(21, patch21)
addPatch(22, patch22)
}
func patch0(scanner *bufio.Scanner) (err error) {
@ -651,4 +652,8 @@ func patch21(scanner *bufio.Scanner) error {
}
return execStmt(qgen.Builder.AddColumn("activity_stream", tblColumn{"createdAt", "createdAt", 0, false, false, ""}, nil))
}
func patch22(scanner *bufio.Scanner) error {
return execStmt(qgen.Builder.AddColumn("forums", tblColumn{"tmpl", "varchar", 200, false, false, ""}, nil))
}

View File

@ -124,7 +124,11 @@ func ViewForum(w http.ResponseWriter, r *http.Request, user c.User, header *c.He
pageList := c.Paginate(forum.TopicCount, c.Config.ItemsPerPage, 5)
pi := c.ForumPage{header, topicList, forum, c.Paginator{pageList, page, lastPage}}
ferr = renderTemplate("forum", w, r, header, pi)
var tmpl = forum.Tmpl
if tmpl == "" {
tmpl = "forum"
}
ferr = renderTemplate(tmpl, w, r, header, pi)
counters.ForumViewCounter.Bump(forum.ID)
return ferr
}

View File

@ -2,6 +2,7 @@ CREATE TABLE [forums] (
[fid] int not null IDENTITY,
[name] nvarchar (100) not null,
[desc] nvarchar (200) not null,
[tmpl] nvarchar (200) not null,
[active] bit DEFAULT 1 not null,
[order] int DEFAULT 0 not null,
[topicCount] int DEFAULT 0 not null,

View File

@ -2,6 +2,7 @@ CREATE TABLE `forums` (
`fid` int not null AUTO_INCREMENT,
`name` varchar(100) not null,
`desc` varchar(200) not null,
`tmpl` varchar(200) not null,
`active` boolean DEFAULT 1 not null,
`order` int DEFAULT 0 not null,
`topicCount` int DEFAULT 0 not null,

View File

@ -2,6 +2,7 @@ CREATE TABLE "forums" (
`fid` serial not null,
`name` varchar (100) not null,
`desc` varchar (200) not null,
`tmpl` varchar (200) not null,
`active` boolean DEFAULT 1 not null,
`order` int DEFAULT 0 not null,
`topicCount` int DEFAULT 0 not null,