Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
7fb318d
button for personal lecture title change
karjo24 Mar 5, 2025
ae9ff7e
frontend structure
karjo24 Mar 5, 2025
bc4ffe7
removed readonly
karjo24 Mar 5, 2025
6844ab7
database base setup of new table
karjo24 Mar 5, 2025
022bd59
cleaned up dao, added updateorcreate
karjo24 Mar 5, 2025
b1f2927
extended backend to include custom lecture naem
karjo24 Mar 6, 2025
c539464
query fix
karjo24 Mar 6, 2025
b8cc26d
frontend includes customName, only api missing
karjo24 Mar 6, 2025
f28bf72
renamed function
karjo24 Mar 8, 2025
3f3c4f1
added api
karjo24 Mar 8, 2025
55ab7b2
added api call
karjo24 Mar 8, 2025
b936700
fixed wrong cache setting
karjo24 Mar 8, 2025
5c6635e
fixed eslint suggestions
karjo24 Mar 8, 2025
d6d7cc4
adapted tests to changes of this PR; Eslint fix
karjo24 Mar 8, 2025
9f9ddf3
golang lint fix
karjo24 Mar 8, 2025
4030e72
golang linter fix
karjo24 Mar 8, 2025
134ebc4
deletes personal lecture name when set to empty string
karjo24 Mar 8, 2025
365a89b
Add custom lecture title to apiv2.proto
carlobortolan Mar 9, 2025
b29498b
shorter menu entry title for conciseness
karjo24 Mar 9, 2025
3b5e4c6
refactored two getCourseBySlugYearAndTerm[User] into one
karjo24 Mar 9, 2025
c580b9d
changed tests to match new function signature
karjo24 Mar 9, 2025
a5d2661
code style changes
karjo24 Mar 14, 2025
0185dee
refactored upsert to save using clauses
karjo24 Mar 14, 2025
5e51dfb
changed file name and edited error message to reflect prior changes
karjo24 Mar 14, 2025
6170ef2
changed where lecture title gets added to stream DTO to avoid future …
karjo24 Mar 14, 2025
076e788
changed cache key to avoid collisions
karjo24 Mar 14, 2025
56f1acd
Merge branch 'dev' into 840-short-decription-for-each-video
karjo24 Mar 14, 2025
c2239a1
incorrect redirect removed
karjo24 Mar 21, 2025
c923a00
eslint fix
karjo24 Mar 21, 2025
681b37a
Trim whitespace from custom lecture title before saving
Copilot Apr 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions api/courses.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,14 @@ func (r coursesRoutes) getCourseBySlug(c *gin.Context) {
Streams []model.StreamDTO
}

course, err := r.CoursesDao.GetCourseBySlugYearAndTerm(c, uri.Slug, query.Term, query.Year)
user := tumLiveContext.User
var course model.Course
var err error
if user == nil {
course, err = r.CoursesDao.GetCourseBySlugYearAndTerm(c, uri.Slug, query.Term, query.Year)
} else {
course, err = r.CoursesDao.GetCourseBySlugYearTermUser(c, uri.Slug, query.Term, query.Year, user.ID)
}
Comment thread
carlobortolan marked this conversation as resolved.
Outdated
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
_ = c.Error(tools.RequestError{
Expand All @@ -367,7 +374,6 @@ func (r coursesRoutes) getCourseBySlug(c *gin.Context) {
return
}

user := tumLiveContext.User
var streams []model.Stream
for _, stream := range course.Streams {
if !stream.Private || (user != nil && user.IsAdminOfCourse(course)) {
Expand Down
10 changes: 5 additions & 5 deletions api/courses_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ func TestCoursesCRUD(t *testing.T) {
mock := mock_dao.NewMockCoursesDao(gomock.NewController(t))
mock.
EXPECT().
GetCourseBySlugYearAndTerm(gomock.Any(), testutils.CourseTensNet.Slug, "S", 2023).
GetCourseBySlugYearTermUser(gomock.Any(), testutils.CourseTensNet.Slug, "S", 2023, testutils.TUMLiveContextStudent.User.ID).
Return(model.Course{}, gorm.ErrRecordNotFound).
AnyTimes()
return mock
Expand All @@ -385,7 +385,7 @@ func TestCoursesCRUD(t *testing.T) {
mock := mock_dao.NewMockCoursesDao(gomock.NewController(t))
mock.
EXPECT().
GetCourseBySlugYearAndTerm(gomock.Any(), testutils.CourseTensNet.Slug, "S", 2023).
GetCourseBySlugYearTermUser(gomock.Any(), testutils.CourseTensNet.Slug, "S", 2023, testutils.TUMLiveContextStudent.User.ID).
Return(model.Course{}, errors.New("")).
AnyTimes()
return mock
Expand All @@ -403,7 +403,7 @@ func TestCoursesCRUD(t *testing.T) {
mock := mock_dao.NewMockCoursesDao(gomock.NewController(t))
mock.
EXPECT().
GetCourseBySlugYearAndTerm(gomock.Any(), testutils.CourseTensNet.Slug, "S", 2023).
GetCourseBySlugYearTermUser(gomock.Any(), testutils.CourseTensNet.Slug, "S", 2023, testutils.TUMLiveContextAdmin.User.ID).
Return(testutils.CourseTensNet, nil).
AnyTimes()
return mock
Expand Down Expand Up @@ -442,7 +442,7 @@ func TestCoursesCRUD(t *testing.T) {
AnyTimes()
coursesMock.
EXPECT().
GetCourseBySlugYearAndTerm(gomock.Any(), testutils.CourseFPV.Slug, testutils.CourseFPV.TeachingTerm, testutils.CourseFPV.Year).
GetCourseBySlugYearTermUser(gomock.Any(), testutils.CourseFPV.Slug, testutils.CourseFPV.TeachingTerm, testutils.CourseFPV.Year, testutils.TUMLiveContextStudent.User.ID).
Return(testutils.CourseFPV, nil).
AnyTimes()
return coursesMock
Expand All @@ -468,7 +468,7 @@ func TestCoursesCRUD(t *testing.T) {
AnyTimes()
coursesMock.
EXPECT().
GetCourseBySlugYearAndTerm(gomock.Any(), testutils.CourseFPV.Slug, testutils.CourseFPV.TeachingTerm, testutils.CourseFPV.Year).
GetCourseBySlugYearTermUser(gomock.Any(), testutils.CourseFPV.Slug, testutils.CourseFPV.TeachingTerm, testutils.CourseFPV.Year, testutils.TUMLiveContextAdmin.User.ID).
Return(testutils.CourseFPV, nil).
AnyTimes()
coursesMock.
Expand Down
65 changes: 65 additions & 0 deletions api/stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ func configGinStreamRestRouter(router *gin.Engine, daoWrapper dao.DaoWrapper) {

streamById.GET("/playlist", routes.getStreamPlaylist)

withUser := streamById.Use(tools.LoggedIn)
{
withUser.PUT("/personalLectureName", routes.changePersonalLectureName)
}

thumbs := streamById.Group("/thumbs")
{
thumbs.GET(":fid", routes.getThumbs)
Expand Down Expand Up @@ -894,3 +899,63 @@ func (r streamRoutes) updateChatEnabled(c *gin.Context) {
return
}
}

type changePersonalLectureNameRequest struct {
PersonalLectureName string `json:"personalLectureName"`
}

func (r streamRoutes) changePersonalLectureName(c *gin.Context) {
ctx := c.MustGet("TUMLiveContext").(tools.TUMLiveContext)

streamIdAsString := c.Param("streamID")
streamId, err := strconv.ParseUint(streamIdAsString, 10, 32)
if err != nil {
logger.Error("can not parse stream id in request url", "err", err)
_ = c.Error(tools.RequestError{
Status: http.StatusBadRequest,
CustomMessage: "can not parse stream id in request url",
Err: err,
})
return
}

var update changePersonalLectureNameRequest
err = c.BindJSON(&update)
if err != nil {
logger.Error("failed to bind personal lecture name JSON", "err", err)
_ = c.Error(tools.RequestError{
Status: http.StatusBadRequest,
CustomMessage: "can not bind body",
Err: err,
})
return
}

if update.PersonalLectureName == "" {
err = r.UserDefinedLectureTitlesDao.Delete(ctx.User.ID, uint(streamId))
if err != nil {
logger.Error("failed to delete personal lecture name", "err", err)
_ = c.Error(tools.RequestError{
Status: http.StatusInternalServerError,
CustomMessage: "can not delete personal lecture name",
Err: err,
})
}
return
}

err = r.UserDefinedLectureTitlesDao.Upsert(&model.UserDefinedLectureTitle{
UserID: ctx.User.ID,
StreamID: uint(streamId),
Title: update.PersonalLectureName,
})
if err != nil {
logger.Error("failed to upsert personal lecture name", "err", err)
_ = c.Error(tools.RequestError{
Status: http.StatusInternalServerError,
CustomMessage: "can not upsert personal lecture name",
Err: err,
})
return
}
}
1 change: 1 addition & 0 deletions cmd/tumlive/tumlive.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ func main() {
&model.TranscodingFailure{},
&model.Email{},
&model.Runner{},
&model.UserDefinedLectureTitle{},
)
if err != nil {
sentry.CaptureException(err)
Expand Down
20 changes: 20 additions & 0 deletions dao/courses.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type CoursesDao interface {
GetCourseById(ctx context.Context, id uint) (course model.Course, err error)
GetInvitedUsersForCourse(course *model.Course) error
GetCourseBySlugYearAndTerm(ctx context.Context, slug string, term string, year int) (model.Course, error)
GetCourseBySlugYearTermUser(ctx context.Context, slug string, term string, year int, userID uint) (model.Course, error)
// GetAllCoursesWithTUMIDFromSemester returns all courses with a non-null tum_identifier from a given semester or later
GetAllCoursesWithTUMIDFromSemester(ctx context.Context, year int, term string) (courses []model.Course, err error)
GetAvailableSemesters(c context.Context, includeTestSemester bool) []model.Semester
Expand Down Expand Up @@ -231,6 +232,25 @@ func (d coursesDao) GetCourseBySlugYearAndTerm(ctx context.Context, slug string,
return course, err
}

// GetCourseBySlugYearTermUser loads courses with streams preloaded. Difference to GetCourseBySlugYearAndTerm: personal lecture titles of user are loaded as well
func (d coursesDao) GetCourseBySlugYearTermUser(ctx context.Context, slug string, term string, year int, userID uint) (model.Course, error) {
Comment thread
carlobortolan marked this conversation as resolved.
Outdated
cachedCourses, found := Cache.Get(fmt.Sprintf("courseBySlugYearTermUser%v%v%v%v", slug, term, year, userID))
if found {
return cachedCourses.(model.Course), nil
}
var course model.Course
err := DB.Preload("Streams.VideoSections").Preload("Streams.Units", func(db *gorm.DB) *gorm.DB {
return db.Order("unit_start desc")
}).Preload("Streams", func(db *gorm.DB) *gorm.DB {
return db.Order("start desc")
}).Preload("Streams.CustomLectureTitles", "user_id = ?", userID).
Preload("Admins").Where("teaching_term = ? AND slug = ? AND year = ?", term, slug, year).First(&course).Error
if err == nil {
Cache.SetWithTTL(fmt.Sprintf("courseBySlugYearTermUser%v%v%v%v", slug, term, year, userID), course, 1, time.Minute)
}
return course, err
}

func (d coursesDao) GetAllCoursesWithTUMIDFromSemester(ctx context.Context, year int, term string) (courses []model.Course, err error) {
var foundCourses []model.Course

Expand Down
50 changes: 26 additions & 24 deletions dao/dao_base.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,33 +37,35 @@ type DaoWrapper struct {
TranscodingFailureDao
EmailDao
RunnerDao RunnerDao
UserDefinedLectureTitlesDao
}

func NewDaoWrapper() DaoWrapper {
return DaoWrapper{
CameraPresetDao: NewCameraPresetDao(),
ChatDao: NewChatDao(),
FileDao: NewFileDao(),
StreamsDao: NewStreamsDao(),
CoursesDao: NewCoursesDao(),
WorkerDao: NewWorkerDao(),
LectureHallsDao: NewLectureHallsDao(),
UsersDao: NewUsersDao(),
UploadKeyDao: NewUploadKeyDao(),
StatisticsDao: NewStatisticsDao(),
ProgressDao: NewProgressDao(),
ServerNotificationDao: NewServerNotificationDao(),
TokenDao: NewTokenDao(),
NotificationsDao: NewNotificiationsDao(),
IngestServerDao: NewIngestServerDao(),
VideoSectionDao: NewVideoSectionDao(),
InfoPageDao: NewInfoPageDao(),
VideoSeekDao: NewVideoSeekDao(),
AuditDao: NewAuditDao(),
BookmarkDao: NewBookmarkDao(),
SubtitlesDao: NewSubtitlesDao(),
TranscodingFailureDao: NewTranscodingFailureDao(),
EmailDao: NewEmailDao(),
RunnerDao: NewRunnerDao(),
CameraPresetDao: NewCameraPresetDao(),
ChatDao: NewChatDao(),
FileDao: NewFileDao(),
StreamsDao: NewStreamsDao(),
CoursesDao: NewCoursesDao(),
WorkerDao: NewWorkerDao(),
LectureHallsDao: NewLectureHallsDao(),
UsersDao: NewUsersDao(),
UploadKeyDao: NewUploadKeyDao(),
StatisticsDao: NewStatisticsDao(),
ProgressDao: NewProgressDao(),
ServerNotificationDao: NewServerNotificationDao(),
TokenDao: NewTokenDao(),
NotificationsDao: NewNotificiationsDao(),
IngestServerDao: NewIngestServerDao(),
VideoSectionDao: NewVideoSectionDao(),
InfoPageDao: NewInfoPageDao(),
VideoSeekDao: NewVideoSeekDao(),
AuditDao: NewAuditDao(),
BookmarkDao: NewBookmarkDao(),
SubtitlesDao: NewSubtitlesDao(),
TranscodingFailureDao: NewTranscodingFailureDao(),
EmailDao: NewEmailDao(),
RunnerDao: NewRunnerDao(),
UserDefinedLectureTitlesDao: NewUserDefinedLectureTitlesDao(),
}
}
50 changes: 50 additions & 0 deletions dao/user_defined_lecture_titles.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package dao
Comment thread
karjo24 marked this conversation as resolved.

import (
"github.com/TUM-Dev/gocast/model"
"gorm.io/gorm"
)

//go:generate mockgen -source=user_defined_lecture_titles.go -destination ../mock_dao/user_defined_lecture_titles.go

type UserDefinedLectureTitlesDao interface {
// Get UserDefinedLectureTitle by ID
Get(uint, uint) (model.UserDefinedLectureTitle, error)

// Create a new UserDefinedLectureTitle for the database
Create(*model.UserDefinedLectureTitle) error

// Delete a UserDefinedLectureTitle by user and stream id.
Delete(uint, uint) error

// Upsert updates the entry if it exists, inserts it else
Upsert(userLectureTitle *model.UserDefinedLectureTitle) error
}

type userDefinedLectureTitlesDao struct {
db *gorm.DB
}

func NewUserDefinedLectureTitlesDao() UserDefinedLectureTitlesDao {
return userDefinedLectureTitlesDao{db: DB}
}

// Get a userDefinedLectureTitlesDao by userID and streamID
func (d userDefinedLectureTitlesDao) Get(userID uint, streamID uint) (res model.UserDefinedLectureTitle, err error) {
return res, d.db.First(&res, "user_id = ? AND stream_id = ?", userID, streamID).Error
}

// Create a userDefinedLectureTitlesDao.
func (d userDefinedLectureTitlesDao) Create(it *model.UserDefinedLectureTitle) error {
return d.db.Create(it).Error
}

// Delete a userDefinedLectureTitlesDao by id.
func (d userDefinedLectureTitlesDao) Delete(userID uint, streamID uint) error {
return d.db.Delete(&model.UserDefinedLectureTitle{}, "user_id = ? AND stream_id = ?", userID, streamID).Error
}

// Upsert updates the entry if it exists, inserts it else
func (d userDefinedLectureTitlesDao) Upsert(userLectureTitle *model.UserDefinedLectureTitle) error {
return d.db.Save(userLectureTitle).Error
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need upserting? With user_id and stream_id being composite primary keys, we can use create with a conflict clause to update a record. See e.g.

err2 = DB.Clauses(clause.OnConflict{

I would go that way and rename the method to Save and drop Create and Upsert

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

15 changes: 15 additions & 0 deletions mock_dao/courses.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading