diff --git a/server/api/tumdev/campus_backend.pb.go b/server/api/tumdev/campus_backend.pb.go index 67efa753..466b672d 100644 --- a/server/api/tumdev/campus_backend.pb.go +++ b/server/api/tumdev/campus_backend.pb.go @@ -1246,13 +1246,18 @@ type GetDishRatingsReply struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Rating []*SingleRatingReply `protobuf:"bytes,1,rep,name=rating,proto3" json:"rating,omitempty"` - Avg float64 `protobuf:"fixed64,2,opt,name=avg,proto3" json:"avg,omitempty"` - Std float64 `protobuf:"fixed64,3,opt,name=std,proto3" json:"std,omitempty"` - Min int32 `protobuf:"varint,4,opt,name=min,proto3" json:"min,omitempty"` - Max int32 `protobuf:"varint,5,opt,name=max,proto3" json:"max,omitempty"` - RatingTags []*RatingTagResult `protobuf:"bytes,6,rep,name=rating_tags,json=ratingTags,proto3" json:"rating_tags,omitempty"` - NameTags []*RatingTagResult `protobuf:"bytes,7,rep,name=name_tags,json=nameTags,proto3" json:"name_tags,omitempty"` + // a number of actual ratings + Rating []*SingleRatingReply `protobuf:"bytes,1,rep,name=rating,proto3" json:"rating,omitempty"` + // average rating for all dish rating tags which were used to rate this dish in this cafeteria + Avg float64 `protobuf:"fixed64,2,opt,name=avg,proto3" json:"avg,omitempty"` + // std of all dish rating tags which were used to rate this dish in this cafeteria + Std float64 `protobuf:"fixed64,3,opt,name=std,proto3" json:"std,omitempty"` + // minimum of all dish rating tags which were used to rate this dish in this cafeteria + Min int32 `protobuf:"varint,4,opt,name=min,proto3" json:"min,omitempty"` + // maximum of all dish rating tags which were used to rate this dish in this cafeteria + Max int32 `protobuf:"varint,5,opt,name=max,proto3" json:"max,omitempty"` + RatingTags []*RatingTagResult `protobuf:"bytes,6,rep,name=rating_tags,json=ratingTags,proto3" json:"rating_tags,omitempty"` + NameTags []*RatingTagResult `protobuf:"bytes,7,rep,name=name_tags,json=nameTags,proto3" json:"name_tags,omitempty"` } func (x *GetDishRatingsReply) Reset() { diff --git a/server/api/tumdev/campus_backend.proto b/server/api/tumdev/campus_backend.proto index f245bb21..6f853173 100644 --- a/server/api/tumdev/campus_backend.proto +++ b/server/api/tumdev/campus_backend.proto @@ -41,6 +41,7 @@ service Campus { }; } + // Allows to query ratings for a specific dish in a specific cafeteria. rpc GetDishRatings(GetDishRatingsRequest) returns (GetDishRatingsReply) { option (google.api.http) = { post: "/dish/rating/get", @@ -295,10 +296,15 @@ message GetDishRatingsRequest { } message GetDishRatingsReply { + // a number of actual ratings repeated SingleRatingReply rating = 1; + // average rating for all dish rating tags which were used to rate this dish in this cafeteria double avg = 2; + // std of all dish rating tags which were used to rate this dish in this cafeteria double std = 3; + // minimum of all dish rating tags which were used to rate this dish in this cafeteria int32 min = 4; + // maximum of all dish rating tags which were used to rate this dish in this cafeteria int32 max = 5; repeated RatingTagResult rating_tags = 6; repeated RatingTagResult name_tags = 7; diff --git a/server/api/tumdev/campus_backend.swagger.json b/server/api/tumdev/campus_backend.swagger.json index 95bca358..b9d04e96 100644 --- a/server/api/tumdev/campus_backend.swagger.json +++ b/server/api/tumdev/campus_backend.swagger.json @@ -426,6 +426,7 @@ }, "/dish/rating/get": { "post": { + "summary": "Allows to query ratings for a specific dish in a specific cafeteria.", "operationId": "Campus_GetDishRatings", "responses": { "200": { @@ -1119,23 +1120,28 @@ "items": { "type": "object", "$ref": "#/definitions/apiSingleRatingReply" - } + }, + "title": "a number of actual ratings" }, "avg": { "type": "number", - "format": "double" + "format": "double", + "title": "average rating for all dish rating tags which were used to rate this dish in this cafeteria" }, "std": { "type": "number", - "format": "double" + "format": "double", + "title": "std of all dish rating tags which were used to rate this dish in this cafeteria" }, "min": { "type": "integer", - "format": "int32" + "format": "int32", + "title": "minimum of all dish rating tags which were used to rate this dish in this cafeteria" }, "max": { "type": "integer", - "format": "int32" + "format": "int32", + "title": "maximum of all dish rating tags which were used to rate this dish in this cafeteria" }, "ratingTags": { "type": "array", diff --git a/server/api/tumdev/campus_backend_grpc.pb.go b/server/api/tumdev/campus_backend_grpc.pb.go index 24dabd0c..f662ce61 100644 --- a/server/api/tumdev/campus_backend_grpc.pb.go +++ b/server/api/tumdev/campus_backend_grpc.pb.go @@ -57,6 +57,7 @@ type CampusClient interface { ListNews(ctx context.Context, in *ListNewsRequest, opts ...grpc.CallOption) (*ListNewsReply, error) // This endpoint retrieves Canteen Ratings from the Backend. ListCanteenRatings(ctx context.Context, in *ListCanteenRatingsRequest, opts ...grpc.CallOption) (*ListCanteenRatingsReply, error) + // Allows to query ratings for a specific dish in a specific cafeteria. GetDishRatings(ctx context.Context, in *GetDishRatingsRequest, opts ...grpc.CallOption) (*GetDishRatingsReply, error) CreateCanteenRating(ctx context.Context, in *CreateCanteenRatingRequest, opts ...grpc.CallOption) (*CreateCanteenRatingReply, error) CreateDishRating(ctx context.Context, in *CreateDishRatingRequest, opts ...grpc.CallOption) (*CreateDishRatingReply, error) @@ -360,6 +361,7 @@ type CampusServer interface { ListNews(context.Context, *ListNewsRequest) (*ListNewsReply, error) // This endpoint retrieves Canteen Ratings from the Backend. ListCanteenRatings(context.Context, *ListCanteenRatingsRequest) (*ListCanteenRatingsReply, error) + // Allows to query ratings for a specific dish in a specific cafeteria. GetDishRatings(context.Context, *GetDishRatingsRequest) (*GetDishRatingsReply, error) CreateCanteenRating(context.Context, *CreateCanteenRatingRequest) (*CreateCanteenRatingReply, error) CreateDishRating(context.Context, *CreateDishRatingRequest) (*CreateDishRatingReply, error) diff --git a/server/backend/cafeteria.go b/server/backend/cafeteria.go index 18e8a2a2..fcd6c3fa 100644 --- a/server/backend/cafeteria.go +++ b/server/backend/cafeteria.go @@ -34,43 +34,34 @@ const ( NAME ModelType = 3 ) -// GetCafeteriaRatings RPC Endpoint +// ListCanteenRatings RPC Endpoint // Allows to query ratings for a specific cafeteria. // It returns the average rating, max/min rating as well as a number of actual ratings and the average ratings for // all cafeteria rating tags which were used to rate this cafeteria. // The parameter limit defines how many actual ratings should be returned. // The optional parameters from and to can define an interval in which the queried ratings have been stored. // If these aren't specified, the newest ratings will be returned as the default -func (s *CampusServer) GetCafeteriaRatings(ctx context.Context, input *pb.ListCanteenRatingsRequest) (*pb.ListCanteenRatingsReply, error) { - var result model.CafeteriaRatingAverage //get the average rating for this specific cafeteria +func (s *CampusServer) ListCanteenRatings(ctx context.Context, input *pb.ListCanteenRatingsRequest) (*pb.ListCanteenRatingsReply, error) { + var statsForCanteen model.CafeteriaRatingStatistic tx := s.db.WithContext(ctx) cafeteriaId := getIDForCafeteriaName(input.CanteenId, tx) - - res := tx.First(&result, "cafeteriaId = ?", cafeteriaId) - if res.Error != nil { - log.WithError(res.Error).Error("Error while querying the cafeteria with Id ", cafeteriaId) - return nil, status.Error(codes.Internal, "This cafeteria has not yet been rated.") - } - if res.RowsAffected > 0 { - ratings := queryLastCafeteriaRatingsWithLimit(input, cafeteriaId, tx) - cafeteriaTags := queryTags(cafeteriaId, -1, CAFETERIA, tx) - - return &pb.ListCanteenRatingsReply{ - Avg: result.Average, - Std: result.Std, - Min: result.Min, - Max: result.Max, - Rating: ratings, - RatingTags: cafeteriaTags, - }, nil - } else { - return &pb.ListCanteenRatingsReply{ - Avg: -1, - Std: -1, - Min: -1, - Max: -1, - }, nil + err := tx.First(&statsForCanteen, "cafeteriaId = ?", cafeteriaId).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, status.Error(codes.NotFound, "No cafeteria with this Id found.") } + if err != nil { + log.WithError(err).Error("Error while querying the cafeteria with Id ", cafeteriaId) + return nil, status.Error(codes.Internal, "could not query the cafeteria with the given Id") + } + + return &pb.ListCanteenRatingsReply{ + Avg: statsForCanteen.Average, + Std: statsForCanteen.Std, + Min: statsForCanteen.Min, + Max: statsForCanteen.Max, + Rating: queryLastCafeteriaRatingsWithLimit(input, cafeteriaId, tx), + RatingTags: queryTags(cafeteriaId, -1, CAFETERIA, tx), + }, nil } // queryLastCafeteriaRatingsWithLimit @@ -99,8 +90,7 @@ func queryLastCafeteriaRatingsWithLimit(input *pb.ListCanteenRatingsRequest, caf } else { to = input.To.AsTime() } - err = tx. - Order("timestamp desc, cafeteriaRating desc"). + err = tx.Order("timestamp desc, cafeteriaRating desc"). Limit(limit). Find(&ratings, "cafeteriaID = ? AND timestamp < ? AND timestamp > ?", cafeteriaID, to, from).Error } else { @@ -129,51 +119,31 @@ func queryLastCafeteriaRatingsWithLimit(input *pb.ListCanteenRatingsRequest, caf } } -// GetDishRatings RPC Endpoint -// Allows to query ratings for a specific dish in a specific cafeteria. -// It returns the average rating, max/min rating as well as a number of actual ratings and the average ratings for -// all dish rating tags which were used to rate this dish in this cafeteria. Additionally, the average, max/min are -// returned for every name tag which matches the name of the dish. -// The parameter limit defines how many actual ratings should be returned. -// The optional parameters from and to can define a interval in which the queried ratings have been stored. -// If these aren't specified, the newest ratings will be returned as the default func (s *CampusServer) GetDishRatings(ctx context.Context, input *pb.GetDishRatingsRequest) (*pb.GetDishRatingsReply, error) { - var result model.DishRatingAverage //get the average rating for this specific dish tx := s.db.WithContext(ctx) cafeteriaID := getIDForCafeteriaName(input.CanteenId, tx) dishID := getIDForDishName(input.Dish, cafeteriaID, tx) - res := tx.First(&result, "cafeteriaID = ? AND dishID = ?", cafeteriaID, dishID) - - if res.Error != nil { + var statsForDish model.DishRatingStatistic + err := tx.First(&statsForDish, "cafeteriaID = ? AND dishID = ?", cafeteriaID, dishID).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, status.Error(codes.NotFound, "No cafeteria with this Id found.") + } + if err != nil { fields := log.Fields{"dishID": dishID, "cafeteriaID": cafeteriaID} - log.WithError(res.Error).WithFields(fields).Error("Error while querying the average ratings") + log.WithError(err).WithFields(fields).Error("Error while querying the average ratings") return nil, status.Error(codes.Internal, "This dish has not yet been rated.") } - if res.RowsAffected > 0 { - ratings := queryLastDishRatingsWithLimit(input, cafeteriaID, dishID, tx) - dishTags := queryTags(cafeteriaID, dishID, DISH, tx) - nameTags := queryTags(cafeteriaID, dishID, NAME, tx) - - return &pb.GetDishRatingsReply{ - Avg: result.Average, - Std: result.Std, - Min: result.Min, - Max: result.Max, - Rating: ratings, - RatingTags: dishTags, - NameTags: nameTags, - }, nil - } else { - return &pb.GetDishRatingsReply{ - Avg: -1, - Min: -1, - Max: -1, - Std: -1, - }, nil - } - + return &pb.GetDishRatingsReply{ + Avg: statsForDish.Average, + Std: statsForDish.Std, + Min: statsForDish.Min, + Max: statsForDish.Max, + Rating: queryLastDishRatingsWithLimit(input, cafeteriaID, dishID, tx), + RatingTags: queryTags(cafeteriaID, dishID, DISH, tx), + NameTags: queryTags(cafeteriaID, dishID, NAME, tx), + }, nil } // queryLastDishRatingsWithLimit @@ -273,14 +243,14 @@ func queryTags(cafeteriaID int32, dishID int32, ratingType ModelType, tx *gorm.D var err error if ratingType == DISH { err = tx.Table("dish_rating_tag_option options"). - Joins("JOIN dish_rating_tag_average results ON options.dishRatingTagOption = results.tagID"). + Joins("JOIN dish_rating_tag_statistics results ON options.dishRatingTagOption = results.tagID"). Select("options.dishRatingTagOption as tagId, results.average as avg, "+ "results.min as min, results.max as max, results.std as std"). Where("results.cafeteriaID = ? AND results.dishID = ?", cafeteriaID, dishID). Scan(&results).Error } else if ratingType == CAFETERIA { err = tx.Table("cafeteria_rating_tag_option options"). - Joins("JOIN cafeteria_rating_tag_average results ON options.cafeteriaRatingTagOption = results.tagID"). + Joins("JOIN cafeteria_rating_tag_statistics results ON options.cafeteriaRatingTagOption = results.tagID"). Select("options.cafeteriaRatingTagOption as tagId, results.average as avg, "+ "results.min as min, results.max as max, results.std as std"). Where("results.cafeteriaID = ?", cafeteriaID). @@ -289,7 +259,7 @@ func queryTags(cafeteriaID int32, dishID int32, ratingType ModelType, tx *gorm.D err = tx.Table("dish_to_dish_name_tag mapping"). Where("mapping.dishID = ?", dishID). Select("mapping.nameTagID as tag"). - Joins("JOIN dish_name_tag_average results ON mapping.nameTagID = results.tagID"). + Joins("JOIN dish_name_tag_statistic results ON mapping.nameTagID = results.tagID"). Joins("JOIN dish_name_tag_option options ON mapping.nameTagID = options.dishNameTagOption"). Select("mapping.nameTagID as tagId, results.average as avg, " + "results.min as min, results.max as max, results.std as std"). diff --git a/server/backend/cron/average_rating_computation.go b/server/backend/cron/average_rating_computation.go deleted file mode 100644 index 9c3324bb..00000000 --- a/server/backend/cron/average_rating_computation.go +++ /dev/null @@ -1,117 +0,0 @@ -package cron - -import ( - "github.com/TUM-Dev/Campus-Backend/server/model" - log "github.com/sirupsen/logrus" -) - -// averageRatingComputation -// This cronjob precomputes average ratings of all cafeteria ratings, dish ratings and all three types of tags. -// They are grouped (e.g. All Ratings for "Mensa_garching") and the computed values will then be stored in a table with the suffix "_result" -func (c *CronService) averageRatingComputation() error { - computeAverageForCafeteria(c) - computeAverageForDishesInCafeterias(c) - computeAverageCafeteriaTags(c) - computeAverageForDishesInCafeteriasTags(c) - computeAverageNameTags(c) - - return nil -} - -func computeAverageNameTags(c *CronService) { - var results []model.DishNameTagAverage - err := c.db.Raw("SELECT mr.cafeteriaID as cafeteriaID, mnt.tagnameID as tagID, AVG(mnt.points) as average, MAX(mnt.points) as max, MIN(mnt.points) as min, STD(mnt.points) as std" + - " FROM dish_rating mr" + - " JOIN dish_name_tag mnt ON mr.dishRating = mnt.correspondingRating" + - " GROUP BY mr.cafeteriaID, mnt.tagnameID").Scan(&results).Error - - if err != nil { - log.WithError(err).Error("while precomputing average name tags.") - } else if len(results) > 0 { - if err := c.db.Where("1=1").Delete(&model.DishNameTagAverage{}).Error; err != nil { - log.WithError(err).Error("Error while deleting old averages in the table.") - } - if err := c.db.Create(&results).Error; err != nil { - log.WithError(err).Error("while creating a new average name tag rating in the database.") - } - } -} - -func computeAverageForDishesInCafeteriasTags(c *CronService) { - var results []model.DishRatingTagAverage //todo namen im select anpassen - err := c.db.Raw("SELECT mr.dishID as dishID, mr.cafeteriaID as cafeteriaID, mrt.tagID as tagID, AVG(mrt.points) as average, MAX(mrt.points) as max, MIN(mrt.points) as min, STD(mrt.points) as std" + - " FROM dish_rating mr" + - " JOIN dish_rating_tag mrt ON mr.dishRating = mrt.parentRating" + - " GROUP BY mr.cafeteriaID, mrt.tagID, mr.dishID").Scan(&results).Error - - if err != nil { - log.WithError(err).Error("while precomputing average dish tags.") - } else if len(results) > 0 { - if err := c.db.Where("1=1").Delete(&model.DishRatingTagAverage{}).Error; err != nil { - log.WithError(err).Error("Error while deleting old averages in the table.") - } - - if err := c.db.Create(&results).Error; err != nil { - log.WithError(err).Error("while creating a new average dish tag rating in the database.") - } - - } -} - -func computeAverageCafeteriaTags(c *CronService) { - var results []model.CafeteriaRatingTagAverage - err := c.db.Raw("SELECT cr.cafeteriaID as cafeteriaID, crt.tagID as tagID, AVG(crt.points) as average, MAX(crt.points) as max, MIN(crt.points) as min, STD(crt.points) as std" + - " FROM cafeteria_rating cr" + - " JOIN cafeteria_rating_tag crt ON cr.cafeteriaRating = crt.correspondingRating" + - " GROUP BY cr.cafeteriaID, crt.tagID").Scan(&results).Error - - if err != nil { - log.WithError(err).Error("while precomputing average cafeteria tags.") - } else if len(results) > 0 { - if err := c.db.Where("1=1").Delete(&model.CafeteriaRatingTagAverage{}).Error; err != nil { - log.WithError(err).Error("Error while deleting old averages in the table.") - } - - if err := c.db.Create(&results).Error; err != nil { - log.WithError(err).Error("while creating a new average cafeteria tag rating in the database.") - } - } -} - -func computeAverageForDishesInCafeterias(c *CronService) { - var results []model.DishRatingAverage - err := c.db.Model(&model.DishRating{}). - Select("cafeteriaID, dishID, AVG(points) as average, MAX(points) as max, MIN(points) as min, STD(points) as std"). - Group("cafeteriaID,dishID").Scan(&results).Error - - if err != nil { - log.WithError(err).Error("while precomputing average dish ratings.") - } else if len(results) > 0 { - if err := c.db.Where("1=1").Delete(&model.DishRatingAverage{}).Error; err != nil { - log.WithError(err).Error("Error while deleting old averages in the table.") - } - if err := c.db.Create(&results).Error; err != nil { - log.WithError(err).Error("while creating a new average dish rating in the database.") - } - } -} - -func computeAverageForCafeteria(c *CronService) { - var results []model.CafeteriaRatingAverage - err := c.db.Model(&model.CafeteriaRating{}). - Select("cafeteriaID, AVG(points) as average, MAX(points) as max, MIN(points) as min, STD(points) as std"). - Group("cafeteriaID").Find(&results).Error - - if err != nil { - log.WithError(err).Error("while precomputing average cafeteria ratings.") - } else if len(results) > 0 { - if err := c.db.Where("1=1").Delete(&model.CafeteriaRatingAverage{}).Error; err != nil { - log.WithError(err).Error("Error while deleting old averages in the table.") - } - - err := c.db.Create(&results).Error - if err != nil { - log.WithError(err).Error("while creating a new average cafeteria rating in the database.") - } - } -} diff --git a/server/backend/cron/cronjobs.go b/server/backend/cron/cronjobs.go index 95ba45a2..82d9b2a8 100644 --- a/server/backend/cron/cronjobs.go +++ b/server/backend/cron/cronjobs.go @@ -25,16 +25,15 @@ var StorageDir = "/Storage/" // target location of files // names for cron jobs as specified in database const ( - NewsType = "news" - FileDownloadType = "fileDownload" - DishNameDownload = "dishNameDownload" - AverageRatingComputation = "averageRatingComputation" - CanteenHeadcount = "canteenHeadCount" - IOSNotifications = "iosNotifications" - IOSActivityReset = "iosActivityReset" - NewExamResultsHook = "newExamResultsHook" - MovieType = "movie" - FeedbackEmail = "feedbackEmail" + NewsType = "news" + FileDownloadType = "fileDownload" + DishNameDownload = "dishNameDownload" + CanteenHeadcount = "canteenHeadCount" + IOSNotifications = "iosNotifications" + IOSActivityReset = "iosActivityReset" + NewExamResultsHook = "newExamResultsHook" + MovieType = "movie" + FeedbackEmail = "feedbackEmail" /* MensaType = "mensa" AlarmType = "alarm" */ @@ -50,23 +49,16 @@ func New(db *gorm.DB) *CronService { func (c *CronService) Run() error { log.WithField("MensaCronActive", env.IsMensaCronActive()).Debug("running cron service") - g := new(errgroup.Group) - - if env.IsMensaCronActive() { - g.Go(func() error { return c.dishNameDownloadCron() }) - g.Go(func() error { return c.averageRatingComputation() }) - } - for { + g := new(errgroup.Group) log.Trace("Cron: checking for pending") var res []model.Crontab c.db.Model(&model.Crontab{}). - Where("`interval` > 0 AND (lastRun+`interval`) < ? AND type IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + Where("`interval` > 0 AND (lastRun+`interval`) < ? AND type IN (?, ?, ?, ?, ?, ?, ?, ?, ?)", time.Now().Unix(), NewsType, FileDownloadType, - AverageRatingComputation, DishNameDownload, CanteenHeadcount, IOSNotifications, @@ -79,18 +71,10 @@ func (c *CronService) Run() error { for _, cronjob := range res { // Persist run to DB right away - var offset int32 = 0 - if env.IsMensaCronActive() { - if cronjob.Type.String == AverageRatingComputation { - if time.Now().Hour() == 16 { - offset = 18 * 3600 // fast-forward 18 Hours to the next day + does not need to be computed overnight - } - } - } - cronFields := log.Fields{"Cron (id)": cronjob.Cron, "type": cronjob.Type.String, "offset": offset, "LastRun": cronjob.LastRun, "interval": cronjob.Interval, "id (not real id)": cronjob.ID.Int64} + cronFields := log.Fields{"Cron (id)": cronjob.Cron, "type": cronjob.Type.String, "LastRun": cronjob.LastRun, "interval": cronjob.Interval, "id (not real id)": cronjob.ID.Int64} log.WithFields(cronFields).Trace("Running cronjob") - cronjob.LastRun = int32(time.Now().Unix()) + offset + cronjob.LastRun = int32(time.Now().Unix()) c.db.Save(&cronjob) // Run each job in a separate goroutine, so we can parallelize them @@ -106,10 +90,6 @@ func (c *CronService) Run() error { if env.IsMensaCronActive() { g.Go(c.dishNameDownloadCron) } - case AverageRatingComputation: //call every five minutes between 11AM and 4 PM on weekdays - if env.IsMensaCronActive() { - g.Go(c.averageRatingComputation) - } case NewExamResultsHook: g.Go(func() error { return c.newExamResultsHookCron() }) case MovieType: diff --git a/server/backend/migration/20220713000000.go b/server/backend/migration/20220713000000.go index b40ef531..4bab3054 100644 --- a/server/backend/migration/20220713000000.go +++ b/server/backend/migration/20220713000000.go @@ -268,7 +268,6 @@ func migrate20220713000000() *gormigrate.Migration { return &gormigrate.Migration{ ID: "20220713000000", Migrate: func(tx *gorm.DB) error { - if err := tx.AutoMigrate( &InitialCafeteria{}, &InitialCafeteriaRating{}, @@ -284,9 +283,9 @@ func migrate20220713000000() *gormigrate.Migration { &InitialDishRatingTag{}, &InitialDishRatingTagOption{}, &InitialDishToDishNameTag{}, + &InitialDishNameTagAverage{}, &InitialCafeteriaRatingAverage{}, &InitialCafeteriaRatingTagAverage{}, - &InitialDishNameTagAverage{}, &InitialDishRatingAverage{}, &InitialDishRatingTagAverage{}, ); err != nil { diff --git a/server/backend/migration/20231015000000.go b/server/backend/migration/20231015000000.go new file mode 100644 index 00000000..9a0ed791 --- /dev/null +++ b/server/backend/migration/20231015000000.go @@ -0,0 +1,167 @@ +package migration + +import ( + "github.com/TUM-Dev/Campus-Backend/server/model" + "github.com/go-gormigrate/gormigrate/v2" + "github.com/guregu/null" + "gorm.io/gorm" +) + +// CafeteriaRatingAverage stores all precomputed values for the cafeteria ratings +type CafeteriaRatingAverage struct { + CafeteriaRatingAverage int64 `gorm:"primary_key;AUTO_INCREMENT;column:cafeteriaRatingAverage;type:int;not null;"` + CafeteriaID int64 `gorm:"column:cafeteriaID;foreignKey:cafeteria;type:int;not null;"` + Average float64 `gorm:"column:average;type:float;not null;"` + Min int32 `gorm:"column:min;type:int;not null;"` + Max int32 `gorm:"column:max;type:int;not null;"` + Std float64 `gorm:"column:std;type:float;not null;"` +} + +// TableName sets the insert table name for this struct type +func (n *CafeteriaRatingAverage) TableName() string { + return "cafeteria_rating_average" +} + +// DishRatingAverage stores all precomputed values for the cafeteria ratings +type DishRatingAverage struct { + DishRatingAverage int64 `gorm:"primary_key;AUTO_INCREMENT;column:dishRatingAverage;type:int;not null;"` + CafeteriaID int64 `gorm:"column:cafeteriaID;foreignKey:cafeteria;type:int;not null;"` + DishID int64 `gorm:"column:dishID;foreignKey:dish;type:int;not null;"` + Average float64 `gorm:"column:average;type:float;not null;"` + Min int32 `gorm:"column:min;type:int;not null;"` + Max int32 `gorm:"column:max;type:int;not null;"` + Std float64 `gorm:"column:std;type:float;not null;"` +} + +// TableName sets the insert table name for this struct type +func (n *DishRatingAverage) TableName() string { + return "dish_rating_average" +} + +// DishRatingTagAverage stores all precomputed values for the cafeteria ratings +type DishRatingTagAverage struct { + DishRatingTagsAverage int64 `gorm:"primary_key;AUTO_INCREMENT;column:dishRatingTagsAverage;type:int;not null;"` + CafeteriaID int64 `gorm:"column:cafeteriaID;foreignKey:cafeteria;type:int;not null;"` + TagID int64 `gorm:"column:tagID;foreignKey:tagID;type:int;not null;"` + DishID int64 `gorm:"column:dishID;foreignKey:dishID;type:int;not null;"` + Average float32 `gorm:"column:average;type:float;not null;"` + Min int8 `gorm:"column:min;type:int;not null;"` + Max int8 `gorm:"column:max;type:int;not null;"` + Std float32 `gorm:"column:std;type:float;not null;"` +} + +// TableName sets the insert table name for this struct type +func (n *DishRatingTagAverage) TableName() string { + return "dish_rating_tag_average" +} + +// CafeteriaRatingTagsAverage stores all precomputed values for the cafeteria ratings +type CafeteriaRatingTagsAverage struct { + CafeteriaRatingTagsAverage int64 `gorm:"primary_key;AUTO_INCREMENT;column:cafeteriaRatingTagsAverage;type:int;not null;" json:"canteenRatingTagsAverage"` + CafeteriaID int64 `gorm:"column:cafeteriaID;foreignKey:cafeteria;type:int;not null;" json:"canteenID"` + TagID int64 `gorm:"column:tagID;foreignKey:cafeteriaRatingTagOption;type:int;not null;" json:"tagID"` + Average float32 `gorm:"column:average;type:float;not null;" json:"average"` + Min int8 `gorm:"column:min;type:int;not null;" json:"min"` + Max int8 `gorm:"column:max;type:int;not null;" json:"max"` + Std float32 `gorm:"column:std;type:float;not null;" json:"std"` +} + +// TableName sets the insert table name for this struct type +func (n *CafeteriaRatingTagsAverage) TableName() string { + return "cafeteria_rating_tag_average" +} + +// DishNameTagAverage stores all precomputed values for the DishName ratings +type DishNameTagAverage struct { + DishNameTagAverage int64 `gorm:"primary_key;AUTO_INCREMENT;column:dishNameTagAverage;type:int;not null;" json:"dishNameTagAverage" ` + CafeteriaID int64 `gorm:"column:cafeteriaID;foreignKey:cafeteria;type:int;not null;" json:"cafeteriaID"` + TagID int64 `gorm:"column:tagID;foreignKey:DishNameTagOption;type:int;not null;" json:"tagID"` + Average float32 `gorm:"column:average;type:float;not null;" json:"average" ` + Min int8 `gorm:"column:min;type:int;not null;" json:"min"` + Max int8 `gorm:"column:max;type:int;not null;" json:"max"` + Std float32 `gorm:"column:std;type:float;not null;" json:"std"` +} + +// TableName sets the insert table name for this struct type +func (n *DishNameTagAverage) TableName() string { + return "dish_name_tag_average" +} + +// migrate20231015000000 +// migrates the static data for the canteen rating system and adds the necessary cronjob entries +func migrate20231015000000() *gormigrate.Migration { + return &gormigrate.Migration{ + ID: "20231015000000", + Migrate: func(tx *gorm.DB) error { + // cronjob + if err := tx.Delete(&model.Crontab{}, "type = 'averageRatingComputation'").Error; err != nil { + return err + } + if err := SafeEnumRemove(tx, &model.Crontab{}, "type", "averageRatingComputation"); err != nil { + return err + } + // tables + tables := []string{"cafeteria_rating_average", "dish_rating_average", "dish_rating_tag_average", "cafeteria_rating_tag_average", "dish_name_tag_average"} + for _, table := range tables { + if err := tx.Migrator().DropTable(table); err != nil { + return err + } + } + // views + if err := tx.Exec(`CREATE VIEW cafeteria_rating_statistics AS +SELECT cafeteriaID, Avg(points) AS average, MIN(points) AS min, Max(points) AS max, STD(points) AS std +FROM cafeteria_rating +GROUP BY cafeteriaID +ORDER BY COUNT(*) DESC, average DESC`).Error; err != nil { + return err + } + if err := tx.Exec(`CREATE VIEW dish_rating_statistics AS +SELECT cafeteriaID, dishID, AVG(points) as average, MAX(points) as max, MIN(points) as min, STD(points) as std +FROM dish_rating +GROUP BY cafeteriaID,dishID +ORDER BY COUNT(*) DESC, average DESC`).Error; err != nil { + return err + } + if err := tx.Exec(`CREATE VIEW dish_rating_tag_statistics AS +SELECT mr.dishID as dishID, mr.cafeteriaID as cafeteriaID, mrt.tagID as tagID, AVG(mrt.points) as average, MAX(mrt.points) as max, MIN(mrt.points) as min, STD(mrt.points) as std +FROM dish_rating mr +JOIN dish_rating_tag mrt ON mr.dishRating = mrt.parentRating +GROUP BY mr.cafeteriaID, mrt.tagID, mr.dishID`).Error; err != nil { + return err + } + if err := tx.Exec(`CREATE VIEW cafeteria_rating_tag_statistics AS +SELECT cr.cafeteriaID as cafeteriaID, crt.tagID as tagID, AVG(crt.points) as average, MAX(crt.points) as max, MIN(crt.points) as min, STD(crt.points) as std +FROM cafeteria_rating cr +JOIN cafeteria_rating_tag crt ON cr.cafeteriaRating = crt.correspondingRating +GROUP BY cr.cafeteriaID, crt.tagID`).Error; err != nil { + return err + } + return tx.Exec(`CREATE VIEW dish_name_tag_statistics AS +SELECT mr.cafeteriaID as cafeteriaID, mnt.tagnameID as tagID, AVG(mnt.points) as average, MAX(mnt.points) as max, MIN(mnt.points) as min, STD(mnt.points) as std +FROM dish_rating mr +JOIN dish_name_tag mnt ON mr.dishRating = mnt.correspondingRating +GROUP BY mr.cafeteriaID, mnt.tagnameID`).Error + }, + Rollback: func(tx *gorm.DB) error { + // views + createdViews := []string{"cafeteria_rating_statistics", "dish_rating_statistics", "dish_rating_tag_statistics", "cafeteria_rating_tag_statistics", "dish_name_tag_statistics"} + for _, view := range createdViews { + if err := tx.Exec("DROP VIEW IF EXISTS " + view).Error; err != nil { + return err + } + } + // tables + if err := tx.AutoMigrate(&CafeteriaRatingAverage{}, &DishRatingAverage{}, &DishRatingTagAverage{}, &CafeteriaRatingTagsAverage{}, &DishNameTagAverage{}); err != nil { + return err + } + // cronjob + if err := SafeEnumAdd(tx, &model.Crontab{}, "type", "averageRatingComputation"); err != nil { + return err + } + return tx.Create(&model.Crontab{ + Interval: 300, + Type: null.StringFrom("averageRatingComputation"), + }).Error + }, + } +} diff --git a/server/backend/migration/migration.go b/server/backend/migration/migration.go index dc1e68f2..cebebb32 100644 --- a/server/backend/migration/migration.go +++ b/server/backend/migration/migration.go @@ -14,23 +14,18 @@ func autoMigrate(db *gorm.DB) error { err := db.AutoMigrate( &model.Cafeteria{}, &model.CafeteriaRating{}, - &model.CafeteriaRatingAverage{}, &model.CafeteriaRatingTag{}, - &model.CafeteriaRatingTagAverage{}, &model.CafeteriaRatingTagOption{}, &model.CanteenHeadCount{}, &model.Crontab{}, &model.Device{}, &model.Dish{}, &model.DishNameTag{}, - &model.DishNameTagAverage{}, &model.DishNameTagOption{}, &model.DishNameTagOptionExcluded{}, &model.DishNameTagOptionIncluded{}, &model.DishRating{}, - &model.DishRatingAverage{}, &model.DishRatingTag{}, - &model.DishRatingTagAverage{}, &model.DishRatingTagOption{}, &model.DishToDishNameTag{}, &model.DishesOfTheWeek{}, @@ -78,6 +73,7 @@ func manualMigrate(db *gorm.DB) error { migrate20230904100000(), migrate20230826000000(), migrate20231003000000(), + migrate20231015000000(), migrate20231023000000(), migrate20240101000000(), migrate20240102000000(), diff --git a/server/model/cafeteria_rating_average.go b/server/model/cafeteria_rating_average.go deleted file mode 100644 index cb624a95..00000000 --- a/server/model/cafeteria_rating_average.go +++ /dev/null @@ -1,16 +0,0 @@ -package model - -// CafeteriaRatingAverage stores all precomputed values for the cafeteria ratings -type CafeteriaRatingAverage struct { - CafeteriaRatingAverage int64 `gorm:"primary_key;AUTO_INCREMENT;column:cafeteriaRatingAverage;type:int;not null;" json:"canteenRatingAverage" ` - CafeteriaID int64 `gorm:"column:cafeteriaID;foreignKey:cafeteria;type:int;not null;" json:"canteenID"` - Average float64 `gorm:"column:average;type:float;not null;" json:"average" ` - Min int32 `gorm:"column:min;type:int;not null;" json:"min"` - Max int32 `gorm:"column:max;type:int;not null;" json:"max"` - Std float64 `gorm:"column:std;type:float;not null;" json:"std"` -} - -// TableName sets the insert table name for this struct type -func (n *CafeteriaRatingAverage) TableName() string { - return "cafeteria_rating_average" -} diff --git a/server/model/cafeteria_rating_statistic.go b/server/model/cafeteria_rating_statistic.go new file mode 100644 index 00000000..7af0faeb --- /dev/null +++ b/server/model/cafeteria_rating_statistic.go @@ -0,0 +1,10 @@ +package model + +// CafeteriaRatingStatistic is a view for statistics of cafeteria ratings +type CafeteriaRatingStatistic struct { + CafeteriaID int64 `gorm:"column:cafeteriaID;foreignKey:cafeteria;type:int;not null;"` + Average float64 `gorm:"column:average;type:float;not null;"` + Min int32 `gorm:"column:min;type:int;not null;"` + Max int32 `gorm:"column:max;type:int;not null;"` + Std float64 `gorm:"column:std;type:float;not null;"` +} diff --git a/server/model/cafeteria_rating_tag_average.go b/server/model/cafeteria_rating_tag_average.go deleted file mode 100644 index b969ebf7..00000000 --- a/server/model/cafeteria_rating_tag_average.go +++ /dev/null @@ -1,17 +0,0 @@ -package model - -// CafeteriaRatingTagAverage stores all precomputed values for the cafeteria ratings -type CafeteriaRatingTagAverage struct { - CafeteriaRatingTagsAverage int64 `gorm:"primary_key;AUTO_INCREMENT;column:cafeteriaRatingTagsAverage;type:int;not null;" json:"canteenRatingTagsAverage"` - CafeteriaID int64 `gorm:"column:cafeteriaID;foreignKey:cafeteria;type:int;not null;" json:"canteenID"` - TagID int64 `gorm:"column:tagID;foreignKey:cafeteriaRatingTagOption;type:int;not null;" json:"tagID"` - Average float32 `gorm:"column:average;type:float;not null;" json:"average"` - Min int8 `gorm:"column:min;type:int;not null;" json:"min"` - Max int8 `gorm:"column:max;type:int;not null;" json:"max"` - Std float32 `gorm:"column:std;type:float;not null;" json:"std"` -} - -// TableName sets the insert table name for this struct type -func (n *CafeteriaRatingTagAverage) TableName() string { - return "cafeteria_rating_tag_average" -} diff --git a/server/model/cafeteria_rating_tag_statistic.go b/server/model/cafeteria_rating_tag_statistic.go new file mode 100644 index 00000000..7793df17 --- /dev/null +++ b/server/model/cafeteria_rating_tag_statistic.go @@ -0,0 +1,11 @@ +package model + +// CafeteriaRatingTagsStatistic is a view for statistics of cafeteria rating tags +type CafeteriaRatingTagsStatistic struct { + CafeteriaID int64 `gorm:"column:cafeteriaID;foreignKey:cafeteria;type:int;not null;" json:"canteenID"` + TagID int64 `gorm:"column:tagID;foreignKey:cafeteriaRatingTagOption;type:int;not null;" json:"tagID"` + Average float32 `gorm:"column:average;type:float;not null;" json:"average"` + Min int8 `gorm:"column:min;type:int;not null;" json:"min"` + Max int8 `gorm:"column:max;type:int;not null;" json:"max"` + Std float32 `gorm:"column:std;type:float;not null;" json:"std"` +} diff --git a/server/model/crontab.go b/server/model/crontab.go index 79d310af..d40d5d6c 100644 --- a/server/model/crontab.go +++ b/server/model/crontab.go @@ -14,6 +14,6 @@ type Crontab struct { Cron int64 `gorm:"primary_key;AUTO_INCREMENT;column:cron;type:int;" json:"cron"` Interval int32 `gorm:"column:interval;type:int;default:7200;" json:"interval"` LastRun int32 `gorm:"column:lastRun;type:int;default:0;" json:"last_run"` - Type null.String `gorm:"column:type;type:enum ('news', 'mensa', 'movie', 'roomfinder', 'alarm', 'fileDownload','dishNameDownload','averageRatingComputation', 'iosNotifications', 'iosActivityReset', 'canteenHeadCount', 'newExamResultsHook');" json:"type"` + Type null.String `gorm:"column:type;type:enum ('news', 'mensa', 'movie', 'roomfinder', 'alarm', 'fileDownload','dishNameDownload', 'iosNotifications', 'iosActivityReset', 'canteenHeadCount', 'newExamResultsHook');" json:"type"` ID null.Int `gorm:"column:id;type:int;" json:"id"` } diff --git a/server/model/dish_name_tag_average.go b/server/model/dish_name_tag_average.go deleted file mode 100644 index 2ee05076..00000000 --- a/server/model/dish_name_tag_average.go +++ /dev/null @@ -1,17 +0,0 @@ -package model - -// DishNameTagAverage stores all precomputed values for the DishName ratings -type DishNameTagAverage struct { - DishNameTagAverage int64 `gorm:"primary_key;AUTO_INCREMENT;column:dishNameTagAverage;type:int;not null;" json:"dishNameTagAverage" ` - CafeteriaID int64 `gorm:"column:cafeteriaID;foreignKey:cafeteria;type:int;not null;" json:"cafeteriaID"` - TagID int64 `gorm:"column:tagID;foreignKey:DishNameTagOption;type:int;not null;" json:"tagID"` - Average float32 `gorm:"column:average;type:float;not null;" json:"average" ` - Min int8 `gorm:"column:min;type:int;not null;" json:"min"` - Max int8 `gorm:"column:max;type:int;not null;" json:"max"` - Std float32 `gorm:"column:std;type:float;not null;" json:"std"` -} - -// TableName sets the insert table name for this struct type -func (n *DishNameTagAverage) TableName() string { - return "dish_name_tag_average" -} diff --git a/server/model/dish_name_tag_statistic.go b/server/model/dish_name_tag_statistic.go new file mode 100644 index 00000000..44539206 --- /dev/null +++ b/server/model/dish_name_tag_statistic.go @@ -0,0 +1,11 @@ +package model + +// DishNameTagStatistic is a view for statistics of DishName ratings +type DishNameTagStatistic struct { + CafeteriaID int64 `gorm:"column:cafeteriaID;foreignKey:cafeteria;type:int;not null;" json:"cafeteriaID"` + TagID int64 `gorm:"column:tagID;foreignKey:DishNameTagOption;type:int;not null;" json:"tagID"` + Average float32 `gorm:"column:average;type:float;not null;" json:"average" ` + Min int8 `gorm:"column:min;type:int;not null;" json:"min"` + Max int8 `gorm:"column:max;type:int;not null;" json:"max"` + Std float32 `gorm:"column:std;type:float;not null;" json:"std"` +} diff --git a/server/model/dish_rating_average.go b/server/model/dish_rating_average.go deleted file mode 100644 index 2db0571c..00000000 --- a/server/model/dish_rating_average.go +++ /dev/null @@ -1,17 +0,0 @@ -package model - -// DishRatingAverage stores all precomputed values for the cafeteria ratings -type DishRatingAverage struct { - DishRatingAverage int64 `gorm:"primary_key;AUTO_INCREMENT;column:dishRatingAverage;type:int;not null;" json:"dishRatingAverage" ` - CafeteriaID int64 `gorm:"column:cafeteriaID;foreignKey:cafeteria;type:int;not null;" json:"cafeteriaID"` - DishID int64 `gorm:"column:dishID;foreignKey:dish;type:int;not null;" json:"dishID"` - Average float64 `gorm:"column:average;type:float;not null;" json:"average" ` - Min int32 `gorm:"column:min;type:int;not null;" json:"min"` - Max int32 `gorm:"column:max;type:int;not null;" json:"max"` - Std float64 `gorm:"column:std;type:float;not null;" json:"std"` -} - -// TableName sets the insert table name for this struct type -func (n *DishRatingAverage) TableName() string { - return "dish_rating_average" -} diff --git a/server/model/dish_rating_statistic.go b/server/model/dish_rating_statistic.go new file mode 100644 index 00000000..bbf56c04 --- /dev/null +++ b/server/model/dish_rating_statistic.go @@ -0,0 +1,11 @@ +package model + +// DishRatingStatistic is a view for statistics of dish ratings +type DishRatingStatistic struct { + CafeteriaID int64 `gorm:"column:cafeteriaID;foreignKey:cafeteria;type:int;not null;"` + DishID int64 `gorm:"column:dishID;foreignKey:dish;type:int;not null;"` + Average float64 `gorm:"column:average;type:float;not null;"` + Min int32 `gorm:"column:min;type:int;not null;"` + Max int32 `gorm:"column:max;type:int;not null;"` + Std float64 `gorm:"column:std;type:float;not null;"` +} diff --git a/server/model/dish_rating_tag_average.go b/server/model/dish_rating_tag_average.go deleted file mode 100644 index b1535171..00000000 --- a/server/model/dish_rating_tag_average.go +++ /dev/null @@ -1,18 +0,0 @@ -package model - -// DishRatingTagAverage stores all precomputed values for the cafeteria ratings -type DishRatingTagAverage struct { - DishRatingTagsAverage int64 `gorm:"primary_key;AUTO_INCREMENT;column:dishRatingTagsAverage;type:int;not null;" json:"dishRatingTagsAverage" ` - CafeteriaID int64 `gorm:"column:cafeteriaID;foreignKey:cafeteria;type:int;not null;" json:"cafeteriaID"` - TagID int64 `gorm:"column:tagID;foreignKey:tagID;type:int;not null;" json:"tagID"` - DishID int64 `gorm:"column:dishID;foreignKey:dishID;type:int;not null;" json:"dishID"` - Average float32 `gorm:"column:average;type:float;not null;" json:"average" ` - Min int8 `gorm:"column:min;type:int;not null;" json:"min"` - Max int8 `gorm:"column:max;type:int;not null;" json:"max"` - Std float32 `gorm:"column:std;type:float;not null;" json:"std"` -} - -// TableName sets the insert table name for this struct type -func (n *DishRatingTagAverage) TableName() string { - return "dish_rating_tag_average" -} diff --git a/server/model/dish_rating_tag_statistic.go b/server/model/dish_rating_tag_statistic.go new file mode 100644 index 00000000..7eb41c65 --- /dev/null +++ b/server/model/dish_rating_tag_statistic.go @@ -0,0 +1,12 @@ +package model + +// DishRatingTagStatistic is a view for statistics of dish rating tags +type DishRatingTagStatistic struct { + CafeteriaID int64 `gorm:"column:cafeteriaID;foreignKey:cafeteria;type:int;not null;" json:"cafeteriaID"` + TagID int64 `gorm:"column:tagID;foreignKey:tagID;type:int;not null;" json:"tagID"` + DishID int64 `gorm:"column:dishID;foreignKey:dishID;type:int;not null;" json:"dishID"` + Average float32 `gorm:"column:average;type:float;not null;" json:"average" ` + Min int8 `gorm:"column:min;type:int;not null;" json:"min"` + Max int8 `gorm:"column:max;type:int;not null;" json:"max"` + Std float32 `gorm:"column:std;type:float;not null;" json:"std"` +}