diff --git a/api/statistics.go b/api/statistics.go index dfc879bad..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{ @@ -87,6 +118,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.GetLectureStats(cid, sid) + if err != nil { + logger.Warn("GetLectureStats 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 { @@ -144,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{ @@ -157,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{ @@ -171,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 01e4963e1..6b5723c3c 100644 --- a/dao/statistics.go +++ b/dao/statistics.go @@ -16,9 +16,15 @@ 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) + 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) } @@ -66,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 @@ -78,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 @@ -102,6 +134,41 @@ func (d statisticsDao) GetCourseStatsHourly(courseID uint) ([]Stat, error) { return res, err } +// 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 + 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 +} + +// 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 + 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 +} + +// 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 + FROM stats + 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 + 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 +222,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/mock_dao/statistics.go b/mock_dao/statistics.go index 349f3d2ea..0be344fb3 100644 --- a/mock_dao/statistics.go +++ b/mock_dao/statistics.go @@ -1,15 +1,20 @@ // 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 import ( + "github.com/golang/mock/gomock" reflect "reflect" dao "github.com/TUM-Dev/gocast/dao" model "github.com/TUM-Dev/gocast/model" - gomock "github.com/golang/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) } diff --git a/web/admin.go b/web/admin.go old mode 100755 new mode 100644 index cfdd7b1c2..95976d943 --- a/web/admin.go +++ b/web/admin.go @@ -182,6 +182,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 { @@ -346,3 +364,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..4a279871c --- /dev/null +++ b/web/template/admin/lecture-stats.gohtml @@ -0,0 +1,188 @@ + + + + + {{.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

+
+ +
+
+
+

VoD activity per day of week

+
+ +
+
+
+

VoD activity per day

+
+ +
+
+ +
+
+ + +
+ + + + + +{{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..07ed1960c 100644 --- a/web/template/watch.gohtml +++ b/web/template/watch.gohtml @@ -457,13 +457,20 @@
-
+
{{if or (.IndexData.TUMLiveContext.User.IsAdminOfCourse .IndexData.TUMLiveContext.Course) .IndexData.IsAdmin}} - + + + + +
{{end}} {{if .IndexData.TUMLiveContext.User}} diff --git a/web/ts/stats.ts b/web/ts/stats.ts index 51aa7145e..98bd0dafe 100644 --- a/web/ts/stats.ts +++ b/web/ts/stats.ts @@ -23,6 +23,22 @@ 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,6 +54,23 @@ export function initStatsPage() { }); } +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}`, + ).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",