diff --git a/common/counters/forums.go b/common/counters/forums.go index 296b832e..57f815dd 100644 --- a/common/counters/forums.go +++ b/common/counters/forums.go @@ -8,7 +8,7 @@ import ( "../../query_gen/lib" ) -// TODO: The forum view counter +var ForumViewCounter *DefaultForumViewCounter // TODO: Unload forum counters without any views over the past 15 minutes, if the admin has configured the forumstore with a cap and it's been hit? // Forums can be reloaded from the database at any time, so we want to keep the counters separate from them @@ -29,7 +29,7 @@ func NewDefaultForumViewCounter() (*DefaultForumViewCounter, error) { insert: acc.Insert("viewchunks_forums").Columns("count, createdAt, forum").Fields("?,UTC_TIMESTAMP(),?").Prepare(), } common.AddScheduledFifteenMinuteTask(counter.Tick) // There could be a lot of routes, so we don't want to be running this every second - //AddScheduledSecondTask(counter.Tick) + //common.AddScheduledSecondTask(counter.Tick) common.AddShutdownTask(counter.Tick) return counter, acc.FirstError() } @@ -83,4 +83,36 @@ func (counter *DefaultForumViewCounter) insertChunk(count int, forum int) error return err } +func (counter *DefaultForumViewCounter) Bump(forumID int) { + // Is the ID even? + if forumID%2 == 0 { + counter.evenLock.RLock() + forum, ok := counter.evenMap[forumID] + counter.evenLock.RUnlock() + if ok { + forum.Lock() + forum.counter++ + forum.Unlock() + } else { + counter.evenLock.Lock() + counter.evenMap[forumID] = &RWMutexCounterBucket{counter: 1} + counter.evenLock.Unlock() + } + return + } + + counter.oddLock.RLock() + forum, ok := counter.oddMap[forumID] + counter.oddLock.RUnlock() + if ok { + forum.Lock() + forum.counter++ + forum.Unlock() + } else { + counter.oddLock.Lock() + counter.oddMap[forumID] = &RWMutexCounterBucket{counter: 1} + counter.oddLock.Unlock() + } +} + // TODO: Add a forum counter backed by two maps which grow as forums are created but never shrinks diff --git a/gen_mssql.go b/gen_mssql.go index be837151..59b35a61 100644 --- a/gen_mssql.go +++ b/gen_mssql.go @@ -23,7 +23,6 @@ type Stmts struct { getActivityEntry *sql.Stmt forumEntryExists *sql.Stmt groupEntryExists *sql.Stmt - getForumTopicsOffset *sql.Stmt getAttachment *sql.Stmt getForumTopics *sql.Stmt getProfileReplies *sql.Stmt @@ -164,13 +163,6 @@ func _gen_mssql() (err error) { return err } - log.Print("Preparing getForumTopicsOffset statement.") - stmts.getForumTopicsOffset, err = db.Prepare("SELECT [tid],[title],[content],[createdBy],[is_closed],[sticky],[createdAt],[lastReplyAt],[lastReplyBy],[parentID],[postCount],[likeCount] FROM [topics] WHERE [parentID] = ?1 ORDER BY sticky DESC,lastReplyAt DESC,createdBy DESC OFFSET ?2 ROWS FETCH NEXT ?3 ROWS ONLY") - if err != nil { - log.Print("Bad Query: ","SELECT [tid],[title],[content],[createdBy],[is_closed],[sticky],[createdAt],[lastReplyAt],[lastReplyBy],[parentID],[postCount],[likeCount] FROM [topics] WHERE [parentID] = ?1 ORDER BY sticky DESC,lastReplyAt DESC,createdBy DESC OFFSET ?2 ROWS FETCH NEXT ?3 ROWS ONLY") - return err - } - log.Print("Preparing getAttachment statement.") stmts.getAttachment, err = db.Prepare("SELECT [sectionID],[sectionTable],[originID],[originTable],[uploadedBy],[path] FROM [attachments] WHERE [path] = ?1 AND [sectionID] = ?2 AND [sectionTable] = ?3") if err != nil { diff --git a/gen_mysql.go b/gen_mysql.go index b5be4b99..73296d55 100644 --- a/gen_mysql.go +++ b/gen_mysql.go @@ -25,7 +25,6 @@ type Stmts struct { getActivityEntry *sql.Stmt forumEntryExists *sql.Stmt groupEntryExists *sql.Stmt - getForumTopicsOffset *sql.Stmt getAttachment *sql.Stmt getForumTopics *sql.Stmt getProfileReplies *sql.Stmt @@ -152,12 +151,6 @@ func _gen_mysql() (err error) { return err } - log.Print("Preparing getForumTopicsOffset statement.") - stmts.getForumTopicsOffset, err = db.Prepare("SELECT `tid`,`title`,`content`,`createdBy`,`is_closed`,`sticky`,`createdAt`,`lastReplyAt`,`lastReplyBy`,`parentID`,`postCount`,`likeCount` FROM `topics` WHERE `parentID` = ? ORDER BY sticky DESC,lastReplyAt DESC,createdBy DESC LIMIT ?,?") - if err != nil { - return err - } - log.Print("Preparing getAttachment statement.") stmts.getAttachment, err = db.Prepare("SELECT `sectionID`,`sectionTable`,`originID`,`originTable`,`uploadedBy`,`path` FROM `attachments` WHERE `path` = ? AND `sectionID` = ? AND `sectionTable` = ?") if err != nil { diff --git a/gen_router.go b/gen_router.go index 4dec4f8a..a4445f22 100644 --- a/gen_router.go +++ b/gen_router.go @@ -21,7 +21,7 @@ var RouteMap = map[string]interface{}{ "routes.Overview": routes.Overview, "routes.CustomPage": routes.CustomPage, "routeForums": routeForums, - "routeForum": routeForum, + "routes.ViewForum": routes.ViewForum, "routeChangeTheme": routeChangeTheme, "routeShowAttachment": routeShowAttachment, "routeWebsockets": routeWebsockets, @@ -61,10 +61,12 @@ var RouteMap = map[string]interface{}{ "routePanelAnalyticsReferrers": routePanelAnalyticsReferrers, "routePanelAnalyticsRouteViews": routePanelAnalyticsRouteViews, "routePanelAnalyticsAgentViews": routePanelAnalyticsAgentViews, + "routePanelAnalyticsForumViews": routePanelAnalyticsForumViews, "routePanelAnalyticsSystemViews": routePanelAnalyticsSystemViews, "routePanelAnalyticsReferrerViews": routePanelAnalyticsReferrerViews, "routePanelAnalyticsPosts": routePanelAnalyticsPosts, "routePanelAnalyticsTopics": routePanelAnalyticsTopics, + "routePanelAnalyticsForums": routePanelAnalyticsForums, "routePanelGroups": routePanelGroups, "routePanelGroupsEdit": routePanelGroupsEdit, "routePanelGroupsEditPerms": routePanelGroupsEditPerms, @@ -124,7 +126,7 @@ var routeMapEnum = map[string]int{ "routes.Overview": 1, "routes.CustomPage": 2, "routeForums": 3, - "routeForum": 4, + "routes.ViewForum": 4, "routeChangeTheme": 5, "routeShowAttachment": 6, "routeWebsockets": 7, @@ -164,68 +166,70 @@ var routeMapEnum = map[string]int{ "routePanelAnalyticsReferrers": 41, "routePanelAnalyticsRouteViews": 42, "routePanelAnalyticsAgentViews": 43, - "routePanelAnalyticsSystemViews": 44, - "routePanelAnalyticsReferrerViews": 45, - "routePanelAnalyticsPosts": 46, - "routePanelAnalyticsTopics": 47, - "routePanelGroups": 48, - "routePanelGroupsEdit": 49, - "routePanelGroupsEditPerms": 50, - "routePanelGroupsEditSubmit": 51, - "routePanelGroupsEditPermsSubmit": 52, - "routePanelGroupsCreateSubmit": 53, - "routePanelBackups": 54, - "routePanelLogsMod": 55, - "routePanelDebug": 56, - "routePanelDashboard": 57, - "routes.AccountEditCritical": 58, - "routeAccountEditCriticalSubmit": 59, - "routeAccountEditAvatar": 60, - "routeAccountEditAvatarSubmit": 61, - "routeAccountEditUsername": 62, - "routeAccountEditUsernameSubmit": 63, - "routeAccountEditEmail": 64, - "routeAccountEditEmailTokenSubmit": 65, - "routeProfile": 66, - "routes.BanUserSubmit": 67, - "routes.UnbanUser": 68, - "routes.ActivateUser": 69, - "routes.IPSearch": 70, - "routes.CreateTopicSubmit": 71, - "routes.EditTopicSubmit": 72, - "routes.DeleteTopicSubmit": 73, - "routes.StickTopicSubmit": 74, - "routes.UnstickTopicSubmit": 75, - "routes.LockTopicSubmit": 76, - "routes.UnlockTopicSubmit": 77, - "routes.MoveTopicSubmit": 78, - "routeLikeTopicSubmit": 79, - "routes.ViewTopic": 80, - "routeCreateReplySubmit": 81, - "routes.ReplyEditSubmit": 82, - "routes.ReplyDeleteSubmit": 83, - "routeReplyLikeSubmit": 84, - "routeProfileReplyCreateSubmit": 85, - "routes.ProfileReplyEditSubmit": 86, - "routes.ProfileReplyDeleteSubmit": 87, - "routes.PollVote": 88, - "routes.PollResults": 89, - "routes.AccountLogin": 90, - "routes.AccountRegister": 91, - "routeLogout": 92, - "routes.AccountLoginSubmit": 93, - "routes.AccountRegisterSubmit": 94, - "routeDynamic": 95, - "routeUploads": 96, - "routes.StaticFile": 97, - "BadRoute": 98, + "routePanelAnalyticsForumViews": 44, + "routePanelAnalyticsSystemViews": 45, + "routePanelAnalyticsReferrerViews": 46, + "routePanelAnalyticsPosts": 47, + "routePanelAnalyticsTopics": 48, + "routePanelAnalyticsForums": 49, + "routePanelGroups": 50, + "routePanelGroupsEdit": 51, + "routePanelGroupsEditPerms": 52, + "routePanelGroupsEditSubmit": 53, + "routePanelGroupsEditPermsSubmit": 54, + "routePanelGroupsCreateSubmit": 55, + "routePanelBackups": 56, + "routePanelLogsMod": 57, + "routePanelDebug": 58, + "routePanelDashboard": 59, + "routes.AccountEditCritical": 60, + "routeAccountEditCriticalSubmit": 61, + "routeAccountEditAvatar": 62, + "routeAccountEditAvatarSubmit": 63, + "routeAccountEditUsername": 64, + "routeAccountEditUsernameSubmit": 65, + "routeAccountEditEmail": 66, + "routeAccountEditEmailTokenSubmit": 67, + "routeProfile": 68, + "routes.BanUserSubmit": 69, + "routes.UnbanUser": 70, + "routes.ActivateUser": 71, + "routes.IPSearch": 72, + "routes.CreateTopicSubmit": 73, + "routes.EditTopicSubmit": 74, + "routes.DeleteTopicSubmit": 75, + "routes.StickTopicSubmit": 76, + "routes.UnstickTopicSubmit": 77, + "routes.LockTopicSubmit": 78, + "routes.UnlockTopicSubmit": 79, + "routes.MoveTopicSubmit": 80, + "routeLikeTopicSubmit": 81, + "routes.ViewTopic": 82, + "routeCreateReplySubmit": 83, + "routes.ReplyEditSubmit": 84, + "routes.ReplyDeleteSubmit": 85, + "routeReplyLikeSubmit": 86, + "routeProfileReplyCreateSubmit": 87, + "routes.ProfileReplyEditSubmit": 88, + "routes.ProfileReplyDeleteSubmit": 89, + "routes.PollVote": 90, + "routes.PollResults": 91, + "routes.AccountLogin": 92, + "routes.AccountRegister": 93, + "routeLogout": 94, + "routes.AccountLoginSubmit": 95, + "routes.AccountRegisterSubmit": 96, + "routeDynamic": 97, + "routeUploads": 98, + "routes.StaticFile": 99, + "BadRoute": 100, } var reverseRouteMapEnum = map[int]string{ 0: "routeAPI", 1: "routes.Overview", 2: "routes.CustomPage", 3: "routeForums", - 4: "routeForum", + 4: "routes.ViewForum", 5: "routeChangeTheme", 6: "routeShowAttachment", 7: "routeWebsockets", @@ -265,61 +269,63 @@ var reverseRouteMapEnum = map[int]string{ 41: "routePanelAnalyticsReferrers", 42: "routePanelAnalyticsRouteViews", 43: "routePanelAnalyticsAgentViews", - 44: "routePanelAnalyticsSystemViews", - 45: "routePanelAnalyticsReferrerViews", - 46: "routePanelAnalyticsPosts", - 47: "routePanelAnalyticsTopics", - 48: "routePanelGroups", - 49: "routePanelGroupsEdit", - 50: "routePanelGroupsEditPerms", - 51: "routePanelGroupsEditSubmit", - 52: "routePanelGroupsEditPermsSubmit", - 53: "routePanelGroupsCreateSubmit", - 54: "routePanelBackups", - 55: "routePanelLogsMod", - 56: "routePanelDebug", - 57: "routePanelDashboard", - 58: "routes.AccountEditCritical", - 59: "routeAccountEditCriticalSubmit", - 60: "routeAccountEditAvatar", - 61: "routeAccountEditAvatarSubmit", - 62: "routeAccountEditUsername", - 63: "routeAccountEditUsernameSubmit", - 64: "routeAccountEditEmail", - 65: "routeAccountEditEmailTokenSubmit", - 66: "routeProfile", - 67: "routes.BanUserSubmit", - 68: "routes.UnbanUser", - 69: "routes.ActivateUser", - 70: "routes.IPSearch", - 71: "routes.CreateTopicSubmit", - 72: "routes.EditTopicSubmit", - 73: "routes.DeleteTopicSubmit", - 74: "routes.StickTopicSubmit", - 75: "routes.UnstickTopicSubmit", - 76: "routes.LockTopicSubmit", - 77: "routes.UnlockTopicSubmit", - 78: "routes.MoveTopicSubmit", - 79: "routeLikeTopicSubmit", - 80: "routes.ViewTopic", - 81: "routeCreateReplySubmit", - 82: "routes.ReplyEditSubmit", - 83: "routes.ReplyDeleteSubmit", - 84: "routeReplyLikeSubmit", - 85: "routeProfileReplyCreateSubmit", - 86: "routes.ProfileReplyEditSubmit", - 87: "routes.ProfileReplyDeleteSubmit", - 88: "routes.PollVote", - 89: "routes.PollResults", - 90: "routes.AccountLogin", - 91: "routes.AccountRegister", - 92: "routeLogout", - 93: "routes.AccountLoginSubmit", - 94: "routes.AccountRegisterSubmit", - 95: "routeDynamic", - 96: "routeUploads", - 97: "routes.StaticFile", - 98: "BadRoute", + 44: "routePanelAnalyticsForumViews", + 45: "routePanelAnalyticsSystemViews", + 46: "routePanelAnalyticsReferrerViews", + 47: "routePanelAnalyticsPosts", + 48: "routePanelAnalyticsTopics", + 49: "routePanelAnalyticsForums", + 50: "routePanelGroups", + 51: "routePanelGroupsEdit", + 52: "routePanelGroupsEditPerms", + 53: "routePanelGroupsEditSubmit", + 54: "routePanelGroupsEditPermsSubmit", + 55: "routePanelGroupsCreateSubmit", + 56: "routePanelBackups", + 57: "routePanelLogsMod", + 58: "routePanelDebug", + 59: "routePanelDashboard", + 60: "routes.AccountEditCritical", + 61: "routeAccountEditCriticalSubmit", + 62: "routeAccountEditAvatar", + 63: "routeAccountEditAvatarSubmit", + 64: "routeAccountEditUsername", + 65: "routeAccountEditUsernameSubmit", + 66: "routeAccountEditEmail", + 67: "routeAccountEditEmailTokenSubmit", + 68: "routeProfile", + 69: "routes.BanUserSubmit", + 70: "routes.UnbanUser", + 71: "routes.ActivateUser", + 72: "routes.IPSearch", + 73: "routes.CreateTopicSubmit", + 74: "routes.EditTopicSubmit", + 75: "routes.DeleteTopicSubmit", + 76: "routes.StickTopicSubmit", + 77: "routes.UnstickTopicSubmit", + 78: "routes.LockTopicSubmit", + 79: "routes.UnlockTopicSubmit", + 80: "routes.MoveTopicSubmit", + 81: "routeLikeTopicSubmit", + 82: "routes.ViewTopic", + 83: "routeCreateReplySubmit", + 84: "routes.ReplyEditSubmit", + 85: "routes.ReplyDeleteSubmit", + 86: "routeReplyLikeSubmit", + 87: "routeProfileReplyCreateSubmit", + 88: "routes.ProfileReplyEditSubmit", + 89: "routes.ProfileReplyDeleteSubmit", + 90: "routes.PollVote", + 91: "routes.PollResults", + 92: "routes.AccountLogin", + 93: "routes.AccountRegister", + 94: "routeLogout", + 95: "routes.AccountLoginSubmit", + 96: "routes.AccountRegisterSubmit", + 97: "routeDynamic", + 98: "routeUploads", + 99: "routes.StaticFile", + 100: "BadRoute", } var osMapEnum = map[string]int{ "unknown": 0, @@ -571,7 +577,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { counters.GlobalViewCounter.Bump() if prefix == "/static" { - counters.RouteViewCounter.Bump(97) + counters.RouteViewCounter.Bump(99) req.URL.Path += extraData routes.StaticFile(w, req) return @@ -745,7 +751,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } case "/forum": counters.RouteViewCounter.Bump(4) - err = routeForum(w,req,user,extraData) + err = routes.ViewForum(w,req,user,extraData) if err != nil { router.handleError(err,w,req,user) } @@ -1052,11 +1058,14 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { case "/panel/analytics/agent/": counters.RouteViewCounter.Bump(43) err = routePanelAnalyticsAgentViews(w,req,user,extraData) - case "/panel/analytics/system/": + case "/panel/analytics/forum/": counters.RouteViewCounter.Bump(44) + err = routePanelAnalyticsForumViews(w,req,user,extraData) + case "/panel/analytics/system/": + counters.RouteViewCounter.Bump(45) err = routePanelAnalyticsSystemViews(w,req,user,extraData) case "/panel/analytics/referrer/": - counters.RouteViewCounter.Bump(45) + counters.RouteViewCounter.Bump(46) err = routePanelAnalyticsReferrerViews(w,req,user,extraData) case "/panel/analytics/posts/": err = common.ParseForm(w,req,user) @@ -1065,7 +1074,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(46) + counters.RouteViewCounter.Bump(47) err = routePanelAnalyticsPosts(w,req,user) case "/panel/analytics/topics/": err = common.ParseForm(w,req,user) @@ -1074,16 +1083,25 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(47) - err = routePanelAnalyticsTopics(w,req,user) - case "/panel/groups/": counters.RouteViewCounter.Bump(48) + err = routePanelAnalyticsTopics(w,req,user) + case "/panel/analytics/forums/": + err = common.ParseForm(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + return + } + + counters.RouteViewCounter.Bump(49) + err = routePanelAnalyticsForums(w,req,user) + case "/panel/groups/": + counters.RouteViewCounter.Bump(50) err = routePanelGroups(w,req,user) case "/panel/groups/edit/": - counters.RouteViewCounter.Bump(49) + counters.RouteViewCounter.Bump(51) err = routePanelGroupsEdit(w,req,user,extraData) case "/panel/groups/edit/perms/": - counters.RouteViewCounter.Bump(50) + counters.RouteViewCounter.Bump(52) err = routePanelGroupsEditPerms(w,req,user,extraData) case "/panel/groups/edit/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1092,7 +1110,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(51) + counters.RouteViewCounter.Bump(53) err = routePanelGroupsEditSubmit(w,req,user,extraData) case "/panel/groups/edit/perms/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1101,7 +1119,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(52) + counters.RouteViewCounter.Bump(54) err = routePanelGroupsEditPermsSubmit(w,req,user,extraData) case "/panel/groups/create/": err = common.NoSessionMismatch(w,req,user) @@ -1110,7 +1128,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(53) + counters.RouteViewCounter.Bump(55) err = routePanelGroupsCreateSubmit(w,req,user) case "/panel/backups/": err = common.SuperAdminOnly(w,req,user) @@ -1119,10 +1137,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(54) + counters.RouteViewCounter.Bump(56) err = routePanelBackups(w,req,user,extraData) case "/panel/logs/mod/": - counters.RouteViewCounter.Bump(55) + counters.RouteViewCounter.Bump(57) err = routePanelLogsMod(w,req,user) case "/panel/debug/": err = common.AdminOnly(w,req,user) @@ -1131,10 +1149,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(56) + counters.RouteViewCounter.Bump(58) err = routePanelDebug(w,req,user) default: - counters.RouteViewCounter.Bump(57) + counters.RouteViewCounter.Bump(59) err = routePanelDashboard(w,req,user) } if err != nil { @@ -1149,7 +1167,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(58) + counters.RouteViewCounter.Bump(60) err = routes.AccountEditCritical(w,req,user) case "/user/edit/critical/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1164,7 +1182,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(59) + counters.RouteViewCounter.Bump(61) err = routeAccountEditCriticalSubmit(w,req,user) case "/user/edit/avatar/": err = common.MemberOnly(w,req,user) @@ -1173,7 +1191,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(60) + counters.RouteViewCounter.Bump(62) err = routeAccountEditAvatar(w,req,user) case "/user/edit/avatar/submit/": err = common.MemberOnly(w,req,user) @@ -1193,7 +1211,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(61) + counters.RouteViewCounter.Bump(63) err = routeAccountEditAvatarSubmit(w,req,user) case "/user/edit/username/": err = common.MemberOnly(w,req,user) @@ -1202,7 +1220,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(62) + counters.RouteViewCounter.Bump(64) err = routeAccountEditUsername(w,req,user) case "/user/edit/username/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1217,7 +1235,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(63) + counters.RouteViewCounter.Bump(65) err = routeAccountEditUsernameSubmit(w,req,user) case "/user/edit/email/": err = common.MemberOnly(w,req,user) @@ -1226,7 +1244,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(64) + counters.RouteViewCounter.Bump(66) err = routeAccountEditEmail(w,req,user) case "/user/edit/token/": err = common.NoSessionMismatch(w,req,user) @@ -1241,11 +1259,11 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(65) + counters.RouteViewCounter.Bump(67) err = routeAccountEditEmailTokenSubmit(w,req,user,extraData) default: req.URL.Path += extraData - counters.RouteViewCounter.Bump(66) + counters.RouteViewCounter.Bump(68) err = routeProfile(w,req,user) } if err != nil { @@ -1266,7 +1284,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(67) + counters.RouteViewCounter.Bump(69) err = routes.BanUserSubmit(w,req,user,extraData) case "/users/unban/": err = common.NoSessionMismatch(w,req,user) @@ -1281,7 +1299,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(68) + counters.RouteViewCounter.Bump(70) err = routes.UnbanUser(w,req,user,extraData) case "/users/activate/": err = common.NoSessionMismatch(w,req,user) @@ -1296,7 +1314,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(69) + counters.RouteViewCounter.Bump(71) err = routes.ActivateUser(w,req,user,extraData) case "/users/ips/": err = common.MemberOnly(w,req,user) @@ -1305,7 +1323,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(70) + counters.RouteViewCounter.Bump(72) err = routes.IPSearch(w,req,user) } if err != nil { @@ -1331,7 +1349,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(71) + counters.RouteViewCounter.Bump(73) err = routes.CreateTopicSubmit(w,req,user) case "/topic/edit/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1346,7 +1364,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(72) + counters.RouteViewCounter.Bump(74) err = routes.EditTopicSubmit(w,req,user,extraData) case "/topic/delete/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1362,7 +1380,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } req.URL.Path += extraData - counters.RouteViewCounter.Bump(73) + counters.RouteViewCounter.Bump(75) err = routes.DeleteTopicSubmit(w,req,user) case "/topic/stick/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1377,7 +1395,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(74) + counters.RouteViewCounter.Bump(76) err = routes.StickTopicSubmit(w,req,user,extraData) case "/topic/unstick/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1392,7 +1410,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(75) + counters.RouteViewCounter.Bump(77) err = routes.UnstickTopicSubmit(w,req,user,extraData) case "/topic/lock/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1408,7 +1426,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } req.URL.Path += extraData - counters.RouteViewCounter.Bump(76) + counters.RouteViewCounter.Bump(78) err = routes.LockTopicSubmit(w,req,user) case "/topic/unlock/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1423,7 +1441,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(77) + counters.RouteViewCounter.Bump(79) err = routes.UnlockTopicSubmit(w,req,user,extraData) case "/topic/move/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1438,7 +1456,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(78) + counters.RouteViewCounter.Bump(80) err = routes.MoveTopicSubmit(w,req,user,extraData) case "/topic/like/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1453,10 +1471,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(79) + counters.RouteViewCounter.Bump(81) err = routeLikeTopicSubmit(w,req,user,extraData) default: - counters.RouteViewCounter.Bump(80) + counters.RouteViewCounter.Bump(82) err = routes.ViewTopic(w,req,user, extraData) } if err != nil { @@ -1482,7 +1500,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(81) + counters.RouteViewCounter.Bump(83) err = routeCreateReplySubmit(w,req,user) case "/reply/edit/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1497,7 +1515,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(82) + counters.RouteViewCounter.Bump(84) err = routes.ReplyEditSubmit(w,req,user,extraData) case "/reply/delete/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1512,7 +1530,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(83) + counters.RouteViewCounter.Bump(85) err = routes.ReplyDeleteSubmit(w,req,user,extraData) case "/reply/like/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1527,7 +1545,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(84) + counters.RouteViewCounter.Bump(86) err = routeReplyLikeSubmit(w,req,user,extraData) } if err != nil { @@ -1548,7 +1566,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(85) + counters.RouteViewCounter.Bump(87) err = routeProfileReplyCreateSubmit(w,req,user) case "/profile/reply/edit/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1563,7 +1581,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(86) + counters.RouteViewCounter.Bump(88) err = routes.ProfileReplyEditSubmit(w,req,user,extraData) case "/profile/reply/delete/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1578,7 +1596,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(87) + counters.RouteViewCounter.Bump(89) err = routes.ProfileReplyDeleteSubmit(w,req,user,extraData) } if err != nil { @@ -1599,10 +1617,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(88) + counters.RouteViewCounter.Bump(90) err = routes.PollVote(w,req,user,extraData) case "/poll/results/": - counters.RouteViewCounter.Bump(89) + counters.RouteViewCounter.Bump(91) err = routes.PollResults(w,req,user,extraData) } if err != nil { @@ -1611,10 +1629,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { case "/accounts": switch(req.URL.Path) { case "/accounts/login/": - counters.RouteViewCounter.Bump(90) + counters.RouteViewCounter.Bump(92) err = routes.AccountLogin(w,req,user) case "/accounts/create/": - counters.RouteViewCounter.Bump(91) + counters.RouteViewCounter.Bump(93) err = routes.AccountRegister(w,req,user) case "/accounts/logout/": err = common.NoSessionMismatch(w,req,user) @@ -1629,7 +1647,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(92) + counters.RouteViewCounter.Bump(94) err = routeLogout(w,req,user) case "/accounts/login/submit/": err = common.ParseForm(w,req,user) @@ -1638,7 +1656,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(93) + counters.RouteViewCounter.Bump(95) err = routes.AccountLoginSubmit(w,req,user) case "/accounts/create/submit/": err = common.ParseForm(w,req,user) @@ -1647,7 +1665,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(94) + counters.RouteViewCounter.Bump(96) err = routes.AccountRegisterSubmit(w,req,user) } if err != nil { @@ -1664,7 +1682,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { common.NotFound(w,req,nil) return } - counters.RouteViewCounter.Bump(96) + counters.RouteViewCounter.Bump(98) req.URL.Path += extraData // TODO: Find a way to propagate errors up from this? router.UploadHandler(w,req) // TODO: Count these views @@ -1707,7 +1725,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { router.RUnlock() if ok { - counters.RouteViewCounter.Bump(95) // TODO: Be more specific about *which* dynamic route it is + counters.RouteViewCounter.Bump(97) // TODO: Be more specific about *which* dynamic route it is req.URL.Path += extraData err = handle(w,req,user) if err != nil { @@ -1721,7 +1739,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { if strings.Contains(lowerPath,"admin") || strings.Contains(lowerPath,"sql") || strings.Contains(lowerPath,"manage") || strings.Contains(lowerPath,"//") || strings.Contains(lowerPath,"\\\\") || strings.Contains(lowerPath,"wp") || strings.Contains(lowerPath,"wordpress") || strings.Contains(lowerPath,"config") || strings.Contains(lowerPath,"setup") || strings.Contains(lowerPath,"install") || strings.Contains(lowerPath,"update") || strings.Contains(lowerPath,"php") { router.SuspiciousRequest(req) } - counters.RouteViewCounter.Bump(98) + counters.RouteViewCounter.Bump(100) common.NotFound(w,req,nil) } } diff --git a/main.go b/main.go index 97aa40e9..dbc52a6c 100644 --- a/main.go +++ b/main.go @@ -130,6 +130,10 @@ func afterDBInit() (err error) { if err != nil { return err } + counters.ForumViewCounter, err = counters.NewDefaultForumViewCounter() + if err != nil { + return err + } counters.ReferrerTracker, err = counters.NewDefaultReferrerTracker() if err != nil { return err diff --git a/panel_routes.go b/panel_routes.go index 668a8266..ef13cfa1 100644 --- a/panel_routes.go +++ b/panel_routes.go @@ -575,6 +575,12 @@ func panelAnalyticsTimeRange(rawTimeRange string) (timeRange AnalyticsTimeRange, timeRange.Slices = 30 timeRange.SliceWidth = 60 * 60 * 24 timeRange.Range = "one-month" + case "one-week": + timeRange.Quantity = 7 + timeRange.Unit = "day" + timeRange.Slices = 14 + timeRange.SliceWidth = 60 * 60 * 12 + timeRange.Range = "one-week" case "two-days": // Two days is experimental timeRange.Quantity = 2 timeRange.Unit = "day" @@ -599,24 +605,9 @@ func panelAnalyticsTimeRange(rawTimeRange string) (timeRange AnalyticsTimeRange, return timeRange, nil } -func routePanelAnalyticsViews(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) - if ferr != nil { - return ferr - } - headerVars.Stylesheets = append(headerVars.Stylesheets, "chartist/chartist.min.css") - headerVars.Scripts = append(headerVars.Scripts, "chartist/chartist.min.js") - - timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange")) - if err != nil { - return common.LocalError(err.Error(), w, r, user) - } - - var revLabelList []int64 - var labelList []int64 - var viewMap = make(map[int64]int64) +func panelAnalyticsTimeRangeToLabelList(timeRange AnalyticsTimeRange) (revLabelList []int64, labelList []int64, viewMap map[int64]int64) { + viewMap = make(map[int64]int64) var currentTime = time.Now().Unix() - for i := 1; i <= timeRange.Slices; i++ { var label = currentTime - int64(i*timeRange.SliceWidth) revLabelList = append(revLabelList, label) @@ -625,9 +616,26 @@ func routePanelAnalyticsViews(w http.ResponseWriter, r *http.Request, user commo for _, value := range revLabelList { labelList = append(labelList, value) } + return revLabelList, labelList, viewMap +} + +func routePanelAnalyticsViews(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { + headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + headerVars.Stylesheets = append(headerVars.Stylesheets, "chartist/chartist.min.css") + headerVars.Scripts = append(headerVars.Scripts, "chartist/chartist.min.js") + headerVars.Scripts = append(headerVars.Scripts, "analytics.js") + + timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange")) + if err != nil { + return common.LocalError(err.Error(), w, r, user) + } + revLabelList, labelList, viewMap := panelAnalyticsTimeRangeToLabelList(timeRange) var viewList []int64 - log.Print("in routePanelAnalyticsViews") + common.DebugLog("in routePanelAnalyticsViews") acc := qgen.Builder.Accumulator() rows, err := acc.Select("viewchunks").Columns("count, createdAt").Where("route = ''").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() @@ -679,28 +687,16 @@ func routePanelAnalyticsRouteViews(w http.ResponseWriter, r *http.Request, user } headerVars.Stylesheets = append(headerVars.Stylesheets, "chartist/chartist.min.css") headerVars.Scripts = append(headerVars.Scripts, "chartist/chartist.min.js") + headerVars.Scripts = append(headerVars.Scripts, "analytics.js") timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange")) if err != nil { return common.LocalError(err.Error(), w, r, user) } - - var revLabelList []int64 - var labelList []int64 - var viewMap = make(map[int64]int64) - var currentTime = time.Now().Unix() - - for i := 1; i <= timeRange.Slices; i++ { - var label = currentTime - int64(i*timeRange.SliceWidth) - revLabelList = append(revLabelList, label) - viewMap[label] = 0 - } - for _, value := range revLabelList { - labelList = append(labelList, value) - } + revLabelList, labelList, viewMap := panelAnalyticsTimeRangeToLabelList(timeRange) var viewList []int64 - log.Print("in routePanelAnalyticsRouteViews") + common.DebugLog("in routePanelAnalyticsRouteViews") acc := qgen.Builder.Accumulator() // TODO: Validate the route is valid @@ -753,28 +749,16 @@ func routePanelAnalyticsAgentViews(w http.ResponseWriter, r *http.Request, user } headerVars.Stylesheets = append(headerVars.Stylesheets, "chartist/chartist.min.css") headerVars.Scripts = append(headerVars.Scripts, "chartist/chartist.min.js") + headerVars.Scripts = append(headerVars.Scripts, "analytics.js") timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange")) if err != nil { return common.LocalError(err.Error(), w, r, user) } - - var revLabelList []int64 - var labelList []int64 - var viewMap = make(map[int64]int64) - var currentTime = time.Now().Unix() - - for i := 1; i <= timeRange.Slices; i++ { - var label = currentTime - int64(i*timeRange.SliceWidth) - revLabelList = append(revLabelList, label) - viewMap[label] = 0 - } - for _, value := range revLabelList { - labelList = append(labelList, value) - } + revLabelList, labelList, viewMap := panelAnalyticsTimeRangeToLabelList(timeRange) var viewList []int64 - log.Print("in routePanelAnalyticsAgentViews") + common.DebugLog("in routePanelAnalyticsAgentViews") acc := qgen.Builder.Accumulator() // TODO: Verify the agent is valid @@ -829,6 +813,79 @@ func routePanelAnalyticsAgentViews(w http.ResponseWriter, r *http.Request, user return panelRenderTemplate("panel_analytics_agent_views", w, r, user, &pi) } +func routePanelAnalyticsForumViews(w http.ResponseWriter, r *http.Request, user common.User, sfid string) common.RouteError { + headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + headerVars.Stylesheets = append(headerVars.Stylesheets, "chartist/chartist.min.css") + headerVars.Scripts = append(headerVars.Scripts, "chartist/chartist.min.js") + headerVars.Scripts = append(headerVars.Scripts, "analytics.js") + + timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange")) + if err != nil { + return common.LocalError(err.Error(), w, r, user) + } + revLabelList, labelList, viewMap := panelAnalyticsTimeRangeToLabelList(timeRange) + + var viewList []int64 + common.DebugLog("in routePanelAnalyticsForumViews") + + acc := qgen.Builder.Accumulator() + // TODO: Verify the agent is valid + rows, err := acc.Select("viewchunks_forums").Columns("count, createdAt").Where("forum = ?").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query(sfid) + if err != nil && err != ErrNoRows { + return common.InternalError(err, w, r) + } + defer rows.Close() + + for rows.Next() { + var count int64 + var createdAt time.Time + err := rows.Scan(&count, &createdAt) + if err != nil { + return common.InternalError(err, w, r) + } + + var unixCreatedAt = createdAt.Unix() + // TODO: Bulk log this + if common.Dev.SuperDebug { + log.Print("count: ", count) + log.Print("createdAt: ", createdAt) + log.Print("unixCreatedAt: ", unixCreatedAt) + } + + for _, value := range labelList { + if unixCreatedAt > value { + viewMap[value] += count + break + } + } + } + err = rows.Err() + if err != nil { + return common.InternalError(err, w, r) + } + + for _, value := range revLabelList { + viewList = append(viewList, viewMap[value]) + } + graph := common.PanelTimeGraph{Series: viewList, Labels: labelList} + log.Printf("graph: %+v\n", graph) + + fid, err := strconv.Atoi(sfid) + if err != nil { + return common.InternalError(err, w, r) + } + forum, err := common.Forums.Get(fid) + if err != nil { + return common.InternalError(err, w, r) + } + + pi := common.PanelAnalyticsAgentPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", sfid, forum.Name, graph, timeRange.Range} + return panelRenderTemplate("panel_analytics_forum_views", w, r, user, &pi) +} + func routePanelAnalyticsSystemViews(w http.ResponseWriter, r *http.Request, user common.User, system string) common.RouteError { headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) if ferr != nil { @@ -836,28 +893,16 @@ func routePanelAnalyticsSystemViews(w http.ResponseWriter, r *http.Request, user } headerVars.Stylesheets = append(headerVars.Stylesheets, "chartist/chartist.min.css") headerVars.Scripts = append(headerVars.Scripts, "chartist/chartist.min.js") + headerVars.Scripts = append(headerVars.Scripts, "analytics.js") timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange")) if err != nil { return common.LocalError(err.Error(), w, r, user) } - - var revLabelList []int64 - var labelList []int64 - var viewMap = make(map[int64]int64) - var currentTime = time.Now().Unix() - - for i := 1; i <= timeRange.Slices; i++ { - var label = currentTime - int64(i*timeRange.SliceWidth) - revLabelList = append(revLabelList, label) - viewMap[label] = 0 - } - for _, value := range revLabelList { - labelList = append(labelList, value) - } + revLabelList, labelList, viewMap := panelAnalyticsTimeRangeToLabelList(timeRange) var viewList []int64 - log.Print("in routePanelAnalyticsSystemViews") + common.DebugLog("in routePanelAnalyticsSystemViews") acc := qgen.Builder.Accumulator() // TODO: Verify the agent is valid @@ -918,28 +963,16 @@ func routePanelAnalyticsReferrerViews(w http.ResponseWriter, r *http.Request, us } headerVars.Stylesheets = append(headerVars.Stylesheets, "chartist/chartist.min.css") headerVars.Scripts = append(headerVars.Scripts, "chartist/chartist.min.js") + headerVars.Scripts = append(headerVars.Scripts, "analytics.js") timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange")) if err != nil { return common.LocalError(err.Error(), w, r, user) } - - var revLabelList []int64 - var labelList []int64 - var viewMap = make(map[int64]int64) - var currentTime = time.Now().Unix() - - for i := 1; i <= timeRange.Slices; i++ { - var label = currentTime - int64(i*timeRange.SliceWidth) - revLabelList = append(revLabelList, label) - viewMap[label] = 0 - } - for _, value := range revLabelList { - labelList = append(labelList, value) - } + revLabelList, labelList, viewMap := panelAnalyticsTimeRangeToLabelList(timeRange) var viewList []int64 - log.Print("in routePanelAnalyticsReferrerViews") + common.DebugLog("in routePanelAnalyticsReferrerViews") acc := qgen.Builder.Accumulator() // TODO: Verify the agent is valid @@ -994,28 +1027,16 @@ func routePanelAnalyticsTopics(w http.ResponseWriter, r *http.Request, user comm } headerVars.Stylesheets = append(headerVars.Stylesheets, "chartist/chartist.min.css") headerVars.Scripts = append(headerVars.Scripts, "chartist/chartist.min.js") + headerVars.Scripts = append(headerVars.Scripts, "analytics.js") timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange")) if err != nil { return common.LocalError(err.Error(), w, r, user) } - - var revLabelList []int64 - var labelList []int64 - var viewMap = make(map[int64]int64) - var currentTime = time.Now().Unix() - - for i := 1; i <= timeRange.Slices; i++ { - var label = currentTime - int64(i*timeRange.SliceWidth) - revLabelList = append(revLabelList, label) - viewMap[label] = 0 - } - for _, value := range revLabelList { - labelList = append(labelList, value) - } + revLabelList, labelList, viewMap := panelAnalyticsTimeRangeToLabelList(timeRange) var viewList []int64 - log.Print("in routePanelAnalyticsTopics") + common.DebugLog("in routePanelAnalyticsTopics") acc := qgen.Builder.Accumulator() rows, err := acc.Select("topicchunks").Columns("count, createdAt").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() @@ -1071,28 +1092,16 @@ func routePanelAnalyticsPosts(w http.ResponseWriter, r *http.Request, user commo } headerVars.Stylesheets = append(headerVars.Stylesheets, "chartist/chartist.min.css") headerVars.Scripts = append(headerVars.Scripts, "chartist/chartist.min.js") + headerVars.Scripts = append(headerVars.Scripts, "analytics.js") timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange")) if err != nil { return common.LocalError(err.Error(), w, r, user) } - - var revLabelList []int64 - var labelList []int64 - var viewMap = make(map[int64]int64) - var currentTime = time.Now().Unix() - - for i := 1; i <= timeRange.Slices; i++ { - var label = currentTime - int64(i*timeRange.SliceWidth) - revLabelList = append(revLabelList, label) - viewMap[label] = 0 - } - for _, value := range revLabelList { - labelList = append(labelList, value) - } + revLabelList, labelList, viewMap := panelAnalyticsTimeRangeToLabelList(timeRange) var viewList []int64 - log.Print("in routePanelAnalyticsPosts") + common.DebugLog("in routePanelAnalyticsPosts") acc := qgen.Builder.Accumulator() rows, err := acc.Select("postchunks").Columns("count, createdAt").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() @@ -1140,6 +1149,67 @@ func routePanelAnalyticsPosts(w http.ResponseWriter, r *http.Request, user commo return panelRenderTemplate("panel_analytics_posts", w, r, user, &pi) } +func routePanelAnalyticsForums(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { + headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + var forumMap = make(map[string]int) + + timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange")) + if err != nil { + return common.LocalError(err.Error(), w, r, user) + } + + acc := qgen.Builder.Accumulator() + rows, err := acc.Select("viewchunks_forums").Columns("count, forum").Where("forum != ''").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() + if err != nil && err != ErrNoRows { + return common.InternalError(err, w, r) + } + defer rows.Close() + + for rows.Next() { + var count int + var forum string + err := rows.Scan(&count, &forum) + if err != nil { + return common.InternalError(err, w, r) + } + + // TODO: Bulk log this + if common.Dev.SuperDebug { + log.Print("count: ", count) + log.Print("forum: ", forum) + } + forumMap[forum] += count + } + err = rows.Err() + if err != nil { + return common.InternalError(err, w, r) + } + + // TODO: Sort this slice + var forumItems []common.PanelAnalyticsAgentsItem + for sfid, count := range forumMap { + fid, err := strconv.Atoi(sfid) + if err != nil { + return common.InternalError(err, w, r) + } + forum, err := common.Forums.Get(fid) + if err != nil { + return common.InternalError(err, w, r) + } + forumItems = append(forumItems, common.PanelAnalyticsAgentsItem{ + Agent: sfid, + FriendlyAgent: forum.Name, + Count: count, + }) + } + + pi := common.PanelAnalyticsAgentsPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", forumItems, timeRange.Range} + return panelRenderTemplate("panel_analytics_forums", w, r, user, &pi) +} + func routePanelAnalyticsRoutes(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) if ferr != nil { diff --git a/public/analytics.js b/public/analytics.js new file mode 100644 index 00000000..0fd534a2 --- /dev/null +++ b/public/analytics.js @@ -0,0 +1,39 @@ +/*addHook(() => { + +})*/ + +function buildStatsChart(rawLabels, seriesData, timeRange) { + let labels = []; + if(timeRange=="one-month") { + labels = ["today","01 days"]; + for(let i = 2; i < 30; i++) { + let label = "0" + i + " days"; + if(label.length > "01 days".length) label = label.substr(1); + labels.push(label); + } + } else if(timeRange=="one-week") { + labels = ["today"]; + for(let i = 2; i < 14; i++) { + if (i%2==0) labels.push(""); + else labels.push(Math.floor(i/2) + " days"); + } + } else { + for(const i in rawLabels) { + let date = new Date(rawLabels[i]*1000); + console.log("date: ", date); + let minutes = "0" + date.getMinutes(); + let label = date.getHours() + ":" + minutes.substr(-2); + console.log("label:", label); + labels.push(label); + } + } + labels = labels.reverse() + seriesData = seriesData.reverse(); + + Chartist.Line('.ct_chart', { + labels: labels, + series: [seriesData], + }, { + height: '250px', + }); +} \ No newline at end of file diff --git a/public/global.js b/public/global.js index 8dac43e1..e7c17953 100644 --- a/public/global.js +++ b/public/global.js @@ -1,14 +1,27 @@ 'use strict'; -var form_vars = {}; +var formVars = {}; var alertList = []; var alertCount = 0; var conn; var selectedTopics = []; var attachItemCallback = function(){} +var hooks = { + "start_init": [], + "end_init": [], +}; // Topic move var forumToMoveTo = 0; +function runHook(name, ...args) { + if(!(name in hooks)) return; + + let hook = hooks[name]; + for (const callback in hook) { + callback(...args); + } +} + // TODO: Write a friendlier error handler which uses a .notice or something, we could have a specialised one for alerts function ajaxError(xhr,status,errstr) { console.log("The AJAX request failed"); @@ -203,6 +216,7 @@ function runWebSockets() { } $(document).ready(function(){ + runHook("start_init"); if(window["WebSocket"]) runWebSockets(); else conn = false; @@ -311,7 +325,7 @@ $(document).ready(function(){ var fieldType = this.getAttribute("data-type"); if(fieldType=="list") { var fieldValue = this.getAttribute("data-value"); - if(fieldName in form_vars) var it = form_vars[fieldName]; + if(fieldName in formVars) var it = formVars[fieldName]; else var it = ['No','Yes']; var itLen = it.length; var out = ""; @@ -624,4 +638,6 @@ $(document).ready(function(){ }); }) }); + + runHook("end_init"); }); diff --git a/query_gen/main.go b/query_gen/main.go index 52fa6574..e27ebd36 100644 --- a/query_gen/main.go +++ b/query_gen/main.go @@ -247,8 +247,6 @@ func writeSelects(adapter qgen.Adapter) error { build.Select("groupEntryExists").Table("users_groups").Columns("gid").Where("name = ''").Orderby("gid ASC").Limit("0,1").Parse() - build.Select("getForumTopicsOffset").Table("topics").Columns("tid, title, content, createdBy, is_closed, sticky, createdAt, lastReplyAt, lastReplyBy, parentID, postCount, likeCount").Where("parentID = ?").Orderby("sticky DESC, lastReplyAt DESC, createdBy DESC").Limit("?,?").Parse() - build.Select("getAttachment").Table("attachments").Columns("sectionID, sectionTable, originID, originTable, uploadedBy, path").Where("path = ? AND sectionID = ? AND sectionTable = ?").Parse() return nil diff --git a/router_gen/routes.go b/router_gen/routes.go index 7a256c0f..7c8cda90 100644 --- a/router_gen/routes.go +++ b/router_gen/routes.go @@ -6,7 +6,7 @@ func routes() { addRoute(View("routes.Overview", "/overview/")) addRoute(View("routes.CustomPage", "/pages/", "extraData")) addRoute(View("routeForums", "/forums/" /*,"&forums"*/)) - addRoute(View("routeForum", "/forum/", "extraData")) + addRoute(View("routes.ViewForum", "/forum/", "extraData")) addRoute(AnonAction("routeChangeTheme", "/theme/")) addRoute( View("routeShowAttachment", "/attachs/", "extraData").Before("ParseForm"), @@ -170,10 +170,12 @@ func buildPanelRoutes() { View("routePanelAnalyticsReferrers", "/panel/analytics/referrers/").Before("ParseForm"), View("routePanelAnalyticsRouteViews", "/panel/analytics/route/", "extraData"), View("routePanelAnalyticsAgentViews", "/panel/analytics/agent/", "extraData"), + View("routePanelAnalyticsForumViews", "/panel/analytics/forum/", "extraData"), View("routePanelAnalyticsSystemViews", "/panel/analytics/system/", "extraData"), View("routePanelAnalyticsReferrerViews", "/panel/analytics/referrer/", "extraData"), View("routePanelAnalyticsPosts", "/panel/analytics/posts/").Before("ParseForm"), View("routePanelAnalyticsTopics", "/panel/analytics/topics/").Before("ParseForm"), + View("routePanelAnalyticsForums", "/panel/analytics/forums/").Before("ParseForm"), View("routePanelGroups", "/panel/groups/"), View("routePanelGroupsEdit", "/panel/groups/edit/", "extraData"), diff --git a/routes.go b/routes.go index 5f9b2e0b..d93f60b1 100644 --- a/routes.go +++ b/routes.go @@ -44,102 +44,6 @@ func routeUploads() { func BadRoute() { } -func routeForum(w http.ResponseWriter, r *http.Request, user common.User, sfid string) common.RouteError { - page, _ := strconv.Atoi(r.FormValue("page")) - - // SEO URLs... - halves := strings.Split(sfid, ".") - if len(halves) < 2 { - halves = append(halves, halves[0]) - } - fid, err := strconv.Atoi(halves[1]) - if err != nil { - return common.PreError("The provided ForumID is not a valid number.", w, r) - } - - headerVars, ferr := common.ForumUserCheck(w, r, &user, fid) - if ferr != nil { - return ferr - } - if !user.Perms.ViewTopic { - return common.NoPermissions(w, r, user) - } - headerVars.Zone = "view_forum" - - // TODO: Fix this double-check - forum, err := common.Forums.Get(fid) - if err == ErrNoRows { - return common.NotFound(w, r, headerVars) - } else if err != nil { - return common.InternalError(err, w, r) - } - - // TODO: Does forum.TopicCount take the deleted items into consideration for guests? We don't have soft-delete yet, only hard-delete - offset, page, lastPage := common.PageOffset(forum.TopicCount, page, common.Config.ItemsPerPage) - - // TODO: Move this to *Forum - rows, err := stmts.getForumTopicsOffset.Query(fid, offset, common.Config.ItemsPerPage) - if err != nil { - return common.InternalError(err, w, r) - } - defer rows.Close() - - // TODO: Use something other than TopicsRow as we don't need to store the forum name and link on each and every topic item? - var topicList []*common.TopicsRow - var reqUserList = make(map[int]bool) - for rows.Next() { - var topicItem = common.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 common.InternalError(err, w, r) - } - - topicItem.Link = common.BuildTopicURL(common.NameToSlug(topicItem.Title), topicItem.ID) - topicItem.RelativeLastReplyAt = common.RelativeTime(topicItem.LastReplyAt) - - common.RunVhook("forum_trow_assign", &topicItem, &forum) - topicList = append(topicList, &topicItem) - reqUserList[topicItem.CreatedBy] = true - reqUserList[topicItem.LastReplyBy] = true - } - err = rows.Err() - if err != nil { - return common.InternalError(err, w, r) - } - - // 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 := common.Users.BulkGetMap(idSlice) - if err != nil { - return common.InternalError(err, w, r) - } - - // 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 := common.Paginate(forum.TopicCount, common.Config.ItemsPerPage, 5) - pi := common.ForumPage{forum.Name, user, headerVars, topicList, forum, pageList, page, lastPage} - if common.RunPreRenderHook("pre_render_forum", w, r, &user, &pi) { - return nil - } - err = common.RunThemeTemplate(headerVars.Theme.Name, "forum", pi, w) - if err != nil { - return common.InternalError(err, w, r) - } - return nil -} - func routeForums(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { headerVars, ferr := common.UserCheck(w, r, &user) if ferr != nil { diff --git a/routes/forum.go b/routes/forum.go new file mode 100644 index 00000000..739387bf --- /dev/null +++ b/routes/forum.go @@ -0,0 +1,125 @@ +package routes + +import ( + "database/sql" + "net/http" + "strconv" + "strings" + + "../common" + "../common/counters" + "../query_gen/lib" +) + +type ForumStmts struct { + getTopics *sql.Stmt +} + +var forumStmts ForumStmts + +// TODO: Move these DbInits into *Forum as Topics() +func init() { + common.DbInits.Add(func(acc *qgen.Accumulator) error { + forumStmts = ForumStmts{ + getTopics: acc.Select("topics").Columns("tid, title, content, createdBy, is_closed, sticky, createdAt, lastReplyAt, lastReplyBy, parentID, postCount, likeCount").Where("parentID = ?").Orderby("sticky DESC, lastReplyAt DESC, createdBy DESC").Limit("?,?").Prepare(), + } + return acc.FirstError() + }) +} + +func ViewForum(w http.ResponseWriter, r *http.Request, user common.User, sfid string) common.RouteError { + page, _ := strconv.Atoi(r.FormValue("page")) + + // SEO URLs... + halves := strings.Split(sfid, ".") + if len(halves) < 2 { + halves = append(halves, halves[0]) + } + fid, err := strconv.Atoi(halves[1]) + if err != nil { + return common.PreError("The provided ForumID is not a valid number.", w, r) + } + + headerVars, ferr := common.ForumUserCheck(w, r, &user, fid) + if ferr != nil { + return ferr + } + if !user.Perms.ViewTopic { + return common.NoPermissions(w, r, user) + } + headerVars.Zone = "view_forum" + + // TODO: Fix this double-check + forum, err := common.Forums.Get(fid) + if err == sql.ErrNoRows { + return common.NotFound(w, r, headerVars) + } else if err != nil { + return common.InternalError(err, w, r) + } + + // TODO: Does forum.TopicCount take the deleted items into consideration for guests? We don't have soft-delete yet, only hard-delete + offset, page, lastPage := common.PageOffset(forum.TopicCount, page, common.Config.ItemsPerPage) + + // TODO: Move this to *Forum + rows, err := forumStmts.getTopics.Query(fid, offset, common.Config.ItemsPerPage) + if err != nil { + return common.InternalError(err, w, r) + } + defer rows.Close() + + // TODO: Use something other than TopicsRow as we don't need to store the forum name and link on each and every topic item? + var topicList []*common.TopicsRow + var reqUserList = make(map[int]bool) + for rows.Next() { + var topicItem = common.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 common.InternalError(err, w, r) + } + + topicItem.Link = common.BuildTopicURL(common.NameToSlug(topicItem.Title), topicItem.ID) + topicItem.RelativeLastReplyAt = common.RelativeTime(topicItem.LastReplyAt) + + common.RunVhook("forum_trow_assign", &topicItem, &forum) + topicList = append(topicList, &topicItem) + reqUserList[topicItem.CreatedBy] = true + reqUserList[topicItem.LastReplyBy] = true + } + err = rows.Err() + if err != nil { + return common.InternalError(err, w, r) + } + + // 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 := common.Users.BulkGetMap(idSlice) + if err != nil { + return common.InternalError(err, w, r) + } + + // 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 := common.Paginate(forum.TopicCount, common.Config.ItemsPerPage, 5) + pi := common.ForumPage{forum.Name, user, headerVars, topicList, forum, pageList, page, lastPage} + if common.RunPreRenderHook("pre_render_forum", w, r, &user, &pi) { + return nil + } + err = common.RunThemeTemplate(headerVars.Theme.Name, "forum", pi, w) + if err != nil { + return common.InternalError(err, w, r) + } + counters.ForumViewCounter.Bump(forum.ID) + return nil +} diff --git a/routes/topic.go b/routes/topic.go index 42d9d9f9..3cb52680 100644 --- a/routes/topic.go +++ b/routes/topic.go @@ -191,6 +191,7 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, urlBit return common.InternalError(err, w, r) } counters.TopicViewCounter.Bump(topic.ID) // TODO: Move this into the router? + counters.ForumViewCounter.Bump(topic.ParentID) return nil } diff --git a/templates/panel-forum-edit.html b/templates/panel-forum-edit.html index fb4d9816..64214c34 100644 --- a/templates/panel-forum-edit.html +++ b/templates/panel-forum-edit.html @@ -3,7 +3,7 @@
{{template "panel-menu.html" . }}
diff --git a/templates/panel-inner-menu.html b/templates/panel-inner-menu.html index 17989d78..c86a869c 100644 --- a/templates/panel-inner-menu.html +++ b/templates/panel-inner-menu.html @@ -35,6 +35,9 @@ + diff --git a/templates/panel_analytics_agent_views.html b/templates/panel_analytics_agent_views.html index 991aeafd..9ec75312 100644 --- a/templates/panel_analytics_agent_views.html +++ b/templates/panel_analytics_agent_views.html @@ -8,6 +8,7 @@ {{.FriendlyAgent}} Views + diff --git a/templates/panel_analytics_forum_views.html b/templates/panel_analytics_forum_views.html new file mode 100644 index 00000000..9f1e222f --- /dev/null +++ b/templates/panel_analytics_forum_views.html @@ -0,0 +1,34 @@ +{{template "header.html" . }} +
+{{template "panel-menu.html" . }} +
+
+
+
+ {{.FriendlyAgent}} Views + +
+
+
+
+
+
+
+
+ +{{template "footer.html" . }} diff --git a/templates/panel_analytics_forums.html b/templates/panel_analytics_forums.html new file mode 100644 index 00000000..f37dcacf --- /dev/null +++ b/templates/panel_analytics_forums.html @@ -0,0 +1,30 @@ +{{template "header.html" . }} +
+{{template "panel-menu.html" . }} +
+
+
+
+ Forums + +
+
+
+
+ {{range .ItemList}} +
+ {{.FriendlyAgent}} + {{.Count}} views +
+ {{else}}
No forum view counts could be found in the selected time range
{{end}} +
+
+
+{{template "footer.html" . }} diff --git a/templates/panel_analytics_posts.html b/templates/panel_analytics_posts.html index 16d04fbb..2837e5f0 100644 --- a/templates/panel_analytics_posts.html +++ b/templates/panel_analytics_posts.html @@ -8,6 +8,7 @@ Post Counts + @@ -17,35 +18,17 @@
-
+
{{template "footer.html" . }} diff --git a/templates/panel_analytics_referrers.html b/templates/panel_analytics_referrers.html index 72c079aa..d765cf43 100644 --- a/templates/panel_analytics_referrers.html +++ b/templates/panel_analytics_referrers.html @@ -8,6 +8,7 @@ Referrers + @@ -17,7 +18,7 @@
-
+
Details
@@ -33,30 +34,12 @@
{{template "footer.html" . }} diff --git a/templates/panel_analytics_routes.html b/templates/panel_analytics_routes.html index f2e4cbe0..05a1cfd6 100644 --- a/templates/panel_analytics_routes.html +++ b/templates/panel_analytics_routes.html @@ -8,6 +8,7 @@ Routes + @@ -17,35 +18,17 @@
-
+
{{template "footer.html" . }} diff --git a/templates/panel_analytics_systems.html b/templates/panel_analytics_systems.html index af80a0ea..056e54b7 100644 --- a/templates/panel_analytics_systems.html +++ b/templates/panel_analytics_systems.html @@ -8,6 +8,7 @@ Operating Systems + @@ -33,30 +34,12 @@ {{template "footer.html" . }} diff --git a/templates/panel_analytics_views.html b/templates/panel_analytics_views.html index fe819c1a..f036f963 100644 --- a/templates/panel_analytics_views.html +++ b/templates/panel_analytics_views.html @@ -5,9 +5,10 @@
- Views + Requests