From 0879f568933fe1d083fbbcbddecf942008b4144a Mon Sep 17 00:00:00 2001 From: Azareal Date: Thu, 22 Feb 2018 02:27:17 +0000 Subject: [PATCH] Added per-forum analytics. Added one week as a time range option. Moved routeForum to routes.ViewForum Renamed form_vars to formVars. Moved the analytics chart JS into it's own file. Changed the header text on the requests chart from Views to Requests. Added simple hooks to global.js Moved some of the in funcname prints to debug mode. De-duplicated some of the back-end analytics logic. --- common/counters/forums.go | 36 +- gen_mssql.go | 8 - gen_mysql.go | 7 - gen_router.go | 358 +++++++++--------- main.go | 4 + panel_routes.go | 286 ++++++++------ public/analytics.js | 39 ++ public/global.js | 20 +- query_gen/main.go | 2 - router_gen/routes.go | 4 +- routes.go | 96 ----- routes/forum.go | 125 ++++++ routes/topic.go | 1 + templates/panel-forum-edit.html | 2 +- templates/panel-inner-menu.html | 3 + templates/panel_analytics_agent_views.html | 23 +- templates/panel_analytics_agents.html | 1 + templates/panel_analytics_forum_views.html | 34 ++ templates/panel_analytics_forums.html | 30 ++ templates/panel_analytics_posts.html | 21 +- templates/panel_analytics_referrer_views.html | 23 +- templates/panel_analytics_referrers.html | 1 + templates/panel_analytics_route_views.html | 23 +- templates/panel_analytics_routes.html | 1 + templates/panel_analytics_system_views.html | 23 +- templates/panel_analytics_systems.html | 1 + templates/panel_analytics_topics.html | 21 +- templates/panel_analytics_views.html | 23 +- templates/panel_forums.html | 2 +- themes/cosora/public/panel.css | 24 ++ 30 files changed, 706 insertions(+), 536 deletions(-) create mode 100644 public/analytics.js create mode 100644 routes/forum.go create mode 100644 templates/panel_analytics_forum_views.html create mode 100644 templates/panel_analytics_forums.html 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 @@
-
+
@@ -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