From b864229a2c0841310df51333984a96b64f4edfce Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 7 Aug 2024 14:59:21 +0200 Subject: [PATCH 01/14] Added custom lecture stats to show live views --- api/statistics.go | 19 +++ dao/statistics.go | 24 ++++ web/admin.go | 28 +++- web/router.go | 1 + web/template/admin/lecture-stats.gohtml | 174 ++++++++++++++++++++++++ web/template/watch.gohtml | 7 + web/ts/stats.ts | 15 ++ 7 files changed, 265 insertions(+), 3 deletions(-) create mode 100644 web/template/admin/lecture-stats.gohtml diff --git a/api/statistics.go b/api/statistics.go index dfc879bad..b112e9565 100644 --- a/api/statistics.go +++ b/api/statistics.go @@ -87,6 +87,25 @@ func (r coursesRoutes) getStats(c *gin.Context) { resp.Data.Datasets[0].Label = "Sum(viewers)" resp.Data.Datasets[0].Data = res c.JSON(http.StatusOK, resp) + case "lecture": + res, err := r.StatisticsDao.GetLectureStatsFromLectureStart(cid) + if err != nil { + logger.Warn("GetCourseStatsHourly failed", "err", err, "courseId", cid) + _ = c.Error(tools.RequestError{ + Status: http.StatusInternalServerError, + CustomMessage: "can not get course stats hourly", + Err: err, + }) + return + } + resp := chartJs{ + ChartType: "bar", + Data: chartJsData{Datasets: []chartJsDataset{newChartJsDataset()}}, + Options: newChartJsOptions(), + } + resp.Data.Datasets[0].Label = "View Count" + resp.Data.Datasets[0].Data = res + c.JSON(http.StatusOK, resp) case "activity-live": resLive, err := r.StatisticsDao.GetStudentActivityCourseStats(cid, true) if err != nil { diff --git a/dao/statistics.go b/dao/statistics.go index 01e4963e1..ed5712a61 100644 --- a/dao/statistics.go +++ b/dao/statistics.go @@ -19,8 +19,10 @@ type StatisticsDao interface { GetCourseNumVodViewsPerDay(courseID uint) ([]Stat, error) GetCourseStatsWeekdays(courseID uint) ([]Stat, error) GetCourseStatsHourly(courseID uint) ([]Stat, error) + GetLectureStatsFromLectureStart(courseID uint) ([]Stat, error) GetStudentActivityCourseStats(courseID uint, live bool) ([]Stat, error) GetStreamNumLiveViews(streamID uint) (int, error) + GetLectureLiveStats(streamID uint) ([]model.Stat, error) } type statisticsDao struct { @@ -102,6 +104,21 @@ func (d statisticsDao) GetCourseStatsHourly(courseID uint) ([]Stat, error) { return res, err } +func (d statisticsDao) GetLectureStatsFromLectureStart(courseID uint) ([]Stat, error) { + var res []Stat + err := DB.Raw(`SELECT Date_FORMAT(stats.time, "%H:%i") AS x, stats.viewers AS y + FROM stats + JOIN streams s ON s.id = stats.stream_id + WHERE s.course_id = ? AND stats.live = 1 + ORDER BY x;`, courseID).Scan(&res).Error + //err := DB.Raw(`SELECT TIMESTAMPDIFF(MINUTE, s.start, stats.time) AS x, stats.viewers AS y + // FROM stats + // JOIN streams s ON s.id = stats.stream_id + // WHERE s.course_id = ? AND stats.live = 1 + // ORDER BY x;`, courseID).Scan(&res).Error + return res, err +} + // GetStreamNumLiveViews returns the number of viewers currently watching a live stream. func (d statisticsDao) GetStreamNumLiveViews(streamID uint) (int, error) { var res int @@ -155,6 +172,13 @@ func (d statisticsDao) GetStudentActivityCourseStats(courseID uint, live bool) ( return retVal, err } +func (d statisticsDao) GetLectureLiveStats(streamID uint) ([]model.Stat, error) { + var res []model.Stat + err := DB.Raw("SELECT * FROM stats WHERE stream_id = ? AND live = 1", streamID).Scan(&res).Error + + return res, err +} + // Stat key value struct that is parsable by Chart.js without further modifications. // See https://www.chartjs.org/docs/master/general/data-structures.html type Stat struct { diff --git a/web/admin.go b/web/admin.go index 775ec19dd..d3c92afe9 100755 --- a/web/admin.go +++ b/web/admin.go @@ -5,9 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "net/http" - "regexp" - "github.com/TUM-Dev/gocast/dao" "github.com/TUM-Dev/gocast/model" "github.com/TUM-Dev/gocast/tools" @@ -15,6 +12,8 @@ import ( "github.com/getsentry/sentry-go" "github.com/gin-gonic/gin" "gorm.io/gorm" + "net/http" + "regexp" ) // AdminPage serves all administration pages. todo: refactor into multiple methods @@ -179,6 +178,24 @@ func (r mainRoutes) LectureUnitsPage(c *gin.Context) { } } +func (r mainRoutes) LectureStatsPage(c *gin.Context) { + foundContext, exists := c.Get("TUMLiveContext") + if !exists { + sentry.CaptureException(errors.New("context should exist but doesn't")) + c.AbortWithStatus(http.StatusInternalServerError) + return + } + tumLiveContext := foundContext.(tools.TUMLiveContext) + indexData := NewIndexData() + indexData.TUMLiveContext = tumLiveContext + if err := templateExecutor.ExecuteTemplate(c.Writer, "lecture-stats.gohtml", LectureStatsPageData{ + IndexData: indexData, + Lecture: *tumLiveContext.Stream, + }); err != nil { + sentry.CaptureException(err) + } +} + func (r mainRoutes) CourseStatsPage(c *gin.Context) { foundContext, exists := c.Get("TUMLiveContext") if !exists { @@ -339,3 +356,8 @@ type LectureUnitsPageData struct { Lecture model.Stream Units []model.StreamUnit } + +type LectureStatsPageData struct { + IndexData IndexData + Lecture model.Stream +} diff --git a/web/router.go b/web/router.go index d2881aa78..f4a34c23a 100755 --- a/web/router.go +++ b/web/router.go @@ -137,6 +137,7 @@ func configMainRoute(router *gin.Engine) { withStream.Use(tools.InitStream(daoWrapper)) withStream.GET("/admin/units/:courseID/:streamID", routes.LectureUnitsPage) withStream.GET("/admin/cut/:courseID/:streamID", routes.LectureCutPage) + withStream.GET("/admin/stats/:courseID/:streamID", routes.LectureStatsPage) // login/logout/password-mgmt router.POST("/login", routes.LoginHandler) diff --git a/web/template/admin/lecture-stats.gohtml b/web/template/admin/lecture-stats.gohtml new file mode 100644 index 000000000..8209f4594 --- /dev/null +++ b/web/template/admin/lecture-stats.gohtml @@ -0,0 +1,174 @@ + + + + + {{.IndexData.Branding.Title}} | Administration + {{template "headImports" .IndexData.VersionTag}} + + + + + + {{$curUser := .IndexData.TUMLiveContext.User}} + {{$indexData := .IndexData}} + {{template "header" .IndexData.TUMLiveContext}} + {{- /*gotype: github.com/TUM-Dev/gocast/web.LectureStatsPageData*/ -}} + +
+ + +

Lecture Statistics

+
+ +

Quick stats

+ +
+ + + + + + + + + + + + + + + + + + + +
Lecture Time + {{.Lecture.FriendlyTime}} +
Enrolled Students + +
Vod Views + +
Max Live Views + +
+ +
+

Student Live activity during lecture

+
+ +
+
+ +
+
+ + +
+ + + + + +{{define "lecturestats"}} + {{- /*gotype: github.com/TUM-Dev/gocast/web.IndexData*/ -}} + + +

Server Statistics

+
+ +

Quick stats

+ +
+ + + + + + + + + + + + + + + + + + + +
Enrolled Students + +
Lectures{{.TUMLiveContext.Course.NumStreams}}
Vod Views + +
Live Views + +
+ +
+

Student Live activity per week

+
+ +
+
+
+

Student VoD activity per week

+
+ +
+
+
+

VoD activity throughout the day

+
+ +
+
+
+

VoD activity per day of week

+
+ +
+
+
+

VoD activity per day

+
+ +
+
+ + Export as JSON + + + Export as CSV + +
+
+

Some of this data is only captured + from June 28th 2021 + onwards.

+ +{{end}} diff --git a/web/template/watch.gohtml b/web/template/watch.gohtml index a4ea2df1e..3c98ae09f 100644 --- a/web/template/watch.gohtml +++ b/web/template/watch.gohtml @@ -458,6 +458,13 @@
+ {{if or (.IndexData.TUMLiveContext.User.IsAdminOfCourse .IndexData.TUMLiveContext.Course) .IndexData.IsAdmin}} + + + + {{end}} {{if or (.IndexData.TUMLiveContext.User.IsAdminOfCourse .IndexData.TUMLiveContext.Course) .IndexData.IsAdmin}} { + getAsync( + `/api/course/${(document.getElementById("courseID") as HTMLInputElement).value}/stats?interval=${endpoint}`, + ).then((res) => { + if (res.status === StatusCodes.OK) { + res.text().then((value) => { + document.getElementById(endpoint).innerHTML = `${JSON.parse(value)["res"]}`; + }); + } + }); + }); +} + export async function getAsync(url = "") { return await fetch(url, { method: "GET", From 8851c78a555b0cc24866d59dedfd918a0b66e201 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 7 Aug 2024 15:52:44 +0200 Subject: [PATCH 02/14] Added some more insights and fixed an error where only course is filtered but not stream --- api/statistics.go | 63 +++++++++++++++++++++--- dao/statistics.go | 64 +++++++++++++++++++++++-- web/template/admin/lecture-stats.gohtml | 18 ++++++- web/ts/stats.ts | 18 ++++++- 4 files changed, 147 insertions(+), 16 deletions(-) diff --git a/api/statistics.go b/api/statistics.go index b112e9565..42b1cdf8f 100644 --- a/api/statistics.go +++ b/api/statistics.go @@ -13,6 +13,7 @@ import ( type statReq struct { Interval string `form:"interval" json:"interval" xml:"interval" binding:"required"` + Lecture string `form:"lecture" json:"lecture" xml:"lecture"` } type statExportReq struct { @@ -46,11 +47,35 @@ func (r coursesRoutes) getStats(c *gin.Context) { } else { // use course from context cid = ctx.(tools.TUMLiveContext).Course.ID } + + var sid uint + if req.Lecture != "" { + sidTemp, err := strconv.ParseUint(req.Lecture, 10, 32) + if err != nil { + logger.Warn("strconv.Atoi failed", "err", err, "courseId", cid) + _ = c.Error(tools.RequestError{ + Status: http.StatusBadRequest, + CustomMessage: "strconv.Atoi failed", + Err: err, + }) + return + } + sid = uint(sidTemp) + } else { + sid = ^uint(0) + } + switch req.Interval { case "week": fallthrough case "day": - res, err := r.StatisticsDao.GetCourseStatsWeekdays(cid) + var res []dao.Stat + var err error + if sid != ^uint(0) { + res, err = r.StatisticsDao.GetLectureStatsWeekdays(cid, sid) + } else { + res, err = r.StatisticsDao.GetCourseStatsWeekdays(cid) + } if err != nil { logger.Warn("GetCourseStatsWeekdays failed", "err", err, "courseId", cid) _ = c.Error(tools.RequestError{ @@ -69,7 +94,13 @@ func (r coursesRoutes) getStats(c *gin.Context) { resp.Data.Datasets[0].Data = res c.JSON(http.StatusOK, resp) case "hour": - res, err := r.StatisticsDao.GetCourseStatsHourly(cid) + var res []dao.Stat + var err error + if sid != ^uint(0) { + res, err = r.StatisticsDao.GetLectureStatsHourly(cid, sid) + } else { + res, err = r.StatisticsDao.GetCourseStatsHourly(cid) + } if err != nil { logger.Warn("GetCourseStatsHourly failed", "err", err, "courseId", cid) _ = c.Error(tools.RequestError{ @@ -88,9 +119,9 @@ func (r coursesRoutes) getStats(c *gin.Context) { resp.Data.Datasets[0].Data = res c.JSON(http.StatusOK, resp) case "lecture": - res, err := r.StatisticsDao.GetLectureStatsFromLectureStart(cid) + res, err := r.StatisticsDao.GetLectureStats(cid, sid) if err != nil { - logger.Warn("GetCourseStatsHourly failed", "err", err, "courseId", cid) + logger.Warn("GetLectureStats failed", "err", err, "courseId", cid) _ = c.Error(tools.RequestError{ Status: http.StatusInternalServerError, CustomMessage: "can not get course stats hourly", @@ -163,7 +194,13 @@ func (r coursesRoutes) getStats(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"res": res}) } case "vodViews": - res, err := r.StatisticsDao.GetCourseNumVodViews(cid) + var res int + var err error + if sid != ^uint(0) { + res, err = r.StatisticsDao.GetLectureNumVodViews(sid) + } else { + res, err = r.StatisticsDao.GetCourseNumVodViews(cid) + } if err != nil { logger.Warn("GetCourseNumVodViews failed", "err", err, "courseId", cid) _ = c.Error(tools.RequestError{ @@ -176,7 +213,13 @@ func (r coursesRoutes) getStats(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"res": res}) } case "liveViews": - res, err := r.StatisticsDao.GetCourseNumLiveViews(cid) + var res int + var err error + if sid != ^uint(0) { + res, err = r.StatisticsDao.GetLectureNumLiveViews(sid) + } else { + res, err = r.StatisticsDao.GetCourseNumLiveViews(cid) + } if err != nil { logger.Warn("GetCourseNumLiveViews failed", "err", err, "courseId", cid) _ = c.Error(tools.RequestError{ @@ -190,7 +233,13 @@ func (r coursesRoutes) getStats(c *gin.Context) { } case "allDays": { - res, err := r.StatisticsDao.GetCourseNumVodViewsPerDay(cid) + var res []dao.Stat + var err error + if sid != ^uint(0) { + res, err = r.StatisticsDao.GetLectureNumVodViewsPerDay(sid) + } else { + res, err = r.StatisticsDao.GetCourseNumVodViewsPerDay(cid) + } if err != nil { logger.Warn("GetCourseNumLiveViews failed", "err", err, "courseId", cid) _ = c.Error(tools.RequestError{ diff --git a/dao/statistics.go b/dao/statistics.go index ed5712a61..d31176332 100644 --- a/dao/statistics.go +++ b/dao/statistics.go @@ -16,13 +16,17 @@ type StatisticsDao interface { GetCourseNumStudents(courseID uint) (int64, error) GetCourseNumVodViews(courseID uint) (int, error) GetCourseNumLiveViews(courseID uint) (int, error) + GetLectureNumVodViews(streamID uint) (int, error) + GetLectureNumLiveViews(streamID uint) (int, error) GetCourseNumVodViewsPerDay(courseID uint) ([]Stat, error) + GetLectureNumVodViewsPerDay(streamID uint) ([]Stat, error) GetCourseStatsWeekdays(courseID uint) ([]Stat, error) GetCourseStatsHourly(courseID uint) ([]Stat, error) - GetLectureStatsFromLectureStart(courseID uint) ([]Stat, error) + GetLectureStatsWeekdays(courseID uint, streamID uint) ([]Stat, error) + GetLectureStatsHourly(courseID uint, streamID uint) ([]Stat, error) + GetLectureStats(courseID uint, lectureID uint) ([]Stat, error) GetStudentActivityCourseStats(courseID uint, live bool) ([]Stat, error) GetStreamNumLiveViews(streamID uint) (int, error) - GetLectureLiveStats(streamID uint) ([]model.Stat, error) } type statisticsDao struct { @@ -68,6 +72,21 @@ func (d statisticsDao) GetCourseNumLiveViews(courseID uint) (int, error) { return res, err } +// GetLectureNumVodViews returns the sum of vod views of a lecture +func (d statisticsDao) GetLectureNumVodViews(streamID uint) (int, error) { + var res int + err := DB.Raw(`SELECT IFNULL(SUM(viewers), 0) FROM stats + WHERE live = 0 AND stream_id = ?`, streamID).Scan(&res).Error + return res, err +} + +// GetLectureNumLiveViews returns the sum of live views of a lecture +func (d statisticsDao) GetLectureNumLiveViews(streamID uint) (int, error) { + var res int + err := DB.Raw(`SELECT MAX(viewers) from stats where stream_id = ?`, streamID).Scan(&res).Error + return res, err +} + // GetCourseNumVodViewsPerDay returns the daily amount of vod views for each day func (d statisticsDao) GetCourseNumVodViewsPerDay(courseID uint) ([]Stat, error) { var res []Stat @@ -80,6 +99,17 @@ func (d statisticsDao) GetCourseNumVodViewsPerDay(courseID uint) ([]Stat, error) return res, err } +// GetLectureNumVodViewsPerDay returns the daily amount of vod views for each day +func (d statisticsDao) GetLectureNumVodViewsPerDay(streamID uint) ([]Stat, error) { + var res []Stat + err := DB.Raw(`SELECT DATE_FORMAT(stats.time, GET_FORMAT(DATE, 'EUR')) AS x, sum(viewers) AS y + FROM stats + WHERE stream_id = ? AND live = 0 + GROUP BY DATE(stats.time);`, + streamID).Scan(&res).Error + return res, err +} + // GetCourseStatsWeekdays returns the days and their sum of vod views of a course func (d statisticsDao) GetCourseStatsWeekdays(courseID uint) ([]Stat, error) { var res []Stat @@ -104,13 +134,37 @@ func (d statisticsDao) GetCourseStatsHourly(courseID uint) ([]Stat, error) { return res, err } -func (d statisticsDao) GetLectureStatsFromLectureStart(courseID uint) ([]Stat, error) { +// GetCourseStatsWeekdays returns the days and their sum of vod views of a course +func (d statisticsDao) GetLectureStatsWeekdays(courseID uint, streamID uint) ([]Stat, error) { + var res []Stat + err := DB.Raw(`SELECT DAYNAME(stats.time) AS x, SUM(stats.viewers) as y + FROM stats + JOIN streams s ON s.id = stats.stream_id + WHERE (s.course_id = ? OR ? = 0) AND stats.live = 0 AND stats.stream_id = ? + GROUP BY DAYOFWEEK(stats.time);`, + courseID, courseID, streamID).Scan(&res).Error + return res, err +} + +// GetCourseStatsHourly returns the hours with most vod viewing activity of a course +func (d statisticsDao) GetLectureStatsHourly(courseID uint, streamID uint) ([]Stat, error) { + var res []Stat + err := DB.Raw(`SELECT HOUR(stats.time) AS x, SUM(stats.viewers) as y + FROM stats + JOIN streams s ON s.id = stats.stream_id + WHERE (s.course_id = ? or ? = 0) AND stats.live = 0 AND stats.stream_id = ? + GROUP BY HOUR(stats.time);`, + courseID, courseID, streamID).Scan(&res).Error + return res, err +} + +func (d statisticsDao) GetLectureStats(courseID uint, streamID uint) ([]Stat, error) { var res []Stat err := DB.Raw(`SELECT Date_FORMAT(stats.time, "%H:%i") AS x, stats.viewers AS y FROM stats JOIN streams s ON s.id = stats.stream_id - WHERE s.course_id = ? AND stats.live = 1 - ORDER BY x;`, courseID).Scan(&res).Error + WHERE s.course_id = ? AND s.id = ? AND stats.live = 1 + ORDER BY x;`, courseID, streamID).Scan(&res).Error //err := DB.Raw(`SELECT TIMESTAMPDIFF(MINUTE, s.start, stats.time) AS x, stats.viewers AS y // FROM stats // JOIN streams s ON s.id = stats.stream_id diff --git a/web/template/admin/lecture-stats.gohtml b/web/template/admin/lecture-stats.gohtml index 8209f4594..4a279871c 100644 --- a/web/template/admin/lecture-stats.gohtml +++ b/web/template/admin/lecture-stats.gohtml @@ -61,6 +61,18 @@ role="img">
+
+

VoD activity per day of week

+
+ +
+
+
+

VoD activity per day

+
+ +
+
diff --git a/web/ts/stats.ts b/web/ts/stats.ts index ecc5c45e7..ca7ec7014 100644 --- a/web/ts/stats.ts +++ b/web/ts/stats.ts @@ -23,6 +23,20 @@ export function loadStats(endpoint: string, targetEl: string) { }); } +export function loadLectureStats(endpoint: string, targetEl: string, streamID: string) { + const canvas = document.getElementById(targetEl); + const ctx = canvas.getContext("2d"); + getAsync( + `/api/course/${(document.getElementById("courseID") as HTMLInputElement).value}/stats?interval=${endpoint}&lecture=${streamID}`, + ).then((res) => { + if (res.status === StatusCodes.OK) { + res.text().then((value) => { + new Chart(ctx, JSON.parse(value)); + }); + } + }); +} + export function initStatsPage() { const dates = ["numStudents", "vodViews", "liveViews"]; dates.forEach((endpoint) => { @@ -38,11 +52,11 @@ export function initStatsPage() { }); } -export function initLectureStatsPage() { +export function initLectureStatsPage(lectureID : string) { const dates = ["numStudents", "vodViews", "liveViews"]; dates.forEach((endpoint) => { getAsync( - `/api/course/${(document.getElementById("courseID") as HTMLInputElement).value}/stats?interval=${endpoint}`, + `/api/course/${(document.getElementById("courseID") as HTMLInputElement).value}/stats?interval=${endpoint}&lecture=${lectureID}`, ).then((res) => { if (res.status === StatusCodes.OK) { res.text().then((value) => { From e2e26dcc4fcb6ed4f52bf26101d4abb93d300bf5 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 7 Aug 2024 16:06:37 +0200 Subject: [PATCH 03/14] Fixed buttons on watch page --- web/template/watch.gohtml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/web/template/watch.gohtml b/web/template/watch.gohtml index 3c98ae09f..07ed1960c 100644 --- a/web/template/watch.gohtml +++ b/web/template/watch.gohtml @@ -457,20 +457,20 @@
-
+
{{if or (.IndexData.TUMLiveContext.User.IsAdminOfCourse .IndexData.TUMLiveContext.Course) .IndexData.IsAdmin}} - - - - {{end}} - {{if or (.IndexData.TUMLiveContext.User.IsAdminOfCourse .IndexData.TUMLiveContext.Course) .IndexData.IsAdmin}} - + + + + +
{{end}} {{if .IndexData.TUMLiveContext.User}} From 4f6723978387d26f50d5e0bb69ec7c86eee2112c Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 7 Aug 2024 16:11:18 +0200 Subject: [PATCH 04/14] Lint fix --- web/ts/stats.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/ts/stats.ts b/web/ts/stats.ts index ca7ec7014..0446b12ce 100644 --- a/web/ts/stats.ts +++ b/web/ts/stats.ts @@ -52,7 +52,7 @@ export function initStatsPage() { }); } -export function initLectureStatsPage(lectureID : string) { +export function initLectureStatsPage(lectureID: string) { const dates = ["numStudents", "vodViews", "liveViews"]; dates.forEach((endpoint) => { getAsync( From 29e0258b77d9a0403befd8bcab45a1d52035c8cd Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 9 Aug 2024 14:19:56 +0200 Subject: [PATCH 05/14] Fixed prettier issue --- web/ts/stats.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/web/ts/stats.ts b/web/ts/stats.ts index 0446b12ce..be92a946f 100644 --- a/web/ts/stats.ts +++ b/web/ts/stats.ts @@ -41,7 +41,9 @@ export function initStatsPage() { const dates = ["numStudents", "vodViews", "liveViews"]; dates.forEach((endpoint) => { getAsync( - `/api/course/${(document.getElementById("courseID") as HTMLInputElement).value}/stats?interval=${endpoint}`, + `/api/course/${ + (document.getElementById("courseID") as HTMLInputElement).value + }/stats?interval=${endpoint}`, ).then((res) => { if (res.status === StatusCodes.OK) { res.text().then((value) => { @@ -56,7 +58,9 @@ export function initLectureStatsPage(lectureID: string) { const dates = ["numStudents", "vodViews", "liveViews"]; dates.forEach((endpoint) => { getAsync( - `/api/course/${(document.getElementById("courseID") as HTMLInputElement).value}/stats?interval=${endpoint}&lecture=${lectureID}`, + `/api/course/${ + (document.getElementById("courseID") as HTMLInputElement).value + }/stats?interval=${endpoint}&lecture=${lectureID}`, ).then((res) => { if (res.status === StatusCodes.OK) { res.text().then((value) => { From c82499aa8ea90c79f98f7b957dcd72c250077874 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 9 Aug 2024 14:22:34 +0200 Subject: [PATCH 06/14] Fixed another issue with eslint --- web/ts/stats.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web/ts/stats.ts b/web/ts/stats.ts index be92a946f..98bd0dafe 100644 --- a/web/ts/stats.ts +++ b/web/ts/stats.ts @@ -27,7 +27,9 @@ export function loadLectureStats(endpoint: string, targetEl: string, streamID: s const canvas = document.getElementById(targetEl); const ctx = canvas.getContext("2d"); getAsync( - `/api/course/${(document.getElementById("courseID") as HTMLInputElement).value}/stats?interval=${endpoint}&lecture=${streamID}`, + `/api/course/${ + (document.getElementById("courseID") as HTMLInputElement).value + }/stats?interval=${endpoint}&lecture=${streamID}`, ).then((res) => { if (res.status === StatusCodes.OK) { res.text().then((value) => { @@ -41,9 +43,7 @@ export function initStatsPage() { const dates = ["numStudents", "vodViews", "liveViews"]; dates.forEach((endpoint) => { getAsync( - `/api/course/${ - (document.getElementById("courseID") as HTMLInputElement).value - }/stats?interval=${endpoint}`, + `/api/course/${(document.getElementById("courseID") as HTMLInputElement).value}/stats?interval=${endpoint}`, ).then((res) => { if (res.status === StatusCodes.OK) { res.text().then((value) => { From 94aabbb00c5213a3534c9737a103b3f73c13a448 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 9 Aug 2024 14:37:37 +0200 Subject: [PATCH 07/14] Fixed missing mock methods --- mock_dao/statistics.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/mock_dao/statistics.go b/mock_dao/statistics.go index 349f3d2ea..c1c950e7e 100644 --- a/mock_dao/statistics.go +++ b/mock_dao/statistics.go @@ -5,6 +5,7 @@ package mock_dao import ( + "github.com/crewjam/httperr" reflect "reflect" dao "github.com/TUM-Dev/gocast/dao" @@ -168,3 +169,22 @@ func (mr *MockStatisticsDaoMockRecorder) GetStudentActivityCourseStats(courseID, mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStudentActivityCourseStats", reflect.TypeOf((*MockStatisticsDao)(nil).GetStudentActivityCourseStats), courseID, live) } + +func (mr *MockStatisticsDaoMockRecorder) GetLectureNumVodViews(streamID uint) (int, error) { + panic(httperr.NotImplemented) +} +func (mr *MockStatisticsDaoMockRecorder) GetLectureNumLiveViews(streamID uint) (int, error) { + panic(httperr.NotImplemented) +} +func (mr *MockStatisticsDaoMockRecorder) GetLectureNumVodViewsPerDay(streamID uint) ([]dao.Stat, error) { + panic(httperr.NotImplemented) +} +func (mr *MockStatisticsDaoMockRecorder) GetLectureStatsWeekdays(courseID uint, streamID uint) ([]dao.Stat, error) { + panic(httperr.NotImplemented) +} +func (mr *MockStatisticsDaoMockRecorder) GetLectureStatsHourly(courseID uint, streamID uint) ([]dao.Stat, error) { + panic(httperr.NotImplemented) +} +func (mr *MockStatisticsDaoMockRecorder) GetLectureStats(courseID uint, lectureID uint) ([]dao.Stat, error) { + panic(httperr.NotImplemented) +} From 649e2ed906b3cfe284fd04a1b2dc6ab21b1f0635 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 9 Aug 2024 14:40:36 +0200 Subject: [PATCH 08/14] Removed manually added methods from mock --- mock_dao/statistics.go | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/mock_dao/statistics.go b/mock_dao/statistics.go index c1c950e7e..349f3d2ea 100644 --- a/mock_dao/statistics.go +++ b/mock_dao/statistics.go @@ -5,7 +5,6 @@ package mock_dao import ( - "github.com/crewjam/httperr" reflect "reflect" dao "github.com/TUM-Dev/gocast/dao" @@ -169,22 +168,3 @@ func (mr *MockStatisticsDaoMockRecorder) GetStudentActivityCourseStats(courseID, mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStudentActivityCourseStats", reflect.TypeOf((*MockStatisticsDao)(nil).GetStudentActivityCourseStats), courseID, live) } - -func (mr *MockStatisticsDaoMockRecorder) GetLectureNumVodViews(streamID uint) (int, error) { - panic(httperr.NotImplemented) -} -func (mr *MockStatisticsDaoMockRecorder) GetLectureNumLiveViews(streamID uint) (int, error) { - panic(httperr.NotImplemented) -} -func (mr *MockStatisticsDaoMockRecorder) GetLectureNumVodViewsPerDay(streamID uint) ([]dao.Stat, error) { - panic(httperr.NotImplemented) -} -func (mr *MockStatisticsDaoMockRecorder) GetLectureStatsWeekdays(courseID uint, streamID uint) ([]dao.Stat, error) { - panic(httperr.NotImplemented) -} -func (mr *MockStatisticsDaoMockRecorder) GetLectureStatsHourly(courseID uint, streamID uint) ([]dao.Stat, error) { - panic(httperr.NotImplemented) -} -func (mr *MockStatisticsDaoMockRecorder) GetLectureStats(courseID uint, lectureID uint) ([]dao.Stat, error) { - panic(httperr.NotImplemented) -} From c1b6c71a7ba024528f69c21c0930568aa9e1bded Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 9 Aug 2024 14:44:40 +0200 Subject: [PATCH 09/14] Called gomock --- mock_dao/statistics.go | 117 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 106 insertions(+), 11 deletions(-) diff --git a/mock_dao/statistics.go b/mock_dao/statistics.go index 349f3d2ea..a0a1c89b5 100644 --- a/mock_dao/statistics.go +++ b/mock_dao/statistics.go @@ -1,5 +1,10 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: statistics.go +// Source: dao/statistics.go +// +// Generated by this command: +// +// mockgen -source dao/statistics.go -destination mock_dao/statistics.go +// // Package mock_dao is a generated GoMock package. package mock_dao @@ -9,7 +14,7 @@ import ( dao "github.com/TUM-Dev/gocast/dao" model "github.com/TUM-Dev/gocast/model" - gomock "github.com/golang/mock/gomock" + gomock "go.uber.org/mock/gomock" ) // MockStatisticsDao is a mock of StatisticsDao interface. @@ -44,7 +49,7 @@ func (m *MockStatisticsDao) AddStat(stat model.Stat) error { } // AddStat indicates an expected call of AddStat. -func (mr *MockStatisticsDaoMockRecorder) AddStat(stat interface{}) *gomock.Call { +func (mr *MockStatisticsDaoMockRecorder) AddStat(stat any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddStat", reflect.TypeOf((*MockStatisticsDao)(nil).AddStat), stat) } @@ -59,7 +64,7 @@ func (m *MockStatisticsDao) GetCourseNumLiveViews(courseID uint) (int, error) { } // GetCourseNumLiveViews indicates an expected call of GetCourseNumLiveViews. -func (mr *MockStatisticsDaoMockRecorder) GetCourseNumLiveViews(courseID interface{}) *gomock.Call { +func (mr *MockStatisticsDaoMockRecorder) GetCourseNumLiveViews(courseID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCourseNumLiveViews", reflect.TypeOf((*MockStatisticsDao)(nil).GetCourseNumLiveViews), courseID) } @@ -74,7 +79,7 @@ func (m *MockStatisticsDao) GetCourseNumStudents(courseID uint) (int64, error) { } // GetCourseNumStudents indicates an expected call of GetCourseNumStudents. -func (mr *MockStatisticsDaoMockRecorder) GetCourseNumStudents(courseID interface{}) *gomock.Call { +func (mr *MockStatisticsDaoMockRecorder) GetCourseNumStudents(courseID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCourseNumStudents", reflect.TypeOf((*MockStatisticsDao)(nil).GetCourseNumStudents), courseID) } @@ -89,7 +94,7 @@ func (m *MockStatisticsDao) GetCourseNumVodViews(courseID uint) (int, error) { } // GetCourseNumVodViews indicates an expected call of GetCourseNumVodViews. -func (mr *MockStatisticsDaoMockRecorder) GetCourseNumVodViews(courseID interface{}) *gomock.Call { +func (mr *MockStatisticsDaoMockRecorder) GetCourseNumVodViews(courseID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCourseNumVodViews", reflect.TypeOf((*MockStatisticsDao)(nil).GetCourseNumVodViews), courseID) } @@ -104,7 +109,7 @@ func (m *MockStatisticsDao) GetCourseNumVodViewsPerDay(courseID uint) ([]dao.Sta } // GetCourseNumVodViewsPerDay indicates an expected call of GetCourseNumVodViewsPerDay. -func (mr *MockStatisticsDaoMockRecorder) GetCourseNumVodViewsPerDay(courseID interface{}) *gomock.Call { +func (mr *MockStatisticsDaoMockRecorder) GetCourseNumVodViewsPerDay(courseID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCourseNumVodViewsPerDay", reflect.TypeOf((*MockStatisticsDao)(nil).GetCourseNumVodViewsPerDay), courseID) } @@ -119,7 +124,7 @@ func (m *MockStatisticsDao) GetCourseStatsHourly(courseID uint) ([]dao.Stat, err } // GetCourseStatsHourly indicates an expected call of GetCourseStatsHourly. -func (mr *MockStatisticsDaoMockRecorder) GetCourseStatsHourly(courseID interface{}) *gomock.Call { +func (mr *MockStatisticsDaoMockRecorder) GetCourseStatsHourly(courseID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCourseStatsHourly", reflect.TypeOf((*MockStatisticsDao)(nil).GetCourseStatsHourly), courseID) } @@ -134,11 +139,101 @@ func (m *MockStatisticsDao) GetCourseStatsWeekdays(courseID uint) ([]dao.Stat, e } // GetCourseStatsWeekdays indicates an expected call of GetCourseStatsWeekdays. -func (mr *MockStatisticsDaoMockRecorder) GetCourseStatsWeekdays(courseID interface{}) *gomock.Call { +func (mr *MockStatisticsDaoMockRecorder) GetCourseStatsWeekdays(courseID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCourseStatsWeekdays", reflect.TypeOf((*MockStatisticsDao)(nil).GetCourseStatsWeekdays), courseID) } +// GetLectureNumLiveViews mocks base method. +func (m *MockStatisticsDao) GetLectureNumLiveViews(streamID uint) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLectureNumLiveViews", streamID) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetLectureNumLiveViews indicates an expected call of GetLectureNumLiveViews. +func (mr *MockStatisticsDaoMockRecorder) GetLectureNumLiveViews(streamID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLectureNumLiveViews", reflect.TypeOf((*MockStatisticsDao)(nil).GetLectureNumLiveViews), streamID) +} + +// GetLectureNumVodViews mocks base method. +func (m *MockStatisticsDao) GetLectureNumVodViews(streamID uint) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLectureNumVodViews", streamID) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetLectureNumVodViews indicates an expected call of GetLectureNumVodViews. +func (mr *MockStatisticsDaoMockRecorder) GetLectureNumVodViews(streamID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLectureNumVodViews", reflect.TypeOf((*MockStatisticsDao)(nil).GetLectureNumVodViews), streamID) +} + +// GetLectureNumVodViewsPerDay mocks base method. +func (m *MockStatisticsDao) GetLectureNumVodViewsPerDay(streamID uint) ([]dao.Stat, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLectureNumVodViewsPerDay", streamID) + ret0, _ := ret[0].([]dao.Stat) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetLectureNumVodViewsPerDay indicates an expected call of GetLectureNumVodViewsPerDay. +func (mr *MockStatisticsDaoMockRecorder) GetLectureNumVodViewsPerDay(streamID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLectureNumVodViewsPerDay", reflect.TypeOf((*MockStatisticsDao)(nil).GetLectureNumVodViewsPerDay), streamID) +} + +// GetLectureStats mocks base method. +func (m *MockStatisticsDao) GetLectureStats(courseID, lectureID uint) ([]dao.Stat, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLectureStats", courseID, lectureID) + ret0, _ := ret[0].([]dao.Stat) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetLectureStats indicates an expected call of GetLectureStats. +func (mr *MockStatisticsDaoMockRecorder) GetLectureStats(courseID, lectureID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLectureStats", reflect.TypeOf((*MockStatisticsDao)(nil).GetLectureStats), courseID, lectureID) +} + +// GetLectureStatsHourly mocks base method. +func (m *MockStatisticsDao) GetLectureStatsHourly(courseID, streamID uint) ([]dao.Stat, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLectureStatsHourly", courseID, streamID) + ret0, _ := ret[0].([]dao.Stat) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetLectureStatsHourly indicates an expected call of GetLectureStatsHourly. +func (mr *MockStatisticsDaoMockRecorder) GetLectureStatsHourly(courseID, streamID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLectureStatsHourly", reflect.TypeOf((*MockStatisticsDao)(nil).GetLectureStatsHourly), courseID, streamID) +} + +// GetLectureStatsWeekdays mocks base method. +func (m *MockStatisticsDao) GetLectureStatsWeekdays(courseID, streamID uint) ([]dao.Stat, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLectureStatsWeekdays", courseID, streamID) + ret0, _ := ret[0].([]dao.Stat) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetLectureStatsWeekdays indicates an expected call of GetLectureStatsWeekdays. +func (mr *MockStatisticsDaoMockRecorder) GetLectureStatsWeekdays(courseID, streamID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLectureStatsWeekdays", reflect.TypeOf((*MockStatisticsDao)(nil).GetLectureStatsWeekdays), courseID, streamID) +} + // GetStreamNumLiveViews mocks base method. func (m *MockStatisticsDao) GetStreamNumLiveViews(streamID uint) (int, error) { m.ctrl.T.Helper() @@ -149,7 +244,7 @@ func (m *MockStatisticsDao) GetStreamNumLiveViews(streamID uint) (int, error) { } // GetStreamNumLiveViews indicates an expected call of GetStreamNumLiveViews. -func (mr *MockStatisticsDaoMockRecorder) GetStreamNumLiveViews(streamID interface{}) *gomock.Call { +func (mr *MockStatisticsDaoMockRecorder) GetStreamNumLiveViews(streamID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStreamNumLiveViews", reflect.TypeOf((*MockStatisticsDao)(nil).GetStreamNumLiveViews), streamID) } @@ -164,7 +259,7 @@ func (m *MockStatisticsDao) GetStudentActivityCourseStats(courseID uint, live bo } // GetStudentActivityCourseStats indicates an expected call of GetStudentActivityCourseStats. -func (mr *MockStatisticsDaoMockRecorder) GetStudentActivityCourseStats(courseID, live interface{}) *gomock.Call { +func (mr *MockStatisticsDaoMockRecorder) GetStudentActivityCourseStats(courseID, live any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStudentActivityCourseStats", reflect.TypeOf((*MockStatisticsDao)(nil).GetStudentActivityCourseStats), courseID, live) } From 060673c44bf0eedb1afcbcb07b9f57831d31cfae Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 9 Aug 2024 14:46:45 +0200 Subject: [PATCH 10/14] MOved from go.uber to gomock --- mock_dao/statistics.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mock_dao/statistics.go b/mock_dao/statistics.go index a0a1c89b5..0be344fb3 100644 --- a/mock_dao/statistics.go +++ b/mock_dao/statistics.go @@ -10,11 +10,11 @@ package mock_dao import ( + "github.com/golang/mock/gomock" reflect "reflect" dao "github.com/TUM-Dev/gocast/dao" model "github.com/TUM-Dev/gocast/model" - gomock "go.uber.org/mock/gomock" ) // MockStatisticsDao is a mock of StatisticsDao interface. From b225b9f9771af0cd43145d5b40a3e6ade800a794 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 9 Aug 2024 14:51:22 +0200 Subject: [PATCH 11/14] Fixed golangci-lint --- dao/statistics.go | 2 +- web/admin.go | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) mode change 100755 => 100644 web/admin.go diff --git a/dao/statistics.go b/dao/statistics.go index d31176332..486b61b10 100644 --- a/dao/statistics.go +++ b/dao/statistics.go @@ -165,7 +165,7 @@ func (d statisticsDao) GetLectureStats(courseID uint, streamID uint) ([]Stat, er JOIN streams s ON s.id = stats.stream_id WHERE s.course_id = ? AND s.id = ? AND stats.live = 1 ORDER BY x;`, courseID, streamID).Scan(&res).Error - //err := DB.Raw(`SELECT TIMESTAMPDIFF(MINUTE, s.start, stats.time) AS x, stats.viewers AS y + // err := DB.Raw(`SELECT TIMESTAMPDIFF(MINUTE, s.start, stats.time) AS x, stats.viewers AS y // FROM stats // JOIN streams s ON s.id = stats.stream_id // WHERE s.course_id = ? AND stats.live = 1 diff --git a/web/admin.go b/web/admin.go old mode 100755 new mode 100644 index d3c92afe9..d785bb0cd --- a/web/admin.go +++ b/web/admin.go @@ -12,8 +12,6 @@ import ( "github.com/getsentry/sentry-go" "github.com/gin-gonic/gin" "gorm.io/gorm" - "net/http" - "regexp" ) // AdminPage serves all administration pages. todo: refactor into multiple methods From e232d60896f980a79532ca3c40a29641a977080e Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 9 Aug 2024 14:54:17 +0200 Subject: [PATCH 12/14] Fixed missing import --- web/admin.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/admin.go b/web/admin.go index d785bb0cd..d3c92afe9 100644 --- a/web/admin.go +++ b/web/admin.go @@ -12,6 +12,8 @@ import ( "github.com/getsentry/sentry-go" "github.com/gin-gonic/gin" "gorm.io/gorm" + "net/http" + "regexp" ) // AdminPage serves all administration pages. todo: refactor into multiple methods From 479140d6f1afca2c005cde74f6feca27a1deaabb Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 9 Aug 2024 18:11:13 +0200 Subject: [PATCH 13/14] Gofumpted --- web/admin.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web/admin.go b/web/admin.go index d3c92afe9..dbfd62d7a 100644 --- a/web/admin.go +++ b/web/admin.go @@ -5,6 +5,9 @@ import ( "encoding/json" "errors" "fmt" + "net/http" + "regexp" + "github.com/TUM-Dev/gocast/dao" "github.com/TUM-Dev/gocast/model" "github.com/TUM-Dev/gocast/tools" @@ -12,8 +15,6 @@ import ( "github.com/getsentry/sentry-go" "github.com/gin-gonic/gin" "gorm.io/gorm" - "net/http" - "regexp" ) // AdminPage serves all administration pages. todo: refactor into multiple methods From 158cc9a8c83fc69f37f933ae305c7e2a568a1986 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 11 Sep 2024 12:07:21 +0200 Subject: [PATCH 14/14] Fixed comments --- dao/statistics.go | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/dao/statistics.go b/dao/statistics.go index 486b61b10..6b5723c3c 100644 --- a/dao/statistics.go +++ b/dao/statistics.go @@ -134,7 +134,7 @@ func (d statisticsDao) GetCourseStatsHourly(courseID uint) ([]Stat, error) { return res, err } -// GetCourseStatsWeekdays returns the days and their sum of vod views of a course +// GetLectureStatsWeekdays returns the days and their sum of vod views of a lecture func (d statisticsDao) GetLectureStatsWeekdays(courseID uint, streamID uint) ([]Stat, error) { var res []Stat err := DB.Raw(`SELECT DAYNAME(stats.time) AS x, SUM(stats.viewers) as y @@ -146,7 +146,7 @@ func (d statisticsDao) GetLectureStatsWeekdays(courseID uint, streamID uint) ([] return res, err } -// GetCourseStatsHourly returns the hours with most vod viewing activity of a course +// GetLectureStatsHourly returns the hours with most vod viewing activity of a lecture func (d statisticsDao) GetLectureStatsHourly(courseID uint, streamID uint) ([]Stat, error) { var res []Stat err := DB.Raw(`SELECT HOUR(stats.time) AS x, SUM(stats.viewers) as y @@ -158,6 +158,7 @@ func (d statisticsDao) GetLectureStatsHourly(courseID uint, streamID uint) ([]St return res, err } +// GetLectureStats returns the number of viewers during a lecture func (d statisticsDao) GetLectureStats(courseID uint, streamID uint) ([]Stat, error) { var res []Stat err := DB.Raw(`SELECT Date_FORMAT(stats.time, "%H:%i") AS x, stats.viewers AS y @@ -165,11 +166,6 @@ func (d statisticsDao) GetLectureStats(courseID uint, streamID uint) ([]Stat, er JOIN streams s ON s.id = stats.stream_id WHERE s.course_id = ? AND s.id = ? AND stats.live = 1 ORDER BY x;`, courseID, streamID).Scan(&res).Error - // err := DB.Raw(`SELECT TIMESTAMPDIFF(MINUTE, s.start, stats.time) AS x, stats.viewers AS y - // FROM stats - // JOIN streams s ON s.id = stats.stream_id - // WHERE s.course_id = ? AND stats.live = 1 - // ORDER BY x;`, courseID).Scan(&res).Error return res, err }