Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions apis/hole/apis.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions apis/hole/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ func RegisterRoutes(app fiber.Router) {
app.Get("/holes/:id<int>", 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<int>/_webvpn", ModifyHole)
Expand Down
50 changes: 50 additions & 0 deletions tests/hole_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
}
Loading