Fix usability bugs and USDA importer performance
All checks were successful
parsley/pipeline/head This commit looks good
All checks were successful
parsley/pipeline/head This commit looks good
- Remove duplicate Save buttons in log creator/editor (fired with null data before recipe loaded) - Redirect to new resource after creating recipe/log instead of dropping back to list - Fix TheFoodCreator Cancel linking to dead route /food → /foods - Refactor AppSearchText to use defineModel; fix search box not initializing from URL - Fix TheCalculator variable shadowing bug (ingredient ref never updated on food select) - Refactor UsdaImporter to use insert_all! instead of per-record save! (~240k branded foods) - Fix string-based ndbn min comparison in build_enumerator (fragile on non-padded IDs) - Add CLAUDE.md with project overview and architecture notes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
df99f800b9
commit
233cea022a
@ -48,7 +48,7 @@ class LogsController < ApplicationController
|
|||||||
@log.source_recipe = @recipe
|
@log.source_recipe = @recipe
|
||||||
|
|
||||||
if @log.save
|
if @log.save
|
||||||
render json: { success: true }
|
render json: { id: @log.id }
|
||||||
else
|
else
|
||||||
render json: @log.errors, status: :unprocessable_entity
|
render json: @log.errors, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|||||||
@ -48,7 +48,7 @@ class RecipesController < ApplicationController
|
|||||||
@recipe.user = current_user
|
@recipe.user = current_user
|
||||||
|
|
||||||
if @recipe.save
|
if @recipe.save
|
||||||
render json: { success: true }
|
render json: { id: @recipe.id }
|
||||||
else
|
else
|
||||||
render json: @recipe.errors, status: :unprocessable_entity
|
render json: @recipe.errors, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
<food-edit :food="food" :validation-errors="validationErrors" action="Creating"></food-edit>
|
<food-edit :food="food" :validation-errors="validationErrors" action="Creating"></food-edit>
|
||||||
|
|
||||||
<button type="button" class="button is-primary" @click="save">Save</button>
|
<button type="button" class="button is-primary" @click="save">Save</button>
|
||||||
<router-link class="button is-secondary" to="/food">Cancel</router-link>
|
<router-link class="button is-secondary" to="/foods">Cancel</router-link>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -8,11 +8,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</log-edit>
|
</log-edit>
|
||||||
|
|
||||||
<div class="buttons">
|
|
||||||
<button type="button" class="button is-primary" @click="save">Save Log</button>
|
|
||||||
<router-link class="button is-secondary" to="/">Cancel</router-link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -51,7 +46,7 @@
|
|||||||
|
|
||||||
loadResource(
|
loadResource(
|
||||||
api.postLog(log)
|
api.postLog(log)
|
||||||
.then(() => router.push('/'))
|
.then(data => router.push({ name: 'log', params: { id: data.id } }))
|
||||||
.catch(Errors.onlyFor(Errors.ApiValidationError, err => validationErrors.value = err.validationErrors()))
|
.catch(Errors.onlyFor(Errors.ApiValidationError, err => validationErrors.value = err.validationErrors()))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,11 +8,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</log-edit>
|
</log-edit>
|
||||||
|
|
||||||
<div class="buttons">
|
|
||||||
<button type="button" class="button is-primary" @click="save">Save Log</button>
|
|
||||||
<router-link class="button is-secondary" to="/">Cancel</router-link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -45,7 +40,7 @@
|
|||||||
validationErrors.value = {};
|
validationErrors.value = {};
|
||||||
loadResource(
|
loadResource(
|
||||||
api.patchLog(log.value)
|
api.patchLog(log.value)
|
||||||
.then(() => router.push('/'))
|
.then(() => router.push({ name: 'log', params: { id: log.value.id } }))
|
||||||
.catch(Errors.onlyFor(Errors.ApiValidationError, err => validationErrors.value = err.validationErrors()))
|
.catch(Errors.onlyFor(Errors.ApiValidationError, err => validationErrors.value = err.validationErrors()))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,7 +42,7 @@
|
|||||||
validationErrors.value = {};
|
validationErrors.value = {};
|
||||||
loadResource(
|
loadResource(
|
||||||
api.postRecipe(recipe.value)
|
api.postRecipe(recipe.value)
|
||||||
.then(() => router.push('/'))
|
.then(data => router.push({ name: 'recipe', params: { id: data.id } }))
|
||||||
.catch(Errors.onlyFor(Errors.ApiValidationError, err => validationErrors.value = err.validationErrors()))
|
.catch(Errors.onlyFor(Errors.ApiValidationError, err => validationErrors.value = err.validationErrors()))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -378,49 +378,62 @@ class UsdaImporter
|
|||||||
end
|
end
|
||||||
|
|
||||||
build_enumerator(opened_files).each_slice(500) do |slice|
|
build_enumerator(opened_files).each_slice(500) do |slice|
|
||||||
UsdaFood.transaction do
|
now = Time.current
|
||||||
slice.each do |data|
|
food_attrs = []
|
||||||
|
weight_groups = []
|
||||||
|
|
||||||
food = UsdaFood.new
|
slice.each do |data|
|
||||||
|
food = UsdaFood.new
|
||||||
|
weight_hashes = []
|
||||||
|
|
||||||
data.each do |name, rows|
|
data.each do |name, rows|
|
||||||
file_info = FILES[name]
|
file_info = FILES[name]
|
||||||
obj = food
|
|
||||||
|
|
||||||
rows.each do |row|
|
|
||||||
if file_info[:map_into]
|
|
||||||
obj = food.send(file_info[:map_into]).build
|
|
||||||
end
|
|
||||||
|
|
||||||
if file_info[:static]
|
|
||||||
file_info[:static].each do |k, v|
|
|
||||||
obj.send("#{k}=", v)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
rows.each do |row|
|
||||||
|
if file_info[:map_into]
|
||||||
|
w = {}
|
||||||
|
file_info[:static]&.each { |k, v| w[k.to_s] = v }
|
||||||
|
file_info[:map].each { |db, col| w[db.to_s] = row[col] }
|
||||||
|
weight_hashes << w
|
||||||
|
else
|
||||||
|
file_info[:static]&.each { |k, v| food.send("#{k}=", v) }
|
||||||
if file_info[:map_function]
|
if file_info[:map_function]
|
||||||
file_info[:map_function].call(obj, row)
|
file_info[:map_function].call(food, row)
|
||||||
else
|
else
|
||||||
file_info[:map].each do |db, col|
|
file_info[:map].each { |db, col| food.send("#{db}=", row[col]) }
|
||||||
obj.send("#{db}=", row[col])
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
food.save!
|
attrs = food.attributes.except('id')
|
||||||
|
attrs['created_at'] = now
|
||||||
|
attrs['updated_at'] = now
|
||||||
|
food_attrs << attrs
|
||||||
|
weight_groups << weight_hashes
|
||||||
|
end
|
||||||
|
|
||||||
|
result = UsdaFood.insert_all!(food_attrs, returning: %w[id ndbn])
|
||||||
|
id_idx = result.columns.index('id')
|
||||||
|
ndbn_idx = result.columns.index('ndbn')
|
||||||
|
ndbn_to_id = result.rows.each_with_object({}) { |row, h| h[row[ndbn_idx]] = row[id_idx] }
|
||||||
|
|
||||||
|
all_weights = []
|
||||||
|
food_attrs.each_with_index do |fa, i|
|
||||||
|
food_id = ndbn_to_id[fa['ndbn']]
|
||||||
|
weight_groups[i].each do |w|
|
||||||
|
all_weights << w.merge('usda_food_id' => food_id, 'created_at' => now, 'updated_at' => now)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
UsdaFoodWeight.insert_all!(all_weights) if all_weights.any?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
ensure
|
ensure
|
||||||
opened_files.each { |k, v| v.close }
|
opened_files.each { |k, v| v.close }
|
||||||
sorted_files.each { |k, v| `rm #{v}` }
|
sorted_files.each { |k, v| `rm #{v}` }
|
||||||
end
|
end
|
||||||
|
|
||||||
Food.where('ndbn != ?', '').where('ndbn IS NOT NULL').each do |i|
|
Food.where('ndbn != ?', '').where('ndbn IS NOT NULL').find_each do |i|
|
||||||
i.set_usda_food(i.usda_food)
|
i.set_usda_food(i.usda_food)
|
||||||
i.save!
|
i.save!
|
||||||
end
|
end
|
||||||
@ -444,7 +457,7 @@ class UsdaImporter
|
|||||||
loop do
|
loop do
|
||||||
break if enumerate_data.values.all? { |d| d[:done] }
|
break if enumerate_data.values.all? { |d| d[:done] }
|
||||||
|
|
||||||
current_ndbn = enumerate_data.select { |_, d| !d[:done] }.values.map { |d| d[:next_ndbn] }.min
|
current_ndbn = enumerate_data.select { |_, d| !d[:done] }.values.min_by { |d| d[:next_ndbn].to_i }[:next_ndbn]
|
||||||
results = Hash.new { |hash, key| hash[key] = [] }
|
results = Hash.new { |hash, key| hash[key] = [] }
|
||||||
|
|
||||||
enumerate_data.each do |name, data|
|
enumerate_data.each do |name, data|
|
||||||
|
|||||||
@ -3,19 +3,45 @@ require 'usda_importer'
|
|||||||
|
|
||||||
RSpec.describe UsdaImporter do
|
RSpec.describe UsdaImporter do
|
||||||
|
|
||||||
it 'imports' do
|
subject(:import) { UsdaImporter.new(Rails.root.join('spec', 'test_data')).import }
|
||||||
i = UsdaImporter.new(Rails.root.join('spec', 'test_data'))
|
|
||||||
i.import
|
it 'imports the correct number of foods with weights' do
|
||||||
|
import
|
||||||
|
|
||||||
expect(UsdaFood.count).to eq 5
|
expect(UsdaFood.count).to eq 5
|
||||||
butter = UsdaFood.where(ndbn: '01001').first
|
|
||||||
|
butter = UsdaFood.find_by(ndbn: '01001')
|
||||||
expect(butter).not_to be_nil
|
expect(butter).not_to be_nil
|
||||||
expect(butter.usda_food_weights.count).to eq 4
|
expect(butter.usda_food_weights.count).to eq 4
|
||||||
|
|
||||||
clif_bar = UsdaFood.where(ndbn: '45042066').first
|
clif_bar = UsdaFood.find_by(ndbn: '45042066')
|
||||||
expect(clif_bar).not_to be_nil
|
expect(clif_bar).not_to be_nil
|
||||||
expect(clif_bar.usda_food_weights.count).to eq 1
|
expect(clif_bar.usda_food_weights.count).to eq 1
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'imports SR28 nutrition fields correctly' do
|
||||||
|
import
|
||||||
|
|
||||||
|
butter = UsdaFood.find_by(ndbn: '01001')
|
||||||
|
expect(butter.kcal).to eq 717
|
||||||
|
expect(butter.protein).to eq 0.85
|
||||||
|
expect(butter.source).to eq 'sr'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'imports branded nutrition fields via map_function' do
|
||||||
|
import
|
||||||
|
|
||||||
|
clif_bar = UsdaFood.find_by(ndbn: '45042066')
|
||||||
expect(clif_bar.kcal).to eq 368
|
expect(clif_bar.kcal).to eq 368
|
||||||
|
expect(clif_bar.protein).to eq 13.24
|
||||||
|
expect(clif_bar.source).to eq 'bf'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates linked Food records with usda nutrition data' do
|
||||||
|
food = create(:food, ndbn: '01001')
|
||||||
|
import
|
||||||
|
food.reload
|
||||||
|
expect(food.kcal).to eq 717
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user