From 0d1e4e5993d911281fd184f588a30ec911ddae31 Mon Sep 17 00:00:00 2001 From: Azareal Date: Mon, 27 May 2019 19:00:40 +1000 Subject: [PATCH] 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. --- cmd/query_gen/tables.go | 1 + common/forum.go | 1 + common/forum_store.go | 103 +++++++++++++++++----------------- common/site.go | 1 + common/template_init.go | 68 ++++++++++++++++------ install/mysql.go | 15 ++--- misc_test.go | 17 ++++++ patcher/patches.go | 5 ++ routes/forum.go | 6 +- schema/mssql/query_forums.sql | 1 + schema/mysql/query_forums.sql | 1 + schema/pgsql/query_forums.sql | 1 + 12 files changed, 143 insertions(+), 77 deletions(-) diff --git a/cmd/query_gen/tables.go b/cmd/query_gen/tables.go index 13fe0606..b84b4653 100644 --- a/cmd/query_gen/tables.go +++ b/cmd/query_gen/tables.go @@ -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"}, diff --git a/common/forum.go b/common/forum.go index 6ca23a7f..c70ca69f 100644 --- a/common/forum.go +++ b/common/forum.go @@ -27,6 +27,7 @@ type Forum struct { Link string Name string Desc string + Tmpl string Active bool Order int Preset string diff --git a/common/forum_store.go b/common/forum_store.go index 4f788915..04740d1a 100644 --- a/common/forum_store.go +++ b/common/forum_store.go @@ -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) } diff --git a/common/site.go b/common/site.go index 8a274edb..5a9a528b 100644 --- a/common/site.go +++ b/common/site.go @@ -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 diff --git a/common/template_init.go b/common/template_init.go index 00ada8da..f4108a40 100644 --- a/common/template_init.go +++ b/common/template_init.go @@ -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 } diff --git a/install/mysql.go b/install/mysql.go index ca6e3f1c..09d2258d 100644 --- a/install/mysql.go +++ b/install/mysql.go @@ -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 diff --git a/misc_test.go b/misc_test.go index 659f5cbf..20bf03bd 100644 --- a/misc_test.go +++ b/misc_test.go @@ -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") diff --git a/patcher/patches.go b/patcher/patches.go index 09028bf8..c8b853bc 100644 --- a/patcher/patches.go +++ b/patcher/patches.go @@ -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)) } \ No newline at end of file diff --git a/routes/forum.go b/routes/forum.go index 162b4749..4b5bd1ce 100644 --- a/routes/forum.go +++ b/routes/forum.go @@ -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 } diff --git a/schema/mssql/query_forums.sql b/schema/mssql/query_forums.sql index d5fdb520..47d5ed1a 100644 --- a/schema/mssql/query_forums.sql +++ b/schema/mssql/query_forums.sql @@ -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, diff --git a/schema/mysql/query_forums.sql b/schema/mysql/query_forums.sql index c7692742..0c7af8fb 100644 --- a/schema/mysql/query_forums.sql +++ b/schema/mysql/query_forums.sql @@ -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, diff --git a/schema/pgsql/query_forums.sql b/schema/pgsql/query_forums.sql index c9ae5927..8f59d6ec 100644 --- a/schema/pgsql/query_forums.sql +++ b/schema/pgsql/query_forums.sql @@ -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,