2017-12-26 07:17:26 +00:00
package common
2018-02-10 15:07:21 +00:00
import (
"strconv"
"sync"
"../query_gen/lib"
)
var TopicList TopicListInt
type TopicListHolder struct {
List [ ] * TopicsRow
ForumList [ ] Forum
PageList [ ] int
Page int
LastPage int
}
type TopicListInt interface {
GetListByGroup ( group * Group , page int ) ( topicList [ ] * TopicsRow , forumList [ ] Forum , pageList [ ] int , outPage int , lastPage int , err error )
GetList ( page int ) ( topicList [ ] * TopicsRow , forumList [ ] Forum , pageList [ ] int , outPage int , lastPage int , err error )
}
type DefaultTopicList struct {
oddGroups map [ int ] * TopicListHolder
evenGroups map [ int ] * TopicListHolder
oddLock sync . RWMutex
evenLock sync . RWMutex
groupList [ ] int // TODO: Use an atomic.Value instead to allow this to be updated on long ticks
}
func NewDefaultTopicList ( ) ( * DefaultTopicList , error ) {
tList := & DefaultTopicList {
oddGroups : make ( map [ int ] * TopicListHolder ) ,
evenGroups : make ( map [ int ] * TopicListHolder ) ,
}
var slots = make ( [ ] int , 8 ) // Only cache the topic list for eight groups
// TODO: Do something more efficient than this
allGroups , err := Groups . GetAll ( )
if err != nil {
return nil , err
}
if len ( allGroups ) > 0 {
var stopHere int
if len ( allGroups ) <= 8 {
stopHere = len ( allGroups )
} else {
stopHere = 8
}
var lowest = allGroups [ 0 ] . UserCount
for i := 0 ; i < stopHere ; i ++ {
slots [ i ] = i
if allGroups [ i ] . UserCount < lowest {
lowest = allGroups [ i ] . UserCount
}
}
var findNewLowest = func ( ) {
for _ , slot := range slots {
if allGroups [ slot ] . UserCount < lowest {
lowest = allGroups [ slot ] . UserCount
}
}
}
for i := 8 ; i < len ( allGroups ) ; i ++ {
if allGroups [ i ] . UserCount > lowest {
for ii , slot := range slots {
if allGroups [ i ] . UserCount > slot {
slots [ ii ] = i
lowest = allGroups [ i ] . UserCount
findNewLowest ( )
break
}
}
}
}
tList . groupList = slots
}
err = tList . Tick ( )
if err != nil {
return nil , err
}
AddScheduledHalfSecondTask ( tList . Tick )
//AddScheduledSecondTask(tList.GroupCountTick) // TODO: Dynamically change the groups in the short list to be optimised every second
return tList , nil
}
// TODO: Add support for groups other than the guest group
func ( tList * DefaultTopicList ) Tick ( ) error {
var oddLists = make ( map [ int ] * TopicListHolder )
var evenLists = make ( map [ int ] * TopicListHolder )
var addList = func ( gid int , topicList [ ] * TopicsRow , forumList [ ] Forum , pageList [ ] int , page int , lastPage int ) {
if gid % 2 == 0 {
evenLists [ gid ] = & TopicListHolder { topicList , forumList , pageList , page , lastPage }
} else {
oddLists [ gid ] = & TopicListHolder { topicList , forumList , pageList , page , lastPage }
}
}
guestGroup , err := Groups . Get ( GuestUser . Group )
if err != nil {
return err
}
topicList , forumList , pageList , page , lastPage , err := tList . getListByGroup ( guestGroup , 1 )
if err != nil {
return err
}
addList ( guestGroup . ID , topicList , forumList , pageList , page , lastPage )
for _ , gid := range tList . groupList {
group , err := Groups . Get ( gid ) // TODO: Bulk load the groups?
if err != nil {
return err
}
2018-02-15 13:15:27 +00:00
if group . UserCount == 0 {
continue
}
2018-02-10 15:07:21 +00:00
topicList , forumList , pageList , page , lastPage , err := tList . getListByGroup ( group , 1 )
if err != nil {
return err
}
addList ( group . ID , topicList , forumList , pageList , page , lastPage )
}
tList . oddLock . Lock ( )
tList . oddGroups = oddLists
tList . oddLock . Unlock ( )
tList . evenLock . Lock ( )
tList . evenGroups = evenLists
tList . evenLock . Unlock ( )
return nil
}
func ( tList * DefaultTopicList ) GetListByGroup ( group * Group , page int ) ( topicList [ ] * TopicsRow , forumList [ ] Forum , pageList [ ] int , outPage int , lastPage int , err error ) {
// TODO: Cache the first three pages not just the first along with all the topics on this beaten track
if page == 1 {
var holder * TopicListHolder
var ok bool
if group . ID % 2 == 0 {
tList . evenLock . RLock ( )
holder , ok = tList . evenGroups [ group . ID ]
tList . evenLock . RUnlock ( )
} else {
tList . oddLock . RLock ( )
holder , ok = tList . oddGroups [ group . ID ]
tList . oddLock . RUnlock ( )
}
if ok {
return holder . List , holder . ForumList , holder . PageList , holder . Page , holder . LastPage , nil
}
}
return tList . getListByGroup ( group , page )
}
func ( tList * DefaultTopicList ) getListByGroup ( group * Group , page int ) ( topicList [ ] * TopicsRow , forumList [ ] Forum , pageList [ ] int , outPage int , lastPage int , err error ) {
// TODO: Make CanSee a method on *Group with a canSee field? Have a CanSee method on *User to cover the case of superadmins?
canSee := group . CanSee
// We need a list of the visible forums for Quick Topic
// ? - Would it be useful, if we could post in social groups from /topics/?
for _ , fid := range canSee {
forum := Forums . DirtyGet ( fid )
if forum . Name != "" && forum . Active && ( forum . ParentType == "" || forum . ParentType == "forum" ) {
fcopy := forum . Copy ( )
// TODO: Add a hook here for plugin_guilds
forumList = append ( forumList , fcopy )
}
}
// ? - Should we be showing plugin_guilds posts on /topics/?
argList , qlist := ForumListToArgQ ( forumList )
if qlist == "" {
// We don't want to kill the page, so pass an empty slice and nil error
return topicList , forumList , pageList , 1 , 1 , nil
}
topicList , pageList , page , lastPage , err = tList . getList ( page , argList , qlist )
return topicList , forumList , pageList , page , lastPage , err
}
// TODO: Reduce the number of returns
func ( tList * DefaultTopicList ) GetList ( page int ) ( topicList [ ] * TopicsRow , forumList [ ] Forum , pageList [ ] int , outPage int , lastPage int , err error ) {
// TODO: Make CanSee a method on *Group with a canSee field? Have a CanSee method on *User to cover the case of superadmins?
canSee , err := Forums . GetAllVisibleIDs ( )
if err != nil {
return nil , nil , nil , 1 , 1 , err
}
// We need a list of the visible forums for Quick Topic
// ? - Would it be useful, if we could post in social groups from /topics/?
for _ , fid := range canSee {
forum := Forums . DirtyGet ( fid )
if forum . Name != "" && forum . Active && ( forum . ParentType == "" || forum . ParentType == "forum" ) {
fcopy := forum . Copy ( )
// TODO: Add a hook here for plugin_guilds
forumList = append ( forumList , fcopy )
}
}
// ? - Should we be showing plugin_guilds posts on /topics/?
argList , qlist := ForumListToArgQ ( forumList )
if qlist == "" {
// If the super admin can't see anything, then things have gone terribly wrong
return topicList , forumList , pageList , 1 , 1 , err
}
topicList , pageList , outPage , lastPage , err = tList . getList ( page , argList , qlist )
return topicList , forumList , pageList , outPage , lastPage , err
}
// 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
func ( tList * DefaultTopicList ) getList ( page int , argList [ ] interface { } , qlist string ) ( topicList [ ] * TopicsRow , pageList [ ] int , outPage int , lastPage int , err error ) {
topicCount , err := ArgQToTopicCount ( argList , qlist )
if err != nil {
return nil , nil , 1 , 1 , err
}
offset , page , lastPage := PageOffset ( topicCount , page , Config . ItemsPerPage )
stmt , err := qgen . Builder . SimpleSelect ( "topics" , "tid, title, content, createdBy, is_closed, sticky, createdAt, lastReplyAt, lastReplyBy, parentID, postCount, likeCount" , "parentID IN(" + qlist + ")" , "sticky DESC, lastReplyAt DESC, createdBy DESC" , "?,?" )
if err != nil {
return nil , nil , 1 , 1 , err
}
defer stmt . Close ( )
argList = append ( argList , offset )
argList = append ( argList , Config . ItemsPerPage )
rows , err := stmt . Query ( argList ... )
if err != nil {
return nil , nil , 1 , 1 , err
}
defer rows . Close ( )
var reqUserList = make ( map [ int ] bool )
for rows . Next ( ) {
topicItem := TopicsRow { ID : 0 }
err := rows . Scan ( & topicItem . ID , & topicItem . Title , & topicItem . Content , & topicItem . CreatedBy , & topicItem . IsClosed , & topicItem . Sticky , & topicItem . CreatedAt , & topicItem . LastReplyAt , & topicItem . LastReplyBy , & topicItem . ParentID , & topicItem . PostCount , & topicItem . LikeCount )
if err != nil {
return nil , nil , 1 , 1 , err
}
topicItem . Link = BuildTopicURL ( NameToSlug ( topicItem . Title ) , topicItem . ID )
forum := Forums . DirtyGet ( topicItem . ParentID )
topicItem . ForumName = forum . Name
topicItem . ForumLink = forum . Link
//topicItem.CreatedAt = RelativeTime(topicItem.CreatedAt)
topicItem . RelativeLastReplyAt = RelativeTime ( topicItem . LastReplyAt )
// TODO: Rename this Vhook to better reflect moving the topic list from /routes/ to /common/
2018-02-15 13:15:27 +00:00
RunVhook ( "topics_topic_row_assign" , & topicItem , & forum )
2018-02-10 15:07:21 +00:00
topicList = append ( topicList , & topicItem )
reqUserList [ topicItem . CreatedBy ] = true
reqUserList [ topicItem . LastReplyBy ] = true
}
err = rows . Err ( )
if err != nil {
return nil , nil , 1 , 1 , err
}
// Convert the user ID map to a slice, then bulk load the users
var idSlice = make ( [ ] int , len ( reqUserList ) )
var i int
for userID := range reqUserList {
idSlice [ i ] = userID
i ++
}
// TODO: What if a user is deleted via the Control Panel?
userList , err := Users . BulkGetMap ( idSlice )
if err != nil {
return nil , nil , 1 , 1 , err
}
// Second pass to the add the user data
// TODO: Use a pointer to TopicsRow instead of TopicsRow itself?
for _ , topicItem := range topicList {
topicItem . Creator = userList [ topicItem . CreatedBy ]
topicItem . LastUser = userList [ topicItem . LastReplyBy ]
}
pageList = Paginate ( topicCount , Config . ItemsPerPage , 5 )
return topicList , pageList , page , lastPage , nil
}
2017-12-26 07:17:26 +00:00
// Internal. Don't rely on it.
func ForumListToArgQ ( forums [ ] Forum ) ( argList [ ] interface { } , qlist string ) {
for _ , forum := range forums {
argList = append ( argList , strconv . Itoa ( forum . ID ) )
qlist += "?,"
}
if qlist != "" {
qlist = qlist [ 0 : len ( qlist ) - 1 ]
}
return argList , qlist
}
// Internal. Don't rely on it.
func ArgQToTopicCount ( argList [ ] interface { } , qlist string ) ( topicCount int , err error ) {
topicCountStmt , err := qgen . Builder . SimpleCount ( "topics" , "parentID IN(" + qlist + ")" , "" )
if err != nil {
return 0 , err
}
defer topicCountStmt . Close ( )
err = topicCountStmt . QueryRow ( argList ... ) . Scan ( & topicCount )
if err != nil && err != ErrNoRows {
return 0 , err
}
return topicCount , err
}
func TopicCountInForums ( forums [ ] Forum ) ( topicCount int , err error ) {
return ArgQToTopicCount ( ForumListToArgQ ( forums ) )
}