github.com/Azareal/Gosora@v0.0.0-20210729070923-553e66b59003/common/topic_list.go (about) 1 package common 2 3 import ( 4 "database/sql" 5 "fmt" 6 "strconv" 7 "sync" 8 "time" 9 10 qgen "github.com/Azareal/Gosora/query_gen" 11 ) 12 13 var TopicList TopicListInt 14 15 const ( 16 TopicListDefault = iota 17 TopicListMostViewed 18 TopicListWeekViews 19 ) 20 21 type TopicListHolder struct { 22 List []*TopicsRow 23 ForumList []Forum 24 Paginator Paginator 25 } 26 27 type ForumTopicListHolder struct { 28 List []*TopicsRow 29 Paginator Paginator 30 } 31 32 // TODO: Should we return no rows errors on empty pages? Is this likely to break something? 33 type TopicListInt interface { 34 GetListByCanSee(canSee []int, page, orderby int, filterIDs []int) (topicList []*TopicsRow, forumList []Forum, pagi Paginator, err error) 35 GetListByGroup(g *Group, page, orderby int, filterIDs []int) (topicList []*TopicsRow, forumList []Forum, pagi Paginator, err error) 36 GetListByForum(f *Forum, page, orderby int) (topicList []*TopicsRow, pagi Paginator, err error) 37 GetList(page, orderby int, filterIDs []int) (topicList []*TopicsRow, forumList []Forum, pagi Paginator, err error) 38 } 39 40 type TopicListIntTest interface { 41 RawGetListByForum(f *Forum, page, orderby int) (topicList []*TopicsRow, pagi Paginator, err error) 42 Tick() error 43 } 44 45 type DefaultTopicList struct { 46 // TODO: Rewrite this to put permTree as the primary and put canSeeStr on each group? 47 oddGroups map[int][2]*TopicListHolder 48 evenGroups map[int][2]*TopicListHolder 49 oddLock sync.RWMutex 50 evenLock sync.RWMutex 51 52 forums map[int]*ForumTopicListHolder 53 forumLock sync.RWMutex 54 55 qcounts map[int]*sql.Stmt 56 qcounts2 map[int]*sql.Stmt 57 qLock sync.RWMutex 58 qLock2 sync.RWMutex 59 60 //permTree atomic.Value // [string(canSee)]canSee 61 //permTree map[string][]int // [string(canSee)]canSee 62 63 getTopicsByForum *sql.Stmt 64 //getTidsByForum *sql.Stmt 65 } 66 67 // We've removed the topic list cache cap as admins really shouldn't be abusing groups like this with plugin_guilds around and it was extremely fiddly. 68 // If this becomes a problem later on, then we can revisit this with a fresh perspective, particularly with regards to what people expect a group to really be 69 // Also, keep in mind that as-long as the groups don't all have unique sets of forums they can see, then we can optimise a large portion of the work away. 70 func NewDefaultTopicList(acc *qgen.Accumulator) (*DefaultTopicList, error) { 71 tList := &DefaultTopicList{ 72 oddGroups: make(map[int][2]*TopicListHolder), 73 evenGroups: make(map[int][2]*TopicListHolder), 74 forums: make(map[int]*ForumTopicListHolder), 75 qcounts: make(map[int]*sql.Stmt), 76 qcounts2: make(map[int]*sql.Stmt), 77 getTopicsByForum: acc.Select("topics").Columns("tid,title,content,createdBy,is_closed,sticky,createdAt,lastReplyAt,lastReplyBy,lastReplyID,views,postCount,likeCount").Where("parentID=?").Orderby("sticky DESC,lastReplyAt DESC,createdBy DESC").Limit("?,?").Prepare(), 78 //getTidsByForum: acc.Select("topics").Columns("tid").Where("parentID=?").Orderby("sticky DESC,lastReplyAt DESC,createdBy DESC").Limit("?,?").Prepare(), 79 } 80 if e := acc.FirstError(); e != nil { 81 return nil, e 82 } 83 if e := tList.Tick(); e != nil { 84 return nil, e 85 } 86 87 Tasks.HalfSec.Add(tList.Tick) 88 //Tasks.Sec.Add(tList.GroupCountTick) // TODO: Dynamically change the groups in the short list to be optimised every second 89 return tList, nil 90 } 91 92 func (tList *DefaultTopicList) Tick() error { 93 //fmt.Println("TopicList.Tick") 94 if !TopicListThaw.Thawed() { 95 return nil 96 } 97 //fmt.Println("building topic list") 98 99 oddLists := make(map[int][2]*TopicListHolder) 100 evenLists := make(map[int][2]*TopicListHolder) 101 addList := func(gid int, h [2]*TopicListHolder) { 102 if gid%2 == 0 { 103 evenLists[gid] = h 104 } else { 105 oddLists[gid] = h 106 } 107 } 108 109 allGroups, err := Groups.GetAll() 110 if err != nil { 111 return err 112 } 113 114 gidToCanSee := make(map[int]string) 115 permTree := make(map[string][]int) // [string(canSee)]canSee 116 for _, g := range allGroups { 117 // ? - Move the user count check to instance initialisation? Might require more book-keeping, particularly when a user moves into a zero user group 118 if g.UserCount == 0 && g.ID != GuestUser.Group { 119 continue 120 } 121 122 canSee := make([]byte, len(g.CanSee)) 123 for i, item := range g.CanSee { 124 canSee[i] = byte(item) 125 } 126 127 canSeeInt := make([]int, len(canSee)) 128 copy(canSeeInt, g.CanSee) 129 sCanSee := string(canSee) 130 permTree[sCanSee] = canSeeInt 131 gidToCanSee[g.ID] = sCanSee 132 } 133 134 canSeeHolders := make(map[string][2]*TopicListHolder) 135 forumCounts := make(map[int]int) 136 for name, canSee := range permTree { 137 topicList, forumList, pagi, err := tList.GetListByCanSee(canSee, 1, 0, nil) 138 if err != nil { 139 return err 140 } 141 topicList2, forumList2, pagi2, err := tList.GetListByCanSee(canSee, 2, 0, nil) 142 if err != nil { 143 return err 144 } 145 canSeeHolders[name] = [2]*TopicListHolder{ 146 {topicList, forumList, pagi}, 147 {topicList2, forumList2, pagi2}, 148 } 149 if len(canSee) > 1 { 150 forumCounts[len(canSee)] += 1 151 } 152 } 153 for gid, canSee := range gidToCanSee { 154 addList(gid, canSeeHolders[canSee]) 155 } 156 157 tList.oddLock.Lock() 158 tList.oddGroups = oddLists 159 tList.oddLock.Unlock() 160 161 tList.evenLock.Lock() 162 tList.evenGroups = evenLists 163 tList.evenLock.Unlock() 164 165 topc := []int{0, 0, 0, 0, 0, 0} 166 addC := func(c int) { 167 lowI, low := 0, topc[0] 168 for i, top := range topc { 169 if top < low { 170 lowI = i 171 low = top 172 } 173 } 174 if c > low { 175 topc[lowI] = c 176 } 177 } 178 for forumCount := range forumCounts { 179 addC(forumCount) 180 } 181 182 qcounts := make(map[int]*sql.Stmt) 183 qcounts2 := make(map[int]*sql.Stmt) 184 for _, top := range topc { 185 if top == 0 { 186 continue 187 } 188 189 qlist := inqbuild2(top - 1) 190 cols := "tid,title,content,createdBy,is_closed,sticky,createdAt,lastReplyAt,lastReplyBy,lastReplyID,parentID,views,postCount,likeCount,attachCount,poll,data" 191 192 stmt, err := qgen.Builder.SimpleSelect("topics", cols, "parentID IN("+qlist+")", "views DESC,lastReplyAt DESC,createdBy DESC", "?,?") 193 if err != nil { 194 return err 195 } 196 qcounts[top] = stmt 197 198 stmt, err = qgen.Builder.SimpleSelect("topics", cols, "parentID IN("+qlist+")", "sticky DESC,lastReplyAt DESC,createdBy DESC", "?,?") 199 if err != nil { 200 return err 201 } 202 qcounts2[top] = stmt 203 } 204 205 tList.qLock.Lock() 206 tList.qcounts = qcounts 207 tList.qLock.Unlock() 208 209 tList.qLock2.Lock() 210 tList.qcounts2 = qcounts2 211 tList.qLock2.Unlock() 212 213 fmt.Printf("Forums: %+v\n", Forums) 214 forums, err := Forums.GetAll() 215 if err != nil { 216 return err 217 } 218 219 top8 := []*Forum{nil, nil, nil, nil, nil, nil, nil, nil} 220 z := true 221 addScore2 := func(f *Forum) { 222 for i, top := range top8 { 223 if top.TopicCount < f.TopicCount { 224 top8[i] = f 225 return 226 } 227 } 228 } 229 addScore := func(f *Forum) { 230 if z { 231 for i, top := range top8 { 232 if top == nil { 233 top8[i] = f 234 return 235 } 236 } 237 z = false 238 addScore2(f) 239 } 240 addScore2(f) 241 } 242 243 var fshort []*Forum 244 for _, f := range forums { 245 if f.Name == "" || !f.Active || (f.ParentType != "" && f.ParentType != "forum") { 246 continue 247 } 248 if f.TopicCount == 0 { 249 fshort = append(fshort, f) 250 continue 251 } 252 addScore(f) 253 } 254 for _, f := range top8 { 255 if f != nil { 256 fshort = append(fshort, f) 257 } 258 } 259 260 // TODO: Avoid rebuilding the entire list on every tick 261 fList := make(map[int]*ForumTopicListHolder) 262 for _, f := range fshort { 263 topicList, pagi := []*TopicsRow{}, tList.defaultPagi() 264 if f.TopicCount != 0 { 265 topicList, pagi, err = tList.RawGetListByForum(f, 1, 0) 266 if err != nil { 267 return err 268 } 269 } 270 fList[f.ID] = &ForumTopicListHolder{topicList, pagi} 271 272 /*topicList, pagi, err := tList.GetListByForum(f, 1, 0) 273 if err != nil { 274 return err 275 } 276 fList[f.ID] = &ForumTopicListHolder{topicList, pagi}*/ 277 } 278 279 //fmt.Printf("fList: %+v\n", fList) 280 tList.setForumList(fList) 281 282 hTbl := GetHookTable() 283 _, _ = hTbl.VhookSkippable("tasks_tick_topic_list", tList) 284 285 return nil 286 } 287 288 func (tList *DefaultTopicList) defaultPagi() Paginator { 289 /*_, page, lastPage := PageOffset(f.TopicCount, page, Config.ItemsPerPage) 290 pageList := Paginate(page, lastPage, 5) 291 return topicList, Paginator{pageList, page, lastPage}, nil*/ 292 return Paginator{[]int{}, 1, 1} 293 } 294 295 func (tList *DefaultTopicList) setForumList(forums map[int]*ForumTopicListHolder) { 296 tList.forumLock.Lock() 297 tList.forums = forums 298 tList.forumLock.Unlock() 299 } 300 301 /*var reloadForumMutex sync.Mutex 302 303 // TODO: Avoid firing this multiple times per sec tick 304 // TODO: Shard the forum topic list map 305 func (tList *DefaultTopicList) ReloadForum(id int) error { 306 reloadForumMutex.Lock() 307 defer reloadForumMutex.Unlock() 308 309 forum, err := Forums.Get(id) 310 if err != nil { 311 return err 312 } 313 314 ofList := make(map[int]*ForumTopicListHolder) 315 fList := make(map[int]*ForumTopicListHolder) 316 tList.forumLock.Lock() 317 ofList = tList.forums 318 for id, f := range ofList { 319 fList[id] = f 320 } 321 tList.forumLock.Unlock() 322 323 topicList, pagi := []*TopicsRow{}, tList.defaultPagi() 324 if forum.TopicCount != 0 { 325 topicList, pagi, err = tList.getListByForum(forum, 1, 0) 326 if err != nil { 327 return err 328 } 329 } 330 fList[forum.ID] = &ForumTopicListHolder{topicList, pagi} 331 332 tList.setForumList(fList) 333 return nil 334 }*/ 335 336 // TODO: Add Topics() method to *Forum? 337 // TODO: Implement orderby 338 func (tList *DefaultTopicList) GetListByForum(f *Forum, page, orderby int) (topicList []*TopicsRow, pagi Paginator, err error) { 339 if page == 0 { 340 page = 1 341 } 342 if f.TopicCount == 0 { 343 return topicList, tList.defaultPagi(), nil 344 } 345 if page == 1 && orderby == 0 { 346 var h *ForumTopicListHolder 347 var ok bool 348 tList.forumLock.RLock() 349 h, ok = tList.forums[f.ID] 350 tList.forumLock.RUnlock() 351 if ok { 352 return h.List, h.Paginator, nil 353 } 354 } 355 return tList.RawGetListByForum(f, page, orderby) 356 } 357 358 func (tList *DefaultTopicList) RawGetListByForum(f *Forum, page, orderby int) (topicList []*TopicsRow, pagi Paginator, err error) { 359 // TODO: Does forum.TopicCount take the deleted items into consideration for guests? We don't have soft-delete yet, only hard-delete 360 offset, page, lastPage := PageOffset(f.TopicCount, page, Config.ItemsPerPage) 361 362 rows, err := tList.getTopicsByForum.Query(f.ID, offset, Config.ItemsPerPage) 363 if err != nil { 364 return nil, tList.defaultPagi(), err 365 } 366 defer rows.Close() 367 368 // TODO: Use something other than TopicsRow as we don't need to store the forum name and link on each and every topic item? 369 reqUserList := make(map[int]bool) 370 for rows.Next() { 371 t := TopicsRow{Topic: Topic{ParentID: f.ID}} 372 err := rows.Scan(&t.ID, &t.Title, &t.Content, &t.CreatedBy, &t.IsClosed, &t.Sticky, &t.CreatedAt, &t.LastReplyAt, &t.LastReplyBy, &t.LastReplyID, &t.ViewCount, &t.PostCount, &t.LikeCount) 373 if err != nil { 374 return nil, tList.defaultPagi(), err 375 } 376 377 t.Link = BuildTopicURL(NameToSlug(t.Title), t.ID) 378 // TODO: Create a specialised function with a bit less overhead for getting the last page for a post count 379 _, _, lastPage := PageOffset(t.PostCount, 1, Config.ItemsPerPage) 380 t.LastPage = lastPage 381 382 //header.Hooks.VhookNoRet("forum_trow_assign", &t, &forum) 383 topicList = append(topicList, &t) 384 reqUserList[t.CreatedBy] = true 385 reqUserList[t.LastReplyBy] = true 386 } 387 if err = rows.Err(); err != nil { 388 return nil, tList.defaultPagi(), err 389 } 390 391 // Convert the user ID map to a slice, then bulk load the users 392 idSlice := make([]int, len(reqUserList)) 393 var i int 394 for userID := range reqUserList { 395 idSlice[i] = userID 396 i++ 397 } 398 399 // TODO: What if a user is deleted via the Control Panel? 400 userList, err := Users.BulkGetMap(idSlice) 401 if err != nil { 402 return nil, tList.defaultPagi(), err 403 } 404 405 // Second pass to the add the user data 406 // TODO: Use a pointer to TopicsRow instead of TopicsRow itself? 407 for _, t := range topicList { 408 t.Creator = userList[t.CreatedBy] 409 t.LastUser = userList[t.LastReplyBy] 410 } 411 412 if len(topicList) == 0 { 413 return topicList, tList.defaultPagi(), nil 414 } 415 pageList := Paginate(page, lastPage, 5) 416 return topicList, Paginator{pageList, page, lastPage}, nil 417 } 418 419 func (tList *DefaultTopicList) GetListByGroup(g *Group, page, orderby int, filterIDs []int) (topicList []*TopicsRow, forumList []Forum, pagi Paginator, err error) { 420 if page == 0 { 421 page = 1 422 } 423 // TODO: Cache the first three pages not just the first along with all the topics on this beaten track 424 // TODO: Move this into CanSee to reduce redundancy 425 if (page == 1 || page == 2) && orderby == 0 && len(filterIDs) == 0 { 426 var h [2]*TopicListHolder 427 var ok bool 428 if g.ID%2 == 0 { 429 tList.evenLock.RLock() 430 h, ok = tList.evenGroups[g.ID] 431 tList.evenLock.RUnlock() 432 } else { 433 tList.oddLock.RLock() 434 h, ok = tList.oddGroups[g.ID] 435 tList.oddLock.RUnlock() 436 } 437 if ok { 438 return h[page-1].List, h[page-1].ForumList, h[page-1].Paginator, nil 439 } 440 } 441 442 // TODO: Make CanSee a method on *Group with a canSee field? Have a CanSee method on *User to cover the case of superadmins? 443 //log.Printf("deoptimising for %d on page %d\n", g.ID, page) 444 return tList.GetListByCanSee(g.CanSee, page, orderby, filterIDs) 445 } 446 447 func (tList *DefaultTopicList) GetListByCanSee(canSee []int, page, orderby int, filterIDs []int) (topicList []*TopicsRow, forumList []Forum, pagi Paginator, err error) { 448 // TODO: Optimise this by filtering canSee and then fetching the forums? 449 // We need a list of the visible forums for Quick Topic 450 // ? - Would it be useful, if we could post in social groups from /topics/? 451 for _, fid := range canSee { 452 f := Forums.DirtyGet(fid) 453 if f.Name != "" && f.Active && (f.ParentType == "" || f.ParentType == "forum") /*&& f.TopicCount != 0*/ { 454 fcopy := f.Copy() 455 // TODO: Add a hook here for plugin_guilds !! 456 forumList = append(forumList, fcopy) 457 } 458 } 459 460 inSlice := func(haystack []int, needle int) bool { 461 for _, it := range haystack { 462 if needle == it { 463 return true 464 } 465 } 466 return false 467 } 468 469 var filteredForums []Forum 470 if len(filterIDs) > 0 { 471 for _, f := range forumList { 472 if inSlice(filterIDs, f.ID) { 473 filteredForums = append(filteredForums, f) 474 } 475 } 476 } else { 477 filteredForums = forumList 478 } 479 if len(filteredForums) == 1 && orderby == 0 { 480 topicList, pagi, err = tList.GetListByForum(&filteredForums[0], page, orderby) 481 return topicList, forumList, pagi, err 482 } 483 484 var topicCount int 485 for _, f := range filteredForums { 486 topicCount += f.TopicCount 487 } 488 489 // ? - Should we be showing plugin_guilds posts on /topics/? 490 argList, qlist := ForumListToArgQ(filteredForums) 491 if qlist == "" { 492 // We don't want to kill the page, so pass an empty slice and nil error 493 return topicList, filteredForums, tList.defaultPagi(), nil 494 } 495 496 topicList, pagi, err = tList.getList(page, orderby, topicCount, argList, qlist) 497 return topicList, filteredForums, pagi, err 498 } 499 500 // TODO: Reduce the number of returns 501 func (tList *DefaultTopicList) GetList(page, orderby int, filterIDs []int) (topicList []*TopicsRow, forumList []Forum, pagi Paginator, err error) { 502 // TODO: Make CanSee a method on *Group with a canSee field? Have a CanSee method on *User to cover the case of superadmins? 503 cCanSee, err := Forums.GetAllVisibleIDs() 504 if err != nil { 505 return nil, nil, tList.defaultPagi(), err 506 } 507 //log.Printf("cCanSee: %+v\n", cCanSee) 508 inSlice := func(haystack []int, needle int) bool { 509 for _, it := range haystack { 510 if needle == it { 511 return true 512 } 513 } 514 return false 515 } 516 517 var canSee []int 518 if len(filterIDs) > 0 { 519 for _, fid := range cCanSee { 520 if inSlice(filterIDs, fid) { 521 canSee = append(canSee, fid) 522 } 523 } 524 } else { 525 canSee = cCanSee 526 } 527 //log.Printf("canSee: %+v\n", canSee) 528 529 // We need a list of the visible forums for Quick Topic 530 // ? - Would it be useful, if we could post in social groups from /topics/? 531 var topicCount int 532 for _, fid := range canSee { 533 f := Forums.DirtyGet(fid) 534 if f.Name != "" && f.Active && (f.ParentType == "" || f.ParentType == "forum") /*&& f.TopicCount != 0*/ { 535 fcopy := f.Copy() 536 // TODO: Add a hook here for plugin_guilds 537 forumList = append(forumList, fcopy) 538 topicCount += fcopy.TopicCount 539 } 540 } 541 if len(forumList) == 1 && orderby == 0 { 542 topicList, pagi, err = tList.GetListByForum(&forumList[0], page, orderby) 543 return topicList, forumList, pagi, err 544 } 545 546 // ? - Should we be showing plugin_guilds posts on /topics/? 547 argList, qlist := ForumListToArgQ(forumList) 548 if qlist == "" { 549 // If the super admin can't see anything, then things have gone terribly wrong 550 return topicList, forumList, tList.defaultPagi(), err 551 } 552 553 topicList, pagi, err = tList.getList(page, orderby, topicCount, argList, qlist) 554 return topicList, forumList, pagi, err 555 } 556 557 // TODO: Rename this to TopicListStore and pass back a TopicList instance holding the pagination data and topic list rather than passing them back one argument at a time 558 // TODO: Make orderby an enum of sorts 559 func (tList *DefaultTopicList) getList(page, orderby, topicCount int, argList []interface{}, qlist string) (topicList []*TopicsRow, paginator Paginator, err error) { 560 if topicCount == 0 { 561 return nil, tList.defaultPagi(), err 562 } 563 //log.Printf("argList: %+v\n",argList) 564 //log.Printf("qlist: %+v\n",qlist) 565 var cols, orderq string 566 var stmt *sql.Stmt 567 switch orderby { 568 case TopicListWeekViews: 569 tList.qLock.RLock() 570 stmt = tList.qcounts[len(argList)-2] 571 tList.qLock.RUnlock() 572 if stmt == nil { 573 orderq = "weekViews DESC,lastReplyAt DESC,createdBy DESC" 574 now := time.Now() 575 _, week := now.ISOWeek() 576 day := int(now.Weekday()) + 1 577 if week%2 == 0 { // is even? 578 cols = "tid,title,content,createdBy,is_closed,sticky,createdAt,lastReplyAt,lastReplyBy,lastReplyID,parentID,views,postCount,likeCount,attachCount,poll,data,FLOOR(weekEvenViews+((weekOddViews/7)*" + strconv.Itoa(day) + ")) AS weekViews" 579 } else { 580 cols = "tid,title,content,createdBy,is_closed,sticky,createdAt,lastReplyAt,lastReplyBy,lastReplyID,parentID,views,postCount,likeCount,attachCount,poll,data,FLOOR(weekOddViews+((weekEvenViews/7)*" + strconv.Itoa(day) + ")) AS weekViews" 581 } 582 topicCount, err = ArgQToWeekViewTopicCount(argList, qlist) 583 if err != nil { 584 return nil, tList.defaultPagi(), err 585 } 586 acc := qgen.NewAcc() 587 stmt = acc.Select("topics").Columns(cols).Where("parentID IN(" + qlist + ") AND (weekEvenViews!=0 OR weekOddViews!=0)").Orderby(orderq).Limit("?,?").ComplexPrepare() 588 if e := acc.FirstError(); e != nil { 589 return nil, tList.defaultPagi(), e 590 } 591 defer stmt.Close() 592 } 593 case TopicListMostViewed: 594 tList.qLock.RLock() 595 stmt = tList.qcounts[len(argList)-2] 596 tList.qLock.RUnlock() 597 if stmt == nil { 598 orderq = "views DESC,lastReplyAt DESC,createdBy DESC" 599 cols = "tid,title,content,createdBy,is_closed,sticky,createdAt,lastReplyAt,lastReplyBy,lastReplyID,parentID,views,postCount,likeCount,attachCount,poll,data,weekEvenViews" 600 } 601 default: 602 tList.qLock2.RLock() 603 stmt = tList.qcounts2[len(argList)-2] 604 tList.qLock2.RUnlock() 605 if stmt == nil { 606 orderq = "sticky DESC,lastReplyAt DESC,createdBy DESC" 607 cols = "tid,title,content,createdBy,is_closed,sticky,createdAt,lastReplyAt,lastReplyBy,lastReplyID,parentID,views,postCount,likeCount,attachCount,poll,data,weekEvenViews" 608 } 609 } 610 offset, page, lastPage := PageOffset(topicCount, page, Config.ItemsPerPage) 611 612 // TODO: Prepare common qlist lengths to speed this up in common cases, prepared statements are prepared lazily anyway, so it probably doesn't matter if we do ten or so 613 if stmt == nil { 614 stmt, err = qgen.Builder.SimpleSelect("topics", cols, "parentID IN("+qlist+")", orderq, "?,?") 615 if err != nil { 616 return nil, tList.defaultPagi(), err 617 } 618 defer stmt.Close() 619 } 620 621 argList = append(argList, offset) 622 argList = append(argList, Config.ItemsPerPage) 623 624 rows, err := stmt.Query(argList...) 625 if err != nil { 626 return nil, tList.defaultPagi(), err 627 } 628 defer rows.Close() 629 630 rc, tc := Rstore.GetCache(), Topics.GetCache() 631 rcap := rc.GetCapacity() 632 rlen := rc.Length() 633 reqUserList := make(map[int]bool) 634 for rows.Next() { 635 // TODO: Embed Topic structs in TopicsRow to make it easier for us to reuse this work in the topic cache 636 t := TopicsRow{} 637 //var weekViews []uint8 638 err := rows.Scan(&t.ID, &t.Title, &t.Content, &t.CreatedBy, &t.IsClosed, &t.Sticky, &t.CreatedAt, &t.LastReplyAt, &t.LastReplyBy, &t.LastReplyID, &t.ParentID, &t.ViewCount, &t.PostCount, &t.LikeCount, &t.AttachCount, &t.Poll, &t.Data, &t.WeekViews) 639 if err != nil { 640 return nil, tList.defaultPagi(), err 641 } 642 //t.WeekViews = int(weekViews[0]) 643 //log.Printf("t: %+v\n", t) 644 //log.Printf("weekViews: %+v\n", weekViews) 645 646 t.Link = BuildTopicURL(NameToSlug(t.Title), t.ID) 647 // TODO: Pass forum to something like topicItem.Forum and use that instead of these two properties? Could be more flexible. 648 forum := Forums.DirtyGet(t.ParentID) 649 t.ForumName = forum.Name 650 t.ForumLink = forum.Link 651 652 // TODO: Create a specialised function with a bit less overhead for getting the last page for a post count 653 _, _, lastPage := PageOffset(t.PostCount, 1, Config.ItemsPerPage) 654 t.LastPage = lastPage 655 656 // TODO: Rename this Vhook to better reflect moving the topic list from /routes/ to /common/ 657 GetHookTable().Vhook("topics_topic_row_assign", &t, &forum) 658 topicList = append(topicList, &t) 659 reqUserList[t.CreatedBy] = true 660 reqUserList[t.LastReplyBy] = true 661 662 //log.Print("rlen: ", rlen) 663 //log.Print("rcap: ", rcap) 664 //log.Print("t.PostCount: ", t.PostCount) 665 //log.Print("t.PostCount == 2 && rlen < rcap: ", t.PostCount == 2 && rlen < rcap) 666 667 // Avoid the extra queries on topic list pages, if we already have what we want... 668 hRids := false 669 if tc != nil { 670 if t, e := tc.Get(t.ID); e == nil { 671 hRids = len(t.Rids) != 0 672 } 673 } 674 675 if t.PostCount == 2 && rlen < rcap && !hRids && page < 5 { 676 rids, err := GetRidsForTopic(t.ID, 0) 677 if err != nil { 678 return nil, tList.defaultPagi(), err 679 } 680 681 //log.Print("rids: ", rids) 682 if len(rids) == 0 { 683 continue 684 } 685 _, _ = Rstore.Get(rids[0]) 686 rlen++ 687 t.Rids = []int{rids[0]} 688 } 689 690 if tc != nil { 691 if _, e := tc.Get(t.ID); e == sql.ErrNoRows { 692 //_ = tc.Set(t.Topic()) 693 _ = tc.Set(&t.Topic) 694 } 695 } 696 } 697 if err = rows.Err(); err != nil { 698 return nil, tList.defaultPagi(), err 699 } 700 701 // TODO: specialcase for when reqUserList only has one or two items to avoid map alloc 702 if len(reqUserList) == 1 { 703 var u *User 704 for uid, _ := range reqUserList { 705 u, err = Users.Get(uid) 706 if err != nil { 707 return nil, tList.defaultPagi(), err 708 } 709 } 710 for _, t := range topicList { 711 t.Creator = u 712 t.LastUser = u 713 } 714 } else if len(reqUserList) > 0 { 715 // Convert the user ID map to a slice, then bulk load the users 716 idSlice := make([]int, len(reqUserList)) 717 var i int 718 for userID := range reqUserList { 719 idSlice[i] = userID 720 i++ 721 } 722 723 // TODO: What if a user is deleted via the Control Panel? 724 userList, err := Users.BulkGetMap(idSlice) 725 if err != nil { 726 return nil, tList.defaultPagi(), err 727 } 728 729 // Second pass to the add the user data 730 // TODO: Use a pointer to TopicsRow instead of TopicsRow itself? 731 for _, t := range topicList { 732 t.Creator = userList[t.CreatedBy] 733 t.LastUser = userList[t.LastReplyBy] 734 } 735 } 736 737 pageList := Paginate(page, lastPage, 5) 738 return topicList, Paginator{pageList, page, lastPage}, nil 739 } 740 741 // Internal. Don't rely on it. 742 func ForumListToArgQ(forums []Forum) (argList []interface{}, qlist string) { 743 for _, forum := range forums { 744 argList = append(argList, strconv.Itoa(forum.ID)) 745 qlist += "?," 746 } 747 if qlist != "" { 748 qlist = qlist[0 : len(qlist)-1] 749 } 750 return argList, qlist 751 } 752 753 // Internal. Don't rely on it. 754 // TODO: Check the TopicCount field on the forums instead? Make sure it's in sync first. 755 func ArgQToTopicCount(argList []interface{}, qlist string) (topicCount int, err error) { 756 topicCountStmt, err := qgen.Builder.SimpleCount("topics", "parentID IN("+qlist+")", "") 757 if err != nil { 758 return 0, err 759 } 760 defer topicCountStmt.Close() 761 762 err = topicCountStmt.QueryRow(argList...).Scan(&topicCount) 763 if err != nil && err != ErrNoRows { 764 return 0, err 765 } 766 return topicCount, err 767 } 768 769 // Internal. Don't rely on it. 770 func ArgQToWeekViewTopicCount(argList []interface{}, qlist string) (topicCount int, err error) { 771 topicCountStmt, err := qgen.Builder.SimpleCount("topics", "parentID IN("+qlist+") AND (weekEvenViews!=0 OR weekOddViews!=0)", "") 772 if err != nil { 773 return 0, err 774 } 775 defer topicCountStmt.Close() 776 777 err = topicCountStmt.QueryRow(argList...).Scan(&topicCount) 778 if err != nil && err != ErrNoRows { 779 return 0, err 780 } 781 return topicCount, err 782 } 783 784 func TopicCountInForums(forums []Forum) (topicCount int, err error) { 785 for _, f := range forums { 786 topicCount += f.TopicCount 787 } 788 return topicCount, nil 789 }