Support species height and width ranges
CI / test (push) Failing after 3m6s

This commit is contained in:
2026-09-16 20:21:41 +02:00
parent 87809b2344
commit ef608551ad
14 changed files with 271 additions and 69 deletions
+7 -1
View File
@@ -94,7 +94,7 @@ func TestGardenAndSpeciesCanBeEdited(t *testing.T) {
if gardenResponse.Code != http.StatusSeeOther || gardenInput.Name == nil || *gardenInput.Name != "Neuer Garten" {
t.Fatalf("garden edit: status=%d input=%+v", gardenResponse.Code, gardenInput)
}
speciesForm := url.Values{"common_name": {"Neue Rose"}, "sow_month_from": {"0"}, "sow_month_to": {"0"}}
speciesForm := url.Values{"common_name": {"Neue Rose"}, "height_cm_from": {"40"}, "height_cm_to": {"80"}, "width_cm_to": {"60"}, "sow_month_from": {"0"}, "sow_month_to": {"0"}}
speciesRequest := httptest.NewRequest(http.MethodPost, "/g/3/species/edit/7", strings.NewReader(speciesForm.Encode()))
speciesRequest.Header.Set("Content-Type", "application/x-www-form-urlencoded")
speciesRequest = taskWebRequest(speciesRequest, app.apiClient, httprouter.Params{{Key: "gardenID", Value: "3"}, {Key: "speciesID", Value: "7"}})
@@ -103,6 +103,12 @@ func TestGardenAndSpeciesCanBeEdited(t *testing.T) {
if speciesResponse.Code != http.StatusSeeOther || speciesInput.CommonName == nil || *speciesInput.CommonName != "Neue Rose" || !speciesInput.ClearSowRange {
t.Fatalf("species edit: status=%d input=%+v", speciesResponse.Code, speciesInput)
}
if speciesInput.HeightCMFrom == nil || *speciesInput.HeightCMFrom != 40 || speciesInput.HeightCMTo == nil || *speciesInput.HeightCMTo != 80 || speciesInput.WidthCMTo == nil || *speciesInput.WidthCMTo != 60 {
t.Fatalf("species dimensions were not submitted: %+v", speciesInput)
}
if speciesInput.ClearHeightCMFrom || speciesInput.ClearHeightCMTo || !speciesInput.ClearWidthCMFrom || speciesInput.ClearWidthCMTo {
t.Fatalf("species dimension clear flags are incorrect: %+v", speciesInput)
}
}
func TestGardenCanBeDeleted(t *testing.T) {
+25 -14
View File
@@ -22,7 +22,10 @@ type speciesForm struct {
SoilReaction string `form:"soil_reaction"`
WinterProtection string `form:"winter_protection"`
SpacingCM int `form:"spacing_cm"`
HeightCM int `form:"height_cm"`
HeightCMFrom int `form:"height_cm_from"`
HeightCMTo int `form:"height_cm_to"`
WidthCMFrom int `form:"width_cm_from"`
WidthCMTo int `form:"width_cm_to"`
Notes string `form:"notes"`
ImageData string `form:"image_data"`
ImageID int `form:"image_id"`
@@ -133,7 +136,13 @@ func speciesFormFromSpecies(value client.Species) speciesForm {
form.WinterProtection = *value.WinterProtection
}
copyOptionalInt(value.SpacingCM, &form.SpacingCM)
copyOptionalInt(value.HeightCM, &form.HeightCM)
copyOptionalInt(value.HeightCMFrom, &form.HeightCMFrom)
copyOptionalInt(value.HeightCMTo, &form.HeightCMTo)
if value.HeightCMTo == nil {
copyOptionalInt(value.HeightCM, &form.HeightCMTo)
}
copyOptionalInt(value.WidthCMFrom, &form.WidthCMFrom)
copyOptionalInt(value.WidthCMTo, &form.WidthCMTo)
copyOptionalInt(value.CategoryID, &form.CategoryID)
copyOptionalInt(value.SowMonthFrom, &form.SowMonthFrom)
copyOptionalInt(value.SowDayFrom, &form.SowDayFrom)
@@ -219,9 +228,10 @@ func speciesInput(form speciesForm) client.SpeciesInput {
if form.SpacingCM > 0 {
input.SpacingCM = &form.SpacingCM
}
if form.HeightCM > 0 {
input.HeightCM = &form.HeightCM
}
input.HeightCMFrom, input.HeightCMTo = optionalPositive(form.HeightCMFrom), optionalPositive(form.HeightCMTo)
input.WidthCMFrom, input.WidthCMTo = optionalPositive(form.WidthCMFrom), optionalPositive(form.WidthCMTo)
input.ClearHeightCMFrom, input.ClearHeightCMTo = form.HeightCMFrom == 0, form.HeightCMTo == 0
input.ClearWidthCMFrom, input.ClearWidthCMTo = form.WidthCMFrom == 0, form.WidthCMTo == 0
if json.Valid([]byte(form.Attributes)) {
input.Attributes = json.RawMessage(form.Attributes)
}
@@ -230,18 +240,12 @@ func speciesInput(form speciesForm) client.SpeciesInput {
} else {
input.ClearCategoryID = true
}
assignPositive := func(value int) *int {
if value > 0 {
return &value
}
return nil
}
form.SowMonthTo, form.SowDayTo = rangeEnd(form.SowMonthFrom, form.SowDayFrom, form.SowDuration, form.SowDurationUnit)
form.PlantingMonthTo, form.PlantingDayTo = rangeEnd(form.PlantingMonthFrom, form.PlantingDayFrom, form.PlantingDuration, form.PlantingDurationUnit)
form.HarvestMonthTo, form.HarvestDayTo = rangeEnd(form.HarvestMonthFrom, form.HarvestDayFrom, form.HarvestDuration, form.HarvestDurationUnit)
input.SowMonthFrom, input.SowDayFrom, input.SowMonthTo, input.SowDayTo = assignPositive(form.SowMonthFrom), assignPositive(form.SowDayFrom), assignPositive(form.SowMonthTo), assignPositive(form.SowDayTo)
input.PlantingMonthFrom, input.PlantingDayFrom, input.PlantingMonthTo, input.PlantingDayTo = assignPositive(form.PlantingMonthFrom), assignPositive(form.PlantingDayFrom), assignPositive(form.PlantingMonthTo), assignPositive(form.PlantingDayTo)
input.HarvestMonthFrom, input.HarvestDayFrom, input.HarvestMonthTo, input.HarvestDayTo = assignPositive(form.HarvestMonthFrom), assignPositive(form.HarvestDayFrom), assignPositive(form.HarvestMonthTo), assignPositive(form.HarvestDayTo)
input.SowMonthFrom, input.SowDayFrom, input.SowMonthTo, input.SowDayTo = optionalPositive(form.SowMonthFrom), optionalPositive(form.SowDayFrom), optionalPositive(form.SowMonthTo), optionalPositive(form.SowDayTo)
input.PlantingMonthFrom, input.PlantingDayFrom, input.PlantingMonthTo, input.PlantingDayTo = optionalPositive(form.PlantingMonthFrom), optionalPositive(form.PlantingDayFrom), optionalPositive(form.PlantingMonthTo), optionalPositive(form.PlantingDayTo)
input.HarvestMonthFrom, input.HarvestDayFrom, input.HarvestMonthTo, input.HarvestDayTo = optionalPositive(form.HarvestMonthFrom), optionalPositive(form.HarvestDayFrom), optionalPositive(form.HarvestMonthTo), optionalPositive(form.HarvestDayTo)
input.ClearSowRange = form.SowMonthFrom == 0 && form.SowMonthTo == 0
input.ClearPlantingRange = form.PlantingMonthFrom == 0 && form.PlantingMonthTo == 0
input.ClearHarvestRange = form.HarvestMonthFrom == 0 && form.HarvestMonthTo == 0
@@ -251,6 +255,13 @@ func speciesInput(form speciesForm) client.SpeciesInput {
return input
}
func optionalPositive(value int) *int {
if value > 0 {
return &value
}
return nil
}
func rangeEnd(month, day, duration int, unit string) (int, int) {
if month < 1 {
return 0, 0
+4
View File
@@ -165,6 +165,10 @@ button, .button { display: inline-block; width: auto; padding: .75rem 1rem; colo
.danger:hover, .template-remove:hover { background: #7f291f; }
.species-template-add { width: 2.5rem; height: 2.5rem; margin-top: .75rem; margin-left: auto; }
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); gap: 1rem; }
.dimension-range { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; overflow: hidden; background: white; border: 1px solid var(--line); border-radius: .55rem; }
.dimension-range:focus-within { border-color: var(--leaf); box-shadow: 0 0 0 .15rem var(--leaf-light); }
.dimension-range input { min-width: 0; border: 0; border-radius: 0; outline: 0; }
.dimension-range > span { color: var(--muted); font-weight: 400; }
.filter-bar { width: 100%; max-width: none; margin-bottom: 2rem; }
.image-library-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(220px,1fr)); gap:1rem; }
.image-library-item { margin:0; overflow:hidden; padding:0; }
@@ -30,7 +30,7 @@
{{range .SpeciesCategories}}{{if or .Active (eqInt $form.CategoryID .ID)}}<option value='{{.ID}}' {{if eqInt $form.CategoryID .ID}}selected{{end}}>{{.Name}}{{with lifecycleName .Lifecycle}} ({{.}}){{end}}{{if not .Active}} (inaktiv){{end}}</option>{{end}}{{end}}
</select>
{{with index $form.Errors "category_id"}}<p class='field-error'>{{.}}</p>{{end}}
<div class='form-grid'><label>Licht<select name='sun_exposure'><option value=''>Nicht angegeben</option><option value='sunny' {{if eqString $form.SunExposure "sunny"}}selected{{end}}>Sonnig</option><option value='partial_shade' {{if eqString $form.SunExposure "partial_shade"}}selected{{end}}>Halbschattig</option><option value='shade' {{if eqString $form.SunExposure "shade"}}selected{{end}}>Schattig</option></select></label><label>Bodenbeschaffenheit<select name='soil_condition'><option value=''>Nicht angegeben</option><option value='dry' {{if eqString $form.SoilCondition "dry"}}selected{{end}}>Trocken</option><option value='moist' {{if eqString $form.SoilCondition "moist"}}selected{{end}}>Feucht</option><option value='boggy' {{if eqString $form.SoilCondition "boggy"}}selected{{end}}>Sumpfig</option></select></label><label>Bodenreaktion<select name='soil_reaction'><option value=''>Nicht angegeben</option><option value='alkaline' {{if eqString $form.SoilReaction "alkaline"}}selected{{end}}>Basisch</option><option value='acidic' {{if eqString $form.SoilReaction "acidic"}}selected{{end}}>Sauer</option><option value='neutral' {{if eqString $form.SoilReaction "neutral"}}selected{{end}}>Neutral</option></select></label><label>Winterschutz<input name='winter_protection' value='{{$form.WinterProtection}}'></label><label>Pflanzabstand in cm<input type='number' min='1' name='spacing_cm' value='{{if $form.SpacingCM}}{{$form.SpacingCM}}{{end}}'></label><label>Höhe in cm<input type='number' min='1' name='height_cm' value='{{if $form.HeightCM}}{{$form.HeightCM}}{{end}}'></label></div>
<div class='form-grid'><label>Licht<select name='sun_exposure'><option value=''>Nicht angegeben</option><option value='sunny' {{if eqString $form.SunExposure "sunny"}}selected{{end}}>Sonnig</option><option value='partial_shade' {{if eqString $form.SunExposure "partial_shade"}}selected{{end}}>Halbschattig</option><option value='shade' {{if eqString $form.SunExposure "shade"}}selected{{end}}>Schattig</option></select></label><label>Bodenbeschaffenheit<select name='soil_condition'><option value=''>Nicht angegeben</option><option value='dry' {{if eqString $form.SoilCondition "dry"}}selected{{end}}>Trocken</option><option value='moist' {{if eqString $form.SoilCondition "moist"}}selected{{end}}>Feucht</option><option value='boggy' {{if eqString $form.SoilCondition "boggy"}}selected{{end}}>Sumpfig</option></select></label><label>Bodenreaktion<select name='soil_reaction'><option value=''>Nicht angegeben</option><option value='alkaline' {{if eqString $form.SoilReaction "alkaline"}}selected{{end}}>Basisch</option><option value='acidic' {{if eqString $form.SoilReaction "acidic"}}selected{{end}}>Sauer</option><option value='neutral' {{if eqString $form.SoilReaction "neutral"}}selected{{end}}>Neutral</option></select></label><label>Winterschutz<input name='winter_protection' value='{{$form.WinterProtection}}'></label><label>Pflanzabstand in cm<input type='number' min='1' name='spacing_cm' value='{{if $form.SpacingCM}}{{$form.SpacingCM}}{{end}}'></label><label>Höhe in cm<span class='dimension-range'><input type='number' min='1' name='height_cm_from' aria-label='Höhe von' placeholder='von' value='{{if $form.HeightCMFrom}}{{$form.HeightCMFrom}}{{end}}'><span aria-hidden='true'></span><input type='number' min='1' name='height_cm_to' aria-label='Höhe bis' placeholder='bis' value='{{if $form.HeightCMTo}}{{$form.HeightCMTo}}{{end}}'></span>{{with index $form.Errors "height_cm_from"}}<span class='field-error'>{{.}}</span>{{end}}{{with index $form.Errors "height_cm_to"}}<span class='field-error'>{{.}}</span>{{end}}</label><label>Breite in cm<span class='dimension-range'><input type='number' min='1' name='width_cm_from' aria-label='Breite von' placeholder='von' value='{{if $form.WidthCMFrom}}{{$form.WidthCMFrom}}{{end}}'><span aria-hidden='true'></span><input type='number' min='1' name='width_cm_to' aria-label='Breite bis' placeholder='bis' value='{{if $form.WidthCMTo}}{{$form.WidthCMTo}}{{end}}'></span>{{with index $form.Errors "width_cm_from"}}<span class='field-error'>{{.}}</span>{{end}}{{with index $form.Errors "width_cm_to"}}<span class='field-error'>{{.}}</span>{{end}}</label></div>
{{template "season_range" (dict "Legend" "Aussaat" "Prefix" "sow" "Month" $form.SowMonthFrom "Day" $form.SowDayFrom "Duration" $form.SowDuration "Unit" $form.SowDurationUnit)}}
{{template "season_range" (dict "Legend" "Pflanzzeit" "Prefix" "planting" "Month" $form.PlantingMonthFrom "Day" $form.PlantingDayFrom "Duration" $form.PlantingDuration "Unit" $form.PlantingDurationUnit)}}
{{template "season_range" (dict "Legend" "Ernte" "Prefix" "harvest" "Month" $form.HarvestMonthFrom "Day" $form.HarvestDayFrom "Duration" $form.HarvestDuration "Unit" $form.HarvestDurationUnit)}}
+10 -2
View File
@@ -371,11 +371,19 @@ func TestEmptyOptionalSpeciesDimensionsDoNotBlockSubmit(t *testing.T) {
response := httptest.NewRecorder()
app.render(response, http.StatusOK, "species_form.tmpl", data)
body := response.Body.String()
for _, field := range []string{"spacing_cm", "height_cm"} {
if !strings.Contains(body, "name='"+field+"' value=''") {
for _, field := range []string{"spacing_cm", "height_cm_from", "height_cm_to", "width_cm_from", "width_cm_to"} {
fieldStart := strings.Index(body, "name='"+field+"'")
fieldEnd := -1
if fieldStart >= 0 {
fieldEnd = strings.Index(body[fieldStart:], ">")
}
if fieldStart < 0 || fieldEnd < 0 || !strings.Contains(body[fieldStart:fieldStart+fieldEnd], "value=''") {
t.Errorf("optional field %s should render empty: %s", field, body)
}
}
if got := strings.Count(body, "class='dimension-range'"); got != 2 {
t.Errorf("dimension ranges: got %d composite controls, want 2: %s", got, body)
}
}
func TestTaskDueFormatsWindows(t *testing.T) {