Skip to content

Commit 06f54d6

Browse files
feat(admin): Edit lecture/series times (#1891)
1 parent e452687 commit 06f54d6

8 files changed

Lines changed: 519 additions & 1 deletion

File tree

api/courses.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ func configGinCourseRouter(router *gin.Engine, daoWrapper dao.DaoWrapper) {
7373
courses.POST("/renameLecture/:streamID", routes.renameLecture)
7474
courses.POST("/updateLectureSeries/:streamID", routes.updateLectureSeries)
7575
courses.PUT("/updateDescription/:streamID", routes.updateDescription)
76+
courses.PUT("/updateLectureTime/:streamID", routes.updateLectureTime)
77+
courses.POST("/updateLectureSeriesTime/:streamID", routes.updateLectureSeriesTime)
7678
courses.DELETE("/deleteLectureSeries/:streamID", routes.deleteLectureSeries)
7779
courses.POST("/submitCut", routes.submitCut)
7880

@@ -1061,6 +1063,68 @@ func (r coursesRoutes) renameLecture(c *gin.Context) {
10611063
}
10621064
}
10631065

1066+
type updateLectureTimeRequest struct {
1067+
Start time.Time `json:"start" binding:"required"`
1068+
End time.Time `json:"end" binding:"required"`
1069+
}
1070+
1071+
func (r coursesRoutes) updateLectureTime(c *gin.Context) {
1072+
sIDInt, err := strconv.Atoi(c.Param("streamID"))
1073+
if err != nil {
1074+
_ = c.Error(tools.RequestError{
1075+
Status: http.StatusBadRequest,
1076+
CustomMessage: "invalid streamID",
1077+
Err: err,
1078+
})
1079+
return
1080+
}
1081+
sID := uint(sIDInt)
1082+
var req updateLectureTimeRequest
1083+
if err = c.BindJSON(&req); err != nil {
1084+
_ = c.Error(tools.RequestError{
1085+
Status: http.StatusBadRequest,
1086+
CustomMessage: "invalid body",
1087+
Err: err,
1088+
})
1089+
return
1090+
}
1091+
if !req.End.After(req.Start) {
1092+
_ = c.Error(tools.RequestError{
1093+
Status: http.StatusBadRequest,
1094+
CustomMessage: "end must be after start",
1095+
})
1096+
return
1097+
}
1098+
stream, err := r.StreamsDao.GetStreamByID(context.Background(), c.Param("streamID"))
1099+
if err != nil {
1100+
_ = c.Error(tools.RequestError{
1101+
Status: http.StatusNotFound,
1102+
CustomMessage: "can not find stream",
1103+
Err: err,
1104+
})
1105+
return
1106+
}
1107+
stream.Start = req.Start
1108+
stream.End = req.End
1109+
if err = r.StreamsDao.UpdateStream(stream); err != nil {
1110+
_ = c.Error(tools.RequestError{
1111+
Status: http.StatusInternalServerError,
1112+
CustomMessage: "couldn't update lecture time",
1113+
Err: err,
1114+
})
1115+
return
1116+
}
1117+
wsMsg := gin.H{
1118+
"start": stream.Start,
1119+
"end": stream.End,
1120+
}
1121+
if msg, err := json.Marshal(wsMsg); err == nil {
1122+
broadcastStream(sID, msg)
1123+
} else {
1124+
logger.Error("couldn't marshal stream time update ws msg", "err", err)
1125+
}
1126+
}
1127+
10641128
func (r coursesRoutes) fetchLectures(c *gin.Context) {
10651129
tlctx := c.MustGet("TUMLiveContext").(tools.TUMLiveContext)
10661130

@@ -1095,6 +1159,31 @@ func (r coursesRoutes) updateLectureSeries(c *gin.Context) {
10951159
// Series changes could be theoretically broadcasted here through the websocket to live listeners.
10961160
}
10971161

1162+
// updateLectureSeriesTime propagates the time-of-day and duration of the stream identified by
1163+
// :streamID to every other stream in its series, keeping each of those streams on its own date.
1164+
// The stream's own Start/End must already be persisted (via updateLectureTime) before calling this.
1165+
func (r coursesRoutes) updateLectureSeriesTime(c *gin.Context) {
1166+
stream, err := r.StreamsDao.GetStreamByID(context.Background(), c.Param("streamID"))
1167+
if err != nil {
1168+
_ = c.Error(tools.RequestError{
1169+
Status: http.StatusNotFound,
1170+
CustomMessage: "can not find stream",
1171+
Err: err,
1172+
})
1173+
return
1174+
}
1175+
1176+
if err = r.StreamsDao.UpdateLectureSeriesTime(stream); err != nil {
1177+
logger.Error("couldn't update lecture series time", "err", err)
1178+
_ = c.Error(tools.RequestError{
1179+
Status: http.StatusInternalServerError,
1180+
CustomMessage: "couldn't update lecture series time",
1181+
Err: err,
1182+
})
1183+
return
1184+
}
1185+
}
1186+
10981187
type renameLectureRequest struct {
10991188
Name string
11001189
}

api/courses_test.go

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1303,6 +1303,97 @@ func TestCoursesLectureActions(t *testing.T) {
13031303
Url(url).
13041304
Run(t, testutils.Equal)
13051305
})
1306+
t.Run("POST/api/course/:courseID/updateLectureSeriesTime/:streamID", func(t *testing.T) {
1307+
url := fmt.Sprintf("/api/course/%d/updateLectureSeriesTime/%d", testutils.CourseFPV.ID, testutils.StreamFPVLive.ID)
1308+
1309+
gomino.TestCases{
1310+
"no context": {
1311+
Router: CourseRouterWrapper,
1312+
Middlewares: testutils.GetMiddlewares(tools.ErrorHandler),
1313+
ExpectedCode: http.StatusInternalServerError,
1314+
},
1315+
"not admin": {
1316+
Router: func(r *gin.Engine) {
1317+
wrapper := dao.DaoWrapper{
1318+
CoursesDao: testutils.GetCoursesMock(t),
1319+
}
1320+
configGinCourseRouter(r, wrapper)
1321+
},
1322+
Middlewares: testutils.GetMiddlewares(tools.ErrorHandler, testutils.TUMLiveContext(testutils.TUMLiveContextStudent)),
1323+
ExpectedCode: http.StatusForbidden,
1324+
},
1325+
"stream not found": {
1326+
Router: func(r *gin.Engine) {
1327+
wrapper := dao.DaoWrapper{
1328+
CoursesDao: testutils.GetCoursesMock(t),
1329+
StreamsDao: func() dao.StreamsDao {
1330+
streamsMock := mock_dao.NewMockStreamsDao(gomock.NewController(t))
1331+
streamsMock.
1332+
EXPECT().
1333+
GetStreamByID(gomock.Any(), fmt.Sprintf("%d", testutils.StreamFPVLive.ID)).
1334+
Return(testutils.StreamFPVLive, errors.New("")).
1335+
AnyTimes()
1336+
return streamsMock
1337+
}(),
1338+
}
1339+
configGinCourseRouter(r, wrapper)
1340+
},
1341+
Middlewares: testutils.GetMiddlewares(tools.ErrorHandler, testutils.TUMLiveContext(testutils.TUMLiveContextAdmin)),
1342+
ExpectedCode: http.StatusNotFound,
1343+
},
1344+
"can not update lecture series time": {
1345+
Router: func(r *gin.Engine) {
1346+
wrapper := dao.DaoWrapper{
1347+
CoursesDao: testutils.GetCoursesMock(t),
1348+
StreamsDao: func() dao.StreamsDao {
1349+
streamsMock := mock_dao.NewMockStreamsDao(gomock.NewController(t))
1350+
streamsMock.
1351+
EXPECT().
1352+
GetStreamByID(gomock.Any(), fmt.Sprintf("%d", testutils.StreamFPVLive.ID)).
1353+
Return(testutils.StreamFPVLive, nil).
1354+
AnyTimes()
1355+
streamsMock.
1356+
EXPECT().
1357+
UpdateLectureSeriesTime(testutils.StreamFPVLive).
1358+
Return(errors.New("")).
1359+
AnyTimes()
1360+
return streamsMock
1361+
}(),
1362+
}
1363+
configGinCourseRouter(r, wrapper)
1364+
},
1365+
Middlewares: testutils.GetMiddlewares(tools.ErrorHandler, testutils.TUMLiveContext(testutils.TUMLiveContextAdmin)),
1366+
ExpectedCode: http.StatusInternalServerError,
1367+
},
1368+
"success": {
1369+
Router: func(r *gin.Engine) {
1370+
wrapper := dao.DaoWrapper{
1371+
CoursesDao: testutils.GetCoursesMock(t),
1372+
StreamsDao: func() dao.StreamsDao {
1373+
streamsMock := mock_dao.NewMockStreamsDao(gomock.NewController(t))
1374+
streamsMock.
1375+
EXPECT().
1376+
GetStreamByID(gomock.Any(), fmt.Sprintf("%d", testutils.StreamFPVLive.ID)).
1377+
Return(testutils.StreamFPVLive, nil).
1378+
AnyTimes()
1379+
streamsMock.
1380+
EXPECT().
1381+
UpdateLectureSeriesTime(testutils.StreamFPVLive).
1382+
Return(nil).
1383+
AnyTimes()
1384+
return streamsMock
1385+
}(),
1386+
}
1387+
configGinCourseRouter(r, wrapper)
1388+
},
1389+
Middlewares: testutils.GetMiddlewares(tools.ErrorHandler, testutils.TUMLiveContext(testutils.TUMLiveContextAdmin)),
1390+
ExpectedCode: http.StatusOK,
1391+
},
1392+
}.
1393+
Method(http.MethodPost).
1394+
Url(url).
1395+
Run(t, testutils.Equal)
1396+
})
13061397
t.Run("DELETE/api/course/:courseID/deleteLectureSeries/:streamID", func(t *testing.T) {
13071398
url := fmt.Sprintf("/api/course/%d/deleteLectureSeries/%d", testutils.CourseFPV.ID, testutils.StreamFPVLive.ID)
13081399

@@ -1520,6 +1611,128 @@ func TestCoursesLectureActions(t *testing.T) {
15201611
Url(url).
15211612
Run(t, testutils.Equal)
15221613
})
1614+
1615+
t.Run("PUT/api/course/:courseID/updateLectureTime/:streamID", func(t *testing.T) {
1616+
url := fmt.Sprintf("/api/course/%d/updateLectureTime/%d", testutils.CourseFPV.ID, testutils.StreamFPVLive.ID)
1617+
1618+
body := updateLectureTimeRequest{
1619+
Start: testutils.StreamFPVLive.Start,
1620+
End: testutils.StreamFPVLive.End,
1621+
}
1622+
invalidBody := updateLectureTimeRequest{
1623+
Start: testutils.StreamFPVLive.End,
1624+
End: testutils.StreamFPVLive.Start,
1625+
}
1626+
gomino.TestCases{
1627+
"no context": {
1628+
Router: CourseRouterWrapper,
1629+
Middlewares: testutils.GetMiddlewares(tools.ErrorHandler),
1630+
ExpectedCode: http.StatusInternalServerError,
1631+
},
1632+
"not admin": {
1633+
Router: func(r *gin.Engine) {
1634+
wrapper := dao.DaoWrapper{
1635+
CoursesDao: testutils.GetCoursesMock(t),
1636+
}
1637+
configGinCourseRouter(r, wrapper)
1638+
},
1639+
Middlewares: testutils.GetMiddlewares(tools.ErrorHandler, testutils.TUMLiveContext(testutils.TUMLiveContextStudent)),
1640+
ExpectedCode: http.StatusForbidden,
1641+
},
1642+
"invalid streamID": {
1643+
Router: func(r *gin.Engine) {
1644+
wrapper := dao.DaoWrapper{
1645+
CoursesDao: testutils.GetCoursesMock(t),
1646+
}
1647+
configGinCourseRouter(r, wrapper)
1648+
},
1649+
Url: fmt.Sprintf("/api/course/%d/updateLectureTime/abc", testutils.CourseFPV.ID),
1650+
Middlewares: testutils.GetMiddlewares(tools.ErrorHandler, testutils.TUMLiveContext(testutils.TUMLiveContextAdmin)),
1651+
ExpectedCode: http.StatusBadRequest,
1652+
},
1653+
"invalid body": {
1654+
Router: func(r *gin.Engine) {
1655+
wrapper := dao.DaoWrapper{
1656+
CoursesDao: testutils.GetCoursesMock(t),
1657+
}
1658+
configGinCourseRouter(r, wrapper)
1659+
},
1660+
Middlewares: testutils.GetMiddlewares(tools.ErrorHandler, testutils.TUMLiveContext(testutils.TUMLiveContextAdmin)),
1661+
ExpectedCode: http.StatusBadRequest,
1662+
},
1663+
"end before start": {
1664+
Router: func(r *gin.Engine) {
1665+
wrapper := dao.DaoWrapper{
1666+
CoursesDao: testutils.GetCoursesMock(t),
1667+
}
1668+
configGinCourseRouter(r, wrapper)
1669+
},
1670+
Middlewares: testutils.GetMiddlewares(tools.ErrorHandler, testutils.TUMLiveContext(testutils.TUMLiveContextAdmin)),
1671+
Body: invalidBody,
1672+
ExpectedCode: http.StatusBadRequest,
1673+
},
1674+
"can not find stream": {
1675+
Router: func(r *gin.Engine) {
1676+
wrapper := dao.DaoWrapper{
1677+
CoursesDao: testutils.GetCoursesMock(t),
1678+
StreamsDao: func() dao.StreamsDao {
1679+
streamsMock := mock_dao.NewMockStreamsDao(gomock.NewController(t))
1680+
streamsMock.
1681+
EXPECT().
1682+
GetStreamByID(gomock.Any(), fmt.Sprintf("%d", testutils.StreamFPVLive.ID)).
1683+
Return(testutils.StreamFPVLive, errors.New("")).
1684+
AnyTimes()
1685+
return streamsMock
1686+
}(),
1687+
}
1688+
configGinCourseRouter(r, wrapper)
1689+
},
1690+
Middlewares: testutils.GetMiddlewares(tools.ErrorHandler, testutils.TUMLiveContext(testutils.TUMLiveContextAdmin)),
1691+
Body: body,
1692+
ExpectedCode: http.StatusNotFound,
1693+
},
1694+
"can not update stream": {
1695+
Router: func(r *gin.Engine) {
1696+
wrapper := dao.DaoWrapper{
1697+
CoursesDao: testutils.GetCoursesMock(t),
1698+
StreamsDao: func() dao.StreamsDao {
1699+
streamsMock := mock_dao.NewMockStreamsDao(gomock.NewController(t))
1700+
streamsMock.
1701+
EXPECT().
1702+
GetStreamByID(gomock.Any(), fmt.Sprintf("%d", testutils.StreamFPVLive.ID)).
1703+
Return(testutils.StreamFPVLive, nil).
1704+
AnyTimes()
1705+
streamsMock.
1706+
EXPECT().
1707+
UpdateStream(gomock.Any()).
1708+
Return(errors.New("")).
1709+
AnyTimes()
1710+
return streamsMock
1711+
}(),
1712+
}
1713+
configGinCourseRouter(r, wrapper)
1714+
},
1715+
Middlewares: testutils.GetMiddlewares(tools.ErrorHandler, testutils.TUMLiveContext(testutils.TUMLiveContextAdmin)),
1716+
Body: body,
1717+
ExpectedCode: http.StatusInternalServerError,
1718+
},
1719+
"success": {
1720+
Router: func(r *gin.Engine) {
1721+
wrapper := dao.DaoWrapper{
1722+
CoursesDao: testutils.GetCoursesMock(t),
1723+
StreamsDao: testutils.GetStreamMock(t),
1724+
}
1725+
configGinCourseRouter(r, wrapper)
1726+
},
1727+
Middlewares: testutils.GetMiddlewares(tools.ErrorHandler, testutils.TUMLiveContext(testutils.TUMLiveContextAdmin)),
1728+
Body: body,
1729+
ExpectedCode: http.StatusOK,
1730+
},
1731+
}.
1732+
Method(http.MethodPut).
1733+
Url(url).
1734+
Run(t, testutils.Equal)
1735+
})
15231736
}
15241737

15251738
func TestUnits(t *testing.T) {

dao/streams.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ type StreamsDao interface {
6868
DeleteUnit(id uint)
6969
DeleteStreamsWithTumID(ids []uint)
7070
UpdateLectureSeries(model.Stream) error
71+
UpdateLectureSeriesTime(model.Stream) error
7172
DeleteLectureSeries(string) error
7273
}
7374

@@ -213,6 +214,38 @@ func (d streamsDao) UpdateLectureSeries(stream model.Stream) error {
213214
return err
214215
}
215216

217+
// UpdateLectureSeriesTime applies the time-of-day and duration of stream to every other stream in
218+
// its series, while keeping each of those streams on their own original date.
219+
func (d streamsDao) UpdateLectureSeriesTime(stream model.Stream) error {
220+
defer Cache.Clear()
221+
222+
if stream.SeriesIdentifier == "" {
223+
return nil
224+
}
225+
226+
var streams []model.Stream
227+
if err := DB.Where("`series_identifier` = ? AND `deleted_at` IS NULL AND `id` != ?",
228+
stream.SeriesIdentifier, stream.ID).Find(&streams).Error; err != nil {
229+
return err
230+
}
231+
232+
duration := stream.End.Sub(stream.Start)
233+
return DB.Transaction(func(tx *gorm.DB) error {
234+
for _, s := range streams {
235+
newStart := time.Date(s.Start.Year(), s.Start.Month(), s.Start.Day(),
236+
stream.Start.Hour(), stream.Start.Minute(), stream.Start.Second(), 0, s.Start.Location())
237+
newEnd := newStart.Add(duration)
238+
if err := tx.Model(&model.Stream{}).Where("id = ?", s.ID).Updates(map[string]interface{}{
239+
"start": newStart,
240+
"end": newEnd,
241+
}).Error; err != nil {
242+
return err
243+
}
244+
}
245+
return nil
246+
})
247+
}
248+
216249
func (d streamsDao) DeleteLectureSeries(seriesIdentifier string) error {
217250
defer Cache.Clear()
218251
err := DB.Delete(&model.Stream{}, "`series_identifier` = ?", seriesIdentifier).Error

0 commit comments

Comments
 (0)