front end work

This commit is contained in:
Dan Elbert 2018-09-11 22:56:26 -05:00
parent 47014118c8
commit 2fd83a5d3d
26 changed files with 411 additions and 395 deletions

View File

@ -95,11 +95,6 @@ class FoodsController < ApplicationController
render :show
end
def prefetch
@foods = Food.all.order(:name)
render :search
end
def search
@foods = Food.search(params[:query]).order(:name)
end

View File

@ -15,7 +15,7 @@
<div class="navbar-start">
<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="/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 v-if="isLoggedIn" to="/logs" class="navbar-item">Log</router-link>
<router-link v-if="isLoggedIn" to="/notes" class="navbar-item">Notes</router-link>

View File

@ -1,13 +1,13 @@
<template>
<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>
<div class="field">
<label class="label is-small-mobile">Name</label>
<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>
@ -15,13 +15,13 @@
<label class="label is-small-mobile">Nutrient Databank Number</label>
<div class="field has-addons">
<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 class="control is-expanded">
<app-autocomplete
:inputClass="'is-small-mobile'"
ref="autocomplete"
v-model="ingredient.usda_food_name"
v-model="food.usda_food_name"
:minLength="2"
valueAttribute="name"
labelAttribute="description"
@ -39,14 +39,14 @@
<div class="field">
<label class="label is-small-mobile">Density</label>
<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 class="field">
<label class="label is-small-mobile">Notes</label>
<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>
@ -66,7 +66,7 @@
<th>Grams</th>
<th></th>
</tr>
<tr v-for="unit in visibleIngredientUnits" :key="unit.id">
<tr v-for="unit in visibleFoodUnits" :key="unit.id">
<td>
<div class="control">
<input type="text" class="input is-small-mobile" v-model="unit.name">
@ -99,7 +99,7 @@
<th>Name</th>
<th>Grams</th>
</tr>
<tr v-for="unit in ingredient.ndbn_units">
<tr v-for="unit in food.ndbn_units">
<td>{{unit.description}}</td>
<td>{{unit.gram_weight}}</td>
</tr>
@ -121,7 +121,7 @@
<label class="label is-small-mobile">{{nutrient.label}}</label>
<div class="field has-addons">
<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 class="control">
<button type="button" tabindex="-1" class="unit-label button is-static is-small-mobile">{{nutrient.unit}}</button>
@ -141,7 +141,7 @@
export default {
props: {
ingredient: {
food: {
required: true,
type: Object
},
@ -191,18 +191,18 @@
},
computed: {
visibleIngredientUnits() {
return this.ingredient.ingredient_units.filter(iu => iu._destroy !== true);
visibleFoodUnits() {
return this.food.food_units.filter(iu => iu._destroy !== true);
},
hasNdbn() {
return this.ingredient.ndbn !== null;
return this.food.ndbn !== null;
}
},
methods: {
addUnit() {
this.ingredient.ingredient_units.push({
this.food.food_units.push({
id: null,
name: null,
gram_weight: null
@ -213,15 +213,15 @@
if (unit.id) {
unit._destroy = true;
} else {
const idx = this.ingredient.ingredient_units.findIndex(i => i === unit);
this.ingredient.ingredient_units.splice(idx, 1);
const idx = this.food.food_units.findIndex(i => i === unit);
this.food.food_units.splice(idx, 1);
}
},
removeNdbn() {
this.ingredient.ndbn = null;
this.ingredient.usda_food_name = null;
this.ingredient.ndbn_units = [];
this.food.ndbn = null;
this.food.usda_food_name = null;
this.food.ndbn_units = [];
},
updateSearchItems(text) {
@ -236,13 +236,13 @@
},
searchItemSelected(food) {
this.ingredient.ndbn = food.ndbn;
this.ingredient.usda_food_name = food.name;
this.ingredient.ndbn_units = [];
this.food.ndbn = food.ndbn;
this.food.usda_food_name = food.name;
this.food.ndbn_units = [];
this.loadResource(
api.postIngredientSelectNdbn(this.ingredient)
.then(i => Object.assign(this.ingredient, i))
api.postIngredientSelectNdbn(this.food)
.then(i => Object.assign(this.food, i))
);
},

View File

@ -1,6 +1,6 @@
<template>
<div>
{{ingredient.name}}
{{food.name}}
</div>
</template>
@ -8,7 +8,7 @@
export default {
props: {
ingredient: {
food: {
required: true,
type: Object
}

View File

@ -28,7 +28,7 @@
</app-modal>
<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>
<button type="button" class="button is-primary" @click="addIngredient">Add Ingredient</button>
@ -117,11 +117,11 @@
this.ingredients.push(this.createIngredient());
},
deleteIngredient(ingredient) {
if (ingredient.id) {
ingredient._destroy = true;
deleteFood(food) {
if (food.id) {
food._destroy = true;
} else {
const idx = this.ingredients.findIndex(i => i === ingredient);
const idx = this.ingredients.findIndex(i => i === food);
this.ingredients.splice(idx, 1);
}
},

View File

@ -37,7 +37,7 @@
</div>
<div class="column is-narrow">
<span class="label is-small-mobile" v-if="showLabels">&nbsp;</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>
</button>
</div>
@ -62,8 +62,8 @@
},
methods: {
deleteIngredient(ingredient) {
this.$emit("deleteIngredient", ingredient);
deleteFood(ingredient) {
this.$emit("deleteFood", ingredient);
},
updateSearchItems(text) {

View File

@ -1,45 +1,45 @@
<template>
<div>
<div v-if="ingredient === null">
<div v-if="food === null">
Loading...
</div>
<div v-else>
<ingredient-show :ingredient="ingredient"></ingredient-show>
<food-show :food="food"></food-show>
</div>
<router-link v-if="isLoggedIn" class="button" :to="{name: 'edit_ingredient', params: { id: ingredientId }}">Edit</router-link>
<router-link class="button" to="/ingredients">Back</router-link>
<router-link v-if="isLoggedIn" class="button" :to="{name: 'edit_food', params: { id: foodId }}">Edit</router-link>
<router-link class="button" to="/foods">Back</router-link>
</div>
</template>
<script>
import IngredientShow from "./IngredientShow";
import FoodShow from "./FoodShow";
import { mapState } from "vuex";
import api from "../lib/Api";
export default {
data: function () {
return {
ingredient: null
food: null
}
},
computed: {
...mapState({
ingredientId: state => state.route.params.id,
foodId: state => state.route.params.id,
})
},
created() {
this.loadResource(
api.getIngredient(this.ingredientId)
.then(data => { this.ingredient = data; return data; })
api.getFood(this.foodId)
.then(data => { this.food = data; return data; })
);
},
components: {
IngredientShow
FoodShow
}
}

View File

@ -1,17 +1,17 @@
<template>
<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>
<router-link class="button is-secondary" to="/ingredients">Cancel</router-link>
<router-link class="button is-secondary" to="/food">Cancel</router-link>
</div>
</template>
<script>
import IngredientEdit from "./IngredientEdit";
import FoodEdit from "./FoodEdit";
import { mapState } from "vuex";
import api from "../lib/Api";
import * as Errors from '../lib/Errors';
@ -19,7 +19,7 @@
export default {
data() {
return {
ingredient: {
food: {
name: null,
notes: null,
ndbn: null,
@ -49,7 +49,7 @@
vit_k: null,
cholesterol: null,
lipids: null,
ingredient_units: []
food_units: []
},
validationErrors: {}
}
@ -59,15 +59,15 @@
save() {
this.validationErrors = {}
this.loadResource(
api.postIngredient(this.ingredient)
.then(() => this.$router.push('/ingredients'))
api.postFood(this.food)
.then(() => this.$router.push('/foods'))
.catch(Errors.onlyFor(Errors.ApiValidationError, err => this.validationErrors = err.validationErrors()))
);
}
},
components: {
IngredientEdit
FoodEdit
}
}

View File

@ -1,22 +1,22 @@
<template>
<div>
<div v-if="ingredient === null">
<div v-if="food === null">
Loading...
</div>
<div v-else>
<ingredient-edit :ingredient="ingredient" :validation-errors="validationErrors"></ingredient-edit>
<food-edit :food="food" :validation-errors="validationErrors"></food-edit>
</div>
<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>
</template>
<script>
import IngredientEdit from "./IngredientEdit";
import FoodEdit from "./FoodEdit";
import { mapState } from "vuex";
import api from "../lib/Api";
import * as Errors from '../lib/Errors';
@ -24,14 +24,14 @@
export default {
data: function () {
return {
ingredient: null,
food: null,
validationErrors: {}
};
},
computed: {
...mapState({
ingredientId: state => state.route.params.id,
foodId: state => state.route.params.id,
})
},
@ -39,8 +39,8 @@
save() {
this.validationErrors = {};
this.loadResource(
api.patchIngredient(this.ingredient)
.then(() => this.$router.push({name: 'ingredient', params: {id: this.ingredientId }}))
api.patchFood(this.food)
.then(() => this.$router.push({name: 'food', params: {id: this.foodId }}))
.catch(Errors.onlyFor(Errors.ApiValidationError, err => this.validationErrors = err.validationErrors()))
);
}
@ -48,13 +48,13 @@
created() {
this.loadResource(
api.getIngredient(this.ingredientId)
.then(data => { this.ingredient = data; return data; })
api.getFood(this.foodId)
.then(data => { this.food = data; return data; })
);
},
components: {
IngredientEdit
FoodEdit
}
}

View File

@ -3,10 +3,10 @@
<h1 class="title">Ingredients</h1>
<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>
<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">
<thead>
@ -29,16 +29,16 @@
</tr>
</thead>
<tbody>
<tr v-for="i in ingredients" :key="i.id">
<td><router-link :to="{name: 'ingredient', params: { id: i.id } }">{{i.name}}</router-link></td>
<tr v-for="i in foods" :key="i.id">
<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>{{i.kcal}}</td>
<td>{{i.density}}</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>
</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>
</button>
</td>
@ -46,13 +46,13 @@
</tbody>
</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">
<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>
<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>
</template>
@ -65,8 +65,8 @@
export default {
data() {
return {
ingredientData: null,
ingredientForDeletion: null,
foodData: null,
foodForDeletion: null,
search: {
page: 1,
per: 25,
@ -76,35 +76,35 @@
},
computed: {
ingredients() {
if (this.ingredientData) {
return this.ingredientData.ingredients;
foods() {
if (this.foodData) {
return this.foodData.foods;
} else {
return [];
}
},
totalPages() {
if (this.ingredientData) {
return this.ingredientData.total_pages
if (this.foodData) {
return this.foodData.total_pages
}
return 0;
},
currentPage() {
if (this.ingredientData) {
return this.ingredientData.current_page
if (this.foodData) {
return this.foodData.current_page
}
return 0;
},
showConfirmIngredientDelete() {
return this.ingredientForDeletion !== null;
showConfirmFoodDelete() {
return this.foodForDeletion !== null;
},
confirmIngredientDeleteMessage() {
if (this.ingredientForDeletion !== null) {
return `Are you sure you want to delete ${this.ingredientForDeletion.name}?`;
confirmFoodDeleteMessage() {
if (this.foodForDeletion !== null) {
return `Are you sure you want to delete ${this.foodForDeletion.name}?`;
} else {
return "??";
}
@ -118,30 +118,30 @@
getList: debounce(function() {
return this.loadResource(
api.getIngredientList(this.search.page, this.search.per, this.search.name)
.then(data => this.ingredientData = data)
api.getFoodList(this.search.page, this.search.per, this.search.name)
.then(data => this.foodData = data)
);
}, 500, {leading: true, trailing: true}),
deleteIngredient(ingredient) {
this.ingredientForDeletion = ingredient;
deleteFood(food) {
this.foodForDeletion = food;
},
ingredientDeleteCancel() {
this.ingredientForDeletion = null;
foodDeleteCancel() {
this.foodForDeletion = null;
},
ingredientDeleteConfirm() {
if (this.ingredientForDeletion !== null) {
foodDeleteConfirm() {
if (this.foodForDeletion !== null) {
this.loadResource(
api.deleteIngredient(this.ingredientForDeletion.id).then(res => {
this.ingredientForDeletion = null;
api.deleteFood(this.foodForDeletion.id).then(res => {
this.foodForDeletion = null;
return this.getList();
})
);
console.log("This is where the thing happens!!");
this.ingredientForDeletion = null;
this.foodForDeletion = null;
}
}
},

View File

@ -213,66 +213,66 @@ class Api {
return this.get("/calculator/calculate", params);
}
getIngredientList(page, per, name) {
getFoodList(page, per, name) {
const params = {
page,
per,
name
};
return this.get("/ingredients/", params);
return this.get("/foods/", params);
}
getIngredient(id) {
return this.get("/ingredients/" + id);
getFood(id) {
return this.get("/foods/" + id);
}
buildIngredientParams(ingredient) {
buildFoodParams(food) {
return {
ingredient: {
name: ingredient.name,
notes: ingredient.notes,
ndbn: ingredient.ndbn,
density: ingredient.density,
food: {
name: food.name,
notes: food.notes,
ndbn: food.ndbn,
density: food.density,
water: ingredient.water,
ash: ingredient.ash,
protein: ingredient.protein,
kcal: ingredient.kcal,
fiber: ingredient.fiber,
sugar: ingredient.sugar,
carbohydrates: ingredient.carbohydrates,
calcium: ingredient.calcium,
iron: ingredient.iron,
magnesium: ingredient.magnesium,
phosphorus: ingredient.phosphorus,
potassium: ingredient.potassium,
sodium: ingredient.sodium,
zinc: ingredient.zinc,
copper: ingredient.copper,
manganese: ingredient.manganese,
vit_c: ingredient.vit_c,
vit_b6: ingredient.vit_b6,
vit_b12: ingredient.vit_b12,
vit_a: ingredient.vit_a,
vit_e: ingredient.vit_e,
vit_d: ingredient.vit_d,
vit_k: ingredient.vit_k,
cholesterol: ingredient.cholesterol,
lipids: ingredient.lipids,
water: food.water,
ash: food.ash,
protein: food.protein,
kcal: food.kcal,
fiber: food.fiber,
sugar: food.sugar,
carbohydrates: food.carbohydrates,
calcium: food.calcium,
iron: food.iron,
magnesium: food.magnesium,
phosphorus: food.phosphorus,
potassium: food.potassium,
sodium: food.sodium,
zinc: food.zinc,
copper: food.copper,
manganese: food.manganese,
vit_c: food.vit_c,
vit_b6: food.vit_b6,
vit_b12: food.vit_b12,
vit_a: food.vit_a,
vit_e: food.vit_e,
vit_d: food.vit_d,
vit_k: food.vit_k,
cholesterol: food.cholesterol,
lipids: food.lipids,
ingredient_units_attributes: ingredient.ingredient_units.map(iu => {
if (iu._destroy) {
food_units_attributes: food.food_units.map(fu => {
if (fu._destroy) {
return {
id: iu.id,
id: fu.id,
_destroy: true
};
} else {
return {
id: iu.id,
name: iu.name,
gram_weight: iu.gram_weight
id: fu.id,
name: fu.name,
gram_weight: fu.gram_weight
};
}
})
@ -280,25 +280,25 @@ class Api {
}
}
postIngredient(ingredient) {
return this.post("/ingredients/", this.buildIngredientParams(ingredient));
postFood(food) {
return this.post("/foods/", this.buildFoodParams(food));
}
patchIngredient(ingredient) {
return this.patch("/ingredients/" + ingredient.id, this.buildIngredientParams(ingredient));
patchFood(food) {
return this.patch("/foods/" + food.id, this.buildFoodParams(food));
}
deleteIngredient(id) {
return this.del("/ingredients/" + id);
deleteFood(id) {
return this.del("/foods/" + id);
}
postIngredientSelectNdbn(ingredient) {
const url = ingredient.id ? "/ingredients/" + ingredient.id + "/select_ndbn" : "/ingredients/select_ndbn";
return this.post(url, this.buildIngredientParams(ingredient));
const url = ingredient.id ? "/foods/" + ingredient.id + "/select_ndbn" : "/foods/select_ndbn";
return this.post(url, this.buildFoodParams(ingredient));
}
getUsdaFoodSearch(query) {
return this.get("/ingredients/usda_food_search", {query: query});
return this.get("/foods/usda_food_search", {query: query});
}
getNoteList() {

View File

@ -10,10 +10,10 @@ import TheLogList from './components/TheLogList';
import TheLogCreator from './components/TheLogCreator';
import TheLogEditor from './components/TheLogEditor';
import TheIngredientList from './components/TheIngredientList';
import TheIngredient from "./components/TheIngredient";
import TheIngredientEditor from "./components/TheIngredientEditor";
import TheIngredientCreator from "./components/TheIngredientCreator";
import TheFoodList from './components/TheFoodList';
import TheFood from "./components/TheFood";
import TheFoodEditor from "./components/TheFoodEditor";
import TheFoodCreator from "./components/TheFoodCreator";
import TheNotesList from './components/TheNotesList';
import TheRecipe from './components/TheRecipe';
import TheRecipeEditor from './components/TheRecipeEditor';
@ -71,24 +71,24 @@ router.addRoutes(
component: TheCalculator
},
{
path: "/ingredients",
name: "ingredients",
component: TheIngredientList
path: "/foods",
name: "foods",
component: TheFoodList
},
{
path: "/ingredients/new",
name: "new_ingredient",
component: TheIngredientCreator
path: "/foods/new",
name: "new_food",
component: TheFoodCreator
},
{
path: "/ingredients/:id/edit",
name: "edit_ingredient",
component: TheIngredientEditor
path: "/foods/:id/edit",
name: "edit_food",
component: TheFoodEditor
},
{
path: "/ingredients/:id",
name: "ingredient",
component: TheIngredient
path: "/foods/:id",
name: "food",
component: TheFood
},
{
path: "/logs",

View File

@ -30,14 +30,18 @@ class Food < ApplicationRecord
units
end
def nutrition_per_100g
def nutrition_data
self
end
def nutrition_per_100g_errors
def nutrition_errors
[]
end
def nutrition_unit
UnitConversion.parse('100 grams')
end
def ndbn=(value)
@usda_food = nil
super

View File

@ -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

View File

@ -36,28 +36,31 @@ class NutritionData
self.instance_variable_set("@#{n}".to_sym, 0.0)
end
valid_ingredients = []
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"
elsif !i.can_convert_to_grams?
@errors << "#{i.name} can't be converted to grams"
else
valid_ingredients << i
next
end
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
valid_ingredients.each do |i|
grams = i.to_grams
missing = []
NUTRIENTS.each do |k, n|
value = i.food.send(k)
value = i.ingredient.nutrition_data.send(k)
if value.present?
value = value.to_f
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)
else
missing << k

View File

@ -76,7 +76,7 @@ class Recipe < ApplicationRecord
end
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
UnitConversion::parse(y)
rescue UntConversion::UnparseableUnitError
@ -106,6 +106,10 @@ class Recipe < ApplicationRecord
@nutrition_data
end
def nutrition_errors
nutrition_data.errors
end
def update_rating!
self.rating = Log.for_recipe(self).for_user(self.user_id).where('rating IS NOT NULL').average(:rating)
save(validate: false)
@ -142,23 +146,30 @@ class Recipe < ApplicationRecord
end
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? }
volume = self.yields_list.select { |y| y.volume? }
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
def nutrition_per_100g
end
def nutrition_per_100g_errors
def nutrition_unit
UnitConversion.parse('1 each')
end
def self.for_criteria(criteria)

View File

@ -100,7 +100,7 @@ class RecipeIngredient < ApplicationRecord
density = UnitConversion.parse(ingredient.density)
if density.density?
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.units = value_unit.unit.to_s
@ -111,65 +111,36 @@ class RecipeIngredient < ApplicationRecord
def to_mass
return unless self.quantity.present?
if ingredient && ingredient.density?
density = UnitConversion.parse(ingredient.density)
if density.density?
value_unit = UnitConversion.parse(self.quantity, self.units)
value_unit = value_unit.to_mass(density)
UnitConversion::with_custom_units(self.ingredient.custom_units) do
density = UnitConversion.parse(ingredient.density)
if density.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.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) }
self.quantity = value_unit.pretty_value
self.units = value_unit.unit.to_s
end
end
pair ? pair[1] : nil
else
nil
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
# supplied by this ingredient
def calculate_nutrition_ratio
end
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
if self.ingredient.blank? || self.quantity.blank?
return nil
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
def log_copy

View File

@ -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

View File

@ -40,7 +40,7 @@ else
json.ndbn_units []
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._destroy false
end

View File

@ -4,6 +4,7 @@ require 'unit_conversion/formatters'
require 'unit_conversion/parsed_number'
require 'unit_conversion/parsed_unit'
require 'unit_conversion/conversions'
require 'unit_conversion/unitwise_patch'
require 'unit_conversion/value_unit'
module UnitConversion
@ -32,5 +33,28 @@ module UnitConversion
[unit.pretty_value, unit.unit.to_s]
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

View File

@ -52,10 +52,14 @@ module UnitConversion
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
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
unit = value_unit.unit.unit
new_unit = unit

View 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

View File

@ -2,6 +2,30 @@ require 'rails_helper'
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
it 'leaves units alone if reasonable' do
expect(UnitConversion.auto_unit('1/2', 'tbsp')).to eq ['1/2', 'tablespoon']

View File

@ -28,7 +28,7 @@ RSpec.describe NutritionData, type: :model do
let(:rec2_ingredients) do
[
RecipeIngredient.new({
quantity: '100',
quantity: '250',
units: 'g',
sort_order: 1,
recipe_as_ingredient: recipe1
@ -48,9 +48,12 @@ RSpec.describe NutritionData, type: :model do
end
end
it 'runs' do
it 'runs and calculates properly' do
n = recipe2.nutrition_data
expect(n.kcal).to eq 20
expect(n.protein).to eq 1
expect(n.lipids).to eq 3
end
end

View File

@ -6,9 +6,8 @@ RSpec.describe RecipeIngredient, type: :model do
it 'converts volume ingredients with density' do
ri = RecipeIngredient.new(quantity: 2, units: 'tbsp', food: create(:food_with_density))
expect(ri.as_value_unit.mass?).to be_falsey
ri.to_mass
expect(ri.as_value_unit.mass?).to be_truthy
expect(ri.units).to eq 'ounce'
end
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)
ri = RecipeIngredient.new(quantity: 2, units: 'pat', food: i)
ri.to_mass
vu = ri.as_value_unit
expect(vu.raw_value).to eq 50
expect(vu.unit.to_s).to eq 'gram'
expect(ri.quantity).to eq '50'
expect(ri.units).to eq 'gram'
end
end
describe '#can_convert_to_grams' do
describe 'with no ingredient detail' do
it 'returns false if no quantity or unit' do
ri = RecipeIngredient.new
expect(ri.can_convert_to_grams?).to be_falsey
end
describe '#calculate_nutrition_ratio' do
it 'returns nil if no ratio can be calculated' do
ri = create(:recipe_ingredient)
expect(ri.calculate_nutrition_ratio).to be_nil
it 'returns false if no quantity' do
ri = RecipeIngredient.new(units: 'lbs')
expect(ri.can_convert_to_grams?).to be_falsey
end
ri.quantity = 50
expect(ri.calculate_nutrition_ratio).to be_nil
it 'returns false if no units' do
ri = RecipeIngredient.new(quantity: '5 1/2')
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
ri.units = 'cats'
expect(ri.calculate_nutrition_ratio).to be_nil
end
describe 'with recipe_as_ingredient' do
it 'return true if unit is volume and recipe has density' do
ri = RecipeIngredient.new(quantity: '5 1/2', units: 'cups', recipe_as_ingredient: create(:recipe, yields: '4 cups, 13 oz'))
expect(ri.can_convert_to_grams?).to be_truthy
let(:recipe) { create(:recipe, yields: '250 g, 0.5 l, 2 rolls') }
let(:recipe_ingredient) { create(:recipe_ingredient, food: nil, recipe_as_ingredient: recipe) }
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
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

View File

@ -2,13 +2,13 @@ require 'rails_helper'
RSpec.describe Recipe, type: :model 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: '')
l = r.yields_list
expect(l.length).to eq 1
expect(l.first).to be_a UnitConversion::ValueUnit
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
it 'caches the list and resets it when yield is changed' do
@ -24,6 +24,35 @@ RSpec.describe Recipe, type: :model do
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
it 'should set rating to nil with no ratings' do