Grow journal: time-stamped notes on gardens, beds and plantings (#52)
"Take notes through the season on each bed and plant." There is already free-text notes on gardens, objects and plants, but it is a single mutable field: writing "powdery mildew on the west bed" overwrites what you wrote in June. The distinction worth keeping is that notes says what this thing IS, while a journal entry says what HAPPENED, and when. Both stay. garden_id is NOT NULL even when an entry is about a bed or a single plop, and that is the whole trick: permission checks reuse requireGardenRole unchanged and no second ACL path is invented. object_id/planting_id narrow the target; the garden always anchors it. Because the garden anchors permission, the target is checked to actually live in that garden — otherwise a valid object id from someone else's garden would be storable here and then leak through the list read. A plop-level entry that also names an object must name the right one, or the two filters would disagree about what an entry is about. observed_at is distinct from created_at: you write up Saturday's observations on Sunday, and Saturday is the date that matters. Listing orders by observation, newest first, with id breaking ties inside a day. Roles follow the existing shape. Editors write, viewers read, strangers get ErrNotFound. An author may edit their own entries; the garden owner may DELETE any entry in their garden but may not edit one — rewriting somebody else's observation under their name is a different act from removing it. Deliberately not wired into the revision history, per the decision on #52: entries are already append-shaped and individually versioned, and undoing a note is just deleting it. There's a test asserting journal writes produce no change sets, so that decision can't quietly reverse itself later. Closes #52 Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
This commit is contained in:
@@ -184,12 +184,12 @@ func TestCreatePlantValidation(t *testing.T) {
|
||||
owner := seedUser(t, s, "[email protected]")
|
||||
|
||||
bad := []PlantInput{
|
||||
{Name: "", Category: domain.CategoryHerb, SpacingCM: 10, Color: "#4a7c3f", Icon: "🌿"}, // empty name
|
||||
{Name: "x", Category: "spaceship", SpacingCM: 10, Color: "#4a7c3f", Icon: "🌿"}, // bad category
|
||||
{Name: "x", Category: domain.CategoryHerb, SpacingCM: 0, Color: "#4a7c3f", Icon: "🌿"}, // zero spacing
|
||||
{Name: "x", Category: domain.CategoryHerb, SpacingCM: -5, Color: "#4a7c3f", Icon: "🌿"}, // negative spacing
|
||||
{Name: "x", Category: domain.CategoryHerb, SpacingCM: 10, Color: "nope", Icon: "🌿"}, // bad color
|
||||
{Name: "x", Category: domain.CategoryHerb, SpacingCM: 10, Color: "#4a7c3f", Icon: ""}, // empty icon
|
||||
{Name: "", Category: domain.CategoryHerb, SpacingCM: 10, Color: "#4a7c3f", Icon: "🌿"}, // empty name
|
||||
{Name: "x", Category: "spaceship", SpacingCM: 10, Color: "#4a7c3f", Icon: "🌿"}, // bad category
|
||||
{Name: "x", Category: domain.CategoryHerb, SpacingCM: 0, Color: "#4a7c3f", Icon: "🌿"}, // zero spacing
|
||||
{Name: "x", Category: domain.CategoryHerb, SpacingCM: -5, Color: "#4a7c3f", Icon: "🌿"}, // negative spacing
|
||||
{Name: "x", Category: domain.CategoryHerb, SpacingCM: 10, Color: "nope", Icon: "🌿"}, // bad color
|
||||
{Name: "x", Category: domain.CategoryHerb, SpacingCM: 10, Color: "#4a7c3f", Icon: ""}, // empty icon
|
||||
{Name: strings.Repeat("x", maxPlantNameLen+1), Category: domain.CategoryHerb, SpacingCM: 10, Color: "#4a7c3f", Icon: "🌿"}, // name too long
|
||||
{Name: "x", Category: domain.CategoryHerb, SpacingCM: 10, Color: "#4a7c3f", Icon: "🌿", DaysToMaturity: ptrInt(0)}, // days must be >= 1
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user