From cb28a6f8bb63946363471322c1cc7a5b9d38567b Mon Sep 17 00:00:00 2001 From: Jon Seager Date: Tue, 10 Feb 2026 18:54:11 +0000 Subject: [PATCH] fix: wire edit forms for Datastar form submission and add CLI tests Add {contentType: 'form'} to all edit template @put() calls so form inputs are submitted by name rather than as Datastar signals. Replace the quick notes text input on the brew edit page with toggle pill buttons matching the add form. Add CLI tests for brew and cup update commands. --- templates/pages/edit_bag.html | 2 +- templates/pages/edit_brew.html | 69 ++++++++++++++++++++++++------- templates/pages/edit_cafe.html | 2 +- templates/pages/edit_cup.html | 2 +- templates/pages/edit_gear.html | 2 +- templates/pages/edit_roast.html | 2 +- templates/pages/edit_roaster.html | 2 +- tests/cli/brews_cli.rs | 45 +++++++++++++++++++- tests/cli/cups_cli.rs | 36 ++++++++++++++++ tests/cli/helpers.rs | 25 +++++++++++ tests/cli/main.rs | 1 + 11 files changed, 167 insertions(+), 21 deletions(-) create mode 100644 tests/cli/cups_cli.rs diff --git a/templates/pages/edit_bag.html b/templates/pages/edit_bag.html index ddb43e9..d21a39e 100644 --- a/templates/pages/edit_bag.html +++ b/templates/pages/edit_bag.html @@ -15,7 +15,7 @@ data-signals:_submit-error="''" data-signals:_roast-date="'{{ roast_date }}'" data-signals:_amount="{{ amount }}" - data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/bags/{{ id }}')" + data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/bags/{{ id }}', {contentType: 'form'})" data-on:datastar-fetch="if (!$_submitting) return; if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Bag updated'); window.location.href = evt.detail.response.headers.get('location') || '/bags/{{ id }}' } else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }" diff --git a/templates/pages/edit_brew.html b/templates/pages/edit_brew.html index 3186979..d2dbed8 100644 --- a/templates/pages/edit_brew.html +++ b/templates/pages/edit_brew.html @@ -19,8 +19,13 @@ data-signals:_water-volume="{{ water_volume }}" data-signals:_water-temp="{{ water_temp }}" data-signals:_brew-time="{{ brew_time }}" - data-signals:_quick-notes="'{{ quick_notes }}'" - data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/brews/{{ id }}')" + data-signals:_qn-good="{% if quick_notes.contains("good") %}true{% else %}false{% endif %}" + data-signals:_qn-too-fast="{% if quick_notes.contains("too-fast") %}true{% else %}false{% endif %}" + data-signals:_qn-too-slow="{% if quick_notes.contains("too-slow") %}true{% else %}false{% endif %}" + data-signals:_qn-too-hot="{% if quick_notes.contains("too-hot") %}true{% else %}false{% endif %}" + data-signals:_qn-under-extracted="{% if quick_notes.contains("under-extracted") %}true{% else %}false{% endif %}" + data-signals:_qn-over-extracted="{% if quick_notes.contains("over-extracted") %}true{% else %}false{% endif %}" + data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/brews/{{ id }}', {contentType: 'form'})" data-on:datastar-fetch="if (!$_submitting) return; if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Brew updated'); window.location.href = evt.detail.response.headers.get('location') || '/brews/{{ id }}' } else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }" @@ -301,19 +306,55 @@

Quick Notes

-
+ {{ img::deferred_upload("edit-brew-image", "Brew Image") }} diff --git a/templates/pages/edit_cafe.html b/templates/pages/edit_cafe.html index 6112002..6e43c7a 100644 --- a/templates/pages/edit_cafe.html +++ b/templates/pages/edit_cafe.html @@ -20,7 +20,7 @@ data-signals:_latitude="{{ latitude }}" data-signals:_longitude="{{ longitude }}" data-signals:_website="'{{ website }}'" - data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/cafes/{{ id }}')" + data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/cafes/{{ id }}', {contentType: 'form'})" data-on:datastar-fetch="if (!$_submitting) return; if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Cafe updated'); window.location.href = evt.detail.response.headers.get('location') || '/cafes/{{ id }}' } else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }" diff --git a/templates/pages/edit_cup.html b/templates/pages/edit_cup.html index aacf872..e92c829 100644 --- a/templates/pages/edit_cup.html +++ b/templates/pages/edit_cup.html @@ -14,7 +14,7 @@ class="flex flex-col gap-4" data-signals:_submitting="false" data-signals:_submit-error="''" - data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/cups/{{ id }}')" + data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/cups/{{ id }}', {contentType: 'form'})" data-on:datastar-fetch="if (!$_submitting) return; if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Cup updated'); window.location.href = evt.detail.response.headers.get('location') || '/cups/{{ id }}' } else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }" diff --git a/templates/pages/edit_gear.html b/templates/pages/edit_gear.html index 2ff71ee..6db4047 100644 --- a/templates/pages/edit_gear.html +++ b/templates/pages/edit_gear.html @@ -16,7 +16,7 @@ data-signals:_submit-error="''" data-signals:_make="'{{ make }}'" data-signals:_model="'{{ model }}'" - data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/gear/{{ id }}')" + data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/gear/{{ id }}', {contentType: 'form'})" data-on:datastar-fetch="if (!$_submitting) return; if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Gear updated'); window.location.href = evt.detail.response.headers.get('location') || '/gear/{{ id }}' } else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }" diff --git a/templates/pages/edit_roast.html b/templates/pages/edit_roast.html index 1eabd74..2234f34 100644 --- a/templates/pages/edit_roast.html +++ b/templates/pages/edit_roast.html @@ -20,7 +20,7 @@ data-signals:_producer="'{{ producer }}'" data-signals:_process="'{{ process }}'" data-signals:_tasting-notes="'{{ tasting_notes }}'" - data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/roasts/{{ id }}')" + data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/roasts/{{ id }}', {contentType: 'form'})" data-on:datastar-fetch="if (!$_submitting) return; if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Roast updated'); window.location.href = evt.detail.response.headers.get('location') || '/roasts/{{ id }}' } else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }" diff --git a/templates/pages/edit_roaster.html b/templates/pages/edit_roaster.html index a99d7ba..6ca7b3f 100644 --- a/templates/pages/edit_roaster.html +++ b/templates/pages/edit_roaster.html @@ -18,7 +18,7 @@ data-signals:_country="'{{ country }}'" data-signals:_city="'{{ city }}'" data-signals:_homepage="'{{ homepage }}'" - data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/roasters/{{ id }}')" + data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/roasters/{{ id }}', {contentType: 'form'})" data-on:datastar-fetch="if (!$_submitting) return; if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Roaster updated'); window.location.href = evt.detail.response.headers.get('location') || '/roasters/{{ id }}' } else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }" diff --git a/tests/cli/brews_cli.rs b/tests/cli/brews_cli.rs index fabb10a..fe66e44 100644 --- a/tests/cli/brews_cli.rs +++ b/tests/cli/brews_cli.rs @@ -1,6 +1,7 @@ use crate::helpers::{ - create_bag, create_gear, create_roast, create_roaster, create_token, run_brewlog, + create_bag, create_brew, create_gear, create_roast, create_roaster, create_token, run_brewlog, }; +use crate::test_macros::define_cli_auth_test; #[test] fn brew_add_creates_brew_via_api() { @@ -235,3 +236,45 @@ fn brew_delete_removes_brew() { "brew get should fail after deletion" ); } + +define_cli_auth_test!( + test_update_brew_requires_authentication, + &["brew", "update", "--id", "123", "--coffee-weight", "16.0"] +); + +#[test] +fn test_update_brew_with_authentication() { + let token = create_token("test-update-brew"); + let roaster_id = create_roaster("Update Brew Roaster", &token); + let roast_id = create_roast(&roaster_id, "Update Brew Roast", &token); + let bag_id = create_bag(&roast_id, &token); + let grinder_id = create_gear("grinder", "Weber", "Key", &token); + let brewer_id = create_gear("brewer", "Origami", "Air", &token); + let brew_id = create_brew(&bag_id, &grinder_id, &brewer_id, &token); + + let output = run_brewlog( + &[ + "brew", + "update", + "--id", + &brew_id, + "--coffee-weight", + "16.0", + "--water-temp", + "94.0", + ], + &[("BREWLOG_TOKEN", &token)], + ); + + assert!( + output.status.success(), + "brew update should succeed: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let stdout = String::from_utf8_lossy(&output.stdout); + let brew: serde_json::Value = serde_json::from_str(&stdout).expect("Should output valid JSON"); + + assert_eq!(brew["coffee_weight"], 16.0); + assert_eq!(brew["water_temp"], 94.0); +} diff --git a/tests/cli/cups_cli.rs b/tests/cli/cups_cli.rs new file mode 100644 index 0000000..96af9fb --- /dev/null +++ b/tests/cli/cups_cli.rs @@ -0,0 +1,36 @@ +use crate::helpers::{ + create_cafe, create_cup, create_roast, create_roaster, create_token, run_brewlog, +}; +use crate::test_macros::define_cli_auth_test; + +define_cli_auth_test!( + test_update_cup_requires_authentication, + &["cup", "update", "--id", "123", "--roast-id", "1"] +); + +#[test] +fn test_update_cup_with_authentication() { + let token = create_token("test-update-cup"); + let roaster_id = create_roaster("Update Cup Roaster", &token); + let roast_id = create_roast(&roaster_id, "Update Cup Roast", &token); + let cafe_id = create_cafe("Update Cup Cafe", "London", "UK", "51.5", "-0.1", &token); + let cup_id = create_cup(&roast_id, &cafe_id, &token); + + // Create a second roast to update the cup to + let roast_id_2 = create_roast(&roaster_id, "Update Cup Roast 2", &token); + + let output = run_brewlog( + &["cup", "update", "--id", &cup_id, "--roast-id", &roast_id_2], + &[("BREWLOG_TOKEN", &token)], + ); + + assert!( + output.status.success(), + "cup update should succeed: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let stdout = String::from_utf8_lossy(&output.stdout); + let cup: serde_json::Value = serde_json::from_str(&stdout).expect("Should output valid JSON"); + assert!(cup["id"].is_i64(), "cup should have an id"); +} diff --git a/tests/cli/helpers.rs b/tests/cli/helpers.rs index 0e935b4..6ebaf2c 100644 --- a/tests/cli/helpers.rs +++ b/tests/cli/helpers.rs @@ -316,3 +316,28 @@ pub fn create_gear(category: &str, make: &str, model: &str, token: &str) -> Stri "gear", ) } + +pub fn create_brew(bag_id: &str, grinder_id: &str, brewer_id: &str, token: &str) -> String { + create_entity_cli( + &[ + "brew", + "add", + "--bag-id", + bag_id, + "--grinder-id", + grinder_id, + "--brewer-id", + brewer_id, + ], + token, + "brew", + ) +} + +pub fn create_cup(roast_id: &str, cafe_id: &str, token: &str) -> String { + create_entity_cli( + &["cup", "add", "--roast-id", roast_id, "--cafe-id", cafe_id], + token, + "cup", + ) +} diff --git a/tests/cli/main.rs b/tests/cli/main.rs index 23d2ca6..e19073f 100644 --- a/tests/cli/main.rs +++ b/tests/cli/main.rs @@ -2,6 +2,7 @@ pub mod backup_cli; pub mod bags_cli; pub mod brews_cli; pub mod cafes_cli; +pub mod cups_cli; pub mod gear_cli; pub mod helpers; pub mod roasters_cli;