From 2bb9fba97afcb1ddb5c84ec00c1f665b43e29ae2 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Mon, 14 Oct 2024 18:25:55 +0200 Subject: [PATCH] added student council support --- server/api/tumdev/campus_backend.proto | 10 +- server/backend/campus.go | 124 ++++++++++++++++++ .../{student_club_test.go => campus_test.go} | 97 +++++++++++++- server/backend/migration/20241024000000.go | 50 +++++++ server/backend/migration/migration.go | 3 + server/backend/student_club.go | 62 --------- server/model/student_council.go | 20 +++ server/model/student_council_collection.go | 11 ++ 8 files changed, 305 insertions(+), 72 deletions(-) create mode 100644 server/backend/campus.go rename server/backend/{student_club_test.go => campus_test.go} (50%) create mode 100644 server/backend/migration/20241024000000.go delete mode 100644 server/backend/student_club.go create mode 100644 server/model/student_council.go create mode 100644 server/model/student_council_collection.go diff --git a/server/api/tumdev/campus_backend.proto b/server/api/tumdev/campus_backend.proto index 81f4d5ca..cca0516a 100644 --- a/server/api/tumdev/campus_backend.proto +++ b/server/api/tumdev/campus_backend.proto @@ -131,7 +131,7 @@ service Campus { option (google.api.http) = {delete: "/device/{device_id}"}; } - // List all information nessesary for the "Campus" tab + // List all information necessary for the "Campus" tab rpc ListStudentClub(ListCampusRequest) returns (ListCampusReply) { option (google.api.http) = {get: "/student_clubs"}; } @@ -561,13 +561,13 @@ message StudentClubCollection { uint64 unstable_collection_id = 4; } message StudentCouncil { - // The name of the club + // The name of the council string name = 1; - // How the club describes itsself + // How the council describes itsself optional string description = 2; - // Where the clubs main internet presence is + // Where the council main internet presence is optional string link_url = 3; - // Where to find a image for this club + // Where to find a image for this council optional string cover_url = 4; } message StudentCouncilCollection { diff --git a/server/backend/campus.go b/server/backend/campus.go new file mode 100644 index 00000000..ec44bc92 --- /dev/null +++ b/server/backend/campus.go @@ -0,0 +1,124 @@ +package backend + +import ( + "context" + + pb "github.com/TUM-Dev/Campus-Backend/server/api/tumdev" + "github.com/TUM-Dev/Campus-Backend/server/model" + log "github.com/sirupsen/logrus" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func (s *CampusServer) ListStudentClub(ctx context.Context, req *pb.ListCampusRequest) (*pb.ListCampusReply, error) { + studentClubs, err := s.getAllStudentClubs(ctx, req.GetLanguage()) + if err != nil { + return nil, status.Error(codes.Internal, "could not query the student clubs. Please retry later") + } + studentCouncils, err := s.getAllStudentCouncils(ctx, req.GetLanguage()) + if err != nil { + return nil, status.Error(codes.Internal, "could not query the student clubs. Please retry later") + } + + return &pb.ListCampusReply{StudentClubs: studentClubs, StudentCouncils: studentCouncils}, nil +} + +func (s *CampusServer) getAllStudentClubs(ctx context.Context, lang pb.Language) ([]*pb.StudentClubCollection, error) { + var dbClubs []model.StudentClub + if err := s.db.WithContext(ctx). + Where(&model.StudentClub{Language: lang.String()}). + Where(&model.StudentClubCollection{Language: lang.String()}). + Joins("Image"). + Joins("StudentClubCollection"). + Find(&dbClubs).Error; err != nil { + log.WithError(err).Error("Error while querying student clubs") + return nil, err + } + + var dbClubCollections []model.StudentClubCollection + if err := s.db.WithContext(ctx). + Where(&model.StudentClubCollection{Language: lang.String()}). + Find(&dbClubCollections).Error; err != nil { + log.WithError(err).Error("Error while querying student club collections") + return nil, err + } + // map from the db to the response + collections := make([]*pb.StudentClubCollection, 0) + for _, dbCollection := range dbClubCollections { + collections = append(collections, &pb.StudentClubCollection{ + Title: dbCollection.Name, + Description: dbCollection.Description, + Clubs: make([]*pb.StudentClub, 0), + UnstableCollectionId: uint64(dbCollection.ID), + }) + } + for _, dbClub := range dbClubs { + resClub := &pb.StudentClub{ + Name: dbClub.Name, + Description: dbClub.Description.Ptr(), + LinkUrl: dbClub.LinkUrl.Ptr(), + } + if dbClub.Image != nil { + cov := dbClub.Image.FullExternalUrl() + resClub.CoverUrl = &cov // go does not allow inlining here + } + + for _, collection := range collections { + if collection.UnstableCollectionId == uint64(dbClub.StudentClubCollectionID) { + collection.Clubs = append(collection.Clubs, resClub) + break + } + } + } + return collections, nil +} + +func (s *CampusServer) getAllStudentCouncils(ctx context.Context, lang pb.Language) ([]*pb.StudentCouncilCollection, error) { + var dbCouncils []model.StudentCouncil + if err := s.db.WithContext(ctx). + Where(&model.StudentCouncil{Language: lang.String()}). + Where(&model.StudentCouncilCollection{Language: lang.String()}). + Joins("Image"). + Joins("StudentCouncilCollection"). + Find(&dbCouncils).Error; err != nil { + log.WithError(err).Error("Error while querying student councils") + return nil, err + } + + var dbCouncilCollections []model.StudentCouncilCollection + if err := s.db.WithContext(ctx). + Where(&model.StudentCouncilCollection{Language: lang.String()}). + Find(&dbCouncilCollections).Error; err != nil { + log.WithError(err).Error("Error while querying student council collections") + return nil, err + } + // map from the db to the response + collections := make([]*pb.StudentCouncilCollection, 0) + for _, dbCollection := range dbCouncilCollections { + collections = append(collections, &pb.StudentCouncilCollection{ + Title: dbCollection.Name, + Description: dbCollection.Description, + Councils: make([]*pb.StudentCouncil, 0), + UnstableCollectionId: uint64(dbCollection.ID), + }) + } + for _, dbCouncil := range dbCouncils { + resCouncil := &pb.StudentCouncil{ + Name: dbCouncil.Name, + Description: dbCouncil.Description.Ptr(), + LinkUrl: dbCouncil.LinkUrl.Ptr(), + } + if dbCouncil.Image != nil { + cov := dbCouncil.Image.FullExternalUrl() + resCouncil.CoverUrl = &cov // go does not allow inlining here + } + + for _, collection := range collections { + if collection.UnstableCollectionId == uint64(dbCouncil.StudentCouncilCollectionID) { + collection.Councils = append(collection.Councils, resCouncil) + break + } + } + } + return collections, nil +} diff --git a/server/backend/student_club_test.go b/server/backend/campus_test.go similarity index 50% rename from server/backend/student_club_test.go rename to server/backend/campus_test.go index 38913381..8d068ce1 100644 --- a/server/backend/student_club_test.go +++ b/server/backend/campus_test.go @@ -18,13 +18,43 @@ func TestCampusServer_ListStudentClub(t *testing.T) { ctx := context.Background() db := utils.SetupTestContainer(ctx, t) exampleClubs := genExampleClubData(db, t) + exampleCouncils := genExampleCouncilData(db, t) server := CampusServer{db: db} language := pb.Language_German - response, err := server.ListStudentClub(ctx, &pb.ListStudentClubRequest{Language: &language}) + response, err := server.ListStudentClub(ctx, &pb.ListCampusRequest{Language: &language}) require.NoError(t, err) - url0 := exampleClubs[0].Image.FullExternalUrl() - expectedResp := &pb.ListStudentClubReply{ - Collections: []*pb.StudentClubCollection{ + urlClub0 := exampleClubs[0].Image.FullExternalUrl() + urlCouncil0 := exampleCouncils[0].Image.FullExternalUrl() + expectedResp := &pb.ListCampusReply{ + StudentCouncils: []*pb.StudentCouncilCollection{ + { + UnstableCollectionId: uint64(exampleCouncils[0].StudentCouncilCollection.ID), + Title: exampleCouncils[0].StudentCouncilCollection.Name, + Description: exampleCouncils[0].StudentCouncilCollection.Description, + Councils: []*pb.StudentCouncil{ + { + Name: exampleCouncils[0].Name, + Description: exampleCouncils[0].Description.Ptr(), + LinkUrl: exampleCouncils[0].LinkUrl.Ptr(), + CoverUrl: &urlCouncil0, + }, + { + Name: exampleCouncils[1].Name, + }, + }, + }, + { + UnstableCollectionId: uint64(exampleCouncils[2].StudentCouncilCollection.ID), + Title: exampleCouncils[2].StudentCouncilCollection.Name, + Description: exampleCouncils[2].StudentCouncilCollection.Description, + Councils: []*pb.StudentCouncil{ + { + Name: exampleCouncils[2].Name, + }, + }, + }, + }, + StudentClubs: []*pb.StudentClubCollection{ { UnstableCollectionId: uint64(exampleClubs[0].StudentClubCollection.ID), Title: exampleClubs[0].StudentClubCollection.Name, @@ -34,7 +64,7 @@ func TestCampusServer_ListStudentClub(t *testing.T) { Name: exampleClubs[0].Name, Description: exampleClubs[0].Description.Ptr(), LinkUrl: exampleClubs[0].LinkUrl.Ptr(), - CoverUrl: &url0, + CoverUrl: &urlClub0, }, { Name: exampleClubs[1].Name, @@ -112,3 +142,60 @@ func genExampleClubData(db *gorm.DB, t *testing.T) []*model.StudentClub { require.NoError(t, err) return []*model.StudentClub{club1, club2, club3} } + +func genExampleCouncilData(db *gorm.DB, t *testing.T) []*model.StudentCouncil { + col1 := model.StudentCouncilCollection{ + Name: "col1", + Language: pb.Language_German.String(), + Description: "Awesome council col", + } + err := db.Create(&col1).Error + require.NoError(t, err) + col2 := model.StudentCouncilCollection{ + Name: "col2", + Description: "Terrible council col", + Language: pb.Language_German.String(), + } + err = db.Create(&col2).Error + require.NoError(t, err) + file1 := &model.File{ + File: 2, + Name: fmt.Sprintf("src_%d.png", 1), + Path: "council/", + Downloads: 1, + URL: null.String{}, + Downloaded: null.BoolFrom(true), + } + err = db.Create(file1).Error + require.NoError(t, err) + club1 := &model.StudentCouncil{ + Name: "Student Council 1", + Language: pb.Language_German.String(), + Description: null.StringFrom("With Description"), + LinkUrl: null.StringFrom("https://example.com"), + ImageID: null.IntFrom(file1.File), + Image: file1, + ImageCaption: null.StringFrom("source: idk, something"), + StudentCouncilCollectionID: col1.ID, + StudentCouncilCollection: col1, + } + err = db.Create(club1).Error + require.NoError(t, err) + club2 := &model.StudentCouncil{ + Name: "Student Council 2", + Language: pb.Language_German.String(), + StudentCouncilCollectionID: col1.ID, + StudentCouncilCollection: col1, + } + err = db.Create(club2).Error + require.NoError(t, err) + club3 := &model.StudentCouncil{ + Name: "Student Council 3", + Language: pb.Language_German.String(), + StudentCouncilCollectionID: col2.ID, + StudentCouncilCollection: col2, + } + err = db.Create(club3).Error + require.NoError(t, err) + return []*model.StudentCouncil{club1, club2, club3} +} diff --git a/server/backend/migration/20241024000000.go b/server/backend/migration/20241024000000.go new file mode 100644 index 00000000..1a2c3fa1 --- /dev/null +++ b/server/backend/migration/20241024000000.go @@ -0,0 +1,50 @@ +package migration + +import ( + "github.com/go-gormigrate/gormigrate/v2" + "gorm.io/gorm" +) + +type newStudentCouncil struct { + gorm.Model + Name string `gorm:"type:varchar(100)"` + Language string `gorm:"type:enum('German','English');default:'German'"` + Description string +} + +// TableName sets the insert table name for this struct type +func (n *newStudentCouncil) TableName() string { + return "student_councils" +} + +type newStudentCouncilCollection struct { + gorm.Model + Name string `gorm:"type:varchar(100)"` + Language string `gorm:"type:enum('German','English');default:'German'"` + Description string +} + +// TableName sets the insert table name for this struct type +func (n *newStudentCouncilCollection) TableName() string { + return "student_council_collections" +} + +// migrate20241024000000 +// - made sure that student councils are supported +func migrate20241024000000() *gormigrate.Migration { + return &gormigrate.Migration{ + ID: "20241024000000", + Migrate: func(tx *gorm.DB) error { + if err := tx.Migrator().AutoMigrate(&newStudentCouncilCollection{}, &newStudentCouncil{}); err != nil { + return err + } + return nil + }, + Rollback: func(tx *gorm.DB) error { + if err := tx.Migrator().DropTable(&newStudentCouncil{}, &newStudentCouncilCollection{}); err != nil { + return err + } + return nil + }, + } +} diff --git a/server/backend/migration/migration.go b/server/backend/migration/migration.go index 4ff90a37..bb0eab3c 100644 --- a/server/backend/migration/migration.go +++ b/server/backend/migration/migration.go @@ -42,6 +42,8 @@ func autoMigrate(db *gorm.DB) error { &model.UpdateNote{}, &model.StudentClub{}, &model.StudentClubCollection{}, + &model.StudentCouncil{}, + &model.StudentCouncilCollection{}, ) return err } @@ -89,6 +91,7 @@ func manualMigrate(db *gorm.DB) error { migrate20240706000000(), migrate20240824000000(), migrate20241023000000(), + migrate20241024000000(), } return gormigrate.New(db, gormigrateOptions, migrations).Migrate() } diff --git a/server/backend/student_club.go b/server/backend/student_club.go deleted file mode 100644 index 15b4363b..00000000 --- a/server/backend/student_club.go +++ /dev/null @@ -1,62 +0,0 @@ -package backend - -import ( - "context" - - pb "github.com/TUM-Dev/Campus-Backend/server/api/tumdev" - "github.com/TUM-Dev/Campus-Backend/server/model" - log "github.com/sirupsen/logrus" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" -) - -func (s *CampusServer) ListStudentClub(ctx context.Context, req *pb.ListStudentClubRequest) (*pb.ListStudentClubReply, error) { - var dbClubs []model.StudentClub - if err := s.db.WithContext(ctx). - Where(&model.StudentClub{Language: req.GetLanguage().String()}). - Where(&model.StudentClubCollection{Language: req.GetLanguage().String()}). - Joins("Image"). - Joins("StudentClubCollection"). - Find(&dbClubs).Error; err != nil { - log.WithError(err).Error("Error while querying student clubs") - return nil, status.Error(codes.Internal, "could not query the student clubs. Please retry later") - } - - var dbClubCollections []model.StudentClubCollection - if err := s.db.WithContext(ctx). - Where(&model.StudentClubCollection{Language: req.GetLanguage().String()}). - Find(&dbClubCollections).Error; err != nil { - log.WithError(err).Error("Error while querying student club collections") - return nil, status.Error(codes.Internal, "could not query the student club collections. Please retry later") - } - // map from the db to the response - collections := make([]*pb.StudentClubCollection, 0) - for _, dbCollection := range dbClubCollections { - collections = append(collections, &pb.StudentClubCollection{ - Title: dbCollection.Name, - Description: dbCollection.Description, - Clubs: make([]*pb.StudentClub, 0), - UnstableCollectionId: uint64(dbCollection.ID), - }) - } - for _, dbClub := range dbClubs { - resClub := &pb.StudentClub{ - Name: dbClub.Name, - Description: dbClub.Description.Ptr(), - LinkUrl: dbClub.LinkUrl.Ptr(), - } - if dbClub.Image != nil { - cov := dbClub.Image.FullExternalUrl() - resClub.CoverUrl = &cov // go does not allow inlining here - } - - for _, collection := range collections { - if collection.UnstableCollectionId == uint64(dbClub.StudentClubCollectionID) { - collection.Clubs = append(collection.Clubs, resClub) - break - } - } - } - - return &pb.ListStudentClubReply{Collections: collections}, nil -} diff --git a/server/model/student_council.go b/server/model/student_council.go new file mode 100644 index 00000000..199b177d --- /dev/null +++ b/server/model/student_council.go @@ -0,0 +1,20 @@ +package model + +import ( + "github.com/guregu/null" + "gorm.io/gorm" +) + +// StudentCouncil stores a student Council +type StudentCouncil struct { + gorm.Model + Name string + Language string `gorm:"type:enum('German','English');default:'German'"` + Description null.String + LinkUrl null.String `gorm:"type:varchar(190);unique;"` + ImageID null.Int + Image *File `gorm:"foreignKey:ImageID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` + ImageCaption null.String + StudentCouncilCollectionID uint + StudentCouncilCollection StudentCouncilCollection `gorm:"foreignKey:StudentCouncilCollectionID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` +} diff --git a/server/model/student_council_collection.go b/server/model/student_council_collection.go new file mode 100644 index 00000000..8b9163ff --- /dev/null +++ b/server/model/student_council_collection.go @@ -0,0 +1,11 @@ +package model + +import "gorm.io/gorm" + +// StudentCouncilCollection stores what collection a ćouncil belongs to +type StudentCouncilCollection struct { + gorm.Model + Name string `gorm:"type:varchar(100)"` + Language string `gorm:"type:enum('German','English');default:'German'"` + Description string +}