From ff5661fdf1461c7486090669a04af219f2095bb5 Mon Sep 17 00:00:00 2001 From: "Christopher C. Wells" Date: Sat, 17 Apr 2021 19:39:07 -0700 Subject: [PATCH] Improve Jorunal Entry validation feedback --- .../Controllers/JournalEntryController.php | 49 +++------------- .../StoreFromNutrientsJournalEntryRequest.php | 36 ++++++++++++ .../Requests/StoreJournalEntryRequest.php | 57 +++++++++++++++++++ app/Http/Requests/UpdateFoodRequest.php | 4 +- app/Http/Requests/UpdateRecipeRequest.php | 4 +- ... => StringIsPositiveDecimalOrFraction.php} | 6 +- app/View/Components/Inputs/Select.php | 7 ++- .../views/components/inputs/select.blade.php | 21 ++++++- .../create-from-nutrients.blade.php | 6 +- .../partials/entry-item-input.blade.php | 11 +++- ...StringIsPositiveDecimalOrFractionTest.php} | 12 ++-- 11 files changed, 150 insertions(+), 63 deletions(-) create mode 100644 app/Http/Requests/StoreFromNutrientsJournalEntryRequest.php create mode 100644 app/Http/Requests/StoreJournalEntryRequest.php rename app/Rules/{StringIsDecimalOrFraction.php => StringIsPositiveDecimalOrFraction.php} (76%) rename tests/Unit/Rules/{StringIsDecimalOrFractionTest.php => StringIsPositiveDecimalOrFractionTest.php} (74%) diff --git a/app/Http/Controllers/JournalEntryController.php b/app/Http/Controllers/JournalEntryController.php index 2f279dc..166e73b 100644 --- a/app/Http/Controllers/JournalEntryController.php +++ b/app/Http/Controllers/JournalEntryController.php @@ -5,13 +5,11 @@ namespace App\Http\Controllers; +use App\Http\Requests\StoreFromNutrientsJournalEntryRequest; +use App\Http\Requests\StoreJournalEntryRequest; use App\Models\Food; use App\Models\JournalEntry; use App\Models\Recipe; -use App\Rules\ArrayNotEmpty; -use App\Rules\InArray; -use App\Rules\StringIsDecimalOrFraction; -use App\Rules\UsesIngredientTrait; use App\Support\ArrayFormat; use App\Support\Number; use App\Support\Nutrients; @@ -85,6 +83,7 @@ class JournalEntryController extends Controller continue; } $ingredients[$key] = [ + 'key' => $key, 'date' => $old['date'][$key], 'meal' => $old['meal'][$key], 'amount' => $amount, @@ -130,28 +129,9 @@ class JournalEntryController extends Controller /** * Store a newly created resource in storage. */ - public function store(Request $request): RedirectResponse + public function store(StoreJournalEntryRequest $request): RedirectResponse { - $input = $request->validate([ - 'ingredients.date' => ['required', 'array', new ArrayNotEmpty], - 'ingredients.date.*' => ['nullable', 'date', 'required_with:ingredients.id.*'], - 'ingredients.meal' => ['required', 'array', new ArrayNotEmpty], - 'ingredients.meal.*' => [ - 'nullable', - 'string', - 'required_with:ingredients.id.*', - new InArray(JournalEntry::meals()->pluck('value')->toArray()) - ], - 'ingredients.amount' => ['required', 'array', new ArrayNotEmpty], - 'ingredients.amount.*' => ['required_with:ingredients.id.*', 'nullable', new StringIsDecimalOrFraction], - 'ingredients.unit' => ['required', 'array'], - 'ingredients.unit.*' => ['required_with:ingredients.id.*'], - 'ingredients.id' => ['required', 'array', new ArrayNotEmpty], - 'ingredients.id.*' => 'required_with:ingredients.amount.*|nullable', - 'ingredients.type' => ['required', 'array', new ArrayNotEmpty], - 'ingredients.type.*' => ['required_with:ingredients.id.*', 'nullable', new UsesIngredientTrait()], - 'group_entries' => ['nullable', 'boolean'], - ]); + $input = $request->validated(); $ingredients = ArrayFormat::flipTwoDimensionalKeys($input['ingredients']); @@ -284,23 +264,8 @@ class JournalEntryController extends Controller /** * Store an entry from nutrients. */ - public function storeFromNutrients(Request $request): RedirectResponse { - $attributes = $request->validate([ - 'date' => ['required', 'date'], - 'meal' => [ - 'required', - 'string', - new InArray(JournalEntry::meals()->pluck('value')->toArray()) - ], - 'summary' => ['required', 'string'], - 'calories' => ['nullable', 'required_without_all:fat,cholesterol,sodium,carbohydrates,protein', 'numeric'], - 'fat' => ['nullable', 'required_without_all:calories,cholesterol,sodium,carbohydrates,protein', 'numeric'], - 'cholesterol' => ['nullable', 'required_without_all:calories,fat,sodium,carbohydrates,protein', 'numeric'], - 'sodium' => ['nullable', 'required_without_all:calories,fat,cholesterol,carbohydrates,protein', 'numeric'], - 'carbohydrates' => ['nullable', 'required_without_all:calories,fat,cholesterol,sodium,protein', 'numeric'], - 'protein' => ['nullable', 'required_without_all:calories,fat,cholesterol,sodium,carbohydrates', 'numeric'], - ]); - + public function storeFromNutrients(StoreFromNutrientsJournalEntryRequest $request): RedirectResponse { + $attributes = $request->validated(); $entry = JournalEntry::make(array_filter($attributes)) ->user()->associate(Auth::user()); $entry->save(); diff --git a/app/Http/Requests/StoreFromNutrientsJournalEntryRequest.php b/app/Http/Requests/StoreFromNutrientsJournalEntryRequest.php new file mode 100644 index 0000000..8fe450a --- /dev/null +++ b/app/Http/Requests/StoreFromNutrientsJournalEntryRequest.php @@ -0,0 +1,36 @@ + ['required', 'date'], + 'meal' => [ + 'required', + 'string', + new InArray(JournalEntry::meals()->pluck('value')->toArray()) + ], + 'summary' => ['required', 'string'], + 'calories' => ['nullable', 'numeric', 'min:0', 'required_without_all:fat,cholesterol,sodium,carbohydrates,protein'], + 'fat' => ['nullable', 'numeric', 'min:0', 'required_without_all:calories,cholesterol,sodium,carbohydrates,protein'], + 'cholesterol' => ['nullable', 'numeric', 'min:0', 'required_without_all:calories,fat,sodium,carbohydrates,protein'], + 'sodium' => ['nullable', 'numeric', 'min:0', 'required_without_all:calories,fat,cholesterol,carbohydrates,protein'], + 'carbohydrates' => ['nullable', 'numeric', 'min:0', 'required_without_all:calories,fat,cholesterol,sodium,protein'], + 'protein' => ['nullable', 'numeric', 'min:0', 'required_without_all:calories,fat,cholesterol,sodium,carbohydrates'], + ]; + } +} diff --git a/app/Http/Requests/StoreJournalEntryRequest.php b/app/Http/Requests/StoreJournalEntryRequest.php new file mode 100644 index 0000000..3d55538 --- /dev/null +++ b/app/Http/Requests/StoreJournalEntryRequest.php @@ -0,0 +1,57 @@ + ['required', 'array', new ArrayNotEmpty], + 'ingredients.date.*' => ['nullable', 'date', 'required_with:ingredients.id.*'], + 'ingredients.meal' => ['required', 'array', new ArrayNotEmpty], + 'ingredients.meal.*' => [ + 'nullable', + 'string', + 'required_with:ingredients.id.*', + new InArray(JournalEntry::meals()->pluck('value')->toArray()) + ], + 'ingredients.amount' => ['required', 'array', new ArrayNotEmpty], + 'ingredients.amount.*' => ['required_with:ingredients.id.*', 'nullable', new StringIsPositiveDecimalOrFraction], + 'ingredients.unit' => ['required', 'array'], + 'ingredients.unit.*' => ['required_with:ingredients.id.*'], + 'ingredients.id.*' => 'required_with:ingredients.amount.*|nullable', + 'ingredients.type.*' => ['required_with:ingredients.id.*', 'nullable', new UsesIngredientTrait()], + 'group_entries' => ['nullable', 'boolean'], + ]; + } + + /** + * @inheritdoc + */ + public function attributes(): array + { + return [ + 'ingredients.amount' => 'amount', + 'ingredients.amount.*' => 'amount', + 'ingredients.date' => 'date', + 'ingredients.date.*' => 'date', + 'ingredients.id.*' => 'item', + 'ingredients.meal' => 'meal', + 'ingredients.meal.*' => 'meal', + 'ingredients.unit' => 'unit', + 'ingredients.unit.*' => 'unit', + ]; + } +} diff --git a/app/Http/Requests/UpdateFoodRequest.php b/app/Http/Requests/UpdateFoodRequest.php index b449a0c..6ad2c14 100644 --- a/app/Http/Requests/UpdateFoodRequest.php +++ b/app/Http/Requests/UpdateFoodRequest.php @@ -2,7 +2,7 @@ namespace App\Http\Requests; -use App\Rules\StringIsDecimalOrFraction; +use App\Rules\StringIsPositiveDecimalOrFraction; use Illuminate\Foundation\Http\FormRequest; class UpdateFoodRequest extends FormRequest @@ -19,7 +19,7 @@ class UpdateFoodRequest extends FormRequest 'brand' => ['nullable', 'string'], 'source' => ['nullable', 'string'], 'notes' => ['nullable', 'string'], - 'serving_size' => ['required', new StringIsDecimalOrFraction], + 'serving_size' => ['required', new StringIsPositiveDecimalOrFraction], 'serving_unit' => ['nullable', 'string'], 'serving_unit_name' => ['nullable', 'string'], 'serving_weight' => ['required', 'numeric', 'min:0'], diff --git a/app/Http/Requests/UpdateRecipeRequest.php b/app/Http/Requests/UpdateRecipeRequest.php index c68c790..dc1e0dd 100644 --- a/app/Http/Requests/UpdateRecipeRequest.php +++ b/app/Http/Requests/UpdateRecipeRequest.php @@ -3,7 +3,7 @@ namespace App\Http\Requests; use App\Rules\ArrayNotEmpty; -use App\Rules\StringIsDecimalOrFraction; +use App\Rules\StringIsPositiveDecimalOrFraction; use App\Rules\UsesIngredientTrait; use Illuminate\Foundation\Http\FormRequest; @@ -27,7 +27,7 @@ class UpdateRecipeRequest extends FormRequest 'weight' => ['nullable', 'numeric'], 'source' => ['nullable', 'string'], 'ingredients.amount' => ['required', 'array', new ArrayNotEmpty], - 'ingredients.amount.*' => ['required_with:ingredients.id.*', 'nullable', new StringIsDecimalOrFraction], + 'ingredients.amount.*' => ['required_with:ingredients.id.*', 'nullable', new StringIsPositiveDecimalOrFraction], 'ingredients.unit' => ['required', 'array'], 'ingredients.unit.*' => ['required_with:ingredients.id.*'], 'ingredients.detail' => ['required', 'array'], diff --git a/app/Rules/StringIsDecimalOrFraction.php b/app/Rules/StringIsPositiveDecimalOrFraction.php similarity index 76% rename from app/Rules/StringIsDecimalOrFraction.php rename to app/Rules/StringIsPositiveDecimalOrFraction.php index 9a0c70e..b875606 100644 --- a/app/Rules/StringIsDecimalOrFraction.php +++ b/app/Rules/StringIsPositiveDecimalOrFraction.php @@ -5,7 +5,7 @@ namespace App\Rules; use App\Support\Number; use Illuminate\Contracts\Validation\Rule; -class StringIsDecimalOrFraction implements Rule +class StringIsPositiveDecimalOrFraction implements Rule { /** * {@inheritdoc} @@ -14,7 +14,7 @@ class StringIsDecimalOrFraction implements Rule { try { $result = Number::floatFromString($value); - return $result != 0; + return $result > 0; } catch (\InvalidArgumentException $e) { // Allow to pass through, method will return false. @@ -27,6 +27,6 @@ class StringIsDecimalOrFraction implements Rule */ public function message(): string { - return 'The :attribute must be a decimal or fraction.'; + return 'The :attribute must be a positive decimal or fraction.'; } } diff --git a/app/View/Components/Inputs/Select.php b/app/View/Components/Inputs/Select.php index 0f26a23..c687727 100644 --- a/app/View/Components/Inputs/Select.php +++ b/app/View/Components/Inputs/Select.php @@ -8,20 +8,20 @@ use Illuminate\View\Component; class Select extends Component { + public ?bool $hasError; public Collection|array $options; public ?string $selectedValue; /** * Select constructor. - * - * @param \Illuminate\Support\Collection|array $options - * @param ?string $selectedValue */ public function __construct( Collection|array $options, + ?bool $hasError = false, ?string $selectedValue = '', ) { $this->options = $options; + $this->hasError = $hasError; $this->selectedValue = $selectedValue; } @@ -29,6 +29,7 @@ class Select extends Component { return view('components.inputs.select') ->with('options', $this->options) + ->with('hasError', $this->hasError) ->with('selectedValue', $this->selectedValue); } diff --git a/resources/views/components/inputs/select.blade.php b/resources/views/components/inputs/select.blade.php index 5b9e250..917cad9 100644 --- a/resources/views/components/inputs/select.blade.php +++ b/resources/views/components/inputs/select.blade.php @@ -1,4 +1,23 @@ -merge(['class' => implode(' ', $classes)]) !!}> {{ $slot }} diff --git a/resources/views/journal-entries/create-from-nutrients.blade.php b/resources/views/journal-entries/create-from-nutrients.blade.php index 6769e18..41b7c4b 100644 --- a/resources/views/journal-entries/create-from-nutrients.blade.php +++ b/resources/views/journal-entries/create-from-nutrients.blade.php @@ -19,6 +19,7 @@ type="date" class="block w-full" :value="old('date', $default_date->toDateString())" + :hasError="$errors->has('date')" required /> @@ -30,6 +31,7 @@ class="block w-full" :options="$meals" :selectedValue="old('meal')" + :hasError="$errors->has('meal')" required> @@ -43,6 +45,7 @@ type="text" class="block w-full" :value="old('summary')" + :hasError="$errors->has('summary')" required /> @@ -58,7 +61,8 @@ type="number" step="any" class="block w-full" - :value="old($nutrient['value'])"/> + :value="old($nutrient['value'])" + :hasError="$errors->has($nutrient['value'])"/> @endforeach diff --git a/resources/views/journal-entries/partials/entry-item-input.blade.php b/resources/views/journal-entries/partials/entry-item-input.blade.php index ba3a167..27a429f 100644 --- a/resources/views/journal-entries/partials/entry-item-input.blade.php +++ b/resources/views/journal-entries/partials/entry-item-input.blade.php @@ -1,3 +1,4 @@ +@php($key = $key ?? null)
@@ -15,6 +16,7 @@ type="date" class="block w-full" :value="$date ?? $default_date->toDateString()" + :hasError="$errors->has('ingredients.date.' . $key)" required />
@@ -25,8 +27,9 @@ class="block w-full" :options="$meals" :selectedValue="$meal ?? null" + :hasError="$errors->has('ingredients.meal.' . $key)" required> - + @if(is_null($key))@endif
@@ -39,6 +42,7 @@ class="block w-full" placeholder="Amount" :value="$amount ?? null" + :hasError="$errors->has('ingredients.amount.' . $key)" required /> @@ -48,8 +52,9 @@ - + :selectedValue="$unit ?? null" + :hasError="$errors->has('ingredients.unit.' . $key)"> + @if(is_null($key))@endif diff --git a/tests/Unit/Rules/StringIsDecimalOrFractionTest.php b/tests/Unit/Rules/StringIsPositiveDecimalOrFractionTest.php similarity index 74% rename from tests/Unit/Rules/StringIsDecimalOrFractionTest.php rename to tests/Unit/Rules/StringIsPositiveDecimalOrFractionTest.php index 438d85f..055824d 100644 --- a/tests/Unit/Rules/StringIsDecimalOrFractionTest.php +++ b/tests/Unit/Rules/StringIsPositiveDecimalOrFractionTest.php @@ -2,9 +2,9 @@ namespace Tests\Unit\Rules; -use App\Rules\StringIsDecimalOrFraction; +use App\Rules\StringIsPositiveDecimalOrFraction; -class StringIsDecimalOrFractionTest extends RulesTestCase +class StringIsPositiveDecimalOrFractionTest extends RulesTestCase { /** @@ -13,7 +13,7 @@ class StringIsDecimalOrFractionTest extends RulesTestCase public function setUp(): void { parent::setUp(); - $this->validator->setRules([new StringIsDecimalOrFraction()]); + $this->validator->setRules([new StringIsPositiveDecimalOrFraction()]); } /** @@ -49,16 +49,16 @@ class StringIsDecimalOrFractionTest extends RulesTestCase /** * Provide valid decimals or fractions * - * @see \Tests\Unit\Rules\StringIsDecimalOrFractionTest::testStringIsDecimalOrFractionRule() + * @see \Tests\Unit\Rules\StringIsPositiveDecimalOrFractionTest::testStringIsDecimalOrFractionRule() */ public function invalidDecimalsAndFractions(): array { - return [['0'], [0], ['string']]; + return [['-1'], [-1], ['0'], [0], ['string']]; } /** * Provide valid decimals or fractions * - * @see \Tests\Unit\Rules\StringIsDecimalOrFractionTest::testStringIsDecimalOrFractionRule() + * @see \Tests\Unit\Rules\StringIsPositiveDecimalOrFractionTest::testStringIsDecimalOrFractionRule() */ public function validDecimalsAndFractions(): array { return [