mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-30 17:53:31 -04:00 
			
		
		
		
	feat: sort by labels in shopping list copy if labels toggled (#3226)
* feat: sort by labels in shopping list copy if labels toggled * fix: call parent validator in shopping list item out (#3227) * fix: add a unit test for (#3227) * fixed messy post_validate logic * feat: label headings in shopping list copy * feat: blank line for each group in shopping list copy --------- Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
This commit is contained in:
		| @@ -337,21 +337,50 @@ export default defineComponent({ | |||||||
|     const copy = useCopyList(); |     const copy = useCopyList(); | ||||||
|  |  | ||||||
|     function copyListItems(copyType: CopyTypes) { |     function copyListItems(copyType: CopyTypes) { | ||||||
|       const items = shoppingList.value?.listItems?.filter((item) => !item.checked); |       const text: string[] = []; | ||||||
|  |  | ||||||
|       if (!items) { |       if (preferences.value.viewByLabel) { | ||||||
|         return; |         // if we're sorting by label, we want the copied text in subsections | ||||||
|  |         Object.entries(itemsByLabel.value).forEach(([label, items], idx) => { | ||||||
|  |           // for every group except the first, add a blank line | ||||||
|  |           if (idx) { | ||||||
|  |             text.push("") | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           // add an appropriate heading for the label depending on the copy format | ||||||
|  |           text.push(formatCopiedLabelHeading(copyType, label)) | ||||||
|  |  | ||||||
|  |           // now add the appropriately formatted list items with the given label | ||||||
|  |           items.forEach((item) => text.push(formatCopiedListItem(copyType, item))) | ||||||
|  |         }) | ||||||
|  |       } else { | ||||||
|  |         // labels are toggled off, so just copy in the order they come in | ||||||
|  |         const items = shoppingList.value?.listItems?.filter((item) => !item.checked) | ||||||
|  |  | ||||||
|  |         items?.forEach((item) => { | ||||||
|  |           text.push(formatCopiedListItem(copyType, item)) | ||||||
|  |         }); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       const text: string[] = items.map((itm) => itm.display || ""); |       copy.copyPlain(text); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function formatCopiedListItem(copyType: CopyTypes, item: ShoppingListItemOut): string { | ||||||
|  |       const display = item.display || "" | ||||||
|       switch (copyType) { |       switch (copyType) { | ||||||
|         case "markdown": |         case "markdown": | ||||||
|           copy.copyMarkdownCheckList(text); |           return `- [ ] ${display}` | ||||||
|           break; |  | ||||||
|         default: |         default: | ||||||
|           copy.copyPlain(text); |           return display | ||||||
|           break; |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function formatCopiedLabelHeading(copyType: CopyTypes, label: string): string { | ||||||
|  |       switch (copyType) { | ||||||
|  |         case "markdown": | ||||||
|  |           return `# ${label}` | ||||||
|  |         default: | ||||||
|  |           return `[${label}]` | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -101,7 +101,7 @@ class ShoppingListItemOut(ShoppingListItemBase): | |||||||
|     update_at: datetime | None = None |     update_at: datetime | None = None | ||||||
|  |  | ||||||
|     @model_validator(mode="after") |     @model_validator(mode="after") | ||||||
|     def post_validate(self): |     def populate_missing_label(self): | ||||||
|         # if we're missing a label, but the food has a label, use that as the label |         # if we're missing a label, but the food has a label, use that as the label | ||||||
|         if (not self.label) and (self.food and self.food.label): |         if (not self.label) and (self.food and self.food.label): | ||||||
|             self.label = self.food.label |             self.label = self.food.label | ||||||
|   | |||||||
| @@ -184,13 +184,13 @@ class Recipe(RecipeSummary): | |||||||
|     model_config = ConfigDict(from_attributes=True) |     model_config = ConfigDict(from_attributes=True) | ||||||
|  |  | ||||||
|     @model_validator(mode="after") |     @model_validator(mode="after") | ||||||
|     def post_validate(self): |     def calculate_missing_food_flags_and_format_display(self): | ||||||
|         # the ingredient disable_amount property is unreliable, |  | ||||||
|         # so we set it here and recalculate the display property |  | ||||||
|         disable_amount = self.settings.disable_amount if self.settings else True |         disable_amount = self.settings.disable_amount if self.settings else True | ||||||
|         for ingredient in self.recipe_ingredient: |         for ingredient in self.recipe_ingredient: | ||||||
|             ingredient.disable_amount = disable_amount |             ingredient.disable_amount = disable_amount | ||||||
|             ingredient.is_food = not ingredient.disable_amount |             ingredient.is_food = not ingredient.disable_amount | ||||||
|  |  | ||||||
|  |             # recalculate the display property, since it depends on the disable_amount flag | ||||||
|             ingredient.display = ingredient._format_display() |             ingredient.display = ingredient._format_display() | ||||||
|  |  | ||||||
|         return self |         return self | ||||||
|   | |||||||
| @@ -145,7 +145,7 @@ class RecipeIngredientBase(MealieModel): | |||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     @model_validator(mode="after") |     @model_validator(mode="after") | ||||||
|     def post_validate(self): |     def calculate_missing_food_flags(self): | ||||||
|         # calculate missing is_food and disable_amount values |         # calculate missing is_food and disable_amount values | ||||||
|         # we can't do this in a validator since they depend on each other |         # we can't do this in a validator since they depend on each other | ||||||
|         if self.is_food is None and self.disable_amount is not None: |         if self.is_food is None and self.disable_amount is not None: | ||||||
| @@ -156,7 +156,10 @@ class RecipeIngredientBase(MealieModel): | |||||||
|             self.is_food = bool(self.food) |             self.is_food = bool(self.food) | ||||||
|             self.disable_amount = not self.is_food |             self.disable_amount = not self.is_food | ||||||
|  |  | ||||||
|         # format the display property |         return self | ||||||
|  |  | ||||||
|  |     @model_validator(mode="after") | ||||||
|  |     def format_display(self): | ||||||
|         if not self.display: |         if not self.display: | ||||||
|             self.display = self._format_display() |             self.display = self._format_display() | ||||||
|  |  | ||||||
|   | |||||||
| @@ -0,0 +1,37 @@ | |||||||
|  | from mealie.schema.group.group_shopping_list import ShoppingListItemOut | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_shopping_list_ingredient_validation(): | ||||||
|  |     db_obj = { | ||||||
|  |         "quantity": 8, | ||||||
|  |         "unit": None, | ||||||
|  |         "food": { | ||||||
|  |             "id": "4cf32eeb-d136-472d-86c7-287b6328d21f", | ||||||
|  |             "name": "bell peppers", | ||||||
|  |             "pluralName": None, | ||||||
|  |             "description": "", | ||||||
|  |             "extras": {}, | ||||||
|  |             "labelId": None, | ||||||
|  |             "aliases": [], | ||||||
|  |             "label": None, | ||||||
|  |             "createdAt": "2024-02-26T18:29:46.190754", | ||||||
|  |             "updateAt": "2024-02-26T18:29:46.190758", | ||||||
|  |         }, | ||||||
|  |         "note": "", | ||||||
|  |         "isFood": True, | ||||||
|  |         "disableAmount": False, | ||||||
|  |         "shoppingListId": "dc8bce82-2da9-49f0-94e6-6d69d311490e", | ||||||
|  |         "checked": False, | ||||||
|  |         "position": 5, | ||||||
|  |         "foodId": "4cf32eeb-d136-472d-86c7-287b6328d21f", | ||||||
|  |         "labelId": None, | ||||||
|  |         "unitId": None, | ||||||
|  |         "extras": {}, | ||||||
|  |         "id": "80f4df25-6139-4d30-be0c-4100f50e5396", | ||||||
|  |         "label": None, | ||||||
|  |         "recipeReferences": [], | ||||||
|  |         "createdAt": "2024-02-27T10:18:19.274677", | ||||||
|  |         "updateAt": "2024-02-27T11:26:32.643392", | ||||||
|  |     } | ||||||
|  |     out = ShoppingListItemOut.model_validate(db_obj) | ||||||
|  |     assert out.display == "8 bell peppers" | ||||||
		Reference in New Issue
	
	Block a user