diff --git a/cmd/elasticsearch/setup.go b/cmd/elasticsearch/setup.go index 24cf9248..f2a874ae 100644 --- a/cmd/elasticsearch/setup.go +++ b/cmd/elasticsearch/setup.go @@ -175,32 +175,92 @@ type ESReply struct { } func setupData(client *elastic.Client) error { - err := qgen.NewAcc().Select("topics").Cols("tid, title, content, createdBy, ipaddress").Each(func(rows *sql.Rows) error { - var tid, createdBy int - var title, content, ip string - err := rows.Scan(&tid, &title, &content, &createdBy, &ip) - if err != nil { - return err + tcount := 4 + errs := make(chan error) + + go func() { + tin := make([]chan ESTopic, tcount) + tf := func(tin chan ESTopic) { + for { + topic, more := <-tin + if !more { + break + } + _, err := client.Index().Index("topics").Type("_doc").Id(strconv.Itoa(topic.ID)).BodyJson(topic).Do(context.Background()) + if err != nil { + errs <- err + } + } + } + for i := 0; i < 4; i++ { + go tf(tin[i]) } - topic := ESTopic{tid, title, content, createdBy, ip} - _, err = client.Index().Index("topics").Type("_doc").Id(strconv.Itoa(tid)).BodyJson(topic).Do(context.Background()) - return err - }) - if err != nil { - return err + oi := 0 + err := qgen.NewAcc().Select("topics").Cols("tid, title, content, createdBy, ipaddress").Each(func(rows *sql.Rows) error { + topic := ESTopic{} + err := rows.Scan(&topic.ID, &topic.Title, &topic.Content, &topic.CreatedBy, &topic.IPAddress) + if err != nil { + return err + } + tin[oi] <- topic + if oi < 3 { + oi++ + } + return nil + }) + for i := 0; i < 4; i++ { + close(tin[i]) + } + errs <- err + }() + + go func() { + rin := make([]chan ESReply, tcount) + rf := func(rin chan ESReply) { + for { + reply, more := <-rin + if !more { + break + } + _, err := client.Index().Index("replies").Type("_doc").Id(strconv.Itoa(reply.ID)).BodyJson(reply).Do(context.Background()) + if err != nil { + errs <- err + } + } + } + for i := 0; i < 4; i++ { + rf(rin[i]) + } + oi := 0 + err := qgen.NewAcc().Select("replies").Cols("rid, tid, content, createdBy, ipaddress").Each(func(rows *sql.Rows) error { + reply := ESReply{} + err := rows.Scan(&reply.ID, &reply.TID, &reply.Content, &reply.CreatedBy, &reply.IPAddress) + if err != nil { + return err + } + rin[oi] <- reply + if oi < 3 { + oi++ + } + return nil + }) + for i := 0; i < 4; i++ { + close(rin[i]) + } + errs <- err + }() + + fin := 0 + for { + err := <-errs + if err == nil { + fin++ + if fin == 2 { + return nil + } + } else { + return err + } } - - return qgen.NewAcc().Select("replies").Cols("rid, tid, content, createdBy, ipaddress").Each(func(rows *sql.Rows) error { - var rid, tid, createdBy int - var content, ip string - err := rows.Scan(&rid, &tid, &content, &createdBy, &ip) - if err != nil { - return err - } - - reply := ESReply{rid, tid, content, createdBy, ip} - _, err = client.Index().Index("replies").Type("_doc").Id(strconv.Itoa(rid)).BodyJson(reply).Do(context.Background()) - return err - }) } diff --git a/common/misc_logs.go b/common/misc_logs.go index a76a5ff2..ea67ceb9 100644 --- a/common/misc_logs.go +++ b/common/misc_logs.go @@ -143,17 +143,20 @@ func (log *LoginLogItem) Create() (id int, err error) { type LoginLogStore interface { GlobalCount() (logCount int) + Count(uid int) (logCount int) GetOffset(uid int, offset int, perPage int) (logs []LoginLogItem, err error) } type SQLLoginLogStore struct { count *sql.Stmt + countForUser *sql.Stmt getOffsetByUser *sql.Stmt } func NewLoginLogStore(acc *qgen.Accumulator) (*SQLLoginLogStore, error) { return &SQLLoginLogStore{ count: acc.Count("login_logs").Prepare(), + countForUser: acc.Count("login_logs").Where("uid = ?").Prepare(), getOffsetByUser: acc.Select("login_logs").Columns("lid, success, ipaddress, doneAt").Where("uid = ?").Orderby("doneAt DESC").Limit("?,?").Prepare(), }, acc.FirstError() } @@ -166,6 +169,14 @@ func (store *SQLLoginLogStore) GlobalCount() (logCount int) { return logCount } +func (store *SQLLoginLogStore) Count(uid int) (logCount int) { + err := store.countForUser.QueryRow(uid).Scan(&logCount) + if err != nil { + LogError(err) + } + return logCount +} + func (store *SQLLoginLogStore) GetOffset(uid int, offset int, perPage int) (logs []LoginLogItem, err error) { rows, err := store.getOffsetByUser.Query(uid, offset, perPage) if err != nil { diff --git a/common/pages.go b/common/pages.go index c5e99e2f..f5a373e8 100644 --- a/common/pages.go +++ b/common/pages.go @@ -277,6 +277,8 @@ type PanelAnalyticsPage struct { Graph PanelTimeGraph ViewItems []PanelAnalyticsItem TimeRange string + Unit string + TimeType string } type PanelAnalyticsRoutesItem struct { @@ -287,9 +289,11 @@ type PanelAnalyticsRoutesItem struct { type PanelAnalyticsRoutesPage struct { *BasePanelPage ItemList []PanelAnalyticsRoutesItem + Graph PanelTimeGraph TimeRange string } +// TODO: Rename the fields as this structure is being used in a generic way now type PanelAnalyticsAgentsItem struct { Agent string FriendlyAgent string diff --git a/common/parser.go b/common/parser.go index 527de1f0..ac0469e5 100644 --- a/common/parser.go +++ b/common/parser.go @@ -880,10 +880,11 @@ func PageOffset(count int, page int, perPage int) (int, int, int) { page = 1 } + // ? - This has been commented out as it created a bug in the user manager where the first user on a page wouldn't be accessible // We don't want the offset to overflow the slices, if everything's in memory - if offset >= (count - 1) { + /*if offset >= (count - 1) { offset = 0 - } + }*/ return offset, page, lastPage } diff --git a/gen_router.go b/gen_router.go index 1cc02f7e..38571705 100644 --- a/gen_router.go +++ b/gen_router.go @@ -768,7 +768,7 @@ func (r *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { for _, item := range StringToBytes(ua) { if (item > 64 && item < 91) || (item > 96 && item < 123) { buffer = append(buffer, item) - } else if item == ' ' || item == '(' || item == ')' || item == '-' || (item > 47 && item < 58) || item == '_' || item == ';' || item == '.' || item == '+' || item == '~' || (item == ':' && bytes.Equal(buffer,[]byte("http"))) || item == ',' || item == '/' { + } else if item == ' ' || item == '(' || item == ')' || item == '-' || (item > 47 && item < 58) || item == '_' || item == ';' || item == ':' || item == '.' || item == '+' || item == '~' || (item == ':' && bytes.Equal(buffer,[]byte("http"))) || item == ',' || item == '/' { if len(buffer) != 0 { if len(buffer) > 2 { // Use an unsafe zero copy conversion here just to use the switch, it's not safe for this string to escape from here, as it will get mutated, so do a regular string conversion in append diff --git a/install/mysql.go b/install/mysql.go index 42a16a26..6089a51a 100644 --- a/install/mysql.go +++ b/install/mysql.go @@ -137,7 +137,6 @@ func (ins *MysqlInstaller) TableDefs() (err error) { _, err = ins.db.Exec(string(data)) if err != nil { fmt.Println("Failed query:", string(data)) - panic("Failed query: " + string(data)) return err } } diff --git a/langs/english.json b/langs/english.json index 1f0dabc1..76219164 100644 --- a/langs/english.json +++ b/langs/english.json @@ -518,6 +518,7 @@ "quick_topic.add_file_button":"Add File", "quick_topic.cancel_button":"Cancel", + "topic_list.search_head":"Search Results", "topic_list.create_topic_tooltip":"Create Topic", "topic_list.create_topic_aria":"Create a topic", "topic_list.moderate":"Moderate", @@ -831,6 +832,7 @@ "panel_statistics_topic_counts_head":"Topic Counts", "panel_statistics_requests_head":"Requests", + "panel_statistics_time_range_one_year":"1 year", "panel_statistics_time_range_three_months":"3 months", "panel_statistics_time_range_one_month":"1 month", "panel_statistics_time_range_one_week":"1 week", diff --git a/public/analytics.js b/public/analytics.js index 71c1d80b..b26b2711 100644 --- a/public/analytics.js +++ b/public/analytics.js @@ -6,7 +6,21 @@ // TODO: Load rawLabels and seriesData dynamically rather than potentially fiddling with nonces for the CSP? function buildStatsChart(rawLabels, seriesData, timeRange, legendNames) { let labels = []; - if(timeRange=="one-month") { + if(timeRange=="one-year") { + labels = ["today","01 months"]; + for(let i = 2; i < 12; i++) { + let label = "0" + i + " months"; + if(label.length > "01 months".length) label = label.substr(1); + labels.push(label); + } + } else if(timeRange=="three-months") { + labels = ["today","01 days"]; + for(let i = 2; i < 90; i = i + 3) { + let label = "0" + i + " days"; + if(label.length > "01 days".length) label = label.substr(1); + labels.push(label); + } + } else if(timeRange=="one-month") { labels = ["today","01 days"]; for(let i = 2; i < 30; i++) { let label = "0" + i + " days"; diff --git a/public/global.js b/public/global.js index 69c752c8..0e1030fb 100644 --- a/public/global.js +++ b/public/global.js @@ -234,7 +234,6 @@ function runWebSockets() { console.log("empty topic list"); return; } - // TODO: Fix the data race where the function hasn't been loaded yet let renTopic = Template_topics_topic(topic); $(".topic_row[data-tid='"+topic.ID+"']").addClass("ajax_topic_dupe"); @@ -318,7 +317,7 @@ function PageOffset(count, page, perPage) { } // We don't want the offset to overflow the slices, if everything's in memory - if(offset >= (count - 1)) offset = 0; + //if(offset >= (count - 1)) offset = 0; return {Offset:offset, Page:page, LastPage:lastPage} } function LastPage(count, perPage) { @@ -517,6 +516,8 @@ function mainInit(){ for(let i = 0; i < topics.length;i++) out += Template_topics_topic(topics[i]); $(".topic_list").html(out); + document.title = phraseBox["topic_list"]["topic_list.search_head"]; + $(".topic_list_title h1").text(phraseBox["topic_list"]["topic_list.search_head"]); let obj = {Title: document.title, Url: url+q}; history.pushState(obj, obj.Title, obj.Url); rebuildPaginator(data.LastPage); @@ -1046,6 +1047,17 @@ function mainInit(){ this.innerText = formattedTime; }); + $(".unix_to_date").each(function(){ + // TODO: Localise this + let monthList = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; + let date = new Date(this.innerText * 1000); + console.log("date: ", date); + let day = "0" + date.getDate(); + let formattedTime = monthList[date.getMonth()] + " " + day.substr(-2) + " " + date.getFullYear(); + console.log("formattedTime:", formattedTime); + this.innerText = formattedTime; + }); + this.onkeyup = function(event) { if(event.which == 37) this.querySelectorAll("#prevFloat a")[0].click(); if(event.which == 39) this.querySelectorAll("#nextFloat a")[0].click(); diff --git a/router_gen/main.go b/router_gen/main.go index df2bc689..571e8ec2 100644 --- a/router_gen/main.go +++ b/router_gen/main.go @@ -560,7 +560,7 @@ func (r *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { for _, item := range StringToBytes(ua) { if (item > 64 && item < 91) || (item > 96 && item < 123) { buffer = append(buffer, item) - } else if item == ' ' || item == '(' || item == ')' || item == '-' || (item > 47 && item < 58) || item == '_' || item == ';' || item == '.' || item == '+' || item == '~' || (item == ':' && bytes.Equal(buffer,[]byte("http"))) || item == ',' || item == '/' { + } else if item == ' ' || item == '(' || item == ')' || item == '-' || (item > 47 && item < 58) || item == '_' || item == ';' || item == ':' || item == '.' || item == '+' || item == '~' || (item == ':' && bytes.Equal(buffer,[]byte("http"))) || item == ',' || item == '/' { if len(buffer) != 0 { if len(buffer) > 2 { // Use an unsafe zero copy conversion here just to use the switch, it's not safe for this string to escape from here, as it will get mutated, so do a regular string conversion in append diff --git a/routes/account.go b/routes/account.go index 6935466c..acf1ca10 100644 --- a/routes/account.go +++ b/routes/account.go @@ -705,7 +705,7 @@ func AccountEditEmailTokenSubmit(w http.ResponseWriter, r *http.Request, user co func AccountLogins(w http.ResponseWriter, r *http.Request, user common.User, header *common.Header) common.RouteError { accountEditHead("account_logins", w, r, &user, header) - logCount := common.LoginLogs.GlobalCount() + logCount := common.LoginLogs.Count(user.ID) page, _ := strconv.Atoi(r.FormValue("page")) perPage := 12 offset, page, lastPage := common.PageOffset(logCount, page, perPage) diff --git a/routes/panel/analytics.go b/routes/panel/analytics.go index 73b92594..49282721 100644 --- a/routes/panel/analytics.go +++ b/routes/panel/analytics.go @@ -31,6 +31,12 @@ func analyticsTimeRange(rawTimeRange string) (timeRange AnalyticsTimeRange, err switch rawTimeRange { // This might be pushing it, we might want to come up with a more efficient scheme for dealing with large timeframes like this + case "one-year": + timeRange.Quantity = 12 + timeRange.Unit = "month" + timeRange.Slices = 12 + timeRange.SliceWidth = 60 * 60 * 24 * 30 + timeRange.Range = "one-year" case "three-months": timeRange.Quantity = 90 timeRange.Unit = "day" @@ -153,8 +159,11 @@ func AnalyticsViews(w http.ResponseWriter, r *http.Request, user common.User) co } graph := common.PanelTimeGraph{Series: [][]int64{viewList}, Labels: labelList} common.DebugLogf("graph: %+v\n", graph) - - pi := common.PanelAnalyticsPage{basePage, graph, viewItems, timeRange.Range} + var ttime string + if timeRange.Range == "six-hours" || timeRange.Range == "twelve-hours" || timeRange.Range == "one-day" { + ttime = "time" + } + pi := common.PanelAnalyticsPage{basePage, graph, viewItems, timeRange.Range, timeRange.Unit, ttime} return renderTemplate("panel_analytics_views", w, r, basePage.Header, &pi) } @@ -416,7 +425,7 @@ func AnalyticsTopics(w http.ResponseWriter, r *http.Request, user common.User) c } graph := common.PanelTimeGraph{Series: [][]int64{viewList}, Labels: labelList} common.DebugLogf("graph: %+v\n", graph) - pi := common.PanelAnalyticsPage{basePage, graph, viewItems, timeRange.Range} + pi := common.PanelAnalyticsPage{basePage, graph, viewItems, timeRange.Range, timeRange.Unit, "time"} return renderTemplate("panel_analytics_topics", w, r, basePage.Header, &pi) } @@ -449,37 +458,10 @@ func AnalyticsPosts(w http.ResponseWriter, r *http.Request, user common.User) co } graph := common.PanelTimeGraph{Series: [][]int64{viewList}, Labels: labelList} common.DebugLogf("graph: %+v\n", graph) - pi := common.PanelAnalyticsPage{basePage, graph, viewItems, timeRange.Range} + pi := common.PanelAnalyticsPage{basePage, graph, viewItems, timeRange.Range, timeRange.Unit, "time"} return renderTemplate("panel_analytics_posts", w, r, basePage.Header, &pi) } -/*func analyticsRowsToViewMap(rows *sql.Rows, labelList []int64, viewMap map[int64]int64) (map[int64]int64, error) { - defer rows.Close() - for rows.Next() { - var count int64 - var createdAt time.Time - err := rows.Scan(&count, &createdAt) - if err != nil { - return viewMap, err - } - - 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 - } - } - } - return viewMap, rows.Err() -}*/ - func analyticsRowsToNameMap(rows *sql.Rows) (map[string]int, error) { nameMap := make(map[string]int) defer rows.Close() @@ -540,24 +522,96 @@ func analyticsRowsToDuoMap(rows *sql.Rows, labelList []int64, viewMap map[int64] return vMap, nameMap, rows.Err() } +type OVItem struct { + name string + count int + viewMap map[int64]int64 +} + +func analyticsVMapToOVList(vMap map[string]map[int64]int64) (ovList []OVItem) { + // Order the map + for name, viewMap := range vMap { + var totcount int + for _, count := range viewMap { + totcount += int(count) + } + ovList = append(ovList, OVItem{name, totcount, viewMap}) + } + + // Use bubble sort for now as there shouldn't be too many items + for i := 0; i < len(ovList)-1; i++ { + for j := 0; j < len(ovList)-1; j++ { + if ovList[j].count > ovList[j+1].count { + temp := ovList[j] + ovList[j] = ovList[j+1] + ovList[j+1] = temp + } + } + } + + // Invert the direction + var tOVList []OVItem + for i := len(ovList) - 1; i >= 0; i-- { + tOVList = append(tOVList, ovList[i]) + } + return tOVList +} + func AnalyticsForums(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - basePage, ferr := buildBasePage(w, r, &user, "analytics", "analytics") + basePage, ferr := PreAnalyticsDetail(w, r, &user) if ferr != nil { return ferr } + basePage.AddScript("chartist/chartist-plugin-legend.min.js") + basePage.AddSheet("chartist/chartist-plugin-legend.css") + timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) if err != nil { return common.LocalError(err.Error(), w, r, user) } + revLabelList, labelList, viewMap := analyticsTimeRangeToLabelList(timeRange) - rows, err := qgen.NewAcc().Select("viewchunks_forums").Columns("count, forum").Where("forum != ''").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() + rows, err := qgen.NewAcc().Select("viewchunks_forums").Columns("count, forum, createdAt").Where("forum != ''").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() if err != nil && err != sql.ErrNoRows { return common.InternalError(err, w, r) } - forumMap, err := analyticsRowsToNameMap(rows) + vMap, forumMap, err := analyticsRowsToDuoMap(rows, labelList, viewMap) if err != nil { return common.InternalError(err, w, r) } + ovList := analyticsVMapToOVList(vMap) + + var vList [][]int64 + var legendList []string + var i int + for _, ovitem := range ovList { + var viewList []int64 + for _, value := range revLabelList { + viewList = append(viewList, ovitem.viewMap[value]) + } + vList = append(vList, viewList) + fid, err := strconv.Atoi(ovitem.name) + if err != nil { + return common.InternalError(err, w, r) + } + var lName string + forum, err := common.Forums.Get(fid) + if err == sql.ErrNoRows { + // TODO: Localise this + lName = "Deleted Forum" + } else if err != nil { + return common.InternalError(err, w, r) + } else { + lName = forum.Name + } + legendList = append(legendList, lName) + if i >= 6 { + break + } + i++ + } + graph := common.PanelTimeGraph{Series: vList, Labels: labelList, Legends: legendList} + common.DebugLogf("graph: %+v\n", graph) // TODO: Sort this slice var forumItems []common.PanelAnalyticsAgentsItem @@ -566,39 +620,68 @@ func AnalyticsForums(w http.ResponseWriter, r *http.Request, user common.User) c if err != nil { return common.InternalError(err, w, r) } + var lName string forum, err := common.Forums.Get(fid) - if err != nil { + if err == sql.ErrNoRows { + // TODO: Localise this + lName = "Deleted Forum" + } else if err != nil { return common.InternalError(err, w, r) + } else { + lName = forum.Name } forumItems = append(forumItems, common.PanelAnalyticsAgentsItem{ Agent: sfid, - FriendlyAgent: forum.Name, + FriendlyAgent: lName, Count: count, }) } - pi := common.PanelAnalyticsAgentsPage{basePage, forumItems, timeRange.Range} + pi := common.PanelAnalyticsDuoPage{basePage, forumItems, graph, timeRange.Range} return renderTemplate("panel_analytics_forums", w, r, basePage.Header, &pi) } func AnalyticsRoutes(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - basePage, ferr := buildBasePage(w, r, &user, "analytics", "analytics") + basePage, ferr := PreAnalyticsDetail(w, r, &user) if ferr != nil { return ferr } + basePage.AddScript("chartist/chartist-plugin-legend.min.js") + basePage.AddSheet("chartist/chartist-plugin-legend.css") + timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) if err != nil { return common.LocalError(err.Error(), w, r, user) } + revLabelList, labelList, viewMap := analyticsTimeRangeToLabelList(timeRange) - rows, err := qgen.NewAcc().Select("viewchunks").Columns("count, route").Where("route != ''").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() + rows, err := qgen.NewAcc().Select("viewchunks").Columns("count, route, createdAt").Where("route != ''").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() if err != nil && err != sql.ErrNoRows { return common.InternalError(err, w, r) } - routeMap, err := analyticsRowsToNameMap(rows) + vMap, routeMap, err := analyticsRowsToDuoMap(rows, labelList, viewMap) if err != nil { return common.InternalError(err, w, r) } + ovList := analyticsVMapToOVList(vMap) + + var vList [][]int64 + var legendList []string + var i int + for _, ovitem := range ovList { + var viewList []int64 + for _, value := range revLabelList { + viewList = append(viewList, ovitem.viewMap[value]) + } + vList = append(vList, viewList) + legendList = append(legendList, ovitem.name) + if i >= 6 { + break + } + i++ + } + graph := common.PanelTimeGraph{Series: vList, Labels: labelList, Legends: legendList} + common.DebugLogf("graph: %+v\n", graph) // TODO: Sort this slice var routeItems []common.PanelAnalyticsRoutesItem @@ -609,16 +692,10 @@ func AnalyticsRoutes(w http.ResponseWriter, r *http.Request, user common.User) c }) } - pi := common.PanelAnalyticsRoutesPage{basePage, routeItems, timeRange.Range} + pi := common.PanelAnalyticsRoutesPage{basePage, routeItems, graph, timeRange.Range} return renderTemplate("panel_analytics_routes", w, r, basePage.Header, &pi) } -type OVItem struct { - name string - count int - viewMap map[int64]int64 -} - // Trialling multi-series charts func AnalyticsAgents(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { basePage, ferr := PreAnalyticsDetail(w, r, &user) @@ -642,26 +719,7 @@ func AnalyticsAgents(w http.ResponseWriter, r *http.Request, user common.User) c if err != nil { return common.InternalError(err, w, r) } - - // Order the map - var ovList []OVItem - for name, viewMap := range vMap { - var totcount int - for _, count := range viewMap { - totcount += int(count) - } - ovList = append(ovList, OVItem{name, totcount, viewMap}) - } - // Use bubble sort for now as there shouldn't be too many items - for i := 0; i < len(ovList)-1; i++ { - for j := 0; j < len(ovList)-1; j++ { - if ovList[j].count > ovList[j+1].count { - temp := ovList[j] - ovList[j] = ovList[j+1] - ovList[j+1] = temp - } - } - } + ovList := analyticsVMapToOVList(vMap) var vList [][]int64 var legendList []string @@ -704,23 +762,50 @@ func AnalyticsAgents(w http.ResponseWriter, r *http.Request, user common.User) c } func AnalyticsSystems(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - basePage, ferr := buildBasePage(w, r, &user, "analytics", "analytics") + basePage, ferr := PreAnalyticsDetail(w, r, &user) if ferr != nil { return ferr } + basePage.AddScript("chartist/chartist-plugin-legend.min.js") + basePage.AddSheet("chartist/chartist-plugin-legend.css") + timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) if err != nil { return common.LocalError(err.Error(), w, r, user) } + revLabelList, labelList, viewMap := analyticsTimeRangeToLabelList(timeRange) - rows, err := qgen.NewAcc().Select("viewchunks_systems").Columns("count, system").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() + rows, err := qgen.NewAcc().Select("viewchunks_systems").Columns("count, system, createdAt").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() if err != nil && err != sql.ErrNoRows { return common.InternalError(err, w, r) } - osMap, err := analyticsRowsToNameMap(rows) + vMap, osMap, err := analyticsRowsToDuoMap(rows, labelList, viewMap) if err != nil { return common.InternalError(err, w, r) } + ovList := analyticsVMapToOVList(vMap) + + var vList [][]int64 + var legendList []string + var i int + for _, ovitem := range ovList { + var viewList []int64 + for _, value := range revLabelList { + viewList = append(viewList, ovitem.viewMap[value]) + } + vList = append(vList, viewList) + lName, ok := phrases.GetOSPhrase(ovitem.name) + if !ok { + lName = ovitem.name + } + legendList = append(legendList, lName) + if i >= 6 { + break + } + i++ + } + graph := common.PanelTimeGraph{Series: vList, Labels: labelList, Legends: legendList} + common.DebugLogf("graph: %+v\n", graph) // TODO: Sort this slice var systemItems []common.PanelAnalyticsAgentsItem @@ -736,28 +821,55 @@ func AnalyticsSystems(w http.ResponseWriter, r *http.Request, user common.User) }) } - pi := common.PanelAnalyticsAgentsPage{basePage, systemItems, timeRange.Range} + pi := common.PanelAnalyticsDuoPage{basePage, systemItems, graph, timeRange.Range} return renderTemplate("panel_analytics_systems", w, r, basePage.Header, &pi) } func AnalyticsLanguages(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - basePage, ferr := buildBasePage(w, r, &user, "analytics", "analytics") + basePage, ferr := PreAnalyticsDetail(w, r, &user) if ferr != nil { return ferr } + basePage.AddScript("chartist/chartist-plugin-legend.min.js") + basePage.AddSheet("chartist/chartist-plugin-legend.css") + timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) if err != nil { return common.LocalError(err.Error(), w, r, user) } + revLabelList, labelList, viewMap := analyticsTimeRangeToLabelList(timeRange) - rows, err := qgen.NewAcc().Select("viewchunks_langs").Columns("count, lang").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() + rows, err := qgen.NewAcc().Select("viewchunks_langs").Columns("count, lang, createdAt").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() if err != nil && err != sql.ErrNoRows { return common.InternalError(err, w, r) } - langMap, err := analyticsRowsToNameMap(rows) + vMap, langMap, err := analyticsRowsToDuoMap(rows, labelList, viewMap) if err != nil { return common.InternalError(err, w, r) } + ovList := analyticsVMapToOVList(vMap) + + var vList [][]int64 + var legendList []string + var i int + for _, ovitem := range ovList { + var viewList []int64 + for _, value := range revLabelList { + viewList = append(viewList, ovitem.viewMap[value]) + } + vList = append(vList, viewList) + lName, ok := phrases.GetHumanLangPhrase(ovitem.name) + if !ok { + lName = ovitem.name + } + legendList = append(legendList, lName) + if i >= 6 { + break + } + i++ + } + graph := common.PanelTimeGraph{Series: vList, Labels: labelList, Legends: legendList} + common.DebugLogf("graph: %+v\n", graph) // TODO: Can we de-duplicate these analytics functions further? // TODO: Sort this slice @@ -774,7 +886,7 @@ func AnalyticsLanguages(w http.ResponseWriter, r *http.Request, user common.User }) } - pi := common.PanelAnalyticsAgentsPage{basePage, langItems, timeRange.Range} + pi := common.PanelAnalyticsDuoPage{basePage, langItems, graph, timeRange.Range} return renderTemplate("panel_analytics_langs", w, r, basePage.Header, &pi) } diff --git a/routes/topic_list.go b/routes/topic_list.go index 47d60d12..a5c5289c 100644 --- a/routes/topic_list.go +++ b/routes/topic_list.go @@ -29,6 +29,7 @@ func TopicListMostViewed(w http.ResponseWriter, r *http.Request, user common.Use // TODO: Implement search func TopicListCommon(w http.ResponseWriter, r *http.Request, user common.User, header *common.Header, torder string, tsorder string) common.RouteError { + header.Title = phrases.GetTitlePhrase("topics") header.Zone = "topics" header.Path = "/topics/" header.MetaDesc = header.Settings["meta_desc"].(string) @@ -189,7 +190,6 @@ func TopicListCommon(w http.ResponseWriter, r *http.Request, user common.User, h return nil } - header.Title = phrases.GetTitlePhrase("topics") pi := common.TopicListPage{header, topicList, forumList, common.Config.DefaultForum, common.TopicListSort{torder, false}, paginator} return renderTemplate("topics", w, r, header, pi) } diff --git a/templates/panel_analytics_forums.html b/templates/panel_analytics_forums.html index c525cc08..dc0ddf25 100644 --- a/templates/panel_analytics_forums.html +++ b/templates/panel_analytics_forums.html @@ -10,6 +10,9 @@ +
+
+
{{range .ItemList}}
@@ -20,4 +23,5 @@
+{{template "panel_analytics_script.html" . }} {{template "footer.html" . }} diff --git a/templates/panel_analytics_langs.html b/templates/panel_analytics_langs.html index bf221840..6fe220ab 100644 --- a/templates/panel_analytics_langs.html +++ b/templates/panel_analytics_langs.html @@ -10,6 +10,9 @@ +
+
+
{{range .ItemList}}
@@ -20,4 +23,5 @@
+{{template "panel_analytics_script.html" . }} {{template "footer.html" . }} diff --git a/templates/panel_analytics_posts.html b/templates/panel_analytics_posts.html index ae874572..03f1e3fd 100644 --- a/templates/panel_analytics_posts.html +++ b/templates/panel_analytics_posts.html @@ -19,7 +19,7 @@
{{range .ViewItems}}
- {{.Time}} + {{.Time}} {{.Count}}{{lang "panel_statistics_posts_suffix"}}
{{else}}
{{lang "panel_statistics_post_counts_no_post_counts"}}
{{end}} diff --git a/templates/panel_analytics_routes.html b/templates/panel_analytics_routes.html index 09af23b6..5dae4e5a 100644 --- a/templates/panel_analytics_routes.html +++ b/templates/panel_analytics_routes.html @@ -10,6 +10,9 @@
+
+
+
{{range .ItemList}}
@@ -20,4 +23,5 @@
+{{template "panel_analytics_script.html" . }} {{template "footer.html" . }} diff --git a/templates/panel_analytics_script.html b/templates/panel_analytics_script.html index 47243110..1d3cbae2 100644 --- a/templates/panel_analytics_script.html +++ b/templates/panel_analytics_script.html @@ -10,5 +10,5 @@ let seriesData = [{{range .Graph.Series}}[{{range .}} let legendNames = [{{range .Graph.Legends}} {{.}},{{end}} ]; -buildStatsChart(rawLabels, seriesData.reverse(), "{{.TimeRange}}",legendNames.reverse()); +buildStatsChart(rawLabels, seriesData, "{{.TimeRange}}",legendNames); \ No newline at end of file diff --git a/templates/panel_analytics_systems.html b/templates/panel_analytics_systems.html index 54b6b574..12c72856 100644 --- a/templates/panel_analytics_systems.html +++ b/templates/panel_analytics_systems.html @@ -10,6 +10,9 @@ +
+
+
{{range .ItemList}}
@@ -20,4 +23,5 @@
+{{template "panel_analytics_script.html" . }} {{template "footer.html" . }} diff --git a/templates/panel_analytics_time_range.html b/templates/panel_analytics_time_range.html index e688d705..02073190 100644 --- a/templates/panel_analytics_time_range.html +++ b/templates/panel_analytics_time_range.html @@ -1,4 +1,5 @@