diff --git a/backend/src/controllers/club.go b/backend/src/controllers/club.go index 4eab57c55..722738801 100644 --- a/backend/src/controllers/club.go +++ b/backend/src/controllers/club.go @@ -75,3 +75,13 @@ func (l *ClubController) DeleteClub(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusNoContent) } + +func (l *ClubController) GetUserFollowersForClub(c *fiber.Ctx) error { + + clubs, err := l.clubService.GetUserFollowersForClub(c.Params("id")) + if err != nil { + return err.FiberError(c) + } + + return c.Status(fiber.StatusOK).JSON(&clubs) +} diff --git a/backend/src/controllers/user.go b/backend/src/controllers/user.go index 1b509bf3d..1edf8d31c 100644 --- a/backend/src/controllers/user.go +++ b/backend/src/controllers/user.go @@ -137,3 +137,49 @@ func (u *UserController) DeleteUser(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusNoContent) } + +func (u *UserController) GetUserTags(c *fiber.Ctx) error { + tags, err := u.userService.GetUserTags(c.Params("uid")) + if err != nil { + return err.FiberError(c) + } + return c.Status(fiber.StatusOK).JSON(&tags) +} + +func (u *UserController) CreateUserTags(c *fiber.Ctx) error { + var requestBody models.CreateUserTagsBody + if err := c.BodyParser(&requestBody); err != nil { + return errors.FailedToParseRequestBody.FiberError(c) + } + + tags, err := u.userService.CreateUserTags(c.Params("uid"), requestBody) + if err != nil { + return err.FiberError(c) + } + return c.Status(fiber.StatusCreated).JSON(&tags) +} + +func (u *UserController) CreateFollowing(c *fiber.Ctx) error { + err := u.userService.CreateFollowing(c.Params("user_id"), c.Params("club_id")) + if err != nil { + return err.FiberError(c) + } + return c.SendStatus(fiber.StatusCreated) +} + +func (u *UserController) DeleteFollowing(c *fiber.Ctx) error { + + err := u.userService.DeleteFollowing(c.Params("user_id"), c.Params("club_id")) + if err != nil { + return err.FiberError(c) + } + return c.SendStatus(fiber.StatusNoContent) +} + +func (u *UserController) GetAllFollowing(c *fiber.Ctx) error { + clubs, err := u.userService.GetFollowing(c.Params("user_id")) + if err != nil { + return err.FiberError(c) + } + return c.Status(fiber.StatusOK).JSON(clubs) +} diff --git a/backend/src/errors/club.go b/backend/src/errors/club.go index 7b05d03bd..725aa68fa 100644 --- a/backend/src/errors/club.go +++ b/backend/src/errors/club.go @@ -39,4 +39,8 @@ var ( StatusCode: fiber.StatusInternalServerError, Message: "failed to get admin ids", } + FailedToGetClubFollowers = Error{ + StatusCode: fiber.StatusInternalServerError, + Message: "failed to get club followers", + } ) diff --git a/backend/src/errors/user.go b/backend/src/errors/user.go index de07afbb7..dbc70d6d8 100644 --- a/backend/src/errors/user.go +++ b/backend/src/errors/user.go @@ -43,4 +43,8 @@ var ( StatusCode: fiber.StatusInternalServerError, Message: "failed to compute password hash", } + FailedToGetUserFollowing = Error{ + StatusCode: fiber.StatusInternalServerError, + Message: "failed to get user following", + } ) diff --git a/backend/src/models/user.go b/backend/src/models/user.go index 46eee1311..09ac25b9d 100644 --- a/backend/src/models/user.go +++ b/backend/src/models/user.go @@ -48,7 +48,7 @@ type User struct { Tag []Tag `gorm:"many2many:user_tags;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"` Member []Club `gorm:"many2many:user_club_members;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"` - Follower []Club `gorm:"many2many:user_club_followers;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"` + Follower []Club `gorm:"many2many:user_club_followers;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"clubs_followed,omitempty" validate:"-"` IntendedApplicant []Club `gorm:"many2many:user_club_intended_applicants;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"` Asked []Comment `gorm:"foreignKey:AskedByID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"-" validate:"-"` Answered []Comment `gorm:"foreignKey:AnsweredByID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"-" validate:"-"` diff --git a/backend/src/server/server.go b/backend/src/server/server.go index 3b892d65f..a3dadca7d 100644 --- a/backend/src/server/server.go +++ b/backend/src/server/server.go @@ -92,6 +92,14 @@ func userRoutes(router fiber.Router, userService services.UserServiceInterface, users.Patch("/:id", userController.UpdateUser) users.Delete("/:id", userController.DeleteUser) + users.Put("/:user_id/follower/:club_id", userController.CreateFollowing) + users.Delete("/:user_id/follower/:club_id", userController.DeleteFollowing) + users.Get("/:user_id/follower", userController.GetAllFollowing) + + userTags := users.Group("/:uid/tags") + + userTags.Post("/", userController.CreateUserTags) + userTags.Get("/", userController.GetUserTags) return users } @@ -119,6 +127,10 @@ func clubRoutes(router fiber.Router, clubService services.ClubServiceInterface, clubsID.Get("/", clubController.GetClub) clubsID.Patch("/", middlewareService.Authorize(types.ClubWrite), clubController.UpdateClub) clubsID.Delete("/", middlewareService.Authorize(types.ClubDelete), clubController.DeleteClub) + clubs.Get("/:id", clubController.GetClub) + clubs.Patch("/:id", clubController.UpdateClub) + clubs.Delete("/:id", clubController.DeleteClub) + clubs.Get("/:id/follower", clubController.GetUserFollowersForClub) } func authRoutes(router fiber.Router, authService services.AuthServiceInterface, authSettings config.AuthSettings) { diff --git a/backend/src/services/club.go b/backend/src/services/club.go index c05610c1f..011a1f6fa 100644 --- a/backend/src/services/club.go +++ b/backend/src/services/club.go @@ -16,6 +16,7 @@ type ClubServiceInterface interface { CreateClub(clubBody models.CreateClubRequestBody) (*models.Club, *errors.Error) UpdateClub(id string, clubBody models.UpdateClubRequestBody) (*models.Club, *errors.Error) DeleteClub(id string) *errors.Error + GetUserFollowersForClub(id string) ([]models.User, *errors.Error) } type ClubService struct { @@ -91,3 +92,11 @@ func (c *ClubService) DeleteClub(id string) *errors.Error { return transactions.DeleteClub(c.DB, *idAsUUID) } + +func (c *ClubService) GetUserFollowersForClub(id string) ([]models.User, *errors.Error) { + idAsUUID, err := utilities.ValidateID(id) + if err != nil { + return nil, &errors.FailedToValidateID + } + return transactions.GetUserFollowersForClub(c.DB, *idAsUUID) +} diff --git a/backend/src/services/user.go b/backend/src/services/user.go index 646976e09..d2585982e 100644 --- a/backend/src/services/user.go +++ b/backend/src/services/user.go @@ -19,6 +19,11 @@ type UserServiceInterface interface { GetUser(id string) (*models.User, *errors.Error) UpdateUser(id string, userBody models.UpdateUserRequestBody) (*models.User, *errors.Error) DeleteUser(id string) *errors.Error + GetUserTags(id string) ([]models.Tag, *errors.Error) + CreateUserTags(id string, tagIDs models.CreateUserTagsBody) ([]models.Tag, *errors.Error) + CreateFollowing(userId string, clubId string) *errors.Error + DeleteFollowing(userId string, clubId string) *errors.Error + GetFollowing(userId string) ([]models.Club, *errors.Error) } type UserService struct { @@ -109,3 +114,67 @@ func (u *UserService) DeleteUser(id string) *errors.Error { return transactions.DeleteUser(u.DB, *idAsUUID) } + +func (u *UserService) GetUserTags(id string) ([]models.Tag, *errors.Error) { + idAsUUID, err := utilities.ValidateID(id) + if err != nil { + return nil, err + } + + return transactions.GetUserTags(u.DB, *idAsUUID) +} + +func (u *UserService) CreateUserTags(id string, tagIDs models.CreateUserTagsBody) ([]models.Tag, *errors.Error) { + // Validate the id: + idAsUUID, err := utilities.ValidateID(id) + if err != nil { + return nil, err + } + + if err := u.Validate.Struct(tagIDs); err != nil { + return nil, &errors.FailedToValidateUserTags + } + + // Retrieve a list of valid tags from the ids: + tags, err := transactions.GetTagsByIDs(u.DB, tagIDs.Tags) + + if err != nil { + return nil, err + } + + // Update the user to reflect the new tags: + return transactions.CreateUserTags(u.DB, *idAsUUID, tags) +} + +func (u *UserService) CreateFollowing(userId string, clubId string) *errors.Error { + userIdAsUUID, err := utilities.ValidateID(userId) + if err != nil { + return err + } + clubIdAsUUID, err := utilities.ValidateID(clubId) + if err != nil { + return err + } + return transactions.CreateFollowing(u.DB, *userIdAsUUID, *clubIdAsUUID) +} + +func (u *UserService) DeleteFollowing(userId string, clubId string) *errors.Error { + userIdAsUUID, err := utilities.ValidateID(userId) + if err != nil { + return err + } + clubIdAsUUID, err := utilities.ValidateID(clubId) + if err != nil { + return err + } + return transactions.DeleteFollowing(u.DB, *userIdAsUUID, *clubIdAsUUID) +} + +func (u *UserService) GetFollowing(userId string) ([]models.Club, *errors.Error) { + userIdAsUUID, err := utilities.ValidateID(userId) + if err != nil { + return nil, err + } + + return transactions.GetClubFollowing(u.DB, *userIdAsUUID) +} diff --git a/backend/src/transactions/club.go b/backend/src/transactions/club.go index a7fe4495c..b83e52bce 100644 --- a/backend/src/transactions/club.go +++ b/backend/src/transactions/club.go @@ -109,6 +109,18 @@ func DeleteClub(db *gorm.DB, id uuid.UUID) *errors.Error { return &errors.FailedToDeleteClub } } - return nil } + +func GetUserFollowersForClub(db *gorm.DB, club_id uuid.UUID) ([]models.User, *errors.Error) { + var users []models.User + club, err := GetClub(db, club_id) + if err != nil { + return nil, &errors.ClubNotFound + } + + if err := db.Model(&club).Association("Follower").Find(&users); err != nil { + return nil, &errors.FailedToGetClubFollowers + } + return users, nil +} diff --git a/backend/src/transactions/user.go b/backend/src/transactions/user.go index 14d52e2f5..3f31a5b60 100644 --- a/backend/src/transactions/user.go +++ b/backend/src/transactions/user.go @@ -55,6 +55,19 @@ func GetUser(db *gorm.DB, id uuid.UUID) (*models.User, *errors.Error) { return &user, nil } +func GetUserWithFollowers(db *gorm.DB, id uuid.UUID) (*models.User, *errors.Error) { + var user models.User + if err := db.Preload("Follower").Omit("password_hash").First(&user, id).Error; err != nil { + if stdliberrors.Is(err, gorm.ErrRecordNotFound) { + return nil, &errors.UserNotFound + } else { + return nil, &errors.FailedToGetUser + } + } + + return &user, nil +} + func UpdateUser(db *gorm.DB, id uuid.UUID, user models.User) (*models.User, *errors.Error) { var existingUser models.User @@ -85,3 +98,55 @@ func DeleteUser(db *gorm.DB, id uuid.UUID) *errors.Error { } return nil } + +// Create following for a user +func CreateFollowing(db *gorm.DB, userId uuid.UUID, clubId uuid.UUID) *errors.Error { + + user, err := GetUserWithFollowers(db, userId) + if err != nil { + return &errors.UserNotFound + } + club, err := GetClub(db, clubId) + if err != nil { + return &errors.ClubNotFound + } + + if err := db.Model(&user).Association("Follower").Replace(append(user.Follower, *club)); err != nil { + return &errors.FailedToUpdateUser + } + return nil +} + +// Delete following for a user +func DeleteFollowing(db *gorm.DB, userId uuid.UUID, clubId uuid.UUID) *errors.Error { + user, err := GetUser(db, userId) + if err != nil { + return &errors.UserNotFound + } + club, err := GetClub(db, clubId) + if err != nil { + return &errors.ClubNotFound + } + //What to return here? + //Should we return User or Success message? + if err := db.Model(&user).Association("Follower").Delete(club); err != nil { + return &errors.FailedToUpdateUser + } + return nil +} + +// Get all following for a user + +func GetClubFollowing(db *gorm.DB, userId uuid.UUID) ([]models.Club, *errors.Error) { + var clubs []models.Club + + user, err := GetUser(db, userId) + if err != nil { + return nil, &errors.UserNotFound + } + + if err := db.Model(&user).Association("Follower").Find(&clubs); err != nil { + return nil, &errors.FailedToGetUserFollowing + } + return clubs, nil +} diff --git a/backend/tests/api/user_test.go b/backend/tests/api/user_test.go index a1f74a8c8..c17106775 100644 --- a/backend/tests/api/user_test.go +++ b/backend/tests/api/user_test.go @@ -550,3 +550,101 @@ func TestCreateUserFailsOnMissingFields(t *testing.T) { } appAssert.Close() } + +// test create user following works +func TestCreateUserFollowingWorks(t *testing.T) { + appAssert, userUUID, clubUUID := CreateSampleClub(t, nil) + + TestRequest{ + Method: fiber.MethodPut, + Path: fmt.Sprintf("/api/v1/users/%s/follower/%s", userUUID, clubUUID), + }.TestOnStatusAndDB(t, &appAssert, + DBTesterWithStatus{ + Status: fiber.StatusCreated, + DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { + var dbUser models.User + err := app.Conn.Preload("Follower").First(&dbUser, userUUID).Error + assert.NilError(err) + + assert.Equal(len(dbUser.Follower), 1) + }, + }, + ) + appAssert.Close() +} + +func TestCreateUserFollowingFailsClubIdBadRequest(t *testing.T) { + appAssert, userUUID := CreateSampleUser(t, nil) + + badRequests := []string{ + "0", + "-1", + "1.1", + "foo", + "null", + } + + for _, badRequest := range badRequests { + TestRequest{ + Method: fiber.MethodPut, + Path: fmt.Sprintf("/api/v1/users/%s/follower/%s", userUUID, badRequest), + }.TestOnError(t, &appAssert, errors.FailedToValidateID).Close() + } +} + +func TestCreateUserFollowingFailsUserIdBadRequest(t *testing.T) { + appAssert, _, clubUUID := CreateSampleClub(t, nil) + + badRequests := []string{ + "0", + "-1", + "1.1", + "foo", + "null", + } + + for _, badRequest := range badRequests { + TestRequest{ + Method: fiber.MethodPut, + Path: fmt.Sprintf("/api/v1/users/%s/follower/%s", badRequest, clubUUID), + }.TestOnError(t, &appAssert, errors.FailedToValidateID).Close() + } +} + +func TestCreateUserFollowingFailsUserNotExist(t *testing.T) { + appAssert, _, clubUUID := CreateSampleClub(t, nil) + userUUIDNotExist := uuid.New() + + TestRequest{ + Method: fiber.MethodPut, + Path: fmt.Sprintf("/api/v1/users/%s/follower/%s", userUUIDNotExist, clubUUID), + }.TestOnErrorAndDB(t, &appAssert, + ErrorWithDBTester{ + Error: errors.UserNotFound, + DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { + var user models.User + err := app.Conn.Where("id = ?", userUUIDNotExist).First(&user).Error + assert.Assert(stdliberrors.Is(err, gorm.ErrRecordNotFound)) + }, + }, + ).Close() +} + +func TestCreateUserFollowingFailsClubNotExist(t *testing.T) { + appAssert, userUUID := CreateSampleUser(t, nil) + clubUUIDNotExist := uuid.New() + + TestRequest{ + Method: fiber.MethodPut, + Path: fmt.Sprintf("/api/v1/users/%s/follower/%s", userUUID, clubUUIDNotExist), + }.TestOnErrorAndDB(t, &appAssert, + ErrorWithDBTester{ + Error: errors.ClubNotFound, + DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { + var club models.Club + err := app.Conn.Where("id = ?", clubUUIDNotExist).First(&club).Error + assert.Assert(stdliberrors.Is(err, gorm.ErrRecordNotFound)) + }, + }, + ).Close() +}