Deployed multi-series charts across the entirety of the analytics panel.
Added the one year time range to the analytics panes. Dates are now shown on detail panes for Request, Topic and Post analytics instead of times for higher time ranges. The labels should now show up properly for the three month time range charts. The paginator should now work properly for login logs. Pushed a potential fix for subsequent pages with only one item not showing. up. Executing a search query should now change the title. Fixed a bug where the user agent parser choked on : characters. Fixed the ordering of items in the multi-series charts which caused the most important items to get booted out rather then the least important ones. Tweaked the padding on the User Manager items for Nox so they won't break onto multiple lines so readily. Fixed a potential issue with topic list titles. Fixed a potential crash bug in the Forum Analytics for deleted forums. Added the Count method to LoginLogStore. Continued work on the ElasticSearch mapping setup utility. Added the topic_list.search_head phrase. Added the panel_statistics_time_range_one_year phrase.
This commit is contained in:
parent
633c9ef2ec
commit
1fb497adf8
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -10,6 +10,9 @@
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div id="panel_analytics_forums_chart" class="colstack_graph_holder">
|
||||
<div class="ct_chart"></div>
|
||||
</div>
|
||||
<div id="panel_analytics_routes" class="colstack_item rowlist">
|
||||
{{range .ItemList}}
|
||||
<div class="rowitem panel_compactrow editable_parent">
|
||||
|
@ -20,4 +23,5 @@
|
|||
</div>
|
||||
</main>
|
||||
</div>
|
||||
{{template "panel_analytics_script.html" . }}
|
||||
{{template "footer.html" . }}
|
||||
|
|
|
@ -10,6 +10,9 @@
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div id="panel_analytics_langs_chart" class="colstack_graph_holder">
|
||||
<div class="ct_chart"></div>
|
||||
</div>
|
||||
<div id="panel_analytics_langs" class="colstack_item rowlist">
|
||||
{{range .ItemList}}
|
||||
<div class="rowitem panel_compactrow editable_parent">
|
||||
|
@ -20,4 +23,5 @@
|
|||
</div>
|
||||
</main>
|
||||
</div>
|
||||
{{template "panel_analytics_script.html" . }}
|
||||
{{template "footer.html" . }}
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<div id="panel_analytics_posts_table" class="colstack_item rowlist" aria-label="{{lang "panel_statistics_post_counts_table_aria"}}">
|
||||
{{range .ViewItems}}
|
||||
<div class="rowitem panel_compactrow editable_parent">
|
||||
<a class="panel_upshift unix_to_24_hour_time">{{.Time}}</a>
|
||||
<a class="panel_upshift {{if or (or (or (eq $.TimeRange "six-hours") (eq $.TimeRange "twelve-hours")) (eq $.TimeRange "one-day")) (eq $.TimeRange "two-days")}}unix_to_24_hour_time{{else}}unix_to_date{{end}}">{{.Time}}</a>
|
||||
<span class="panel_compacttext to_right">{{.Count}}{{lang "panel_statistics_posts_suffix"}}</span>
|
||||
</div>
|
||||
{{else}}<div class="rowitem passive rowmsg">{{lang "panel_statistics_post_counts_no_post_counts"}}</div>{{end}}
|
||||
|
|
|
@ -10,6 +10,9 @@
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div id="panel_analytics_routes_chart" class="colstack_graph_holder">
|
||||
<div class="ct_chart"></div>
|
||||
</div>
|
||||
<div id="panel_analytics_routes" class="colstack_item rowlist">
|
||||
{{range .ItemList}}
|
||||
<div class="rowitem panel_compactrow editable_parent">
|
||||
|
@ -20,4 +23,5 @@
|
|||
</div>
|
||||
</main>
|
||||
</div>
|
||||
{{template "panel_analytics_script.html" . }}
|
||||
{{template "footer.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);
|
||||
</script>
|
|
@ -10,6 +10,9 @@
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div id="panel_analytics_systems_chart" class="colstack_graph_holder">
|
||||
<div class="ct_chart"></div>
|
||||
</div>
|
||||
<div id="panel_analytics_systems" class="colstack_item rowlist">
|
||||
{{range .ItemList}}
|
||||
<div class="rowitem panel_compactrow editable_parent">
|
||||
|
@ -20,4 +23,5 @@
|
|||
</div>
|
||||
</main>
|
||||
</div>
|
||||
{{template "panel_analytics_script.html" . }}
|
||||
{{template "footer.html" . }}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<select class="timeRangeSelector to_right" name="timeRange">
|
||||
<option val="one-year"{{if eq .TimeRange "one-year"}} selected{{end}}>{{lang "panel_statistics_time_range_one_year"}}</option>
|
||||
<option val="three-months"{{if eq .TimeRange "three-months"}} selected{{end}}>{{lang "panel_statistics_time_range_three_months"}}</option>
|
||||
<option val="one-month"{{if eq .TimeRange "one-month"}} selected{{end}}>{{lang "panel_statistics_time_range_one_month"}}</option>
|
||||
<option val="one-week"{{if eq .TimeRange "one-week"}} selected{{end}}>{{lang "panel_statistics_time_range_one_week"}}</option>
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<div id="panel_analytics_topics_table" class="colstack_item rowlist" aria-label="{{lang "panel_statistics_topic_counts_table_aria"}}">
|
||||
{{range .ViewItems}}
|
||||
<div class="rowitem panel_compactrow editable_parent">
|
||||
<a class="panel_upshift unix_to_24_hour_time">{{.Time}}</a>
|
||||
<a class="panel_upshift {{if or (or (or (eq $.TimeRange "six-hours") (eq $.TimeRange "twelve-hours")) (eq $.TimeRange "one-day")) (eq $.TimeRange "two-days")}}unix_to_24_hour_time{{else}}unix_to_date{{end}}">{{.Time}}</a>
|
||||
<span class="panel_compacttext to_right">{{.Count}}{{lang "panel_statistics_topics_suffix"}}</span>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<div id="panel_analytics_views_table" class="colstack_item rowlist" aria-label="{{lang "panel_statistics_requests_table_aria"}}">
|
||||
{{range .ViewItems}}
|
||||
<div class="rowitem panel_compactrow editable_parent">
|
||||
<a class="panel_upshift unix_to_24_hour_time">{{.Time}}</a>
|
||||
<a class="panel_upshift {{if or (or (or (eq $.TimeRange "six-hours") (eq $.TimeRange "twelve-hours")) (eq $.TimeRange "one-day")) (eq $.TimeRange "two-days")}}unix_to_24_hour_time{{else}}unix_to_date{{end}}">{{.Time}}</a>
|
||||
<span class="panel_compacttext to_right">{{.Count}}{{lang "panel_statistics_views_suffix"}}</span>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
|
@ -87,6 +87,10 @@
|
|||
padding-right: 6px;
|
||||
}
|
||||
|
||||
#panel_users .rowitem {
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
button, .formbutton, .panel_right_button:not(.has_inner_button), #panel_users .profile_url {
|
||||
background: rgb(100,100,200);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue