front end work
This commit is contained in:
parent
47014118c8
commit
2fd83a5d3d
@ -95,11 +95,6 @@ class FoodsController < ApplicationController
|
|||||||
render :show
|
render :show
|
||||||
end
|
end
|
||||||
|
|
||||||
def prefetch
|
|
||||||
@foods = Food.all.order(:name)
|
|
||||||
render :search
|
|
||||||
end
|
|
||||||
|
|
||||||
def search
|
def search
|
||||||
@foods = Food.search(params[:query]).order(:name)
|
@foods = Food.search(params[:query]).order(:name)
|
||||||
end
|
end
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item" v-if="updateAvailable" href="#" @click.prevent="updateApp">UPDATE AVAILABLE!</a>
|
<a class="navbar-item" v-if="updateAvailable" href="#" @click.prevent="updateApp">UPDATE AVAILABLE!</a>
|
||||||
<router-link to="/" class="navbar-item">Recipes</router-link>
|
<router-link to="/" class="navbar-item">Recipes</router-link>
|
||||||
<router-link to="/ingredients" class="navbar-item">Ingredients</router-link>
|
<router-link to="/foods" class="navbar-item">Ingredients</router-link>
|
||||||
<router-link to="/calculator" class="navbar-item">Calculator</router-link>
|
<router-link to="/calculator" class="navbar-item">Calculator</router-link>
|
||||||
<router-link v-if="isLoggedIn" to="/logs" class="navbar-item">Log</router-link>
|
<router-link v-if="isLoggedIn" to="/logs" class="navbar-item">Log</router-link>
|
||||||
<router-link v-if="isLoggedIn" to="/notes" class="navbar-item">Notes</router-link>
|
<router-link v-if="isLoggedIn" to="/notes" class="navbar-item">Notes</router-link>
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="title">{{action}} {{ingredient.name || "[Unnamed Ingredient]"}}</h1>
|
<h1 class="title">{{action}} {{food.name || "[Unnamed Food]"}}</h1>
|
||||||
|
|
||||||
<app-validation-errors :errors="validationErrors"></app-validation-errors>
|
<app-validation-errors :errors="validationErrors"></app-validation-errors>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label is-small-mobile">Name</label>
|
<label class="label is-small-mobile">Name</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input type="text" class="input is-small-mobile" v-model="ingredient.name">
|
<input type="text" class="input is-small-mobile" v-model="food.name">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -15,13 +15,13 @@
|
|||||||
<label class="label is-small-mobile">Nutrient Databank Number</label>
|
<label class="label is-small-mobile">Nutrient Databank Number</label>
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<button type="button" class="button" :class="{'is-primary': hasNdbn}"><app-icon :icon="hasNdbn ? 'link-intact' : 'link-broken'" size="sm"></app-icon><span>{{ingredient.ndbn}}</span></button>
|
<button type="button" class="button" :class="{'is-primary': hasNdbn}"><app-icon :icon="hasNdbn ? 'link-intact' : 'link-broken'" size="sm"></app-icon><span>{{food.ndbn}}</span></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="control is-expanded">
|
<div class="control is-expanded">
|
||||||
<app-autocomplete
|
<app-autocomplete
|
||||||
:inputClass="'is-small-mobile'"
|
:inputClass="'is-small-mobile'"
|
||||||
ref="autocomplete"
|
ref="autocomplete"
|
||||||
v-model="ingredient.usda_food_name"
|
v-model="food.usda_food_name"
|
||||||
:minLength="2"
|
:minLength="2"
|
||||||
valueAttribute="name"
|
valueAttribute="name"
|
||||||
labelAttribute="description"
|
labelAttribute="description"
|
||||||
@ -39,14 +39,14 @@
|
|||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label is-small-mobile">Density</label>
|
<label class="label is-small-mobile">Density</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input type="text" class="input is-small-mobile" v-model="ingredient.density">
|
<input type="text" class="input is-small-mobile" v-model="food.density">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label is-small-mobile">Notes</label>
|
<label class="label is-small-mobile">Notes</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<textarea type="text" class="textarea is-small-mobile" v-model="ingredient.notes"></textarea>
|
<textarea type="text" class="textarea is-small-mobile" v-model="food.notes"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -66,7 +66,7 @@
|
|||||||
<th>Grams</th>
|
<th>Grams</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-for="unit in visibleIngredientUnits" :key="unit.id">
|
<tr v-for="unit in visibleFoodUnits" :key="unit.id">
|
||||||
<td>
|
<td>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input type="text" class="input is-small-mobile" v-model="unit.name">
|
<input type="text" class="input is-small-mobile" v-model="unit.name">
|
||||||
@ -99,7 +99,7 @@
|
|||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Grams</th>
|
<th>Grams</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-for="unit in ingredient.ndbn_units">
|
<tr v-for="unit in food.ndbn_units">
|
||||||
<td>{{unit.description}}</td>
|
<td>{{unit.description}}</td>
|
||||||
<td>{{unit.gram_weight}}</td>
|
<td>{{unit.gram_weight}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -121,7 +121,7 @@
|
|||||||
<label class="label is-small-mobile">{{nutrient.label}}</label>
|
<label class="label is-small-mobile">{{nutrient.label}}</label>
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
<div class="control is-expanded">
|
<div class="control is-expanded">
|
||||||
<input type="text" class="input is-small-mobile" :disabled="hasNdbn" v-model="ingredient[name]">
|
<input type="text" class="input is-small-mobile" :disabled="hasNdbn" v-model="food[name]">
|
||||||
</div>
|
</div>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<button type="button" tabindex="-1" class="unit-label button is-static is-small-mobile">{{nutrient.unit}}</button>
|
<button type="button" tabindex="-1" class="unit-label button is-static is-small-mobile">{{nutrient.unit}}</button>
|
||||||
@ -141,7 +141,7 @@
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
ingredient: {
|
food: {
|
||||||
required: true,
|
required: true,
|
||||||
type: Object
|
type: Object
|
||||||
},
|
},
|
||||||
@ -191,18 +191,18 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
visibleIngredientUnits() {
|
visibleFoodUnits() {
|
||||||
return this.ingredient.ingredient_units.filter(iu => iu._destroy !== true);
|
return this.food.food_units.filter(iu => iu._destroy !== true);
|
||||||
},
|
},
|
||||||
|
|
||||||
hasNdbn() {
|
hasNdbn() {
|
||||||
return this.ingredient.ndbn !== null;
|
return this.food.ndbn !== null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
addUnit() {
|
addUnit() {
|
||||||
this.ingredient.ingredient_units.push({
|
this.food.food_units.push({
|
||||||
id: null,
|
id: null,
|
||||||
name: null,
|
name: null,
|
||||||
gram_weight: null
|
gram_weight: null
|
||||||
@ -213,15 +213,15 @@
|
|||||||
if (unit.id) {
|
if (unit.id) {
|
||||||
unit._destroy = true;
|
unit._destroy = true;
|
||||||
} else {
|
} else {
|
||||||
const idx = this.ingredient.ingredient_units.findIndex(i => i === unit);
|
const idx = this.food.food_units.findIndex(i => i === unit);
|
||||||
this.ingredient.ingredient_units.splice(idx, 1);
|
this.food.food_units.splice(idx, 1);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
removeNdbn() {
|
removeNdbn() {
|
||||||
this.ingredient.ndbn = null;
|
this.food.ndbn = null;
|
||||||
this.ingredient.usda_food_name = null;
|
this.food.usda_food_name = null;
|
||||||
this.ingredient.ndbn_units = [];
|
this.food.ndbn_units = [];
|
||||||
},
|
},
|
||||||
|
|
||||||
updateSearchItems(text) {
|
updateSearchItems(text) {
|
||||||
@ -236,13 +236,13 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
searchItemSelected(food) {
|
searchItemSelected(food) {
|
||||||
this.ingredient.ndbn = food.ndbn;
|
this.food.ndbn = food.ndbn;
|
||||||
this.ingredient.usda_food_name = food.name;
|
this.food.usda_food_name = food.name;
|
||||||
this.ingredient.ndbn_units = [];
|
this.food.ndbn_units = [];
|
||||||
|
|
||||||
this.loadResource(
|
this.loadResource(
|
||||||
api.postIngredientSelectNdbn(this.ingredient)
|
api.postIngredientSelectNdbn(this.food)
|
||||||
.then(i => Object.assign(this.ingredient, i))
|
.then(i => Object.assign(this.food, i))
|
||||||
);
|
);
|
||||||
|
|
||||||
},
|
},
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
{{ingredient.name}}
|
{{food.name}}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
ingredient: {
|
food: {
|
||||||
required: true,
|
required: true,
|
||||||
type: Object
|
type: Object
|
||||||
}
|
}
|
@ -28,7 +28,7 @@
|
|||||||
</app-modal>
|
</app-modal>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<recipe-edit-ingredient-item v-for="(i, idx) in visibleIngredients" :key="i.id" :ingredient="i" :show-labels="idx === 0 || isMobile" @deleteIngredient="deleteIngredient"></recipe-edit-ingredient-item>
|
<recipe-edit-ingredient-item v-for="(i, idx) in visibleIngredients" :key="i.id" :ingredient="i" :show-labels="idx === 0 || isMobile" @deleteFood="deleteFood"></recipe-edit-ingredient-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="button" class="button is-primary" @click="addIngredient">Add Ingredient</button>
|
<button type="button" class="button is-primary" @click="addIngredient">Add Ingredient</button>
|
||||||
@ -117,11 +117,11 @@
|
|||||||
this.ingredients.push(this.createIngredient());
|
this.ingredients.push(this.createIngredient());
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteIngredient(ingredient) {
|
deleteFood(food) {
|
||||||
if (ingredient.id) {
|
if (food.id) {
|
||||||
ingredient._destroy = true;
|
food._destroy = true;
|
||||||
} else {
|
} else {
|
||||||
const idx = this.ingredients.findIndex(i => i === ingredient);
|
const idx = this.ingredients.findIndex(i => i === food);
|
||||||
this.ingredients.splice(idx, 1);
|
this.ingredients.splice(idx, 1);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow">
|
||||||
<span class="label is-small-mobile" v-if="showLabels"> </span>
|
<span class="label is-small-mobile" v-if="showLabels"> </span>
|
||||||
<button type="button" class="button is-danger is-small" @click="deleteIngredient(ingredient)">
|
<button type="button" class="button is-danger is-small" @click="deleteFood(ingredient)">
|
||||||
<app-icon icon="x" size="md"></app-icon>
|
<app-icon icon="x" size="md"></app-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -62,8 +62,8 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
deleteIngredient(ingredient) {
|
deleteFood(ingredient) {
|
||||||
this.$emit("deleteIngredient", ingredient);
|
this.$emit("deleteFood", ingredient);
|
||||||
},
|
},
|
||||||
|
|
||||||
updateSearchItems(text) {
|
updateSearchItems(text) {
|
||||||
|
@ -1,45 +1,45 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div v-if="ingredient === null">
|
<div v-if="food === null">
|
||||||
Loading...
|
Loading...
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<ingredient-show :ingredient="ingredient"></ingredient-show>
|
<food-show :food="food"></food-show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<router-link v-if="isLoggedIn" class="button" :to="{name: 'edit_ingredient', params: { id: ingredientId }}">Edit</router-link>
|
<router-link v-if="isLoggedIn" class="button" :to="{name: 'edit_food', params: { id: foodId }}">Edit</router-link>
|
||||||
<router-link class="button" to="/ingredients">Back</router-link>
|
<router-link class="button" to="/foods">Back</router-link>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
import IngredientShow from "./IngredientShow";
|
import FoodShow from "./FoodShow";
|
||||||
import { mapState } from "vuex";
|
import { mapState } from "vuex";
|
||||||
import api from "../lib/Api";
|
import api from "../lib/Api";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
ingredient: null
|
food: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
ingredientId: state => state.route.params.id,
|
foodId: state => state.route.params.id,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.loadResource(
|
this.loadResource(
|
||||||
api.getIngredient(this.ingredientId)
|
api.getFood(this.foodId)
|
||||||
.then(data => { this.ingredient = data; return data; })
|
.then(data => { this.food = data; return data; })
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
IngredientShow
|
FoodShow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,17 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
<ingredient-edit :ingredient="ingredient" :validation-errors="validationErrors" action="Creating"></ingredient-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="/ingredients">Cancel</router-link>
|
<router-link class="button is-secondary" to="/food">Cancel</router-link>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
import IngredientEdit from "./IngredientEdit";
|
import FoodEdit from "./FoodEdit";
|
||||||
import { mapState } from "vuex";
|
import { mapState } from "vuex";
|
||||||
import api from "../lib/Api";
|
import api from "../lib/Api";
|
||||||
import * as Errors from '../lib/Errors';
|
import * as Errors from '../lib/Errors';
|
||||||
@ -19,7 +19,7 @@
|
|||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
ingredient: {
|
food: {
|
||||||
name: null,
|
name: null,
|
||||||
notes: null,
|
notes: null,
|
||||||
ndbn: null,
|
ndbn: null,
|
||||||
@ -49,7 +49,7 @@
|
|||||||
vit_k: null,
|
vit_k: null,
|
||||||
cholesterol: null,
|
cholesterol: null,
|
||||||
lipids: null,
|
lipids: null,
|
||||||
ingredient_units: []
|
food_units: []
|
||||||
},
|
},
|
||||||
validationErrors: {}
|
validationErrors: {}
|
||||||
}
|
}
|
||||||
@ -59,15 +59,15 @@
|
|||||||
save() {
|
save() {
|
||||||
this.validationErrors = {}
|
this.validationErrors = {}
|
||||||
this.loadResource(
|
this.loadResource(
|
||||||
api.postIngredient(this.ingredient)
|
api.postFood(this.food)
|
||||||
.then(() => this.$router.push('/ingredients'))
|
.then(() => this.$router.push('/foods'))
|
||||||
.catch(Errors.onlyFor(Errors.ApiValidationError, err => this.validationErrors = err.validationErrors()))
|
.catch(Errors.onlyFor(Errors.ApiValidationError, err => this.validationErrors = err.validationErrors()))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
IngredientEdit
|
FoodEdit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,22 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
<div v-if="ingredient === null">
|
<div v-if="food === null">
|
||||||
Loading...
|
Loading...
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<ingredient-edit :ingredient="ingredient" :validation-errors="validationErrors"></ingredient-edit>
|
<food-edit :food="food" :validation-errors="validationErrors"></food-edit>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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="/ingredients">Cancel</router-link>
|
<router-link class="button is-secondary" to="/foods">Cancel</router-link>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
import IngredientEdit from "./IngredientEdit";
|
import FoodEdit from "./FoodEdit";
|
||||||
import { mapState } from "vuex";
|
import { mapState } from "vuex";
|
||||||
import api from "../lib/Api";
|
import api from "../lib/Api";
|
||||||
import * as Errors from '../lib/Errors';
|
import * as Errors from '../lib/Errors';
|
||||||
@ -24,14 +24,14 @@
|
|||||||
export default {
|
export default {
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
ingredient: null,
|
food: null,
|
||||||
validationErrors: {}
|
validationErrors: {}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
ingredientId: state => state.route.params.id,
|
foodId: state => state.route.params.id,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -39,8 +39,8 @@
|
|||||||
save() {
|
save() {
|
||||||
this.validationErrors = {};
|
this.validationErrors = {};
|
||||||
this.loadResource(
|
this.loadResource(
|
||||||
api.patchIngredient(this.ingredient)
|
api.patchFood(this.food)
|
||||||
.then(() => this.$router.push({name: 'ingredient', params: {id: this.ingredientId }}))
|
.then(() => this.$router.push({name: 'food', params: {id: this.foodId }}))
|
||||||
.catch(Errors.onlyFor(Errors.ApiValidationError, err => this.validationErrors = err.validationErrors()))
|
.catch(Errors.onlyFor(Errors.ApiValidationError, err => this.validationErrors = err.validationErrors()))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -48,13 +48,13 @@
|
|||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.loadResource(
|
this.loadResource(
|
||||||
api.getIngredient(this.ingredientId)
|
api.getFood(this.foodId)
|
||||||
.then(data => { this.ingredient = data; return data; })
|
.then(data => { this.food = data; return data; })
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
IngredientEdit
|
FoodEdit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3,10 +3,10 @@
|
|||||||
<h1 class="title">Ingredients</h1>
|
<h1 class="title">Ingredients</h1>
|
||||||
|
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<router-link v-if="isLoggedIn" :to="{name: 'new_ingredient'}" class="button is-primary">Create Ingredient</router-link>
|
<router-link v-if="isLoggedIn" :to="{name: 'new_food'}" class="button is-primary">Create Ingredient</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<app-pager :current-page="currentPage" :total-pages="totalPages" paged-item-name="ingredient" @changePage="changePage"></app-pager>
|
<app-pager :current-page="currentPage" :total-pages="totalPages" paged-item-name="food" @changePage="changePage"></app-pager>
|
||||||
|
|
||||||
<table class="table is-fullwidth is-narrow">
|
<table class="table is-fullwidth is-narrow">
|
||||||
<thead>
|
<thead>
|
||||||
@ -29,16 +29,16 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="i in ingredients" :key="i.id">
|
<tr v-for="i in foods" :key="i.id">
|
||||||
<td><router-link :to="{name: 'ingredient', params: { id: i.id } }">{{i.name}}</router-link></td>
|
<td><router-link :to="{name: 'food', params: { id: i.id } }">{{i.name}}</router-link></td>
|
||||||
<td><app-icon v-if="i.usda" icon="check"></app-icon></td>
|
<td><app-icon v-if="i.usda" icon="check"></app-icon></td>
|
||||||
<td>{{i.kcal}}</td>
|
<td>{{i.kcal}}</td>
|
||||||
<td>{{i.density}}</td>
|
<td>{{i.density}}</td>
|
||||||
<td>
|
<td>
|
||||||
<router-link v-if="isLoggedIn" class="button" :to="{name: 'edit_ingredient', params: { id: i.id } }">
|
<router-link v-if="isLoggedIn" class="button" :to="{name: 'edit_food', params: { id: i.id } }">
|
||||||
<app-icon icon="pencil"></app-icon>
|
<app-icon icon="pencil"></app-icon>
|
||||||
</router-link>
|
</router-link>
|
||||||
<button v-if="isLoggedIn" type="button" class="button is-danger" @click="deleteIngredient(i)">
|
<button v-if="isLoggedIn" type="button" class="button is-danger" @click="deleteFood(i)">
|
||||||
<app-icon icon="x"></app-icon>
|
<app-icon icon="x"></app-icon>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
@ -46,13 +46,13 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<app-pager :current-page="currentPage" :total-pages="totalPages" paged-item-name="ingredient" @changePage="changePage"></app-pager>
|
<app-pager :current-page="currentPage" :total-pages="totalPages" paged-item-name="food" @changePage="changePage"></app-pager>
|
||||||
|
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<router-link v-if="isLoggedIn" :to="{name: 'new_ingredient'}" class="button is-primary">Create Ingredient</router-link>
|
<router-link v-if="isLoggedIn" :to="{name: 'new_food'}" class="button is-primary">Create Ingredient</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<app-confirm :open="showConfirmIngredientDelete" :message="confirmIngredientDeleteMessage" :cancel="ingredientDeleteCancel" :confirm="ingredientDeleteConfirm"></app-confirm>
|
<app-confirm :open="showConfirmFoodDelete" :message="confirmFoodDeleteMessage" :cancel="foodDeleteCancel" :confirm="foodDeleteConfirm"></app-confirm>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -65,8 +65,8 @@
|
|||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
ingredientData: null,
|
foodData: null,
|
||||||
ingredientForDeletion: null,
|
foodForDeletion: null,
|
||||||
search: {
|
search: {
|
||||||
page: 1,
|
page: 1,
|
||||||
per: 25,
|
per: 25,
|
||||||
@ -76,35 +76,35 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
ingredients() {
|
foods() {
|
||||||
if (this.ingredientData) {
|
if (this.foodData) {
|
||||||
return this.ingredientData.ingredients;
|
return this.foodData.foods;
|
||||||
} else {
|
} else {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
totalPages() {
|
totalPages() {
|
||||||
if (this.ingredientData) {
|
if (this.foodData) {
|
||||||
return this.ingredientData.total_pages
|
return this.foodData.total_pages
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
currentPage() {
|
currentPage() {
|
||||||
if (this.ingredientData) {
|
if (this.foodData) {
|
||||||
return this.ingredientData.current_page
|
return this.foodData.current_page
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
showConfirmIngredientDelete() {
|
showConfirmFoodDelete() {
|
||||||
return this.ingredientForDeletion !== null;
|
return this.foodForDeletion !== null;
|
||||||
},
|
},
|
||||||
|
|
||||||
confirmIngredientDeleteMessage() {
|
confirmFoodDeleteMessage() {
|
||||||
if (this.ingredientForDeletion !== null) {
|
if (this.foodForDeletion !== null) {
|
||||||
return `Are you sure you want to delete ${this.ingredientForDeletion.name}?`;
|
return `Are you sure you want to delete ${this.foodForDeletion.name}?`;
|
||||||
} else {
|
} else {
|
||||||
return "??";
|
return "??";
|
||||||
}
|
}
|
||||||
@ -118,30 +118,30 @@
|
|||||||
|
|
||||||
getList: debounce(function() {
|
getList: debounce(function() {
|
||||||
return this.loadResource(
|
return this.loadResource(
|
||||||
api.getIngredientList(this.search.page, this.search.per, this.search.name)
|
api.getFoodList(this.search.page, this.search.per, this.search.name)
|
||||||
.then(data => this.ingredientData = data)
|
.then(data => this.foodData = data)
|
||||||
);
|
);
|
||||||
}, 500, {leading: true, trailing: true}),
|
}, 500, {leading: true, trailing: true}),
|
||||||
|
|
||||||
deleteIngredient(ingredient) {
|
deleteFood(food) {
|
||||||
this.ingredientForDeletion = ingredient;
|
this.foodForDeletion = food;
|
||||||
},
|
},
|
||||||
|
|
||||||
ingredientDeleteCancel() {
|
foodDeleteCancel() {
|
||||||
this.ingredientForDeletion = null;
|
this.foodForDeletion = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
ingredientDeleteConfirm() {
|
foodDeleteConfirm() {
|
||||||
if (this.ingredientForDeletion !== null) {
|
if (this.foodForDeletion !== null) {
|
||||||
this.loadResource(
|
this.loadResource(
|
||||||
api.deleteIngredient(this.ingredientForDeletion.id).then(res => {
|
api.deleteFood(this.foodForDeletion.id).then(res => {
|
||||||
this.ingredientForDeletion = null;
|
this.foodForDeletion = null;
|
||||||
return this.getList();
|
return this.getList();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log("This is where the thing happens!!");
|
console.log("This is where the thing happens!!");
|
||||||
this.ingredientForDeletion = null;
|
this.foodForDeletion = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
@ -213,66 +213,66 @@ class Api {
|
|||||||
return this.get("/calculator/calculate", params);
|
return this.get("/calculator/calculate", params);
|
||||||
}
|
}
|
||||||
|
|
||||||
getIngredientList(page, per, name) {
|
getFoodList(page, per, name) {
|
||||||
const params = {
|
const params = {
|
||||||
page,
|
page,
|
||||||
per,
|
per,
|
||||||
name
|
name
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.get("/ingredients/", params);
|
return this.get("/foods/", params);
|
||||||
}
|
}
|
||||||
|
|
||||||
getIngredient(id) {
|
getFood(id) {
|
||||||
return this.get("/ingredients/" + id);
|
return this.get("/foods/" + id);
|
||||||
}
|
}
|
||||||
|
|
||||||
buildIngredientParams(ingredient) {
|
buildFoodParams(food) {
|
||||||
return {
|
return {
|
||||||
ingredient: {
|
food: {
|
||||||
name: ingredient.name,
|
name: food.name,
|
||||||
notes: ingredient.notes,
|
notes: food.notes,
|
||||||
ndbn: ingredient.ndbn,
|
ndbn: food.ndbn,
|
||||||
density: ingredient.density,
|
density: food.density,
|
||||||
|
|
||||||
water: ingredient.water,
|
water: food.water,
|
||||||
ash: ingredient.ash,
|
ash: food.ash,
|
||||||
protein: ingredient.protein,
|
protein: food.protein,
|
||||||
kcal: ingredient.kcal,
|
kcal: food.kcal,
|
||||||
fiber: ingredient.fiber,
|
fiber: food.fiber,
|
||||||
sugar: ingredient.sugar,
|
sugar: food.sugar,
|
||||||
carbohydrates: ingredient.carbohydrates,
|
carbohydrates: food.carbohydrates,
|
||||||
calcium: ingredient.calcium,
|
calcium: food.calcium,
|
||||||
iron: ingredient.iron,
|
iron: food.iron,
|
||||||
magnesium: ingredient.magnesium,
|
magnesium: food.magnesium,
|
||||||
phosphorus: ingredient.phosphorus,
|
phosphorus: food.phosphorus,
|
||||||
potassium: ingredient.potassium,
|
potassium: food.potassium,
|
||||||
sodium: ingredient.sodium,
|
sodium: food.sodium,
|
||||||
zinc: ingredient.zinc,
|
zinc: food.zinc,
|
||||||
copper: ingredient.copper,
|
copper: food.copper,
|
||||||
manganese: ingredient.manganese,
|
manganese: food.manganese,
|
||||||
vit_c: ingredient.vit_c,
|
vit_c: food.vit_c,
|
||||||
vit_b6: ingredient.vit_b6,
|
vit_b6: food.vit_b6,
|
||||||
vit_b12: ingredient.vit_b12,
|
vit_b12: food.vit_b12,
|
||||||
vit_a: ingredient.vit_a,
|
vit_a: food.vit_a,
|
||||||
vit_e: ingredient.vit_e,
|
vit_e: food.vit_e,
|
||||||
vit_d: ingredient.vit_d,
|
vit_d: food.vit_d,
|
||||||
vit_k: ingredient.vit_k,
|
vit_k: food.vit_k,
|
||||||
cholesterol: ingredient.cholesterol,
|
cholesterol: food.cholesterol,
|
||||||
lipids: ingredient.lipids,
|
lipids: food.lipids,
|
||||||
|
|
||||||
|
|
||||||
ingredient_units_attributes: ingredient.ingredient_units.map(iu => {
|
food_units_attributes: food.food_units.map(fu => {
|
||||||
if (iu._destroy) {
|
if (fu._destroy) {
|
||||||
return {
|
return {
|
||||||
id: iu.id,
|
id: fu.id,
|
||||||
_destroy: true
|
_destroy: true
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
id: iu.id,
|
id: fu.id,
|
||||||
name: iu.name,
|
name: fu.name,
|
||||||
gram_weight: iu.gram_weight
|
gram_weight: fu.gram_weight
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -280,25 +280,25 @@ class Api {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
postIngredient(ingredient) {
|
postFood(food) {
|
||||||
return this.post("/ingredients/", this.buildIngredientParams(ingredient));
|
return this.post("/foods/", this.buildFoodParams(food));
|
||||||
}
|
}
|
||||||
|
|
||||||
patchIngredient(ingredient) {
|
patchFood(food) {
|
||||||
return this.patch("/ingredients/" + ingredient.id, this.buildIngredientParams(ingredient));
|
return this.patch("/foods/" + food.id, this.buildFoodParams(food));
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteIngredient(id) {
|
deleteFood(id) {
|
||||||
return this.del("/ingredients/" + id);
|
return this.del("/foods/" + id);
|
||||||
}
|
}
|
||||||
|
|
||||||
postIngredientSelectNdbn(ingredient) {
|
postIngredientSelectNdbn(ingredient) {
|
||||||
const url = ingredient.id ? "/ingredients/" + ingredient.id + "/select_ndbn" : "/ingredients/select_ndbn";
|
const url = ingredient.id ? "/foods/" + ingredient.id + "/select_ndbn" : "/foods/select_ndbn";
|
||||||
return this.post(url, this.buildIngredientParams(ingredient));
|
return this.post(url, this.buildFoodParams(ingredient));
|
||||||
}
|
}
|
||||||
|
|
||||||
getUsdaFoodSearch(query) {
|
getUsdaFoodSearch(query) {
|
||||||
return this.get("/ingredients/usda_food_search", {query: query});
|
return this.get("/foods/usda_food_search", {query: query});
|
||||||
}
|
}
|
||||||
|
|
||||||
getNoteList() {
|
getNoteList() {
|
||||||
|
@ -10,10 +10,10 @@ import TheLogList from './components/TheLogList';
|
|||||||
import TheLogCreator from './components/TheLogCreator';
|
import TheLogCreator from './components/TheLogCreator';
|
||||||
import TheLogEditor from './components/TheLogEditor';
|
import TheLogEditor from './components/TheLogEditor';
|
||||||
|
|
||||||
import TheIngredientList from './components/TheIngredientList';
|
import TheFoodList from './components/TheFoodList';
|
||||||
import TheIngredient from "./components/TheIngredient";
|
import TheFood from "./components/TheFood";
|
||||||
import TheIngredientEditor from "./components/TheIngredientEditor";
|
import TheFoodEditor from "./components/TheFoodEditor";
|
||||||
import TheIngredientCreator from "./components/TheIngredientCreator";
|
import TheFoodCreator from "./components/TheFoodCreator";
|
||||||
import TheNotesList from './components/TheNotesList';
|
import TheNotesList from './components/TheNotesList';
|
||||||
import TheRecipe from './components/TheRecipe';
|
import TheRecipe from './components/TheRecipe';
|
||||||
import TheRecipeEditor from './components/TheRecipeEditor';
|
import TheRecipeEditor from './components/TheRecipeEditor';
|
||||||
@ -71,24 +71,24 @@ router.addRoutes(
|
|||||||
component: TheCalculator
|
component: TheCalculator
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/ingredients",
|
path: "/foods",
|
||||||
name: "ingredients",
|
name: "foods",
|
||||||
component: TheIngredientList
|
component: TheFoodList
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/ingredients/new",
|
path: "/foods/new",
|
||||||
name: "new_ingredient",
|
name: "new_food",
|
||||||
component: TheIngredientCreator
|
component: TheFoodCreator
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/ingredients/:id/edit",
|
path: "/foods/:id/edit",
|
||||||
name: "edit_ingredient",
|
name: "edit_food",
|
||||||
component: TheIngredientEditor
|
component: TheFoodEditor
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/ingredients/:id",
|
path: "/foods/:id",
|
||||||
name: "ingredient",
|
name: "food",
|
||||||
component: TheIngredient
|
component: TheFood
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/logs",
|
path: "/logs",
|
||||||
|
@ -30,14 +30,18 @@ class Food < ApplicationRecord
|
|||||||
units
|
units
|
||||||
end
|
end
|
||||||
|
|
||||||
def nutrition_per_100g
|
def nutrition_data
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
def nutrition_per_100g_errors
|
def nutrition_errors
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def nutrition_unit
|
||||||
|
UnitConversion.parse('100 grams')
|
||||||
|
end
|
||||||
|
|
||||||
def ndbn=(value)
|
def ndbn=(value)
|
||||||
@usda_food = nil
|
@usda_food = nil
|
||||||
super
|
super
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
class IngredientProxy
|
|
||||||
|
|
||||||
attr_reader :ingredient
|
|
||||||
|
|
||||||
def initialize(food)
|
|
||||||
@food = food
|
|
||||||
end
|
|
||||||
|
|
||||||
def name
|
|
||||||
@food.name
|
|
||||||
end
|
|
||||||
|
|
||||||
def density
|
|
||||||
@food.density
|
|
||||||
end
|
|
||||||
|
|
||||||
def density?
|
|
||||||
@food.density.present?
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_custom_unit_equivalent(custom_unit_name)
|
|
||||||
@food.custom_unit_weight(custom_unit_name)
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
@ -36,28 +36,31 @@ class NutritionData
|
|||||||
self.instance_variable_set("@#{n}".to_sym, 0.0)
|
self.instance_variable_set("@#{n}".to_sym, 0.0)
|
||||||
end
|
end
|
||||||
|
|
||||||
valid_ingredients = []
|
|
||||||
|
|
||||||
recipe_ingredients.each do |i|
|
recipe_ingredients.each do |i|
|
||||||
if i.ingredient_id.nil?
|
if i.ingredient.nil? || i.ingredient.nutrition_data.nil?
|
||||||
@errors << "#{i.name} has no nutrition data"
|
@errors << "#{i.name} has no nutrition data"
|
||||||
elsif !i.can_convert_to_grams?
|
next
|
||||||
@errors << "#{i.name} can't be converted to grams"
|
end
|
||||||
else
|
|
||||||
valid_ingredients << i
|
nutrient_scale = i.calculate_nutrition_ratio
|
||||||
|
|
||||||
|
if nutrient_scale.nil?
|
||||||
|
@errors << "#{i.name} has an unknown quantity or unit"
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
unless i.ingredient.nutrition_errors.empty?
|
||||||
|
@errors << "#{i.name} has errors: #{i.ingredient.nutrition_errors.join(", ")}"
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
valid_ingredients.each do |i|
|
|
||||||
grams = i.to_grams
|
|
||||||
missing = []
|
missing = []
|
||||||
|
|
||||||
NUTRIENTS.each do |k, n|
|
NUTRIENTS.each do |k, n|
|
||||||
value = i.food.send(k)
|
value = i.ingredient.nutrition_data.send(k)
|
||||||
if value.present?
|
if value.present?
|
||||||
value = value.to_f
|
value = value.to_f
|
||||||
running_total = self.instance_variable_get("@#{k}".to_sym)
|
running_total = self.instance_variable_get("@#{k}".to_sym)
|
||||||
delta = (grams / 100.0) * value
|
delta = value * nutrient_scale
|
||||||
self.instance_variable_set("@#{k}".to_sym, running_total + delta)
|
self.instance_variable_set("@#{k}".to_sym, running_total + delta)
|
||||||
else
|
else
|
||||||
missing << k
|
missing << k
|
||||||
|
@ -76,7 +76,7 @@ class Recipe < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def yields_list
|
def yields_list
|
||||||
@yields_list ||= self.yields.to_s.split(',').concat(['1 recipe']).map { |y| y.strip }.select { |y| y.present? }.map do |y|
|
@yields_list ||= self.yields.to_s.split(',').concat(['1 each']).map { |y| y.strip }.select { |y| y.present? }.map do |y|
|
||||||
begin
|
begin
|
||||||
UnitConversion::parse(y)
|
UnitConversion::parse(y)
|
||||||
rescue UntConversion::UnparseableUnitError
|
rescue UntConversion::UnparseableUnitError
|
||||||
@ -106,6 +106,10 @@ class Recipe < ApplicationRecord
|
|||||||
@nutrition_data
|
@nutrition_data
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def nutrition_errors
|
||||||
|
nutrition_data.errors
|
||||||
|
end
|
||||||
|
|
||||||
def update_rating!
|
def update_rating!
|
||||||
self.rating = Log.for_recipe(self).for_user(self.user_id).where('rating IS NOT NULL').average(:rating)
|
self.rating = Log.for_recipe(self).for_user(self.user_id).where('rating IS NOT NULL').average(:rating)
|
||||||
save(validate: false)
|
save(validate: false)
|
||||||
@ -142,23 +146,30 @@ class Recipe < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def custom_units
|
def custom_units
|
||||||
arbitrary = self.yields_list.select { |y| !y.mass? && !y.volume }
|
arbitrary = self.yields_list.select { |y| !y.mass? && !y.volume? }
|
||||||
mass = self.yields_list.select { |y| y.mass? }
|
mass = self.yields_list.select { |y| y.mass? }
|
||||||
volume = self.yields_list.select { |y| y.volume? }
|
volume = self.yields_list.select { |y| y.volume? }
|
||||||
|
|
||||||
primary_unit = mass.first || volume.first
|
primary_unit = mass.first || volume.first
|
||||||
|
|
||||||
Hash[arbitrary.map { |y| [y.unit.unit, primary_unit] }]
|
|
||||||
|
|
||||||
Hash[yields_list.select { |y| !y.mass? && !y.volume? && y.unit }.map { [y.unit.unit, y] }]
|
if primary_unit
|
||||||
|
cus = {}
|
||||||
|
arbitrary.each do |y|
|
||||||
|
ratio = 1
|
||||||
|
if y.value.value != 1
|
||||||
|
ratio = 1.0 / y.value.value
|
||||||
|
end
|
||||||
|
|
||||||
|
cus[y.unit.unit] = primary_unit.scale(ratio)
|
||||||
|
end
|
||||||
|
cus
|
||||||
|
else
|
||||||
|
{}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def nutrition_per_100g
|
def nutrition_unit
|
||||||
|
UnitConversion.parse('1 each')
|
||||||
end
|
|
||||||
|
|
||||||
def nutrition_per_100g_errors
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.for_criteria(criteria)
|
def self.for_criteria(criteria)
|
||||||
|
@ -100,7 +100,7 @@ class RecipeIngredient < ApplicationRecord
|
|||||||
density = UnitConversion.parse(ingredient.density)
|
density = UnitConversion.parse(ingredient.density)
|
||||||
if density.density?
|
if density.density?
|
||||||
value_unit = UnitConversion.parse(self.quantity, self.units)
|
value_unit = UnitConversion.parse(self.quantity, self.units)
|
||||||
value_unit = value_unit.to_volume(density)
|
value_unit = value_unit.to_volume(density).auto_unit
|
||||||
|
|
||||||
self.quantity = value_unit.pretty_value
|
self.quantity = value_unit.pretty_value
|
||||||
self.units = value_unit.unit.to_s
|
self.units = value_unit.unit.to_s
|
||||||
@ -111,65 +111,36 @@ class RecipeIngredient < ApplicationRecord
|
|||||||
def to_mass
|
def to_mass
|
||||||
return unless self.quantity.present?
|
return unless self.quantity.present?
|
||||||
if ingredient && ingredient.density?
|
if ingredient && ingredient.density?
|
||||||
density = UnitConversion.parse(ingredient.density)
|
UnitConversion::with_custom_units(self.ingredient.custom_units) do
|
||||||
if density.density?
|
density = UnitConversion.parse(ingredient.density)
|
||||||
value_unit = UnitConversion.parse(self.quantity, self.units)
|
if density.density?
|
||||||
value_unit = value_unit.to_mass(density)
|
value_unit = UnitConversion.parse(self.quantity, self.units)
|
||||||
|
value_unit = value_unit.to_mass(density).auto_unit
|
||||||
|
|
||||||
self.quantity = value_unit.pretty_value
|
self.quantity = value_unit.pretty_value
|
||||||
self.units = value_unit.unit.to_s
|
self.units = value_unit.unit.to_s
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_custom_unit_equivalent
|
|
||||||
if self.ingredient
|
|
||||||
unit = self.units.present? ? self.units.downcase : ''
|
|
||||||
pair = self.ingredient.custom_units.detect do |u, e|
|
|
||||||
if unit.empty?
|
|
||||||
['each', 'ech', 'item', 'per', 'recipe'].include?(u.downcase)
|
|
||||||
else
|
|
||||||
[u.downcase, u.downcase.singularize, u.downcase.pluralize].any? { |uv| [unit, unit.singularize, unit.pluralize].include?(uv) }
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
pair ? pair[1] : nil
|
|
||||||
else
|
|
||||||
nil
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def can_convert_to_grams?
|
|
||||||
vu = as_value_unit
|
|
||||||
vu.present? && (vu.mass? || (vu.volume? && self.ingredient && self.ingredient.density?))
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_grams
|
|
||||||
value_unit = as_value_unit
|
|
||||||
gram_unit = value_unit.convert('g', self.ingredient ? self.ingredient.density : nil)
|
|
||||||
gram_unit.raw_value
|
|
||||||
end
|
|
||||||
|
|
||||||
# Based on current quantity and units, return the value with with to multiply each nutrient to get the total amount
|
# Based on current quantity and units, return the value with with to multiply each nutrient to get the total amount
|
||||||
# supplied by this ingredient
|
# supplied by this ingredient
|
||||||
def calculate_nutrition_ratio
|
def calculate_nutrition_ratio
|
||||||
|
if self.ingredient.blank? || self.quantity.blank?
|
||||||
end
|
return nil
|
||||||
|
|
||||||
def as_value_unit
|
|
||||||
|
|
||||||
custom_unit = self.get_custom_unit_equivalent
|
|
||||||
|
|
||||||
case
|
|
||||||
when self.quantity.blank?
|
|
||||||
nil
|
|
||||||
when custom_unit.present?
|
|
||||||
vu = UnitConversion.parse(custom_unit)
|
|
||||||
vu.scale(self.quantity)
|
|
||||||
when self.units.present?
|
|
||||||
UnitConversion.parse(self.quantity, self.units)
|
|
||||||
else
|
|
||||||
nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
UnitConversion::with_custom_units(self.ingredient.custom_units) do
|
||||||
|
unit_is_each = self.units.blank? || %w(each ech item items per recipe recipes).include?(self.units.downcase)
|
||||||
|
unit = UnitConversion::parse(self.quantity, unit_is_each ? 'each' : self.units)
|
||||||
|
nutrition_unit = self.ingredient.nutrition_unit
|
||||||
|
|
||||||
|
converted_unit = unit.convert(nutrition_unit.unit.unit, self.ingredient.density)
|
||||||
|
converted_unit.scale(1.0 / nutrition_unit.value.value).raw_value
|
||||||
|
end
|
||||||
|
rescue UnitConversion::UnparseableUnitError
|
||||||
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def log_copy
|
def log_copy
|
||||||
|
@ -1,71 +0,0 @@
|
|||||||
class RecipeProxy
|
|
||||||
|
|
||||||
attr_reader :recipe
|
|
||||||
|
|
||||||
def initialize(recipe)
|
|
||||||
@recipe = recipe
|
|
||||||
parse_yields
|
|
||||||
end
|
|
||||||
|
|
||||||
def name
|
|
||||||
@recipe.name
|
|
||||||
end
|
|
||||||
|
|
||||||
def density
|
|
||||||
@density
|
|
||||||
end
|
|
||||||
|
|
||||||
def density?
|
|
||||||
!self.density.nil?
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_custom_unit_equivalent(custom_unit_name)
|
|
||||||
unit = @custom_yields.detect { |vu| vu.unit.unit == custom_unit_name }
|
|
||||||
known_unit = @unit_yields.first
|
|
||||||
if unit && known_unit
|
|
||||||
# ex:
|
|
||||||
# custom_unit_name: "rolls"
|
|
||||||
# yields: "3 rolls, 500 g"
|
|
||||||
# desired return: 166.6 g
|
|
||||||
|
|
||||||
ValueUnit.for(known_unit.value.value / unit.value.value, known_unit.unit)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def parse_yields
|
|
||||||
@custom_yields = []
|
|
||||||
@unit_yields = []
|
|
||||||
@density = nil
|
|
||||||
|
|
||||||
@recipe.yields_list.each do |y|
|
|
||||||
begin
|
|
||||||
vu = UnitConversion::parse(yield_string)
|
|
||||||
rescue UnparseableUnitError
|
|
||||||
vu = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
if vu
|
|
||||||
if vu.unit.nil?
|
|
||||||
@custom_yields << ValueUnit.for(vu.value, 'servings')
|
|
||||||
elsif vu.mass? || vu.volume?
|
|
||||||
@unit_yields << vu
|
|
||||||
else
|
|
||||||
@custom_yields << vu
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
vol = @unit_yields.detect { |u| u.volume? }
|
|
||||||
mas = @unit_yields.detect { |u| u.mass? }
|
|
||||||
# ex
|
|
||||||
# vol: 2 cups
|
|
||||||
# mas: 7 oz
|
|
||||||
if vol && mas
|
|
||||||
@density = "#{mas.value.value / vol.value.value} #{mas.unit.original_unit}/#{vol.unit.original_unit}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
@ -40,7 +40,7 @@ else
|
|||||||
json.ndbn_units []
|
json.ndbn_units []
|
||||||
end
|
end
|
||||||
|
|
||||||
json.ingredient_units @food.ingredient_units do |iu|
|
json.food_units @food.food_units do |iu|
|
||||||
json.extract! iu, :id, :name, :gram_weight
|
json.extract! iu, :id, :name, :gram_weight
|
||||||
json._destroy false
|
json._destroy false
|
||||||
end
|
end
|
@ -4,6 +4,7 @@ require 'unit_conversion/formatters'
|
|||||||
require 'unit_conversion/parsed_number'
|
require 'unit_conversion/parsed_number'
|
||||||
require 'unit_conversion/parsed_unit'
|
require 'unit_conversion/parsed_unit'
|
||||||
require 'unit_conversion/conversions'
|
require 'unit_conversion/conversions'
|
||||||
|
require 'unit_conversion/unitwise_patch'
|
||||||
require 'unit_conversion/value_unit'
|
require 'unit_conversion/value_unit'
|
||||||
|
|
||||||
module UnitConversion
|
module UnitConversion
|
||||||
@ -32,5 +33,28 @@ module UnitConversion
|
|||||||
[unit.pretty_value, unit.unit.to_s]
|
[unit.pretty_value, unit.unit.to_s]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def with_custom_units(unit_map, &block)
|
||||||
|
|
||||||
|
atom_configs = []
|
||||||
|
|
||||||
|
unit_map.each do |custom_unit, value_unit|
|
||||||
|
raise(UnknownUnitError, "#{value_unit.unit} is not valid") unless (value_unit.mass? || value_unit.volume?)
|
||||||
|
base_name = custom_unit.to_s.strip.downcase
|
||||||
|
|
||||||
|
atom_configs << {
|
||||||
|
names: [base_name, base_name.pluralize, base_name.singularize].uniq,
|
||||||
|
primary_code: "[prlsy_#{base_name[0]}]",
|
||||||
|
scale: {
|
||||||
|
value: value_unit.value.value,
|
||||||
|
unit_code: value_unit.unitwise.unit.to_s
|
||||||
|
},
|
||||||
|
property: value_unit.mass? ? 'mass' : 'volume'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
Unitwise::with_custom_units(atom_configs, &block)
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
@ -52,10 +52,14 @@ module UnitConversion
|
|||||||
|
|
||||||
def convert(value_unit)
|
def convert(value_unit)
|
||||||
|
|
||||||
unless known_auto_unit?(value_unit.unit)
|
if value_unit.unit.nil? || (!value_unit.mass? && !value_unit.volume?)
|
||||||
return value_unit
|
return value_unit
|
||||||
end
|
end
|
||||||
|
|
||||||
|
unless known_auto_unit?(value_unit.unit)
|
||||||
|
value_unit = value_unit.mass? ? value_unit.convert("g") : value_unit.convert("ml")
|
||||||
|
end
|
||||||
|
|
||||||
value = value_unit.raw_value
|
value = value_unit.raw_value
|
||||||
unit = value_unit.unit.unit
|
unit = value_unit.unit.unit
|
||||||
new_unit = unit
|
new_unit = unit
|
||||||
|
43
lib/unit_conversion/unitwise_patch.rb
Normal file
43
lib/unit_conversion/unitwise_patch.rb
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
module Unitwise
|
||||||
|
module Expression
|
||||||
|
class Decomposer
|
||||||
|
class << self
|
||||||
|
def reset(n = {})
|
||||||
|
old = {
|
||||||
|
parsers: @parsers,
|
||||||
|
transformer: @transformer,
|
||||||
|
cache: @cache
|
||||||
|
}
|
||||||
|
@parsers = n[:parsers]
|
||||||
|
@transformer = n[:transformer]
|
||||||
|
@cache = n[:cache]
|
||||||
|
old
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.with_custom_units(unit_list, &block)
|
||||||
|
|
||||||
|
atoms = []
|
||||||
|
|
||||||
|
unit_list.each do |u|
|
||||||
|
atom = Unitwise::Atom.new(u)
|
||||||
|
atom.validate!
|
||||||
|
Unitwise::Atom.all.push(atom)
|
||||||
|
atoms.push(atom)
|
||||||
|
end
|
||||||
|
rem = Unitwise::Expression::Decomposer.send(:reset)
|
||||||
|
|
||||||
|
ret_val = block.call
|
||||||
|
|
||||||
|
atoms.each do |a|
|
||||||
|
idx = Unitwise::Atom.all.index { |b| b.equal?(a) }
|
||||||
|
Unitwise::Atom.all.delete_at(idx)
|
||||||
|
# Unitwise::Atom.all.pop
|
||||||
|
end
|
||||||
|
Unitwise::Expression::Decomposer.send(:reset, rem)
|
||||||
|
|
||||||
|
ret_val
|
||||||
|
end
|
||||||
|
end
|
@ -2,6 +2,30 @@ require 'rails_helper'
|
|||||||
|
|
||||||
RSpec.describe UnitConversion do
|
RSpec.describe UnitConversion do
|
||||||
|
|
||||||
|
describe '.with_custom_units' do
|
||||||
|
it 'can convert' do
|
||||||
|
UnitConversion.with_custom_units({
|
||||||
|
'recipe': UnitConversion.parse('4 cups')
|
||||||
|
}) do
|
||||||
|
vu = UnitConversion.parse('2 1/2 recipes')
|
||||||
|
expect(vu.volume?).to be_truthy
|
||||||
|
lu = vu.convert("liters")
|
||||||
|
expect(lu.raw_value).to be_between(2.3, 2.4)
|
||||||
|
expect(lu.unit.to_s).to eq 'liter'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the value of the block' do
|
||||||
|
val = UnitConversion.with_custom_units({
|
||||||
|
'recipe': UnitConversion.parse('4 cups')
|
||||||
|
}) do
|
||||||
|
42
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(val).to eq 42
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '.auto_unit' do
|
describe '.auto_unit' do
|
||||||
it 'leaves units alone if reasonable' do
|
it 'leaves units alone if reasonable' do
|
||||||
expect(UnitConversion.auto_unit('1/2', 'tbsp')).to eq ['1/2', 'tablespoon']
|
expect(UnitConversion.auto_unit('1/2', 'tbsp')).to eq ['1/2', 'tablespoon']
|
||||||
|
@ -28,7 +28,7 @@ RSpec.describe NutritionData, type: :model do
|
|||||||
let(:rec2_ingredients) do
|
let(:rec2_ingredients) do
|
||||||
[
|
[
|
||||||
RecipeIngredient.new({
|
RecipeIngredient.new({
|
||||||
quantity: '100',
|
quantity: '250',
|
||||||
units: 'g',
|
units: 'g',
|
||||||
sort_order: 1,
|
sort_order: 1,
|
||||||
recipe_as_ingredient: recipe1
|
recipe_as_ingredient: recipe1
|
||||||
@ -48,9 +48,12 @@ RSpec.describe NutritionData, type: :model do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'runs' do
|
it 'runs and calculates properly' do
|
||||||
n = recipe2.nutrition_data
|
n = recipe2.nutrition_data
|
||||||
|
|
||||||
|
expect(n.kcal).to eq 20
|
||||||
|
expect(n.protein).to eq 1
|
||||||
|
expect(n.lipids).to eq 3
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -6,9 +6,8 @@ RSpec.describe RecipeIngredient, type: :model do
|
|||||||
|
|
||||||
it 'converts volume ingredients with density' do
|
it 'converts volume ingredients with density' do
|
||||||
ri = RecipeIngredient.new(quantity: 2, units: 'tbsp', food: create(:food_with_density))
|
ri = RecipeIngredient.new(quantity: 2, units: 'tbsp', food: create(:food_with_density))
|
||||||
expect(ri.as_value_unit.mass?).to be_falsey
|
|
||||||
ri.to_mass
|
ri.to_mass
|
||||||
expect(ri.as_value_unit.mass?).to be_truthy
|
expect(ri.units).to eq 'ounce'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'converts ingredients with custom units' do
|
it 'converts ingredients with custom units' do
|
||||||
@ -16,66 +15,68 @@ RSpec.describe RecipeIngredient, type: :model do
|
|||||||
i.food_units << FoodUnit.new(name: 'pat', gram_weight: 25)
|
i.food_units << FoodUnit.new(name: 'pat', gram_weight: 25)
|
||||||
ri = RecipeIngredient.new(quantity: 2, units: 'pat', food: i)
|
ri = RecipeIngredient.new(quantity: 2, units: 'pat', food: i)
|
||||||
ri.to_mass
|
ri.to_mass
|
||||||
vu = ri.as_value_unit
|
expect(ri.quantity).to eq '50'
|
||||||
expect(vu.raw_value).to eq 50
|
expect(ri.units).to eq 'gram'
|
||||||
expect(vu.unit.to_s).to eq 'gram'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#can_convert_to_grams' do
|
describe '#calculate_nutrition_ratio' do
|
||||||
describe 'with no ingredient detail' do
|
it 'returns nil if no ratio can be calculated' do
|
||||||
it 'returns false if no quantity or unit' do
|
ri = create(:recipe_ingredient)
|
||||||
ri = RecipeIngredient.new
|
expect(ri.calculate_nutrition_ratio).to be_nil
|
||||||
expect(ri.can_convert_to_grams?).to be_falsey
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns false if no quantity' do
|
ri.quantity = 50
|
||||||
ri = RecipeIngredient.new(units: 'lbs')
|
expect(ri.calculate_nutrition_ratio).to be_nil
|
||||||
expect(ri.can_convert_to_grams?).to be_falsey
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns false if no units' do
|
ri.units = 'cats'
|
||||||
ri = RecipeIngredient.new(quantity: '5 1/2')
|
expect(ri.calculate_nutrition_ratio).to be_nil
|
||||||
expect(ri.can_convert_to_grams?).to be_falsey
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns false if weird units' do
|
|
||||||
ri = RecipeIngredient.new(quantity: '5 1/2', units: 'dogs')
|
|
||||||
expect(ri.can_convert_to_grams?).to be_falsey
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns true if unit is mass' do
|
|
||||||
ri = RecipeIngredient.new(quantity: '5 1/2', units: 'lbs')
|
|
||||||
expect(ri.can_convert_to_grams?).to be_truthy
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns false if unit is volume' do
|
|
||||||
ri = RecipeIngredient.new(quantity: '5 1/2', units: 'cups')
|
|
||||||
expect(ri.can_convert_to_grams?).to be_falsey
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'with ingredient' do
|
|
||||||
it 'returns false if unit is volume and ingredient has no density' do
|
|
||||||
ri = RecipeIngredient.new(quantity: '5 1/2', units: 'cups', food: create(:food))
|
|
||||||
expect(ri.can_convert_to_grams?).to be_falsey
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns true if unit is volume and ingredient has density' do
|
|
||||||
ri = RecipeIngredient.new(quantity: '5 1/2', units: 'cups', food: create(:food_with_density))
|
|
||||||
expect(ri.can_convert_to_grams?).to be_truthy
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'with recipe_as_ingredient' do
|
describe 'with recipe_as_ingredient' do
|
||||||
it 'return true if unit is volume and recipe has density' do
|
let(:recipe) { create(:recipe, yields: '250 g, 0.5 l, 2 rolls') }
|
||||||
ri = RecipeIngredient.new(quantity: '5 1/2', units: 'cups', recipe_as_ingredient: create(:recipe, yields: '4 cups, 13 oz'))
|
let(:recipe_ingredient) { create(:recipe_ingredient, food: nil, recipe_as_ingredient: recipe) }
|
||||||
expect(ri.can_convert_to_grams?).to be_truthy
|
|
||||||
|
it 'returns nil with no quantity' do
|
||||||
|
ri = recipe_ingredient
|
||||||
|
expect(ri.calculate_nutrition_ratio).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns returns a proper scale with a unitless quantity' do
|
||||||
|
ri = recipe_ingredient
|
||||||
|
ri.quantity = 2
|
||||||
|
expect(ri.calculate_nutrition_ratio).to eq 2
|
||||||
|
|
||||||
|
ri.quantity = 4
|
||||||
|
expect(ri.calculate_nutrition_ratio).to eq 4
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns a proper scale with a counted unit' do
|
||||||
|
ri = recipe_ingredient
|
||||||
|
ri.quantity = 3
|
||||||
|
ri.units = "rolls"
|
||||||
|
expect(ri.calculate_nutrition_ratio).to eq 1.5
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns a proper scale with a mass unit' do
|
||||||
|
ri = recipe_ingredient
|
||||||
|
ri.quantity = 500
|
||||||
|
ri.units = 'g'
|
||||||
|
expect(ri.calculate_nutrition_ratio).to eq 2
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'with food' do
|
||||||
|
let(:food) { create(:food) }
|
||||||
|
let(:recipe_ingredient) { create(:recipe_ingredient, food: food) }
|
||||||
|
|
||||||
|
it 'returns a proper scale with a mass unit' do
|
||||||
|
ri = recipe_ingredient
|
||||||
|
ri.quantity = 500
|
||||||
|
ri.units = 'g'
|
||||||
|
expect(ri.calculate_nutrition_ratio).to eq 5
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -2,13 +2,13 @@ require 'rails_helper'
|
|||||||
|
|
||||||
RSpec.describe Recipe, type: :model do
|
RSpec.describe Recipe, type: :model do
|
||||||
describe '#yields_list' do
|
describe '#yields_list' do
|
||||||
it 'always has "1 recipe" as a yield' do
|
it 'always has "1 each" as a yield' do
|
||||||
r = create(:recipe, yields: '')
|
r = create(:recipe, yields: '')
|
||||||
l = r.yields_list
|
l = r.yields_list
|
||||||
expect(l.length).to eq 1
|
expect(l.length).to eq 1
|
||||||
expect(l.first).to be_a UnitConversion::ValueUnit
|
expect(l.first).to be_a UnitConversion::ValueUnit
|
||||||
expect(l.first.value.value).to eq 1
|
expect(l.first.value.value).to eq 1
|
||||||
expect(l.first.unit.unit).to eq 'recipe'
|
expect(l.first.unit.unit).to eq 'each'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'caches the list and resets it when yield is changed' do
|
it 'caches the list and resets it when yield is changed' do
|
||||||
@ -24,6 +24,35 @@ RSpec.describe Recipe, type: :model do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#custom_units' do
|
||||||
|
it 'returns nothing if no conversion possible' do
|
||||||
|
r = create(:recipe, yields: '')
|
||||||
|
expect(r.custom_units).to be_empty
|
||||||
|
|
||||||
|
r = create(:recipe, yields: '3 rolls, 2 buttercups')
|
||||||
|
expect(r.custom_units).to be_empty
|
||||||
|
|
||||||
|
r = create(:recipe, yields: '1 dealybob')
|
||||||
|
expect(r.custom_units).to be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns a each mapping for a convertable yields' do
|
||||||
|
r = create(:recipe, yields: '2 1/2 cups')
|
||||||
|
expect(r.custom_units.length).to eq 1
|
||||||
|
expect(r.custom_units['each']).to be_a UnitConversion::ValueUnit
|
||||||
|
expect(r.custom_units['each'].value.value).to eq 2.5
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates properly scaled arbitrary units' do
|
||||||
|
r = create(:recipe, yields: '6 Tbsp, 3 glarps')
|
||||||
|
expect(r.custom_units.length).to eq 2
|
||||||
|
expect(r.custom_units['each']).to be_a UnitConversion::ValueUnit
|
||||||
|
expect(r.custom_units['glarps']).to be_a UnitConversion::ValueUnit
|
||||||
|
expect(r.custom_units['glarps'].value.value).to eq 2
|
||||||
|
expect(r.custom_units['glarps'].unit.unit).to eq '[tbs_us]'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#update_rating!' do
|
describe '#update_rating!' do
|
||||||
|
|
||||||
it 'should set rating to nil with no ratings' do
|
it 'should set rating to nil with no ratings' do
|
||||||
|
Loading…
Reference in New Issue
Block a user