diff --git a/apis/hole/apis.go b/apis/hole/apis.go index f044f7a..623578c 100644 --- a/apis/hole/apis.go +++ b/apis/hole/apis.go @@ -161,6 +161,48 @@ func ListGoodHoles(c *fiber.Ctx) error { return Serialize(c, &holes) } +// ListSfwHoles +// +// @Summary List safe for work holes +// @Tags Hole +// @Produce json +// @Router /holes/_sfw [get] +// @Param object query QueryTime false "query" +// @Success 200 {array} Hole +func ListSfwHoles(c *fiber.Ctx) error { + var query QueryTime + err := common.ValidateQuery(c, &query) + if err != nil { + return err + } + _, err = common.GetUserID(c) + if err != nil { + return err + } + + // get holes + var holes Holes + querySet, err := holes.MakeQuerySet(query.Offset, query.Size, query.Order, c) + if err != nil { + return err + } + + // Exclude holes that have any NSFW tags + // Use LEFT JOIN to find holes without NSFW tags for better performance + nsfwTagIDs := DB.Table("tag").Select("id").Where("nsfw = ?", true) + querySet = querySet. + Joins("LEFT JOIN hole_tags ON hole.id = hole_tags.hole_id AND hole_tags.tag_id IN (?)", nsfwTagIDs). + Where("hole_tags.hole_id IS NULL"). + Group("hole.id") + + err = querySet.Find(&holes).Error + if err != nil { + return err + } + + return Serialize(c, &holes) +} + // ListHoles // // @Summary API for Listing Holes diff --git a/apis/hole/routes.go b/apis/hole/routes.go index cafc83d..a06ec45 100644 --- a/apis/hole/routes.go +++ b/apis/hole/routes.go @@ -13,6 +13,7 @@ func RegisterRoutes(app fiber.Router) { app.Get("/holes/:id", GetHole) app.Get("/holes", ListHoles) app.Get("/holes/_good", ListGoodHoles) + app.Get("/holes/_sfw", ListSfwHoles) app.Post("/divisions/:id/holes", utils.MiddlewareHasAnsweredQuestions, CreateHole) app.Post("/holes", utils.MiddlewareHasAnsweredQuestions, CreateHoleOld) app.Patch("/holes/:id/_webvpn", ModifyHole) diff --git a/tests/hole_test.go b/tests/hole_test.go index 5713e11..7fe4757 100644 --- a/tests/hole_test.go +++ b/tests/hole_test.go @@ -140,3 +140,53 @@ func TestHoleStats(t *testing.T) { } } + +func TestListSfwHoles(t *testing.T) { + // Create test holes with and without NSFW tags + // Note: Tag names starting with '*' automatically get Nsfw=true via BeforeCreate hook + nsfwTag := Tag{Name: "*nsfw_test"} + safeTag := Tag{Name: "safe_test"} + + holeWithNsfw := Hole{ + DivisionID: 1, + Tags: Tags{&nsfwTag}, + Floors: Floors{{Content: "test nsfw hole"}}, + } + + holeWithoutNsfw := Hole{ + DivisionID: 1, + Tags: Tags{&safeTag}, + Floors: Floors{{Content: "test safe hole"}}, + } + + err := DB.Create(&holeWithNsfw).Error + assert.Nil(t, err) + + err = DB.Create(&holeWithoutNsfw).Error + assert.Nil(t, err) + + // Cleanup: delete in order that respects foreign keys + // With CASCADE constraints, deleting holes will clean up hole_tags associations + defer func() { + DB.Unscoped().Delete(&holeWithNsfw) + DB.Unscoped().Delete(&holeWithoutNsfw) + DB.Unscoped().Delete(&nsfwTag) + DB.Unscoped().Delete(&safeTag) + }() + + // Get SFW holes + var sfwHoles Holes + testAPIModel(t, "get", "/api/holes/_sfw", 200, &sfwHoles) + + // Verify that holes with NSFW tags are excluded + for _, hole := range sfwHoles { + var tags Tags + err := DB.Model(hole).Association("Tags").Find(&tags) + assert.Nil(t, err) + + // Check that none of the tags are NSFW + for _, tag := range tags { + assert.False(t, tag.Nsfw, "SFW holes should not have NSFW tags") + } + } +}