mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-27 16:24:31 -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:
		| @@ -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"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user