mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-06-03 05:35:02 -04:00
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:
parent
592b1de39d
commit
52fbf6b833
@ -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 ###
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
|
@ -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")
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
@ -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"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user