feat: add unit abbreviation support (#1332)

* add 'use-abbreviation' db column

* type generation

* add view and edit elements

* check for use_abbreviation to display

* fix: alembic version check

* test: add use_abbreviation prop tests
This commit is contained in:
Hayden 2022-06-01 11:59:50 -08:00 committed by GitHub
parent 592b1de39d
commit 52fbf6b833
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 77 additions and 9 deletions

View File

@ -0,0 +1,30 @@
"""Add use_abbreviation column to ingredients
Revision ID: ab0bae02578f
Revises: 09dfc897ad62
Create Date: 2022-06-01 11:12:06.748383
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "ab0bae02578f"
down_revision = "09dfc897ad62"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("ingredient_units", sa.Column("use_abbreviation", sa.Boolean(), nullable=True))
op.execute("UPDATE ingredient_units SET use_abbreviation = FALSE WHERE use_abbreviation IS NULL")
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("ingredient_units", "use_abbreviation")
# ### end Alembic commands ###

View File

@ -19,6 +19,8 @@ export function parseIngredientText(ingredient: RecipeIngredient, disableAmount:
let returnQty = ""; let returnQty = "";
let unitDisplay = unit?.name;
// casting to number is required as sometimes quantity is a string // casting to number is required as sometimes quantity is a string
if (quantity && Number(quantity) !== 0) { if (quantity && Number(quantity) !== 0) {
console.log("Using Quantity", quantity, typeof quantity); console.log("Using Quantity", quantity, typeof quantity);
@ -34,8 +36,12 @@ export function parseIngredientText(ingredient: RecipeIngredient, disableAmount:
} else { } else {
returnQty = (quantity * scale).toString(); returnQty = (quantity * scale).toString();
} }
if (unit?.useAbbreviation && unit.abbreviation) {
unitDisplay = unit.abbreviation;
}
} }
const text = `${returnQty} ${unit?.name || " "} ${food?.name || " "} ${note || " "}`.replace(/ {2,}/g, " "); const text = `${returnQty} ${unitDisplay || " "} ${food?.name || " "} ${note || " "}`.replace(/ {2,}/g, " ");
return sanitizeIngredientHTML(text); return sanitizeIngredientHTML(text);
} }

View File

@ -36,6 +36,7 @@
<v-text-field v-model="editTarget.abbreviation" label="Abbreviation"></v-text-field> <v-text-field v-model="editTarget.abbreviation" label="Abbreviation"></v-text-field>
<v-text-field v-model="editTarget.description" label="Description"></v-text-field> <v-text-field v-model="editTarget.description" label="Description"></v-text-field>
<v-checkbox v-model="editTarget.fraction" hide-details label="Display as Fraction"></v-checkbox> <v-checkbox v-model="editTarget.fraction" hide-details label="Display as Fraction"></v-checkbox>
<v-checkbox v-model="editTarget.useAbbreviation" hide-details label="Use Abbreviation"></v-checkbox>
</v-form> </v-form>
</v-card-text> </v-card-text>
</BaseDialog> </BaseDialog>
@ -106,6 +107,11 @@
Combine Combine
</BaseButton> </BaseButton>
</template> </template>
<template #item.useAbbreviation="{ item }">
<v-icon :color="item.useAbbreviation ? 'success' : undefined">
{{ item.useAbbreviation ? $globals.icons.check : $globals.icons.close }}
</v-icon>
</template>
<template #item.fraction="{ item }"> <template #item.fraction="{ item }">
<v-icon :color="item.fraction ? 'success' : undefined"> <v-icon :color="item.fraction ? 'success' : undefined">
{{ item.fraction ? $globals.icons.check : $globals.icons.close }} {{ item.fraction ? $globals.icons.check : $globals.icons.close }}
@ -153,10 +159,15 @@ export default defineComponent({
value: "abbreviation", value: "abbreviation",
show: true, show: true,
}, },
{
text: "Use Abbv.",
value: "useAbbreviation",
show: true,
},
{ {
text: "Description", text: "Description",
value: "description", value: "description",
show: true, show: false,
}, },
{ {
text: "Fraction", text: "Fraction",

View File

@ -133,6 +133,7 @@ export interface IngredientUnit {
description?: string; description?: string;
fraction?: boolean; fraction?: boolean;
abbreviation?: string; abbreviation?: string;
useAbbreviation?: boolean;
id: string; id: string;
} }
export interface CreateIngredientUnit { export interface CreateIngredientUnit {
@ -140,6 +141,7 @@ export interface CreateIngredientUnit {
description?: string; description?: string;
fraction?: boolean; fraction?: boolean;
abbreviation?: string; abbreviation?: string;
useAbbreviation?: boolean;
} }
export interface IngredientFood { export interface IngredientFood {
name: string; name: string;

View File

@ -112,6 +112,7 @@ export interface IngredientUnit {
description?: string; description?: string;
fraction?: boolean; fraction?: boolean;
abbreviation?: string; abbreviation?: string;
useAbbreviation?: boolean;
id: string; id: string;
} }
export interface CreateIngredientUnit { export interface CreateIngredientUnit {
@ -119,6 +120,7 @@ export interface CreateIngredientUnit {
description?: string; description?: string;
fraction?: boolean; fraction?: boolean;
abbreviation?: string; abbreviation?: string;
useAbbreviation?: boolean;
} }
export interface IngredientFood { export interface IngredientFood {
name: string; name: string;

View File

@ -207,6 +207,7 @@ export interface IngredientUnit {
description?: string; description?: string;
fraction?: boolean; fraction?: boolean;
abbreviation?: string; abbreviation?: string;
useAbbreviation?: boolean;
id: string; id: string;
} }
export interface ReadGroupPreferences { export interface ReadGroupPreferences {
@ -287,6 +288,7 @@ export interface CreateIngredientUnit {
description?: string; description?: string;
fraction?: boolean; fraction?: boolean;
abbreviation?: string; abbreviation?: string;
useAbbreviation?: boolean;
} }
export interface CreateIngredientFood { export interface CreateIngredientFood {
name: string; name: string;

View File

@ -148,6 +148,7 @@ export interface IngredientUnit {
description?: string; description?: string;
fraction?: boolean; fraction?: boolean;
abbreviation?: string; abbreviation?: string;
useAbbreviation?: boolean;
id: string; id: string;
} }
export interface CreateIngredientUnit { export interface CreateIngredientUnit {
@ -155,6 +156,7 @@ export interface CreateIngredientUnit {
description?: string; description?: string;
fraction?: boolean; fraction?: boolean;
abbreviation?: string; abbreviation?: string;
useAbbreviation?: boolean;
} }
export interface IngredientFood { export interface IngredientFood {
name: string; name: string;

View File

@ -49,6 +49,7 @@ export interface CreateIngredientUnit {
description?: string; description?: string;
fraction?: boolean; fraction?: boolean;
abbreviation?: string; abbreviation?: string;
useAbbreviation?: boolean;
} }
export interface CreateRecipe { export interface CreateRecipe {
name: string; name: string;
@ -117,6 +118,7 @@ export interface IngredientUnit {
description?: string; description?: string;
fraction?: boolean; fraction?: boolean;
abbreviation?: string; abbreviation?: string;
useAbbreviation?: boolean;
id: string; id: string;
} }
export interface IngredientsRequest { export interface IngredientsRequest {
@ -340,6 +342,7 @@ export interface SaveIngredientUnit {
description?: string; description?: string;
fraction?: boolean; fraction?: boolean;
abbreviation?: string; abbreviation?: string;
useAbbreviation?: boolean;
groupId: string; groupId: string;
} }
export interface ScrapeRecipe { export interface ScrapeRecipe {

View File

@ -164,6 +164,7 @@ export interface IngredientUnit {
description?: string; description?: string;
fraction?: boolean; fraction?: boolean;
abbreviation?: string; abbreviation?: string;
useAbbreviation?: boolean;
id: string; id: string;
} }
export interface CreateIngredientUnit { export interface CreateIngredientUnit {
@ -171,6 +172,7 @@ export interface CreateIngredientUnit {
description?: string; description?: string;
fraction?: boolean; fraction?: boolean;
abbreviation?: string; abbreviation?: string;
useAbbreviation?: boolean;
} }
export interface IngredientFood { export interface IngredientFood {
name: string; name: string;

View File

@ -18,6 +18,7 @@ class IngredientUnitModel(SqlAlchemyBase, BaseMixins):
name = Column(String) name = Column(String)
description = Column(String) description = Column(String)
abbreviation = Column(String) abbreviation = Column(String)
use_abbreviation = Column(Boolean, default=False)
fraction = Column(Boolean, default=True) fraction = Column(Boolean, default=True)
ingredients = orm.relationship("RecipeIngredient", back_populates="unit") ingredients = orm.relationship("RecipeIngredient", back_populates="unit")

View File

@ -34,6 +34,7 @@ class IngredientFood(CreateIngredientFood):
class CreateIngredientUnit(UnitFoodBase): class CreateIngredientUnit(UnitFoodBase):
fraction: bool = True fraction: bool = True
abbreviation: str = "" abbreviation: str = ""
use_abbreviation: bool = False
class SaveIngredientUnit(CreateIngredientUnit): class SaveIngredientUnit(CreateIngredientUnit):

View File

@ -9,17 +9,19 @@ from tests.utils.fixture_schemas import TestUser
class Routes: class Routes:
base = "/api/units" base = "/api/units"
@staticmethod
def item(item_id: int) -> str: def item(item_id: int) -> str:
return f"{Routes.base}/{item_id}" return f"{Routes.base}/{item_id}"
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def unit(api_client: TestClient, unique_user: TestUser) -> dict: def unit(api_client: TestClient, unique_user: TestUser):
data = CreateIngredientUnit( data = CreateIngredientUnit(
name=random_string(10), name=random_string(10),
description=random_string(10), description=random_string(10),
fraction=random_bool(), fraction=random_bool(),
abbreviation=random_string(3) + ".", abbreviation=f"{random_string(3)}.",
use_abbreviation=random_bool(),
).dict(by_alias=True) ).dict(by_alias=True)
response = api_client.post(Routes.base, json=data, headers=unique_user.token) response = api_client.post(Routes.base, json=data, headers=unique_user.token)
@ -52,6 +54,7 @@ def test_read_unit(api_client: TestClient, unit: dict, unique_user: TestUser):
assert as_json["description"] == unit["description"] assert as_json["description"] == unit["description"]
assert as_json["fraction"] == unit["fraction"] assert as_json["fraction"] == unit["fraction"]
assert as_json["abbreviation"] == unit["abbreviation"] assert as_json["abbreviation"] == unit["abbreviation"]
assert as_json["useAbbreviation"] == unit["useAbbreviation"]
def test_update_unit(api_client: TestClient, unit: dict, unique_user: TestUser): def test_update_unit(api_client: TestClient, unit: dict, unique_user: TestUser):
@ -60,8 +63,10 @@ def test_update_unit(api_client: TestClient, unit: dict, unique_user: TestUser):
"name": random_string(10), "name": random_string(10),
"description": random_string(10), "description": random_string(10),
"fraction": not unit["fraction"], "fraction": not unit["fraction"],
"abbreviation": random_string(3) + ".", "abbreviation": f"{random_string(3)}.",
"useAbbreviation": not unit["useAbbreviation"],
} }
response = api_client.put(Routes.item(unit["id"]), json=update_data, headers=unique_user.token) response = api_client.put(Routes.item(unit["id"]), json=update_data, headers=unique_user.token)
assert response.status_code == 200 assert response.status_code == 200
as_json = response.json() as_json = response.json()
@ -71,14 +76,15 @@ def test_update_unit(api_client: TestClient, unit: dict, unique_user: TestUser):
assert as_json["description"] == update_data["description"] assert as_json["description"] == update_data["description"]
assert as_json["fraction"] == update_data["fraction"] assert as_json["fraction"] == update_data["fraction"]
assert as_json["abbreviation"] == update_data["abbreviation"] assert as_json["abbreviation"] == update_data["abbreviation"]
assert as_json["useAbbreviation"] == update_data["useAbbreviation"]
def test_delete_unit(api_client: TestClient, unit: dict, unique_user: TestUser): def test_delete_unit(api_client: TestClient, unit: dict, unique_user: TestUser):
id = unit["id"] item_id = unit["id"]
response = api_client.delete(Routes.item(id), headers=unique_user.token) response = api_client.delete(Routes.item(item_id), headers=unique_user.token)
assert response.status_code == 200 assert response.status_code == 200
response = api_client.get(Routes.item(id), headers=unique_user.token) response = api_client.get(Routes.item(item_id), headers=unique_user.token)
assert response.status_code == 404 assert response.status_code == 404

View File

@ -4,7 +4,7 @@ from mealie.core.config import get_app_settings
from mealie.services.backups_v2.alchemy_exporter import AlchemyExporter from mealie.services.backups_v2.alchemy_exporter import AlchemyExporter
ALEMBIC_VERSIONS = [ ALEMBIC_VERSIONS = [
{"version_num": "09dfc897ad62"}, {"version_num": "ab0bae02578f"},
] ]