diff --git a/backend/src/controllers/club_tag.go b/backend/src/controllers/club_tag.go new file mode 100644 index 000000000..e958f1a7b --- /dev/null +++ b/backend/src/controllers/club_tag.go @@ -0,0 +1,87 @@ +package controllers + +import ( + "github.com/GenerateNU/sac/backend/src/errors" + "github.com/GenerateNU/sac/backend/src/models" + "github.com/GenerateNU/sac/backend/src/services" + "github.com/gofiber/fiber/v2" +) + +type ClubTagController struct { + clubTagService services.ClubTagServiceInterface +} + +func NewClubTagController(clubTagService services.ClubTagServiceInterface) *ClubTagController { + return &ClubTagController{clubTagService: clubTagService} +} + +// CreateClubTags godoc +// +// @Summary Create Club Tags +// @Description Adds Tags to a Club +// @ID create-club-tags +// @Tags club +// @Accept json +// @Produce json +// @Success 201 {object} []models.Tag +// @Failure 404 {string} string "club not found" +// @Failure 400 {string} string "invalid request body" +// @Failure 400 {string} string "failed to validate id" +// @Failure 500 {string} string "database error" +// @Router /api/v1/clubs/:id/tags [post] +func (l *ClubTagController) CreateClubTags(c *fiber.Ctx) error { + var clubTagsBody models.CreateClubTagsRequestBody + if err := c.BodyParser(&clubTagsBody); err != nil { + return errors.FailedToParseRequestBody.FiberError(c) + } + + clubTags, err := l.clubTagService.CreateClubTags(c.Params("clubID"), clubTagsBody) + if err != nil { + return err.FiberError(c) + } + + return c.Status(fiber.StatusCreated).JSON(clubTags) +} + +// GetClubTags godoc +// +// @Summary Get Club Tags +// @Description Retrieves the tags for a club +// @ID get-club-tags +// @Tags club +// @Produce json +// @Success 200 {object} []models.Tag +// @Failure 404 {string} string "club not found" +// @Failure 400 {string} string "invalid request body" +// @Failure 400 {string} string "failed to validate id" +// @Failure 500 {string} string "database error" +// @Router /api/v1/clubs/:id/tags [get] +func (l *ClubTagController) GetClubTags(c *fiber.Ctx) error { + clubTags, err := l.clubTagService.GetClubTags(c.Params("clubID")) + if err != nil { + return err.FiberError(c) + } + + return c.Status(fiber.StatusOK).JSON(clubTags) +} + +// DeleteClubTags godoc +// +// @Summary Delete Club Tags +// @Description Deletes the tags for a club +// @ID delete-club-tags +// @Tags club +// @Success 204 +// @Failure 404 {string} string "club not found" +// @Failure 400 {string} string "invalid request body" +// @Failure 400 {string} string "failed to validate id" +// @Failure 500 {string} string "database error" +// @Router /api/v1/clubs/:id/tags/:tagId [delete] +func (l *ClubTagController) DeleteClubTag(c *fiber.Ctx) error { + err := l.clubTagService.DeleteClubTag(c.Params("clubID"), c.Params("tagID")) + if err != nil { + return err.FiberError(c) + } + + return c.SendStatus(fiber.StatusNoContent) +} diff --git a/backend/src/errors/club.go b/backend/src/errors/club.go index f8cf38edc..ccb3a1de8 100644 --- a/backend/src/errors/club.go +++ b/backend/src/errors/club.go @@ -35,6 +35,10 @@ var ( StatusCode: fiber.StatusNotFound, Message: "club not found", } + FailedToValidateClubTags = Error{ + StatusCode: fiber.StatusBadRequest, + Message: "failed to validate club tags", + } FailedToGetMembers = Error{ StatusCode: fiber.StatusNotFound, Message: "failed to get members", diff --git a/backend/src/models/club.go b/backend/src/models/club.go index 3c56614d6..4092a270e 100644 --- a/backend/src/models/club.go +++ b/backend/src/models/club.go @@ -72,7 +72,11 @@ type UpdateClubRequestBody struct { RecruitmentCycle RecruitmentCycle `gorm:"type:varchar(255);default:always" json:"recruitment_cycle" validate:"required,max=255,oneof=fall spring fallSpring always"` RecruitmentType RecruitmentType `gorm:"type:varchar(255);default:unrestricted" json:"recruitment_type" validate:"required,max=255,oneof=unrestricted tryout application"` ApplicationLink string `json:"application_link" validate:"omitempty,required,max=255,http_url"` - Logo string `json:"logo" validate:"omitempty,http_url,s3_url,max=255"` // S3 URL + Logo string `json:"logo" validate:"omitempty,s3_url,max=255,http_url"` // S3 URL +} + +type CreateClubTagsRequestBody struct { + Tags []uuid.UUID `json:"tags" validate:"required"` } func (c *Club) AfterCreate(tx *gorm.DB) (err error) { diff --git a/backend/src/server/routes/club_tag.go b/backend/src/server/routes/club_tag.go new file mode 100644 index 000000000..4d570b6b9 --- /dev/null +++ b/backend/src/server/routes/club_tag.go @@ -0,0 +1,17 @@ +package routes + +import ( + "github.com/GenerateNU/sac/backend/src/controllers" + "github.com/GenerateNU/sac/backend/src/services" + "github.com/gofiber/fiber/v2" +) + +func ClubTag(router fiber.Router, clubTagService services.ClubTagServiceInterface) { + clubTagController := controllers.NewClubTagController(clubTagService) + + clubTags := router.Group("/:clubID/tags") + + clubTags.Post("/", clubTagController.CreateClubTags) + clubTags.Get("/", clubTagController.GetClubTags) + clubTags.Delete("/:tagID", clubTagController.DeleteClubTag) +} diff --git a/backend/src/server/server.go b/backend/src/server/server.go index f5a74d060..ba7ea880f 100644 --- a/backend/src/server/server.go +++ b/backend/src/server/server.go @@ -48,6 +48,7 @@ func Init(db *gorm.DB, settings config.Settings) *fiber.App { routes.Contact(apiv1, services.NewContactService(db, validate)) clubsIDRouter := routes.Club(apiv1, services.NewClubService(db, validate), middlewareService) + routes.ClubTag(clubsIDRouter, services.NewClubTagService(db, validate)) routes.ClubFollower(clubsIDRouter, services.NewClubFollowerService(db)) routes.ClubMember(clubsIDRouter, services.NewClubMemberService(db, validate)) routes.ClubContact(clubsIDRouter, services.NewClubContactService(db, validate)) diff --git a/backend/src/services/club_tag.go b/backend/src/services/club_tag.go new file mode 100644 index 000000000..c1ff23d87 --- /dev/null +++ b/backend/src/services/club_tag.go @@ -0,0 +1,66 @@ +package services + +import ( + "github.com/GenerateNU/sac/backend/src/errors" + "github.com/GenerateNU/sac/backend/src/models" + "github.com/GenerateNU/sac/backend/src/transactions" + "github.com/GenerateNU/sac/backend/src/utilities" + "github.com/go-playground/validator/v10" + "gorm.io/gorm" +) + +type ClubTagServiceInterface interface { + CreateClubTags(id string, clubTagsBody models.CreateClubTagsRequestBody) ([]models.Tag, *errors.Error) + GetClubTags(id string) ([]models.Tag, *errors.Error) + DeleteClubTag(id string, tagId string) *errors.Error +} + +type ClubTagService struct { + DB *gorm.DB + Validate *validator.Validate +} + +func NewClubTagService(db *gorm.DB, validate *validator.Validate) ClubTagServiceInterface { + return &ClubTagService{DB: db, Validate: validate} +} + +func (c *ClubTagService) CreateClubTags(id string, clubTagsBody models.CreateClubTagsRequestBody) ([]models.Tag, *errors.Error) { + idAsUUID, err := utilities.ValidateID(id) + if err != nil { + return nil, err + } + + if err := c.Validate.Struct(clubTagsBody); err != nil { + return nil, &errors.FailedToValidateClubTags + } + + tags, err := transactions.GetTagsByIDs(c.DB, clubTagsBody.Tags) + if err != nil { + return nil, err + } + + return transactions.CreateClubTags(c.DB, *idAsUUID, tags) +} + +func (c *ClubTagService) GetClubTags(id string) ([]models.Tag, *errors.Error) { + idAsUUID, err := utilities.ValidateID(id) + if err != nil { + return nil, &errors.FailedToValidateID + } + + return transactions.GetClubTags(c.DB, *idAsUUID) +} + +func (c *ClubTagService) DeleteClubTag(id string, tagId string) *errors.Error { + idAsUUID, err := utilities.ValidateID(id) + if err != nil { + return &errors.FailedToValidateID + } + + tagIdAsUUID, err := utilities.ValidateID(tagId) + if err != nil { + return &errors.FailedToValidateID + } + + return transactions.DeleteClubTag(c.DB, *idAsUUID, *tagIdAsUUID) +} diff --git a/backend/src/services/tag.go b/backend/src/services/tag.go index a7f7523d5..d703bb23d 100644 --- a/backend/src/services/tag.go +++ b/backend/src/services/tag.go @@ -11,9 +11,9 @@ import ( type TagServiceInterface interface { CreateTag(tagBody models.TagRequestBody) (*models.Tag, *errors.Error) - GetTag(tagID string) (*models.Tag, *errors.Error) - UpdateTag(tagID string, tagBody models.TagRequestBody) (*models.Tag, *errors.Error) - DeleteTag(tagID string) *errors.Error + GetTag(id string) (*models.Tag, *errors.Error) + UpdateTag(id string, tagBody models.TagRequestBody) (*models.Tag, *errors.Error) + DeleteTag(id string) *errors.Error } type TagService struct { diff --git a/backend/src/transactions/club_tag.go b/backend/src/transactions/club_tag.go new file mode 100644 index 000000000..03127e89b --- /dev/null +++ b/backend/src/transactions/club_tag.go @@ -0,0 +1,56 @@ +package transactions + +import ( + "github.com/GenerateNU/sac/backend/src/errors" + "github.com/GenerateNU/sac/backend/src/models" + "github.com/google/uuid" + + "gorm.io/gorm" +) + +// Create tags for a club +func CreateClubTags(db *gorm.DB, id uuid.UUID, tags []models.Tag) ([]models.Tag, *errors.Error) { + user, err := GetClub(db, id) + if err != nil { + return nil, &errors.UserNotFound + } + + if err := db.Model(&user).Association("Tag").Replace(tags); err != nil { + return nil, &errors.FailedToUpdateUser + } + + return tags, nil +} + +// Get tags for a club +func GetClubTags(db *gorm.DB, id uuid.UUID) ([]models.Tag, *errors.Error) { + var tags []models.Tag + + club, err := GetClub(db, id) + if err != nil { + return nil, &errors.ClubNotFound + } + + if err := db.Model(&club).Association("Tag").Find(&tags); err != nil { + return nil, &errors.FailedToGetTag + } + return tags, nil +} + +// Delete tag for a club +func DeleteClubTag(db *gorm.DB, id uuid.UUID, tagId uuid.UUID) *errors.Error { + club, err := GetClub(db, id) + if err != nil { + return &errors.ClubNotFound + } + + tag, err := GetTag(db, tagId) + if err != nil { + return &errors.TagNotFound + } + + if err := db.Model(&club).Association("Tag").Delete(&tag); err != nil { + return &errors.FailedToUpdateClub + } + return nil +} diff --git a/backend/src/transactions/tag.go b/backend/src/transactions/tag.go index 503468f0c..dd939e4a2 100644 --- a/backend/src/transactions/tag.go +++ b/backend/src/transactions/tag.go @@ -83,3 +83,18 @@ func GetTagsByIDs(db *gorm.DB, selectedTagIDs []uuid.UUID) ([]models.Tag, *error } return []models.Tag{}, nil } + +// Get clubs for a tag +func GetTagClubs(db *gorm.DB, id uuid.UUID) ([]models.Club, *errors.Error) { + var clubs []models.Club + + tag, err := GetTag(db, id) + if err != nil { + return nil, &errors.ClubNotFound + } + + if err := db.Model(&tag).Association("Club").Find(&clubs); err != nil { + return nil, &errors.FailedToGetTag + } + return clubs, nil +} diff --git a/backend/tests/api/club_tag_test.go b/backend/tests/api/club_tag_test.go new file mode 100644 index 000000000..47764f445 --- /dev/null +++ b/backend/tests/api/club_tag_test.go @@ -0,0 +1,201 @@ +package tests + +// func AssertClubTagsRespDB(app TestApp, assert *assert.A, resp *http.Response, id uuid.UUID) { +// var respTags []models.Tag + +// // Retrieve the tags from the response: +// err := json.NewDecoder(resp.Body).Decode(&respTags) + +// assert.NilError(err) + +// // Retrieve the club connected to the tags: +// var dbClub models.Club +// err = app.Conn.First(&dbClub, id).Error + +// assert.NilError(err) + +// // Retrieve the tags in the bridge table associated with the club: +// var dbTags []models.Tag +// err = app.Conn.Model(&dbClub).Association("Tag").Find(&dbTags) + +// assert.NilError(err) + +// // Confirm all the resp tags are equal to the db tags: +// for i, respTag := range respTags { +// assert.Equal(respTag.ID, dbTags[i].ID) +// assert.Equal(respTag.Name, dbTags[i].Name) +// assert.Equal(respTag.CategoryID, dbTags[i].CategoryID) +// } +// } + +// func TestCreateClubTagsWorks(t *testing.T) { +// appAssert, _, uuid := CreateSampleClub(t, nil) + +// // Create a set of tags: +// tagUUIDs := CreateSetOfTags(t, appAssert) + +// // Confirm adding real tags adds them to the club: +// TestRequest{ +// Method: fiber.MethodPost, +// Path: fmt.Sprintf("/api/v1/clubs/%s/tags/", uuid), +// Body: SampleTagIDsFactory(&tagUUIDs), +// }.TestOnStatusAndDB(t, &appAssert, +// DBTesterWithStatus{ +// Status: fiber.StatusCreated, +// DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { +// AssertClubTagsRespDB(app, assert, resp, uuid) +// }, +// }, +// ) + +// appAssert.Close() +// } + +// // func TestCreateClubTagsFailsOnInvalidDataType(t *testing.T) { +// // _, _, uuid := CreateSampleClub(t, nil) + +// // // Invalid tag data types: +// // invalidTags := []interface{}{ +// // []string{"1", "2", "34"}, +// // []models.Tag{{Name: "Test", CategoryID: uuid.UUID{}}, {Name: "Test2", CategoryID: uuid.UUID{}}}, +// // []float32{1.32, 23.5, 35.1}, +// // } + +// // // Test each of the invalid tags: +// // for _, tag := range invalidTags { +// // malformedTag := *SampleTagIDsFactory(nil) +// // malformedTag["tags"] = tag + +// // TestRequest{ +// // Method: fiber.MethodPost, +// // Path: fmt.Sprintf("/api/v1/clubs/%s/tags/", uuid), +// // Body: &malformedTag, +// // }.TestOnError(t, nil, errors.FailedToParseRequestBody).Close() +// // } +// // } + +// func TestCreateClubTagsFailsOnInvalidClubID(t *testing.T) { +// badRequests := []string{ +// "0", +// "-1", +// "1.1", +// "foo", +// "null", +// } + +// for _, badRequest := range badRequests { +// TestRequest{ +// Method: fiber.MethodPost, +// Path: fmt.Sprintf("/api/v1/clubs/%s/tags", badRequest), +// Body: SampleTagIDsFactory(nil), +// }.TestOnError(t, nil, errors.FailedToValidateID).Close() +// } +// } + +// func TestCreateClubTagsFailsOnInvalidKey(t *testing.T) { +// appAssert, _, uuid := CreateSampleClub(t, nil) + +// invalidBody := []map[string]interface{}{ +// { +// "tag": UUIDSlice{testUUID, testUUID}, +// }, +// { +// "tagIDs": []uint{1, 2, 3}, +// }, +// } + +// for _, body := range invalidBody { +// TestRequest{ +// Method: fiber.MethodPost, +// Path: fmt.Sprintf("/api/v1/clubs/%s/tags/", uuid), +// Body: &body, +// }.TestOnError(t, &appAssert, errors.FailedToValidateClubTags) +// } + +// appAssert.Close() +// } + +// func TestCreateClubTagsNoneAddedIfInvalid(t *testing.T) { +// appAssert, _, uuid := CreateSampleClub(t, nil) + +// TestRequest{ +// Method: fiber.MethodPost, +// Path: fmt.Sprintf("/api/v1/clubs/%s/tags/", uuid), +// Body: SampleTagIDsFactory(nil), +// }.TestOnStatusAndDB(t, &appAssert, +// DBTesterWithStatus{ +// Status: fiber.StatusCreated, +// DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { +// var respTags []models.Tag + +// err := json.NewDecoder(resp.Body).Decode(&respTags) + +// assert.NilError(err) + +// assert.Equal(len(respTags), 0) +// }, +// }, +// ) + +// appAssert.Close() +// } + +// func TestGetClubTagsFailsOnNonExistentClub(t *testing.T) { +// TestRequest{ +// Method: fiber.MethodGet, +// Path: fmt.Sprintf("/api/v1/clubs/%s/tags/", uuid.New()), +// }.TestOnError(t, nil, errors.ClubNotFound).Close() +// } + +// func TestGetClubTagsReturnsEmptyListWhenNoneAdded(t *testing.T) { +// appAssert, _, uuid := CreateSampleClub(t, nil) + +// TestRequest{ +// Method: fiber.MethodGet, +// Path: fmt.Sprintf("/api/v1/clubs/%s/tags/", uuid), +// }.TestOnStatusAndDB(t, &appAssert, +// DBTesterWithStatus{ +// Status: 200, +// DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { +// var respTags []models.Tag + +// err := json.NewDecoder(resp.Body).Decode(&respTags) + +// assert.NilError(err) + +// assert.Equal(len(respTags), 0) +// }, +// }, +// ) + +// appAssert.Close() +// } + +// func TestGetClubTagsReturnsCorrectList(t *testing.T) { +// appAssert, _, uuid := CreateSampleClub(t, nil) + +// // Create a set of tags: +// tagUUIDs := CreateSetOfTags(t, appAssert) + +// // Add the tags: +// TestRequest{ +// Method: fiber.MethodPost, +// Path: fmt.Sprintf("/api/v1/clubs/%s/tags/", uuid), +// Body: SampleTagIDsFactory(&tagUUIDs), +// }.TestOnStatus(t, &appAssert, fiber.StatusCreated) + +// // Get the tags: +// TestRequest{ +// Method: fiber.MethodGet, +// Path: fmt.Sprintf("/api/v1/clubs/%s/tags/", uuid), +// }.TestOnStatusAndDB(t, &appAssert, +// DBTesterWithStatus{ +// Status: fiber.StatusOK, +// DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { +// AssertClubTagsRespDB(app, assert, resp, uuid) +// }, +// }, +// ) + +// appAssert.Close() +// } diff --git a/go.work.sum b/go.work.sum index 1f5aab939..38b6bc0be 100644 --- a/go.work.sum +++ b/go.work.sum @@ -23,6 +23,7 @@ github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=