Compare commits
No commits in common. "main" and "production" have entirely different histories.
main
...
production
@ -7,4 +7,3 @@ public/packs
|
|||||||
node_modules/
|
node_modules/
|
||||||
.yarn
|
.yarn
|
||||||
.pnp.*
|
.pnp.*
|
||||||
.claude
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -37,4 +37,3 @@ yarn-debug.log*
|
|||||||
.yarn-integrity
|
.yarn-integrity
|
||||||
.yarn
|
.yarn
|
||||||
.pnp.*
|
.pnp.*
|
||||||
.claude
|
|
||||||
@ -1 +1 @@
|
|||||||
4.0.2
|
3.3.5
|
||||||
|
|||||||
73
CLAUDE.md
73
CLAUDE.md
@ -1,73 +0,0 @@
|
|||||||
# CLAUDE.md
|
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
Parsley is a personal recipe manager and meal planning app. It is a monolithic Rails 7.2 application with a Vue 3 SPA frontend.
|
|
||||||
|
|
||||||
**Backend:** Ruby 4.0.2, Rails 8.1.3, SQLite (dev/test), PostgreSQL (production)
|
|
||||||
**Frontend:** Vue 3 (Composition API / `<script setup>`), Pinia, Vue Router 4, Bulma CSS
|
|
||||||
**Asset bundling:** Shakapacker 8 (Webpack 5 + SWC)
|
|
||||||
**Testing:** RSpec, FactoryBot, Guard
|
|
||||||
|
|
||||||
## Common Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Development
|
|
||||||
foreman start # Start Rails + Shakapacker dev server together
|
|
||||||
|
|
||||||
# Dependencies
|
|
||||||
bundle install && yarn install
|
|
||||||
|
|
||||||
# Database
|
|
||||||
bundle exec rake db:create db:migrate db:seed
|
|
||||||
bundle exec rake db:reset # Drop, recreate, seed
|
|
||||||
|
|
||||||
# Tests
|
|
||||||
bundle exec rspec # All tests
|
|
||||||
bundle exec rspec spec/models/recipe_spec.rb # Single file
|
|
||||||
bundle exec rspec spec/models/recipe_spec.rb:42 # Single test by line
|
|
||||||
bundle exec guard # Watch mode
|
|
||||||
|
|
||||||
# Build
|
|
||||||
bundle exec rake shakapacker:compile # Compile assets for production
|
|
||||||
```
|
|
||||||
|
|
||||||
**Test environment note:** Set `FAST=true` to skip FactoryBot linting on startup.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
|
|
||||||
**Models follow an `Ingredient` abstraction:** `Recipe` and `Food` both inherit from an abstract `Ingredient` base class. This allows recipes to contain both foods and other recipes as ingredients (`RecipeIngredient` is a polymorphic join). Polymorphic ingredient IDs are encoded as strings: `F{id}` for foods, `R{id}` for recipes.
|
|
||||||
|
|
||||||
**Custom unit conversion library** (`lib/unit_conversion/`): The `UnitConversion` module handles all quantity parsing and conversion. It is density-aware (can convert volume ↔ mass if food density is known) and supports custom per-food units defined in `FoodUnit` records. Key entry points: `UnitConversion.parse`, `UnitConversion.auto_unit`, `UnitConversion.with_custom_units`.
|
|
||||||
|
|
||||||
**Recipe scaling:** Recipes can be scaled by a factor and converted between unit systems (standard/metric) and unit types (mass/volume). Cache keys incorporate scale and unit system: `recipes/{id}/{scale}/{system}/{unit}`. Memcache is optional; enable with `RAILS_USE_MEMCACHE=true`.
|
|
||||||
|
|
||||||
**USDA nutrition data:** The `UsdaImporter` (lib/) imports from `vendor/data/usda/` during `db:seed`. `UsdaFood`/`UsdaFoodWeight` tables are a read-only cache; `Food` records link to them and inherit nutritional data.
|
|
||||||
|
|
||||||
**Serializers** (`app/serializers/`) handle all JSON API responses.
|
|
||||||
|
|
||||||
### Frontend
|
|
||||||
|
|
||||||
Vue 3 components live in `app/javascript/components/`. They use `<script setup>` Composition API style. Pinia stores (`app/javascript/stores/`) hold global state. Vue Router (`app/javascript/router/`) manages navigation.
|
|
||||||
|
|
||||||
Page-level components are named `The*` (e.g., `TheRecipeList`, `TheFood`, `TheCalculator`). Shared UI primitives are named `App*` (e.g., `AppModal`, `AppAutocomplete`, `AppIcon`).
|
|
||||||
|
|
||||||
### Key Environment Variables
|
|
||||||
|
|
||||||
| Variable | Purpose |
|
|
||||||
|----------|---------|
|
|
||||||
| `RAILS_USE_MEMCACHE` | Enable Memcache caching |
|
|
||||||
| `RAILS_MEMCACHE_HOST` | Memcache host (default: `memcache`) |
|
|
||||||
| `PARSLEY_DB_HOST/USER/PG_PASSWORD/DB_NAME` | Production PostgreSQL config |
|
|
||||||
|
|
||||||
### Docker
|
|
||||||
|
|
||||||
`docker-compose up` starts PostgreSQL, Memcached, Nginx, and two Rails service instances. See `docker-compose.yml` and `Dockerfile`.
|
|
||||||
|
|
||||||
### Seed Data
|
|
||||||
|
|
||||||
Default dev user: username `dan`, password `qwerty`. Comes with sample recipes and foods.
|
|
||||||
@ -1,9 +1,8 @@
|
|||||||
FROM ruby:4.0.2-trixie
|
FROM ruby:3.3.5-bookworm
|
||||||
|
|
||||||
RUN curl -sL https://deb.nodesource.com/setup_lts.x | bash - && \
|
RUN curl -sL https://deb.nodesource.com/setup_lts.x | bash - && \
|
||||||
apt-get update && apt-get dist-upgrade -y && \
|
apt-get update && apt-get dist-upgrade -y && \
|
||||||
apt-get install -y \
|
apt-get install -y \
|
||||||
pkg-config \
|
|
||||||
nodejs \
|
nodejs \
|
||||||
nginx && \
|
nginx && \
|
||||||
apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/*
|
apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||||
|
|||||||
2
Gemfile
2
Gemfile
@ -1,6 +1,6 @@
|
|||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
gem 'rails', '8.1.3'
|
gem 'rails', '7.2.1'
|
||||||
gem 'pg', '~> 1.5.8'
|
gem 'pg', '~> 1.5.8'
|
||||||
|
|
||||||
gem 'shakapacker', '8.0.2'
|
gem 'shakapacker', '8.0.2'
|
||||||
|
|||||||
288
Gemfile.lock
288
Gemfile.lock
@ -1,112 +1,106 @@
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
action_text-trix (2.1.18)
|
actioncable (7.2.1)
|
||||||
railties
|
actionpack (= 7.2.1)
|
||||||
actioncable (8.1.3)
|
activesupport (= 7.2.1)
|
||||||
actionpack (= 8.1.3)
|
|
||||||
activesupport (= 8.1.3)
|
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
actionmailbox (8.1.3)
|
actionmailbox (7.2.1)
|
||||||
actionpack (= 8.1.3)
|
actionpack (= 7.2.1)
|
||||||
activejob (= 8.1.3)
|
activejob (= 7.2.1)
|
||||||
activerecord (= 8.1.3)
|
activerecord (= 7.2.1)
|
||||||
activestorage (= 8.1.3)
|
activestorage (= 7.2.1)
|
||||||
activesupport (= 8.1.3)
|
activesupport (= 7.2.1)
|
||||||
mail (>= 2.8.0)
|
mail (>= 2.8.0)
|
||||||
actionmailer (8.1.3)
|
actionmailer (7.2.1)
|
||||||
actionpack (= 8.1.3)
|
actionpack (= 7.2.1)
|
||||||
actionview (= 8.1.3)
|
actionview (= 7.2.1)
|
||||||
activejob (= 8.1.3)
|
activejob (= 7.2.1)
|
||||||
activesupport (= 8.1.3)
|
activesupport (= 7.2.1)
|
||||||
mail (>= 2.8.0)
|
mail (>= 2.8.0)
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
actionpack (8.1.3)
|
actionpack (7.2.1)
|
||||||
actionview (= 8.1.3)
|
actionview (= 7.2.1)
|
||||||
activesupport (= 8.1.3)
|
activesupport (= 7.2.1)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
rack (>= 2.2.4)
|
racc
|
||||||
|
rack (>= 2.2.4, < 3.2)
|
||||||
rack-session (>= 1.0.1)
|
rack-session (>= 1.0.1)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.6)
|
rails-html-sanitizer (~> 1.6)
|
||||||
useragent (~> 0.16)
|
useragent (~> 0.16)
|
||||||
actiontext (8.1.3)
|
actiontext (7.2.1)
|
||||||
action_text-trix (~> 2.1.15)
|
actionpack (= 7.2.1)
|
||||||
actionpack (= 8.1.3)
|
activerecord (= 7.2.1)
|
||||||
activerecord (= 8.1.3)
|
activestorage (= 7.2.1)
|
||||||
activestorage (= 8.1.3)
|
activesupport (= 7.2.1)
|
||||||
activesupport (= 8.1.3)
|
|
||||||
globalid (>= 0.6.0)
|
globalid (>= 0.6.0)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (8.1.3)
|
actionview (7.2.1)
|
||||||
activesupport (= 8.1.3)
|
activesupport (= 7.2.1)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.11)
|
erubi (~> 1.11)
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.6)
|
rails-html-sanitizer (~> 1.6)
|
||||||
activejob (8.1.3)
|
activejob (7.2.1)
|
||||||
activesupport (= 8.1.3)
|
activesupport (= 7.2.1)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (8.1.3)
|
activemodel (7.2.1)
|
||||||
activesupport (= 8.1.3)
|
activesupport (= 7.2.1)
|
||||||
activerecord (8.1.3)
|
activerecord (7.2.1)
|
||||||
activemodel (= 8.1.3)
|
activemodel (= 7.2.1)
|
||||||
activesupport (= 8.1.3)
|
activesupport (= 7.2.1)
|
||||||
timeout (>= 0.4.0)
|
timeout (>= 0.4.0)
|
||||||
activestorage (8.1.3)
|
activestorage (7.2.1)
|
||||||
actionpack (= 8.1.3)
|
actionpack (= 7.2.1)
|
||||||
activejob (= 8.1.3)
|
activejob (= 7.2.1)
|
||||||
activerecord (= 8.1.3)
|
activerecord (= 7.2.1)
|
||||||
activesupport (= 8.1.3)
|
activesupport (= 7.2.1)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
activesupport (8.1.3)
|
activesupport (7.2.1)
|
||||||
base64
|
base64
|
||||||
bigdecimal
|
bigdecimal
|
||||||
concurrent-ruby (~> 1.0, >= 1.3.1)
|
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||||
connection_pool (>= 2.2.5)
|
connection_pool (>= 2.2.5)
|
||||||
drb
|
drb
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
json
|
|
||||||
logger (>= 1.4.2)
|
logger (>= 1.4.2)
|
||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
securerandom (>= 0.3)
|
securerandom (>= 0.3)
|
||||||
tzinfo (~> 2.0, >= 2.0.5)
|
tzinfo (~> 2.0, >= 2.0.5)
|
||||||
uri (>= 0.13.1)
|
base64 (0.2.0)
|
||||||
base64 (0.3.0)
|
bcrypt (3.1.20)
|
||||||
bcrypt (3.1.22)
|
bigdecimal (3.1.8)
|
||||||
bigdecimal (4.1.2)
|
bootsnap (1.18.4)
|
||||||
bootsnap (1.23.0)
|
|
||||||
msgpack (~> 1.2)
|
msgpack (~> 1.2)
|
||||||
builder (3.3.0)
|
builder (3.3.0)
|
||||||
coderay (1.1.3)
|
coderay (1.1.3)
|
||||||
concurrent-ruby (1.3.6)
|
concurrent-ruby (1.3.4)
|
||||||
connection_pool (3.0.2)
|
connection_pool (2.4.1)
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
csv (3.3.5)
|
csv (3.3.0)
|
||||||
dalli (3.2.8)
|
dalli (3.2.8)
|
||||||
database_cleaner (2.0.2)
|
database_cleaner (2.0.2)
|
||||||
database_cleaner-active_record (>= 2, < 3)
|
database_cleaner-active_record (>= 2, < 3)
|
||||||
database_cleaner-active_record (2.2.2)
|
database_cleaner-active_record (2.2.0)
|
||||||
activerecord (>= 5.a)
|
activerecord (>= 5.a)
|
||||||
database_cleaner-core (~> 2.0)
|
database_cleaner-core (~> 2.0.0)
|
||||||
database_cleaner-core (2.0.1)
|
database_cleaner-core (2.0.1)
|
||||||
date (3.5.1)
|
date (3.3.4)
|
||||||
diff-lcs (1.6.2)
|
diff-lcs (1.5.1)
|
||||||
drb (2.2.3)
|
drb (2.2.1)
|
||||||
erb (6.0.3)
|
erubi (1.13.0)
|
||||||
erubi (1.13.1)
|
factory_bot (6.5.0)
|
||||||
factory_bot (6.5.6)
|
activesupport (>= 5.0.0)
|
||||||
activesupport (>= 6.1.0)
|
factory_bot_rails (6.4.3)
|
||||||
factory_bot_rails (6.4.4)
|
factory_bot (~> 6.4)
|
||||||
factory_bot (~> 6.5)
|
|
||||||
railties (>= 5.0.0)
|
railties (>= 5.0.0)
|
||||||
ffi (1.17.4)
|
ffi (1.17.0)
|
||||||
formatador (1.2.3)
|
formatador (1.1.0)
|
||||||
reline
|
globalid (1.2.1)
|
||||||
globalid (1.3.0)
|
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
guard (2.18.1)
|
guard (2.18.1)
|
||||||
formatador (>= 0.2.4)
|
formatador (>= 0.2.4)
|
||||||
@ -122,15 +116,12 @@ GEM
|
|||||||
guard (~> 2.1)
|
guard (~> 2.1)
|
||||||
guard-compat (~> 1.1)
|
guard-compat (~> 1.1)
|
||||||
rspec (>= 2.99.0, < 4.0)
|
rspec (>= 2.99.0, < 4.0)
|
||||||
i18n (1.14.8)
|
i18n (1.14.6)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
io-console (0.8.2)
|
io-console (0.7.2)
|
||||||
irb (1.17.0)
|
irb (1.14.1)
|
||||||
pp (>= 0.6.0)
|
|
||||||
prism (>= 1.3.0)
|
|
||||||
rdoc (>= 4.0.0)
|
rdoc (>= 4.0.0)
|
||||||
reline (>= 0.4.2)
|
reline (>= 0.4.2)
|
||||||
json (2.19.4)
|
|
||||||
kaminari (1.2.2)
|
kaminari (1.2.2)
|
||||||
activesupport (>= 4.1.0)
|
activesupport (>= 4.1.0)
|
||||||
kaminari-actionview (= 1.2.2)
|
kaminari-actionview (= 1.2.2)
|
||||||
@ -144,136 +135,124 @@ GEM
|
|||||||
kaminari-core (= 1.2.2)
|
kaminari-core (= 1.2.2)
|
||||||
kaminari-core (1.2.2)
|
kaminari-core (1.2.2)
|
||||||
liner (0.2.4)
|
liner (0.2.4)
|
||||||
listen (3.10.0)
|
listen (3.9.0)
|
||||||
logger
|
|
||||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||||
rb-inotify (~> 0.9, >= 0.9.10)
|
rb-inotify (~> 0.9, >= 0.9.10)
|
||||||
logger (1.7.0)
|
logger (1.6.1)
|
||||||
loofah (2.25.1)
|
loofah (2.22.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
lumberjack (1.4.2)
|
lumberjack (1.2.10)
|
||||||
mail (2.9.0)
|
mail (2.8.1)
|
||||||
logger
|
|
||||||
mini_mime (>= 0.1.1)
|
mini_mime (>= 0.1.1)
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
marcel (1.1.0)
|
marcel (1.0.4)
|
||||||
memoizable (0.5.1)
|
memoizable (0.4.2)
|
||||||
|
thread_safe (~> 0.3, >= 0.3.1)
|
||||||
method_source (1.1.0)
|
method_source (1.1.0)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.9)
|
mini_portile2 (2.8.7)
|
||||||
minitest (6.0.4)
|
minitest (5.25.1)
|
||||||
drb (~> 2.0)
|
msgpack (1.7.2)
|
||||||
prism (~> 1.5)
|
|
||||||
msgpack (1.8.0)
|
|
||||||
nenv (0.3.0)
|
nenv (0.3.0)
|
||||||
net-imap (0.6.3)
|
net-imap (0.4.16)
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
net-pop (0.1.2)
|
net-pop (0.1.2)
|
||||||
net-protocol
|
net-protocol
|
||||||
net-protocol (0.2.2)
|
net-protocol (0.2.2)
|
||||||
timeout
|
timeout
|
||||||
net-smtp (0.5.1)
|
net-smtp (0.5.0)
|
||||||
net-protocol
|
net-protocol
|
||||||
nio4r (2.7.5)
|
nio4r (2.7.3)
|
||||||
nokogiri (1.19.2)
|
nokogiri (1.16.7)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
notiffany (0.1.3)
|
notiffany (0.1.3)
|
||||||
nenv (~> 0.1)
|
nenv (~> 0.1)
|
||||||
shellany (~> 0.0)
|
shellany (~> 0.0)
|
||||||
oj (3.16.17)
|
oj (3.16.6)
|
||||||
bigdecimal (>= 3.0)
|
bigdecimal (>= 3.0)
|
||||||
ostruct (>= 0.2)
|
ostruct (>= 0.2)
|
||||||
ostruct (0.6.3)
|
ostruct (0.6.0)
|
||||||
package_json (0.2.0)
|
package_json (0.1.0)
|
||||||
parslet (2.0.0)
|
parslet (2.0.0)
|
||||||
pg (1.5.9)
|
pg (1.5.8)
|
||||||
pp (0.6.3)
|
pry (0.14.2)
|
||||||
prettyprint
|
|
||||||
prettyprint (0.2.0)
|
|
||||||
prism (1.9.0)
|
|
||||||
pry (0.16.0)
|
|
||||||
coderay (~> 1.1)
|
coderay (~> 1.1)
|
||||||
method_source (~> 1.0)
|
method_source (~> 1.0)
|
||||||
reline (>= 0.6.0)
|
psych (5.1.2)
|
||||||
psych (5.3.1)
|
|
||||||
date
|
|
||||||
stringio
|
stringio
|
||||||
puma (6.6.1)
|
puma (6.4.3)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
rack (3.2.6)
|
rack (3.1.7)
|
||||||
rack-proxy (0.7.7)
|
rack-proxy (0.7.7)
|
||||||
rack
|
rack
|
||||||
rack-session (2.1.2)
|
rack-session (2.0.0)
|
||||||
base64 (>= 0.1.0)
|
|
||||||
rack (>= 3.0.0)
|
rack (>= 3.0.0)
|
||||||
rack-test (2.2.0)
|
rack-test (2.1.0)
|
||||||
rack (>= 1.3)
|
rack (>= 1.3)
|
||||||
rackup (2.3.1)
|
rackup (2.1.0)
|
||||||
rack (>= 3)
|
rack (>= 3)
|
||||||
rails (8.1.3)
|
webrick (~> 1.8)
|
||||||
actioncable (= 8.1.3)
|
rails (7.2.1)
|
||||||
actionmailbox (= 8.1.3)
|
actioncable (= 7.2.1)
|
||||||
actionmailer (= 8.1.3)
|
actionmailbox (= 7.2.1)
|
||||||
actionpack (= 8.1.3)
|
actionmailer (= 7.2.1)
|
||||||
actiontext (= 8.1.3)
|
actionpack (= 7.2.1)
|
||||||
actionview (= 8.1.3)
|
actiontext (= 7.2.1)
|
||||||
activejob (= 8.1.3)
|
actionview (= 7.2.1)
|
||||||
activemodel (= 8.1.3)
|
activejob (= 7.2.1)
|
||||||
activerecord (= 8.1.3)
|
activemodel (= 7.2.1)
|
||||||
activestorage (= 8.1.3)
|
activerecord (= 7.2.1)
|
||||||
activesupport (= 8.1.3)
|
activestorage (= 7.2.1)
|
||||||
|
activesupport (= 7.2.1)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 8.1.3)
|
railties (= 7.2.1)
|
||||||
rails-controller-testing (1.0.5)
|
rails-controller-testing (1.0.5)
|
||||||
actionpack (>= 5.0.1.rc1)
|
actionpack (>= 5.0.1.rc1)
|
||||||
actionview (>= 5.0.1.rc1)
|
actionview (>= 5.0.1.rc1)
|
||||||
activesupport (>= 5.0.1.rc1)
|
activesupport (>= 5.0.1.rc1)
|
||||||
rails-dom-testing (2.3.0)
|
rails-dom-testing (2.2.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
minitest
|
minitest
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
rails-html-sanitizer (1.7.0)
|
rails-html-sanitizer (1.6.0)
|
||||||
loofah (~> 2.25)
|
loofah (~> 2.21)
|
||||||
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
nokogiri (~> 1.14)
|
||||||
railties (8.1.3)
|
railties (7.2.1)
|
||||||
actionpack (= 8.1.3)
|
actionpack (= 7.2.1)
|
||||||
activesupport (= 8.1.3)
|
activesupport (= 7.2.1)
|
||||||
irb (~> 1.13)
|
irb (~> 1.13)
|
||||||
rackup (>= 1.0.0)
|
rackup (>= 1.0.0)
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
thor (~> 1.0, >= 1.2.2)
|
thor (~> 1.0, >= 1.2.2)
|
||||||
tsort (>= 0.2)
|
|
||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
rake (13.4.2)
|
rake (13.2.1)
|
||||||
rb-fsevent (0.11.2)
|
rb-fsevent (0.11.2)
|
||||||
rb-inotify (0.11.1)
|
rb-inotify (0.11.1)
|
||||||
ffi (~> 1.0)
|
ffi (~> 1.0)
|
||||||
rdoc (7.2.0)
|
rdoc (6.7.0)
|
||||||
erb
|
|
||||||
psych (>= 4.0.0)
|
psych (>= 4.0.0)
|
||||||
tsort
|
redcarpet (3.6.0)
|
||||||
redcarpet (3.6.1)
|
reline (0.5.10)
|
||||||
reline (0.6.3)
|
|
||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
rspec (3.13.2)
|
rspec (3.13.0)
|
||||||
rspec-core (~> 3.13.0)
|
rspec-core (~> 3.13.0)
|
||||||
rspec-expectations (~> 3.13.0)
|
rspec-expectations (~> 3.13.0)
|
||||||
rspec-mocks (~> 3.13.0)
|
rspec-mocks (~> 3.13.0)
|
||||||
rspec-core (3.13.6)
|
rspec-core (3.13.1)
|
||||||
rspec-support (~> 3.13.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-expectations (3.13.5)
|
rspec-expectations (3.13.3)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.13.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-mocks (3.13.8)
|
rspec-mocks (3.13.1)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.13.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-rails (7.0.2)
|
rspec-rails (7.0.1)
|
||||||
actionpack (>= 7.0)
|
actionpack (>= 7.0)
|
||||||
activesupport (>= 7.0)
|
activesupport (>= 7.0)
|
||||||
railties (>= 7.0)
|
railties (>= 7.0)
|
||||||
@ -281,9 +260,9 @@ GEM
|
|||||||
rspec-expectations (~> 3.13)
|
rspec-expectations (~> 3.13)
|
||||||
rspec-mocks (~> 3.13)
|
rspec-mocks (~> 3.13)
|
||||||
rspec-support (~> 3.13)
|
rspec-support (~> 3.13)
|
||||||
rspec-support (3.13.7)
|
rspec-support (3.13.1)
|
||||||
securerandom (0.4.1)
|
securerandom (0.3.1)
|
||||||
semantic_range (3.1.1)
|
semantic_range (3.0.0)
|
||||||
shakapacker (8.0.2)
|
shakapacker (8.0.2)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
package_json
|
package_json
|
||||||
@ -292,28 +271,27 @@ GEM
|
|||||||
semantic_range (>= 2.3.0)
|
semantic_range (>= 2.3.0)
|
||||||
shellany (0.0.1)
|
shellany (0.0.1)
|
||||||
signed_multiset (0.2.1)
|
signed_multiset (0.2.1)
|
||||||
sqlite3 (2.1.1)
|
sqlite3 (2.1.0)
|
||||||
mini_portile2 (~> 2.8.0)
|
mini_portile2 (~> 2.8.0)
|
||||||
stringio (3.2.0)
|
stringio (3.1.1)
|
||||||
thor (1.5.0)
|
thor (1.3.2)
|
||||||
timeout (0.6.1)
|
thread_safe (0.3.6)
|
||||||
tsort (0.2.0)
|
timeout (0.4.1)
|
||||||
tzinfo (2.0.6)
|
tzinfo (2.0.6)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
tzinfo-data (1.2026.1)
|
tzinfo-data (1.2024.2)
|
||||||
tzinfo (>= 1.0.0)
|
tzinfo (>= 1.0.0)
|
||||||
unitwise (2.3.0)
|
unitwise (2.3.0)
|
||||||
liner (~> 0.2)
|
liner (~> 0.2)
|
||||||
memoizable (~> 0.4)
|
memoizable (~> 0.4)
|
||||||
parslet (~> 2.0)
|
parslet (~> 2.0)
|
||||||
signed_multiset (~> 0.2)
|
signed_multiset (~> 0.2)
|
||||||
uri (1.1.1)
|
useragent (0.16.10)
|
||||||
useragent (0.16.11)
|
webrick (1.8.2)
|
||||||
websocket-driver (0.8.0)
|
websocket-driver (0.7.6)
|
||||||
base64
|
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.5)
|
websocket-extensions (0.1.5)
|
||||||
zeitwerk (2.7.5)
|
zeitwerk (2.6.18)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
ruby
|
ruby
|
||||||
@ -331,7 +309,7 @@ DEPENDENCIES
|
|||||||
oj (~> 3.16.6)
|
oj (~> 3.16.6)
|
||||||
pg (~> 1.5.8)
|
pg (~> 1.5.8)
|
||||||
puma (~> 6.4)
|
puma (~> 6.4)
|
||||||
rails (= 8.1.3)
|
rails (= 7.2.1)
|
||||||
rails-controller-testing
|
rails-controller-testing
|
||||||
redcarpet (~> 3.6.0)
|
redcarpet (~> 3.6.0)
|
||||||
rspec-rails (~> 7.0.1)
|
rspec-rails (~> 7.0.1)
|
||||||
@ -341,4 +319,4 @@ DEPENDENCIES
|
|||||||
unitwise (~> 2.3.0)
|
unitwise (~> 2.3.0)
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
4.0.10
|
2.5.20
|
||||||
|
|||||||
@ -48,7 +48,7 @@ class LogsController < ApplicationController
|
|||||||
@log.source_recipe = @recipe
|
@log.source_recipe = @recipe
|
||||||
|
|
||||||
if @log.save
|
if @log.save
|
||||||
render json: { id: @log.id }
|
render json: { success: true }
|
||||||
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: { id: @recipe.id }
|
render json: { success: true }
|
||||||
else
|
else
|
||||||
render json: @recipe.errors, status: :unprocessable_entity
|
render json: @recipe.errors, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|||||||
@ -42,7 +42,7 @@ class TaskItemsController < ApplicationController
|
|||||||
new_status = !params[:invert].present?
|
new_status = !params[:invert].present?
|
||||||
TaskItem.transaction do
|
TaskItem.transaction do
|
||||||
@task_items = @task_list.task_items.find(ids)
|
@task_items = @task_list.task_items.find(ids)
|
||||||
@task_items.each { |i| i.update_column(:completed, new_status) }
|
@task_items.each { |i| i.update_attribute(:completed, new_status) }
|
||||||
end
|
end
|
||||||
|
|
||||||
TaskChannel.update_task_list(@task_list)
|
TaskChannel.update_task_list(@task_list)
|
||||||
|
|||||||
@ -8,27 +8,32 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|
||||||
import { ref, watch } from "vue";
|
import { ref } from "vue";
|
||||||
import debounce from "lodash/debounce";
|
import debounce from "lodash/debounce";
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:modelValue"]);
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
placeholder: {
|
placeholder: {
|
||||||
required: false,
|
required: false,
|
||||||
type: String,
|
type: String,
|
||||||
default: ""
|
default: ""
|
||||||
|
},
|
||||||
|
|
||||||
|
modelValue: {
|
||||||
|
required: false,
|
||||||
|
type: String,
|
||||||
|
default: ""
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const model = defineModel({ type: String, default: null });
|
const text = ref(null);
|
||||||
const text = ref(model.value);
|
|
||||||
|
|
||||||
watch(model, (val) => { text.value = val; });
|
|
||||||
|
|
||||||
const triggerInput = debounce(function() {
|
const triggerInput = debounce(function() {
|
||||||
model.value = text.value;
|
emit("update:modelValue", text.value);
|
||||||
},
|
},
|
||||||
250,
|
250,
|
||||||
{ leading: false, trailing: true });
|
{ leading: false, trailing: true })
|
||||||
|
|
||||||
function userUpdateText(newText) {
|
function userUpdateText(newText) {
|
||||||
if (text.value !== newText) {
|
if (text.value !== newText) {
|
||||||
@ -37,4 +42,10 @@ function userUpdateText(newText) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function propUpdateText(newText) {
|
||||||
|
if (text.value === null && text.value !== newText) {
|
||||||
|
text.value = newText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@ -87,10 +87,10 @@
|
|||||||
return api.getSearchIngredients(text);
|
return api.getSearchIngredients(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
function searchItemSelected(selectedIngredient) {
|
function searchItemSelected(ingredient) {
|
||||||
ingredient.value = selectedIngredient || null;
|
ingredient.value = ingredient || null;
|
||||||
ingredient_name.value = selectedIngredient.name || null;
|
ingredient_name.value = ingredient.name || null;
|
||||||
density.value = selectedIngredient.density || null;
|
density.value = ingredient.density || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getErrors(type) {
|
function getErrors(type) {
|
||||||
|
|||||||
@ -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="/foods">Cancel</router-link>
|
<router-link class="button is-secondary" to="/food">Cancel</router-link>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -8,6 +8,11 @@
|
|||||||
</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>
|
||||||
|
|
||||||
@ -46,7 +51,7 @@
|
|||||||
|
|
||||||
loadResource(
|
loadResource(
|
||||||
api.postLog(log)
|
api.postLog(log)
|
||||||
.then(data => router.push({ name: 'log', params: { id: data.id } }))
|
.then(() => router.push('/'))
|
||||||
.catch(Errors.onlyFor(Errors.ApiValidationError, err => validationErrors.value = err.validationErrors()))
|
.catch(Errors.onlyFor(Errors.ApiValidationError, err => validationErrors.value = err.validationErrors()))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,11 @@
|
|||||||
</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>
|
||||||
|
|
||||||
@ -40,7 +45,7 @@
|
|||||||
validationErrors.value = {};
|
validationErrors.value = {};
|
||||||
loadResource(
|
loadResource(
|
||||||
api.patchLog(log.value)
|
api.patchLog(log.value)
|
||||||
.then(() => router.push({ name: 'log', params: { id: log.value.id } }))
|
.then(() => router.push('/'))
|
||||||
.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(data => router.push({ name: 'recipe', params: { id: data.id } }))
|
.then(() => router.push('/'))
|
||||||
.catch(Errors.onlyFor(Errors.ApiValidationError, err => validationErrors.value = err.validationErrors()))
|
.catch(Errors.onlyFor(Errors.ApiValidationError, err => validationErrors.value = err.validationErrors()))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,10 +25,10 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<app-search-text placeholder="search names" :model-value="search.name" @update:modelValue="setSearchName($event)"></app-search-text>
|
<app-search-text placeholder="search names" :value="search.name" @update:modelValue="setSearchName($event)"></app-search-text>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<app-search-text placeholder="search tags" :model-value="search.tags" @update:modelValue="setSearchTags($event)"></app-search-text>
|
<app-search-text placeholder="search tags" :value="search.tags" @update:modelValue="setSearchTags($event)"></app-search-text>
|
||||||
</td>
|
</td>
|
||||||
<td colspan="5"></td>
|
<td colspan="5"></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@ -9,7 +9,7 @@ class User < ApplicationRecord
|
|||||||
validates :username, presence: true, uniqueness: { case_sensitive: false }
|
validates :username, presence: true, uniqueness: { case_sensitive: false }
|
||||||
|
|
||||||
def self.authenticate(username, password)
|
def self.authenticate(username, password)
|
||||||
find_by(username: username)&.authenticate(password)
|
find_by_username(username).try(:authenticate, password)
|
||||||
end
|
end
|
||||||
|
|
||||||
def display_name
|
def display_name
|
||||||
|
|||||||
@ -24,7 +24,7 @@ require_relative '../lib/unit_conversion'
|
|||||||
module Parsley
|
module Parsley
|
||||||
class Application < Rails::Application
|
class Application < Rails::Application
|
||||||
# Initialize configuration defaults for originally generated Rails version.
|
# Initialize configuration defaults for originally generated Rails version.
|
||||||
config.load_defaults 8.0
|
config.load_defaults 7.2
|
||||||
|
|
||||||
config.autoload_lib(ignore: %w(assets tasks unit_conversion unit_conversion.rb))
|
config.autoload_lib(ignore: %w(assets tasks unit_conversion unit_conversion.rb))
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
Rails.application.configure do
|
Rails.application.configure do
|
||||||
|
# Verifies that versions and hashed value of the package contents in the project's package.json
|
||||||
|
config.webpacker.check_yarn_integrity = false
|
||||||
# Settings specified here will take precedence over those in config/application.rb.
|
# Settings specified here will take precedence over those in config/application.rb.
|
||||||
|
|
||||||
# Code is not reloaded between requests.
|
# Code is not reloaded between requests.
|
||||||
config.enable_reloading = false
|
config.cache_classes = true
|
||||||
|
|
||||||
# Eager load code on boot. This eager loads most of Rails and
|
# Eager load code on boot. This eager loads most of Rails and
|
||||||
# your application in memory, allowing both threaded web servers
|
# your application in memory, allowing both threaded web servers
|
||||||
@ -66,8 +68,8 @@ Rails.application.configure do
|
|||||||
# the I18n.default_locale when a translation cannot be found).
|
# the I18n.default_locale when a translation cannot be found).
|
||||||
config.i18n.fallbacks = true
|
config.i18n.fallbacks = true
|
||||||
|
|
||||||
# Don't log any deprecations.
|
# Send deprecation notices to registered listeners.
|
||||||
config.active_support.report_deprecations = false
|
config.active_support.deprecation = :notify
|
||||||
|
|
||||||
# Use default logging formatter so that PID and timestamp are not suppressed.
|
# Use default logging formatter so that PID and timestamp are not suppressed.
|
||||||
config.log_formatter = ::Logger::Formatter.new
|
config.log_formatter = ::Logger::Formatter.new
|
||||||
|
|||||||
@ -2,7 +2,7 @@ Rails.application.configure do
|
|||||||
# Settings specified here will take precedence over those in config/application.rb.
|
# Settings specified here will take precedence over those in config/application.rb.
|
||||||
|
|
||||||
# Code is not reloaded between requests.
|
# Code is not reloaded between requests.
|
||||||
config.enable_reloading = false
|
config.cache_classes = true
|
||||||
|
|
||||||
# Eager load code on boot. This eager loads most of Rails and
|
# Eager load code on boot. This eager loads most of Rails and
|
||||||
# your application in memory, allowing both threaded web servers
|
# your application in memory, allowing both threaded web servers
|
||||||
@ -56,8 +56,8 @@ Rails.application.configure do
|
|||||||
# the I18n.default_locale when a translation cannot be found).
|
# the I18n.default_locale when a translation cannot be found).
|
||||||
config.i18n.fallbacks = true
|
config.i18n.fallbacks = true
|
||||||
|
|
||||||
# Don't log any deprecations.
|
# Send deprecation notices to registered listeners.
|
||||||
config.active_support.report_deprecations = false
|
config.active_support.deprecation = :notify
|
||||||
|
|
||||||
# Use default logging formatter so that PID and timestamp are not suppressed.
|
# Use default logging formatter so that PID and timestamp are not suppressed.
|
||||||
config.log_formatter = ::Logger::Formatter.new
|
config.log_formatter = ::Logger::Formatter.new
|
||||||
|
|||||||
184
db/schema.rb
184
db/schema.rb
@ -10,107 +10,107 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.1].define(version: 2018_09_15_134841) do
|
ActiveRecord::Schema[7.2].define(version: 2018_09_15_134841) do
|
||||||
create_table "food_units", force: :cascade do |t|
|
create_table "food_units", force: :cascade do |t|
|
||||||
t.integer "food_id", null: false
|
t.integer "food_id", null: false
|
||||||
t.decimal "gram_weight", precision: 10, scale: 2, null: false
|
|
||||||
t.string "name", null: false
|
t.string "name", null: false
|
||||||
|
t.decimal "gram_weight", precision: 10, scale: 2, null: false
|
||||||
t.index ["food_id"], name: "index_food_units_on_food_id"
|
t.index ["food_id"], name: "index_food_units_on_food_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "foods", force: :cascade do |t|
|
create_table "foods", force: :cascade do |t|
|
||||||
t.decimal "ash", precision: 10, scale: 2
|
|
||||||
t.integer "calcium"
|
|
||||||
t.decimal "carbohydrates", precision: 10, scale: 2
|
|
||||||
t.decimal "cholesterol", precision: 10, scale: 3
|
|
||||||
t.decimal "copper", precision: 10, scale: 3
|
|
||||||
t.datetime "created_at", precision: nil, null: false
|
|
||||||
t.string "density"
|
|
||||||
t.decimal "fiber", precision: 10, scale: 1
|
|
||||||
t.decimal "iron", precision: 10, scale: 2
|
|
||||||
t.integer "kcal"
|
|
||||||
t.decimal "lipids", precision: 10, scale: 2
|
|
||||||
t.integer "magnesium"
|
|
||||||
t.decimal "manganese", precision: 10, scale: 3
|
|
||||||
t.string "name"
|
t.string "name"
|
||||||
t.string "ndbn", limit: 25
|
t.string "density"
|
||||||
t.text "notes"
|
t.text "notes"
|
||||||
|
t.datetime "created_at", precision: nil, null: false
|
||||||
|
t.datetime "updated_at", precision: nil, null: false
|
||||||
|
t.string "ndbn", limit: 25
|
||||||
|
t.decimal "water", precision: 10, scale: 2
|
||||||
|
t.decimal "protein", precision: 10, scale: 2
|
||||||
|
t.decimal "lipids", precision: 10, scale: 2
|
||||||
|
t.decimal "ash", precision: 10, scale: 2
|
||||||
|
t.decimal "carbohydrates", precision: 10, scale: 2
|
||||||
|
t.integer "kcal"
|
||||||
|
t.decimal "fiber", precision: 10, scale: 1
|
||||||
|
t.decimal "sugar", precision: 10, scale: 2
|
||||||
|
t.integer "user_id"
|
||||||
|
t.integer "calcium"
|
||||||
|
t.decimal "iron", precision: 10, scale: 2
|
||||||
|
t.integer "magnesium"
|
||||||
t.integer "phosphorus"
|
t.integer "phosphorus"
|
||||||
t.integer "potassium"
|
t.integer "potassium"
|
||||||
t.decimal "protein", precision: 10, scale: 2
|
|
||||||
t.integer "sodium"
|
t.integer "sodium"
|
||||||
t.decimal "sugar", precision: 10, scale: 2
|
|
||||||
t.datetime "updated_at", precision: nil, null: false
|
|
||||||
t.integer "user_id"
|
|
||||||
t.integer "vit_a"
|
|
||||||
t.decimal "vit_b12", precision: 10, scale: 2
|
|
||||||
t.decimal "vit_b6", precision: 10, scale: 3
|
|
||||||
t.decimal "vit_c", precision: 10, scale: 1
|
|
||||||
t.decimal "vit_d", precision: 10, scale: 1
|
|
||||||
t.decimal "vit_e", precision: 10, scale: 2
|
|
||||||
t.decimal "vit_k", precision: 10, scale: 1
|
|
||||||
t.decimal "water", precision: 10, scale: 2
|
|
||||||
t.decimal "zinc", precision: 10, scale: 2
|
t.decimal "zinc", precision: 10, scale: 2
|
||||||
|
t.decimal "copper", precision: 10, scale: 3
|
||||||
|
t.decimal "manganese", precision: 10, scale: 3
|
||||||
|
t.decimal "vit_c", precision: 10, scale: 1
|
||||||
|
t.decimal "vit_b6", precision: 10, scale: 3
|
||||||
|
t.decimal "vit_b12", precision: 10, scale: 2
|
||||||
|
t.integer "vit_a"
|
||||||
|
t.decimal "vit_e", precision: 10, scale: 2
|
||||||
|
t.decimal "vit_d", precision: 10, scale: 1
|
||||||
|
t.decimal "vit_k", precision: 10, scale: 1
|
||||||
|
t.decimal "cholesterol", precision: 10, scale: 3
|
||||||
t.index ["ndbn"], name: "index_foods_on_ndbn"
|
t.index ["ndbn"], name: "index_foods_on_ndbn"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "logs", force: :cascade do |t|
|
create_table "logs", force: :cascade do |t|
|
||||||
t.datetime "created_at", precision: nil, null: false
|
t.integer "user_id"
|
||||||
t.datetime "date", precision: nil
|
|
||||||
t.text "notes"
|
|
||||||
t.integer "rating"
|
|
||||||
t.integer "recipe_id"
|
t.integer "recipe_id"
|
||||||
t.integer "source_recipe_id"
|
t.integer "source_recipe_id"
|
||||||
|
t.datetime "date", precision: nil
|
||||||
|
t.integer "rating"
|
||||||
|
t.datetime "created_at", precision: nil, null: false
|
||||||
t.datetime "updated_at", precision: nil, null: false
|
t.datetime "updated_at", precision: nil, null: false
|
||||||
t.integer "user_id"
|
t.text "notes"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "notes", force: :cascade do |t|
|
create_table "notes", force: :cascade do |t|
|
||||||
|
t.integer "user_id", null: false
|
||||||
t.text "content", null: false
|
t.text "content", null: false
|
||||||
t.datetime "created_at", precision: nil, null: false
|
t.datetime "created_at", precision: nil, null: false
|
||||||
t.datetime "updated_at", precision: nil, null: false
|
t.datetime "updated_at", precision: nil, null: false
|
||||||
t.integer "user_id", null: false
|
|
||||||
t.index ["user_id"], name: "index_notes_on_user_id"
|
t.index ["user_id"], name: "index_notes_on_user_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "recipe_ingredients", force: :cascade do |t|
|
create_table "recipe_ingredients", force: :cascade do |t|
|
||||||
t.datetime "created_at", precision: nil, null: false
|
|
||||||
t.integer "food_id"
|
t.integer "food_id"
|
||||||
t.string "name"
|
|
||||||
t.text "preparation"
|
|
||||||
t.string "quantity"
|
|
||||||
t.integer "recipe_as_ingredient_id"
|
|
||||||
t.integer "recipe_id"
|
t.integer "recipe_id"
|
||||||
|
t.string "name"
|
||||||
t.integer "sort_order"
|
t.integer "sort_order"
|
||||||
|
t.string "quantity"
|
||||||
t.string "units"
|
t.string "units"
|
||||||
|
t.datetime "created_at", precision: nil, null: false
|
||||||
t.datetime "updated_at", precision: nil, null: false
|
t.datetime "updated_at", precision: nil, null: false
|
||||||
|
t.text "preparation"
|
||||||
|
t.integer "recipe_as_ingredient_id"
|
||||||
t.index ["recipe_id"], name: "index_recipe_ingredients_on_recipe_id"
|
t.index ["recipe_id"], name: "index_recipe_ingredients_on_recipe_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "recipe_steps", force: :cascade do |t|
|
create_table "recipe_steps", force: :cascade do |t|
|
||||||
t.datetime "created_at", precision: nil, null: false
|
|
||||||
t.integer "recipe_id"
|
t.integer "recipe_id"
|
||||||
t.integer "sort_order"
|
t.integer "sort_order"
|
||||||
t.text "step"
|
t.text "step"
|
||||||
|
t.datetime "created_at", precision: nil, null: false
|
||||||
t.datetime "updated_at", precision: nil, null: false
|
t.datetime "updated_at", precision: nil, null: false
|
||||||
t.index ["recipe_id"], name: "index_recipe_steps_on_recipe_id"
|
t.index ["recipe_id"], name: "index_recipe_steps_on_recipe_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "recipes", force: :cascade do |t|
|
create_table "recipes", force: :cascade do |t|
|
||||||
|
t.string "name"
|
||||||
|
t.text "description"
|
||||||
|
t.text "source"
|
||||||
|
t.string "yields"
|
||||||
|
t.integer "total_time"
|
||||||
t.integer "active_time"
|
t.integer "active_time"
|
||||||
t.datetime "created_at", precision: nil, null: false
|
t.datetime "created_at", precision: nil, null: false
|
||||||
t.boolean "deleted"
|
|
||||||
t.text "description"
|
|
||||||
t.boolean "is_ingredient"
|
|
||||||
t.boolean "is_log"
|
|
||||||
t.string "name"
|
|
||||||
t.float "rating"
|
|
||||||
t.text "source"
|
|
||||||
t.text "step_text"
|
|
||||||
t.integer "total_time"
|
|
||||||
t.datetime "updated_at", precision: nil, null: false
|
t.datetime "updated_at", precision: nil, null: false
|
||||||
|
t.boolean "deleted"
|
||||||
t.integer "user_id"
|
t.integer "user_id"
|
||||||
t.string "yields"
|
t.boolean "is_log"
|
||||||
|
t.float "rating"
|
||||||
|
t.text "step_text"
|
||||||
|
t.boolean "is_ingredient"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "recipes_tags", id: false, force: :cascade do |t|
|
create_table "recipes_tags", id: false, force: :cascade do |t|
|
||||||
@ -121,94 +121,94 @@ ActiveRecord::Schema[8.1].define(version: 2018_09_15_134841) do
|
|||||||
end
|
end
|
||||||
|
|
||||||
create_table "tags", force: :cascade do |t|
|
create_table "tags", force: :cascade do |t|
|
||||||
t.datetime "created_at", precision: nil, null: false
|
|
||||||
t.string "lowercase_name"
|
|
||||||
t.string "name"
|
t.string "name"
|
||||||
|
t.string "lowercase_name"
|
||||||
|
t.datetime "created_at", precision: nil, null: false
|
||||||
t.datetime "updated_at", precision: nil, null: false
|
t.datetime "updated_at", precision: nil, null: false
|
||||||
t.index ["lowercase_name"], name: "index_tags_on_lowercase_name", unique: true
|
t.index ["lowercase_name"], name: "index_tags_on_lowercase_name", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "task_items", force: :cascade do |t|
|
create_table "task_items", force: :cascade do |t|
|
||||||
t.boolean "completed"
|
t.integer "task_list_id", null: false
|
||||||
t.datetime "created_at", precision: nil, null: false
|
|
||||||
t.string "name"
|
t.string "name"
|
||||||
t.string "quantity"
|
t.string "quantity"
|
||||||
t.integer "task_list_id", null: false
|
t.datetime "created_at", precision: nil, null: false
|
||||||
t.datetime "updated_at", precision: nil, null: false
|
t.datetime "updated_at", precision: nil, null: false
|
||||||
|
t.boolean "completed"
|
||||||
t.index ["task_list_id"], name: "index_task_items_on_task_list_id"
|
t.index ["task_list_id"], name: "index_task_items_on_task_list_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "task_lists", force: :cascade do |t|
|
create_table "task_lists", force: :cascade do |t|
|
||||||
t.datetime "created_at", precision: nil, null: false
|
|
||||||
t.string "name"
|
|
||||||
t.datetime "updated_at", precision: nil, null: false
|
|
||||||
t.integer "user_id", null: false
|
t.integer "user_id", null: false
|
||||||
|
t.string "name"
|
||||||
|
t.datetime "created_at", precision: nil, null: false
|
||||||
|
t.datetime "updated_at", precision: nil, null: false
|
||||||
t.index ["user_id"], name: "index_task_lists_on_user_id"
|
t.index ["user_id"], name: "index_task_lists_on_user_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "usda_food_weights", force: :cascade do |t|
|
create_table "usda_food_weights", force: :cascade do |t|
|
||||||
|
t.integer "usda_food_id", null: false
|
||||||
t.decimal "amount", precision: 7, scale: 3
|
t.decimal "amount", precision: 7, scale: 3
|
||||||
t.datetime "created_at", precision: nil, null: false
|
|
||||||
t.string "description"
|
t.string "description"
|
||||||
t.decimal "gram_weight", precision: 7, scale: 1
|
t.decimal "gram_weight", precision: 7, scale: 1
|
||||||
|
t.datetime "created_at", precision: nil, null: false
|
||||||
t.datetime "updated_at", precision: nil, null: false
|
t.datetime "updated_at", precision: nil, null: false
|
||||||
t.integer "usda_food_id", null: false
|
|
||||||
t.index ["usda_food_id"], name: "index_usda_food_weights_on_usda_food_id"
|
t.index ["usda_food_id"], name: "index_usda_food_weights_on_usda_food_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "usda_foods", force: :cascade do |t|
|
create_table "usda_foods", force: :cascade do |t|
|
||||||
|
t.string "ndbn", limit: 25
|
||||||
|
t.string "long_description"
|
||||||
|
t.string "short_description"
|
||||||
|
t.decimal "water", precision: 10, scale: 2
|
||||||
|
t.integer "kcal"
|
||||||
|
t.decimal "protein", precision: 10, scale: 2
|
||||||
|
t.decimal "lipid", precision: 10, scale: 2
|
||||||
t.decimal "ash", precision: 10, scale: 2
|
t.decimal "ash", precision: 10, scale: 2
|
||||||
t.integer "calcium"
|
|
||||||
t.decimal "carbohydrates", precision: 10, scale: 2
|
t.decimal "carbohydrates", precision: 10, scale: 2
|
||||||
t.decimal "cholesterol", precision: 10, scale: 3
|
|
||||||
t.decimal "copper", precision: 10, scale: 3
|
|
||||||
t.datetime "created_at", precision: nil, null: false
|
|
||||||
t.decimal "fiber", precision: 10, scale: 1
|
t.decimal "fiber", precision: 10, scale: 1
|
||||||
|
t.decimal "sugar", precision: 10, scale: 2
|
||||||
t.decimal "gram_weight_1", precision: 9, scale: 2
|
t.decimal "gram_weight_1", precision: 9, scale: 2
|
||||||
t.decimal "gram_weight_2", precision: 9, scale: 2
|
t.decimal "gram_weight_2", precision: 9, scale: 2
|
||||||
t.string "gram_weight_desc_1"
|
t.string "gram_weight_desc_1"
|
||||||
t.string "gram_weight_desc_2"
|
t.string "gram_weight_desc_2"
|
||||||
t.text "ingredients"
|
|
||||||
t.decimal "iron", precision: 10, scale: 2
|
|
||||||
t.integer "kcal"
|
|
||||||
t.decimal "lipid", precision: 10, scale: 2
|
|
||||||
t.string "long_description"
|
|
||||||
t.integer "magnesium"
|
|
||||||
t.decimal "manganese", precision: 10, scale: 3
|
|
||||||
t.string "manufacturer"
|
|
||||||
t.string "ndbn", limit: 25
|
|
||||||
t.string "nutrient_unit"
|
|
||||||
t.integer "phosphorus"
|
|
||||||
t.integer "potassium"
|
|
||||||
t.decimal "protein", precision: 10, scale: 2
|
|
||||||
t.string "refuse_description"
|
t.string "refuse_description"
|
||||||
t.integer "refuse_percent"
|
t.integer "refuse_percent"
|
||||||
t.string "scientific_name"
|
t.string "scientific_name"
|
||||||
t.string "short_description"
|
t.datetime "created_at", precision: nil, null: false
|
||||||
t.integer "sodium"
|
|
||||||
t.string "source"
|
|
||||||
t.decimal "sugar", precision: 10, scale: 2
|
|
||||||
t.datetime "updated_at", precision: nil, null: false
|
t.datetime "updated_at", precision: nil, null: false
|
||||||
t.integer "vit_a"
|
t.integer "calcium"
|
||||||
t.decimal "vit_b12", precision: 10, scale: 2
|
t.decimal "iron", precision: 10, scale: 2
|
||||||
t.decimal "vit_b6", precision: 10, scale: 3
|
t.integer "magnesium"
|
||||||
t.decimal "vit_c", precision: 10, scale: 1
|
t.integer "phosphorus"
|
||||||
t.decimal "vit_d", precision: 10, scale: 1
|
t.integer "potassium"
|
||||||
t.decimal "vit_e", precision: 10, scale: 2
|
t.integer "sodium"
|
||||||
t.decimal "vit_k", precision: 10, scale: 1
|
|
||||||
t.decimal "water", precision: 10, scale: 2
|
|
||||||
t.decimal "zinc", precision: 10, scale: 2
|
t.decimal "zinc", precision: 10, scale: 2
|
||||||
|
t.decimal "copper", precision: 10, scale: 3
|
||||||
|
t.decimal "manganese", precision: 10, scale: 3
|
||||||
|
t.decimal "vit_c", precision: 10, scale: 1
|
||||||
|
t.decimal "vit_b6", precision: 10, scale: 3
|
||||||
|
t.decimal "vit_b12", precision: 10, scale: 2
|
||||||
|
t.integer "vit_a"
|
||||||
|
t.decimal "vit_e", precision: 10, scale: 2
|
||||||
|
t.decimal "vit_d", precision: 10, scale: 1
|
||||||
|
t.decimal "vit_k", precision: 10, scale: 1
|
||||||
|
t.decimal "cholesterol", precision: 10, scale: 3
|
||||||
|
t.string "source"
|
||||||
|
t.string "manufacturer"
|
||||||
|
t.text "ingredients"
|
||||||
|
t.string "nutrient_unit"
|
||||||
t.index ["long_description"], name: "index_usda_foods_on_long_description"
|
t.index ["long_description"], name: "index_usda_foods_on_long_description"
|
||||||
t.index ["ndbn"], name: "index_usda_foods_on_ndbn"
|
t.index ["ndbn"], name: "index_usda_foods_on_ndbn"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "users", force: :cascade do |t|
|
create_table "users", force: :cascade do |t|
|
||||||
t.boolean "admin"
|
t.string "username"
|
||||||
t.datetime "created_at", precision: nil, null: false
|
|
||||||
t.string "email"
|
t.string "email"
|
||||||
t.string "full_name"
|
t.string "full_name"
|
||||||
t.string "password_digest"
|
t.string "password_digest"
|
||||||
|
t.boolean "admin"
|
||||||
|
t.datetime "created_at", precision: nil, null: false
|
||||||
t.datetime "updated_at", precision: nil, null: false
|
t.datetime "updated_at", precision: nil, null: false
|
||||||
t.string "username"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -378,62 +378,49 @@ class UsdaImporter
|
|||||||
end
|
end
|
||||||
|
|
||||||
build_enumerator(opened_files).each_slice(500) do |slice|
|
build_enumerator(opened_files).each_slice(500) do |slice|
|
||||||
now = Time.current
|
UsdaFood.transaction do
|
||||||
food_attrs = []
|
|
||||||
weight_groups = []
|
|
||||||
|
|
||||||
slice.each do |data|
|
slice.each do |data|
|
||||||
|
|
||||||
food = UsdaFood.new
|
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|
|
rows.each do |row|
|
||||||
if file_info[:map_into]
|
if file_info[:map_into]
|
||||||
w = {}
|
obj = food.send(file_info[:map_into]).build
|
||||||
file_info[:static]&.each { |k, v| w[k.to_s] = v }
|
end
|
||||||
file_info[:map].each { |db, col| w[db.to_s] = row[col] }
|
|
||||||
weight_hashes << w
|
if file_info[:static]
|
||||||
else
|
file_info[:static].each do |k, v|
|
||||||
file_info[:static]&.each { |k, v| food.send("#{k}=", v) }
|
obj.send("#{k}=", v)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if file_info[:map_function]
|
if file_info[:map_function]
|
||||||
file_info[:map_function].call(food, row)
|
file_info[:map_function].call(obj, row)
|
||||||
else
|
else
|
||||||
file_info[:map].each { |db, col| food.send("#{db}=", row[col]) }
|
file_info[:map].each do |db, col|
|
||||||
|
obj.send("#{db}=", row[col])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
attrs = food.attributes.except('id')
|
food.save!
|
||||||
attrs['created_at'] = now
|
|
||||||
attrs['updated_at'] = now
|
end
|
||||||
food_attrs << attrs
|
end
|
||||||
weight_groups << weight_hashes
|
|
||||||
end
|
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
|
|
||||||
UsdaFoodWeight.insert_all!(all_weights) if all_weights.any?
|
|
||||||
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').find_each do |i|
|
Food.where('ndbn != ?', '').where('ndbn IS NOT NULL').each do |i|
|
||||||
i.set_usda_food(i.usda_food)
|
i.set_usda_food(i.usda_food)
|
||||||
i.save!
|
i.save!
|
||||||
end
|
end
|
||||||
@ -457,7 +444,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.min_by { |d| d[:next_ndbn].to_i }[:next_ndbn]
|
current_ndbn = enumerate_data.select { |_, d| !d[:done] }.values.map { |d| d[:next_ndbn] }.min
|
||||||
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|
|
||||||
|
|||||||
@ -99,15 +99,9 @@ RSpec.describe NotesController, type: :controller do
|
|||||||
|
|
||||||
it 'redirects if note is not owned' do
|
it 'redirects if note is not owned' do
|
||||||
note = create(:note)
|
note = create(:note)
|
||||||
put :update, params: {id: note.to_param, note: valid_attributes, format: :html}, session: valid_session
|
put :update, params: {id: note.to_param, note: valid_attributes}, session: valid_session
|
||||||
expect(response).to redirect_to(root_path)
|
expect(response).to redirect_to(root_path)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns a 403 if note is not owned for json requests' do
|
|
||||||
note = create(:note)
|
|
||||||
put :update, params: {id: note.to_param, note: valid_attributes, format: :json}, session: valid_session
|
|
||||||
expect(response).to have_http_status 403
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context "with invalid params" do
|
context "with invalid params" do
|
||||||
|
|||||||
@ -3,45 +3,19 @@ require 'usda_importer'
|
|||||||
|
|
||||||
RSpec.describe UsdaImporter do
|
RSpec.describe UsdaImporter do
|
||||||
|
|
||||||
subject(:import) { UsdaImporter.new(Rails.root.join('spec', 'test_data')).import }
|
it 'imports' do
|
||||||
|
i = UsdaImporter.new(Rails.root.join('spec', 'test_data'))
|
||||||
it 'imports the correct number of foods with weights' do
|
i.import
|
||||||
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.find_by(ndbn: '45042066')
|
clif_bar = UsdaFood.where(ndbn: '45042066').first
|
||||||
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