[Racket] Ferti hydroponic nutrient solver, redux.
Remove all references to nutrient targets.
Changed files
db/migrations.rkt
@@ -62,36 +62,25 @@
62
62
#:if-not-exists nutrient_value_sets
63
63
#:columns [id integer #:not-null]
64
64
[nutrient_measurement_id integer]
65
Removed:
[nutrient_target_id integer]
66
65
[crop_requirement_id integer]
67
66
[fertilizer_product_id integer]
68
67
#:constraints (primary-key id)
69
68
(foreign-key nutrient_measurement_id #:references (nutrient_measurements id) #:on-delete #:cascade)
70
Removed:
(foreign-key nutrient_target_id #:references (nutrient_targets id) #:on-delete #:cascade)
71
69
(foreign-key crop_requirement_id #:references (crop_requirements id) #:on-delete #:cascade)
72
70
(foreign-key fertilizer_product_id #:references (fertilizer_products id) #:on-delete #:cascade)
73
71
(unique nutrient_measurement_id)
74
Removed:
(unique nutrient_target_id)
75
72
(unique crop_requirement_id)
76
73
(unique fertilizer_product_id)
77
74
(check (or (and (is-not-null nutrient_measurement_id)
78
Removed:
(is-null nutrient_target_id)
79
75
(is-null crop_requirement_id)
80
76
(is-null fertilizer_product_id))
81
77
(and (is-null nutrient_measurement_id)
82
Removed:
(is-not-null nutrient_target_id)
83
78
(is-null crop_requirement_id)
84
79
(is-null fertilizer_product_id))
85
80
(and (is-null nutrient_measurement_id)
86
Removed:
(is-null nutrient_target_id)
87
81
(is-not-null crop_requirement_id)
88
Removed:
(is-null fertilizer_product_id))
89
Removed:
(and (is-null nutrient_measurement_id)
90
Removed:
(is-null nutrient_target_id)
91
Removed:
(is-null crop_requirement_id)
92
Removed:
(is-not-null fertilizer_product_id)))))
82
Added:
(is-null fertilizer_product_id)))))
93
83
"CREATE INDEX IF NOT EXISTS idx_nvs_meas ON nutrient_value_sets(nutrient_measurement_id)"
94
Removed:
"CREATE INDEX IF NOT EXISTS idx_nvs_targ ON nutrient_value_sets(nutrient_target_id)"
95
84
"CREATE INDEX IF NOT EXISTS idx_nvs_crop ON nutrient_value_sets(crop_requirement_id)"
96
85
"CREATE INDEX IF NOT EXISTS idx_nvs_prod ON nutrient_value_sets(fertilizer_product_id)"))
97
86
@@ -115,13 +104,6 @@
115
104
#:constraints (primary-key id)
116
105
(unique measurement_date))))
117
106
118
Removed:
(define-migration "create table nutrient_targets"
119
Removed:
(list (create-table #:if-not-exists nutrient_targets
120
Removed:
#:columns [id integer #:not-null]
121
Removed:
;; ISO8601 date
122
Removed:
[target_date text #:not-null]
123
Removed:
#:constraints (primary-key id)
124
Removed:
(unique target_date))))
125
107
126
108
;;;;;;;;
127
109
;; CROPS
handlers.rkt
@@ -32,7 +32,7 @@
32
32
[("ferti" "measurement" "create") #:method "post" create-measurement]
33
33
[("ferti" "measurement" (integer-arg)) #:method "get" show-measurement]
34
34
[("ferti" "measurement" "destroy" (integer-arg)) #:method "get" destroy-measurement]
35
Removed:
;; Nutrient targets
35
Added:
;; Crop rotations
36
36
[("ferti" "rotation" "new") #:method "get" new-rotation]
37
37
[("ferti" "rotation" "new" (string-arg)) #:method "get" new-rotation-for-date]
38
38
[("ferti" "rotation" "create") #:method "post" create-rotation]
models/crop-rotation.rkt
@@ -101,7 +101,7 @@
101
101
(match grouped-rows
102
102
['() #f]
103
103
[(list grouped-row) (grouped-row->crop-rotation grouped-row)]
104
Removed:
[many (error 'get-crop-rotation "expected 1 nutrient target, got ~a" (length many))]))
104
Added:
[many (error 'get-crop-rotation "expected 1 crop rotation, got ~a" (length many))]))
105
105
106
106
(define (get-latest-crop-rotation)
107
107
(define rotations (get-crop-rotations))
models/nutrient-target.rkt
@@ -1,239 +0,0 @@
1
Removed:
#lang racket
2
Removed:
3
Removed:
(provide nutrient-target
4
Removed:
nutrient-target?
5
Removed:
nutrient-target-id
6
Removed:
(rename-out [nutrient-target-target-date nutrient-target-date]
7
Removed:
[nutrient-target-nutrient-values nutrient-target-values])
8
Removed:
(contract-out
9
Removed:
[create-nutrient-target! (-> string? nutrient-value-hash/c nutrient-target?)]
10
Removed:
[get-nutrient-targets (-> (listof nutrient-target?))]
11
Removed:
[get-nutrient-target
12
Removed:
(->* () (#:id exact-nonnegative-integer? #:date string?) (or/c nutrient-target? #f))]
13
Removed:
[get-nutrient-target-values (-> nutrient-target-or-id/c nutrient-value-hash/c)]
14
Removed:
[get-nutrient-target-value (-> nutrient-target-or-id/c nutrient? maybe-nutrient-value?)]
15
Removed:
[get-latest-nutrient-target-value (-> nutrient? maybe-nutrient-value?)]
16
Removed:
[get-latest-nutrient-target-values (-> nutrient-value-hash/c)]
17
Removed:
[delete-nutrient-target! (-> nutrient-target-or-id/c void?)]))
18
Removed:
19
Removed:
(require racket/contract
20
Removed:
db
21
Removed:
sql
22
Removed:
"../db/conn.rkt"
23
Removed:
"nutrient.rkt")
24
Removed:
25
Removed:
(struct nutrient-target (id target-date nutrient-values)
26
Removed:
#:transparent
27
Removed:
#:property prop:custom-write
28
Removed:
(λ (v out _)
29
Removed:
(fprintf out "Target #~a on ~a\n" (nutrient-target-id v) (nutrient-target-target-date v))
30
Removed:
(for ([(n v) (in-hash (nutrient-target-nutrient-values v))])
31
Removed:
(fprintf out
32
Removed:
"~a ~a\n"
33
Removed:
(~a (nutrient-canonical-name n) #:min-width 14)
34
Removed:
(~a v #:max-width 6 #:align 'right)))))
35
Removed:
36
Removed:
(define nutrient-target-id? exact-nonnegative-integer?)
37
Removed:
(define nutrient-target-or-id/c (or/c nutrient-target? nutrient-target-id?))
38
Removed:
39
Removed:
(define (->nt-id nt-or-id)
40
Removed:
(match nt-or-id
41
Removed:
[(? nutrient-target-id? id) id]
42
Removed:
[(nutrient-target id _ _) id]))
43
Removed:
44
Removed:
;; CREATE
45
Removed:
46
Removed:
(define (create-nutrient-target! target-date nutrient-values)
47
Removed:
(or (get-nutrient-target #:date target-date)
48
Removed:
(with-tx
49
Removed:
(query-exec (current-conn) (insert #:into nutrient_targets #:set [target_date ,target-date]))
50
Removed:
(define nt-id
51
Removed:
(query-value (current-conn)
52
Removed:
(select id #:from nutrient_targets #:where (= target_date ,target-date))))
53
Removed:
(query-exec (current-conn)
54
Removed:
(insert #:into nutrient_value_sets #:set [nutrient_target_id ,nt-id]))
55
Removed:
(define nvs-id
56
Removed:
(query-value (current-conn)
57
Removed:
(select id #:from nutrient_value_sets #:where (= nutrient_target_id ,nt-id))))
58
Removed:
(for ([(n v) (in-hash nutrient-values)])
59
Removed:
(query-exec (current-conn)
60
Removed:
(insert #:into nutrient_values
61
Removed:
#:set [value_set_id ,nvs-id]
62
Removed:
[nutrient_id ,(nutrient-id n)]
63
Removed:
[value_ppm ,v])))
64
Removed:
(get-nutrient-target #:date target-date))))
65
Removed:
66
Removed:
;; READ
67
Removed:
68
Removed:
(define joined
69
Removed:
(table-expr-qq (inner-join (inner-join (inner-join (as nutrient_targets nt)
70
Removed:
(as nutrient_value_sets nvs)
71
Removed:
#:on (= nvs.nutrient_target_id nt.id))
72
Removed:
(as nutrient_values nv)
73
Removed:
#:on (= nv.value_set_id nvs.id))
74
Removed:
(as nutrients n)
75
Removed:
#:on (= n.id nv.nutrient_id))))
76
Removed:
77
Removed:
(define (grouped-row->nutrient-target grouped-row)
78
Removed:
(match-define (vector nt-id target-date residuals) grouped-row)
79
Removed:
(nutrient-target nt-id target-date (residuals->nutrient-value-hash residuals)))
80
Removed:
81
Removed:
(define (get-nutrient-targets)
82
Removed:
(define grouped-rows
83
Removed:
(query-rows (current-conn)
84
Removed:
(select nt.id
85
Removed:
nt.target_date
86
Removed:
n.id
87
Removed:
n.canonical_name
88
Removed:
n.french_name
89
Removed:
n.formula
90
Removed:
nv.value_ppm
91
Removed:
#:from (TableExpr:AST ,joined)
92
Removed:
#:order-by nt.target_date
93
Removed:
#:desc)
94
Removed:
#:group '#(0 1)))
95
Removed:
(map grouped-row->nutrient-target grouped-rows))
96
Removed:
97
Removed:
(define (get-nutrient-target #:id [nt-id #f] #:date [target-date #f])
98
Removed:
(define where
99
Removed:
(cond
100
Removed:
[(and nt-id target-date)
101
Removed:
(scalar-expr-qq (and (= nt.id ,nt-id) (= nt.target_date ,target-date)))]
102
Removed:
[nt-id (scalar-expr-qq (= nt.id ,nt-id))]
103
Removed:
[target-date (scalar-expr-qq (= nt.target_date ,target-date))]
104
Removed:
[else (error 'get-nutrient-target "either #:id or #:date must be provided")]))
105
Removed:
(define grouped-rows
106
Removed:
(query-rows (current-conn)
107
Removed:
(select nt.id
108
Removed:
nt.target_date
109
Removed:
n.id
110
Removed:
n.canonical_name
111
Removed:
n.french_name
112
Removed:
n.formula
113
Removed:
nv.value_ppm
114
Removed:
#:from (TableExpr:AST ,joined)
115
Removed:
#:where (ScalarExpr:AST ,where)
116
Removed:
#:order-by nt.target_date
117
Removed:
#:desc)
118
Removed:
#:group '#(0 1)))
119
Removed:
(match grouped-rows
120
Removed:
['() #f]
121
Removed:
[(list grouped-row) (grouped-row->nutrient-target grouped-row)]
122
Removed:
[many (error 'get-nutrient-target "expected 1 nutrient target, got ~a" (length many))]))
123
Removed:
124
Removed:
(define (get-nutrient-target-values nt-or-id)
125
Removed:
(for/hash ([(nutrient-id canonical-name french-name formula value_ppm)
126
Removed:
(in-query (current-conn)
127
Removed:
(select n.id
128
Removed:
n.canonical_name
129
Removed:
n.french_name
130
Removed:
n.formula
131
Removed:
nv.value_ppm
132
Removed:
#:from (TableExpr:AST ,joined)
133
Removed:
#:where (= nt.id ,(->nt-id nt-or-id))))])
134
Removed:
(values (nutrient nutrient-id canonical-name french-name formula) value_ppm)))
135
Removed:
136
Removed:
(define (get-nutrient-target-value nt-or-id nutrient)
137
Removed:
(query-maybe-value (current-conn)
138
Removed:
(select value_ppm
139
Removed:
#:from (TableExpr:AST ,joined)
140
Removed:
#:where (and (= nt.id ,(->nt-id nt-or-id))
141
Removed:
(= nv.nutrient_id ,(nutrient-id nutrient))))))
142
Removed:
143
Removed:
(define (get-latest-nutrient-target-value nutrient)
144
Removed:
(query-maybe-value (current-conn)
145
Removed:
(select value_ppm
146
Removed:
#:from (TableExpr:AST ,joined)
147
Removed:
#:where (= nv.nutrient_id ,(nutrient-id nutrient))
148
Removed:
#:order-by nt.target_date
149
Removed:
#:desc
150
Removed:
#:limit 1)))
151
Removed:
152
Removed:
(define (get-latest-nutrient-target-values)
153
Removed:
(define grouped-rows
154
Removed:
(query-rows (current-conn)
155
Removed:
(select n.id
156
Removed:
n.canonical_name
157
Removed:
n.french_name
158
Removed:
n.formula
159
Removed:
nt.target_date
160
Removed:
nv.value_ppm
161
Removed:
#:from (TableExpr:AST ,joined)
162
Removed:
#:order-by nt.target_date
163
Removed:
#:desc)
164
Removed:
#:group '(#(0 1 2 3))))
165
Removed:
(for/hash ([row grouped-rows])
166
Removed:
(match-define (vector n-id n-canonical-name n-french-name n-formula residual-rows) row)
167
Removed:
;; residual-rows is a non-empty list of vectors: #(target_date value_ppm)
168
Removed:
(match-define (vector _ value-ppm) (first residual-rows))
169
Removed:
(values (nutrient n-id n-canonical-name n-french-name n-formula) value-ppm)))
170
Removed:
171
Removed:
;; UPDATE
172
Removed:
173
Removed:
;; DELETE
174
Removed:
175
Removed:
(define (delete-nutrient-target! nt-or-id)
176
Removed:
(query-exec (current-conn) (delete #:from nutrient_targets #:where (= id ,(->nt-id nt-or-id)))))
177
Removed:
178
Removed:
(module+ test
179
Removed:
(require rackunit
180
Removed:
rackunit/text-ui
181
Removed:
"../db/conn.rkt"
182
Removed:
"../db/migrations.rkt"
183
Removed:
"../models/nutrient.rkt")
184
Removed:
185
Removed:
(define target-date "2025-09-01")
186
Removed:
187
Removed:
(run-tests
188
Removed:
(test-suite "Nutrient target model"
189
Removed:
#:before (λ ()
190
Removed:
(connect! #:path 'memory)
191
Removed:
(migrate-all!)
192
Removed:
(create-nutrient! "Nitrogen" "" "N")
193
Removed:
(create-nutrient! "Phosphorus" "" "P")
194
Removed:
(create-nutrient! "Potassium" "" "K"))
195
Removed:
#:after (λ () (disconnect!))
196
Removed:
197
Removed:
(test-case "Create target with date and values"
198
Removed:
(define nitrogen (get-nutrient #:name "Nitrogen"))
199
Removed:
(define phosphorus (get-nutrient #:name "Phosphorus"))
200
Removed:
(create-nutrient-target! target-date (hash nitrogen 12.3 phosphorus 4.5))
201
Removed:
(check-equal? (length (get-nutrient-targets)) 1)
202
Removed:
(define nt (get-nutrient-target #:date target-date))
203
Removed:
(check-true (nutrient-target? nt))
204
Removed:
(check-equal? (nutrient-target-target-date nt) target-date))
205
Removed:
206
Removed:
(test-case "Check all target values"
207
Removed:
(define nitrogen (get-nutrient #:name "Nitrogen"))
208
Removed:
(define phosphorus (get-nutrient #:name "Phosphorus"))
209
Removed:
210
Removed:
(define nt (get-nutrient-target #:date target-date))
211
Removed:
(check-equal? (get-nutrient-target-value nt nitrogen) 12.3)
212
Removed:
(check-equal? (get-nutrient-target-value nt phosphorus) 4.5)
213
Removed:
214
Removed:
(define ntv (nutrient-target-nutrient-values nt))
215
Removed:
(check-equal?
216
Removed:
(get-nutrient-target-values nt)
217
Removed:
ntv
218
Removed:
"return value of get-nutrient-target-values ≠ nutrient-target-values struct accessor")
219
Removed:
(check-equal? (hash-count ntv) 2)
220
Removed:
(check-equal? (hash-ref ntv nitrogen) 12.3)
221
Removed:
(check-equal? (hash-ref ntv phosphorus) 4.5))
222
Removed:
223
Removed:
(test-case "Retrieve latest target values"
224
Removed:
(define nitrogen (get-nutrient #:name "Nitrogen"))
225
Removed:
(define phosphorus (get-nutrient #:name "Phosphorus"))
226
Removed:
(define second-target-date "2025-09-02")
227
Removed:
(create-nutrient-target! second-target-date (hash nitrogen 6.7 phosphorus 8.9))
228
Removed:
229
Removed:
(check-equal? (get-latest-nutrient-target-value nitrogen) 6.7)
230
Removed:
(check-equal? (get-latest-nutrient-target-value phosphorus) 8.9))
231
Removed:
232
Removed:
(test-case "Delete target and cascade to target values"
233
Removed:
(define nt (get-nutrient-target #:date target-date))
234
Removed:
(delete-nutrient-target! nt)
235
Removed:
(check-false (get-nutrient-target #:id (nutrient-target-id nt)))
236
Removed:
(check-equal? (length (get-nutrient-targets))
237
Removed:
1
238
Removed:
"wrong number of nutrient targets were deleted")
239
Removed:
(check-true (hash-empty? (get-nutrient-target-values nt)))))))
views.rkt
@@ -134,29 +134,6 @@
134
134
"Ajouter un relevé"))
135
135
,table)))
136
136
137
Removed:
#;
138
Removed:
(define (ferti-targets-page latest-measurement-hash latest-target-hash)
139
Removed:
(define table
140
Removed:
`(table ((class "table"))
141
Removed:
(thead (tr (th "Nutriment")
142
Removed:
(th ((class "text-end")) "Dernier Relevé")
143
Removed:
(th ((class "text-end")) "Dernière Cible")))
144
Removed:
(tbody ,@(for/list ([n (get-nutrients)])
145
Removed:
(define latest-measurement (hash-ref latest-measurement-hash n #f))
146
Removed:
(define latest-target (hash-ref latest-target-hash n #f))
147
Removed:
`(tr (td ,(nutrient-french-name n))
148
Removed:
(td ((class "text-end font-monospace"))
149
Removed:
,(if latest-measurement
150
Removed:
(round 2 latest-measurement)
151
Removed:
"—"))
152
Removed:
(td ((class "text-end font-monospace"))
153
Removed:
,(if latest-target
154
Removed:
(round 2 latest-target)
155
Removed:
"—")))))))
156
Removed:
(ferti-template `((h2 () "Dernière Cible")
157
Removed:
(a ((class "btn btn-primary mb-3") [href "/ferti/target/new"]) "Créer une cible")
158
Removed:
,table)))
159
Removed:
160
137
(define (ferti-recipe-page recipe-date fertilizer-recipe)
161
138
(define table
162
139
`(table ((class "table"))