feat: Migrate from Tandoor (#2438)

* added tandoor migration to backend

* added tandoor migration to frontend

* updated tests

* ignore 0 amounts

* refactored ingredient display calculation

* fix parsing tandoor recipes with optional data

* generated frontend types

* fixed inconsistent default handling and from_orm

* removed unused imports
This commit is contained in:
Michael Genson
2023-07-23 12:52:09 -05:00
committed by GitHub
parent c25b58e404
commit 0f896107f9
18 changed files with 559 additions and 236 deletions

View File

@@ -14,11 +14,7 @@ from .group_events import (
from .group_exports import GroupDataExport
from .group_migration import DataMigrationCreate, SupportedMigrations
from .group_permissions import SetPermissions
from .group_preferences import (
CreateGroupPreferences,
ReadGroupPreferences,
UpdateGroupPreferences,
)
from .group_preferences import CreateGroupPreferences, ReadGroupPreferences, UpdateGroupPreferences
from .group_seeder import SeederConfig
from .group_shopping_list import (
ShoppingListAddRecipeParams,
@@ -26,6 +22,7 @@ from .group_shopping_list import (
ShoppingListItemBase,
ShoppingListItemCreate,
ShoppingListItemOut,
ShoppingListItemPagination,
ShoppingListItemRecipeRefCreate,
ShoppingListItemRecipeRefOut,
ShoppingListItemRecipeRefUpdate,
@@ -44,31 +41,11 @@ from .group_shopping_list import (
ShoppingListUpdate,
)
from .group_statistics import GroupStatistics, GroupStorage
from .invite_token import (
CreateInviteToken,
EmailInitationResponse,
EmailInvitation,
ReadInviteToken,
SaveInviteToken,
)
from .webhook import (
CreateWebhook,
ReadWebhook,
SaveWebhook,
WebhookPagination,
WebhookType,
)
from .invite_token import CreateInviteToken, EmailInitationResponse, EmailInvitation, ReadInviteToken, SaveInviteToken
from .webhook import CreateWebhook, ReadWebhook, SaveWebhook, WebhookPagination, WebhookType
__all__ = [
"CreateGroupPreferences",
"ReadGroupPreferences",
"UpdateGroupPreferences",
"GroupDataExport",
"CreateWebhook",
"ReadWebhook",
"SaveWebhook",
"WebhookPagination",
"WebhookType",
"GroupAdminUpdate",
"GroupEventNotifierCreate",
"GroupEventNotifierOptions",
"GroupEventNotifierOptionsOut",
@@ -78,14 +55,20 @@ __all__ = [
"GroupEventNotifierSave",
"GroupEventNotifierUpdate",
"GroupEventPagination",
"GroupDataExport",
"DataMigrationCreate",
"SupportedMigrations",
"SetPermissions",
"CreateGroupPreferences",
"ReadGroupPreferences",
"UpdateGroupPreferences",
"SeederConfig",
"ShoppingListAddRecipeParams",
"ShoppingListCreate",
"ShoppingListItemBase",
"ShoppingListItemCreate",
"ShoppingListItemOut",
"ShoppingListItemPagination",
"ShoppingListItemRecipeRefCreate",
"ShoppingListItemRecipeRefOut",
"ShoppingListItemRecipeRefUpdate",
@@ -102,8 +85,6 @@ __all__ = [
"ShoppingListSave",
"ShoppingListSummary",
"ShoppingListUpdate",
"GroupAdminUpdate",
"SetPermissions",
"GroupStatistics",
"GroupStorage",
"CreateInviteToken",
@@ -111,4 +92,9 @@ __all__ = [
"EmailInvitation",
"ReadInviteToken",
"SaveInviteToken",
"CreateWebhook",
"ReadWebhook",
"SaveWebhook",
"WebhookPagination",
"WebhookType",
]

View File

@@ -9,6 +9,7 @@ class SupportedMigrations(str, enum.Enum):
copymethat = "copymethat"
paprika = "paprika"
mealie_alpha = "mealie_alpha"
tandoor = "tandoor"
class DataMigrationCreate(MealieModel):

View File

@@ -1,7 +1,6 @@
from __future__ import annotations
from datetime import datetime
from fractions import Fraction
from pydantic import UUID4, validator
from sqlalchemy.orm import joinedload, selectinload
@@ -20,25 +19,13 @@ from mealie.schema.getter_dict import ExtrasGetterDict
from mealie.schema.labels.multi_purpose_label import MultiPurposeLabelSummary
from mealie.schema.recipe.recipe import RecipeSummary
from mealie.schema.recipe.recipe_ingredient import (
INGREDIENT_QTY_PRECISION,
MAX_INGREDIENT_DENOMINATOR,
IngredientFood,
IngredientUnit,
RecipeIngredient,
RecipeIngredientBase,
)
from mealie.schema.response.pagination import PaginationBase
SUPERSCRIPT = dict(zip("1234567890", "¹²³⁴⁵⁶⁷⁸⁹⁰", strict=False))
SUBSCRIPT = dict(zip("1234567890", "₁₂₃₄₅₆₇₈₉₀", strict=False))
def display_fraction(fraction: Fraction):
return (
"".join([SUPERSCRIPT[c] for c in str(fraction.numerator)])
+ "/"
+ "".join([SUBSCRIPT[c] for c in str(fraction.denominator)])
)
class ShoppingListItemRecipeRefCreate(MealieModel):
recipe_id: UUID4
@@ -63,20 +50,18 @@ class ShoppingListItemRecipeRefOut(ShoppingListItemRecipeRefUpdate):
orm_mode = True
class ShoppingListItemBase(MealieModel):
class ShoppingListItemBase(RecipeIngredientBase):
shopping_list_id: UUID4
checked: bool = False
position: int = 0
is_food: bool = False
note: str | None = ""
quantity: float = 1
food_id: UUID4 | None = None
label_id: UUID4 | None = None
unit_id: UUID4 | None = None
is_food: bool = False
extras: dict | None = {}
@@ -96,12 +81,6 @@ class ShoppingListItemUpdateBulk(ShoppingListItemUpdate):
class ShoppingListItemOut(ShoppingListItemBase):
id: UUID4
display: str = ""
"""
How the ingredient should be displayed
Automatically calculated after the object is created
"""
food: IngredientFood | None
label: MultiPurposeLabelSummary | None
@@ -120,63 +99,6 @@ class ShoppingListItemOut(ShoppingListItemBase):
self.label = self.food.label
self.label_id = self.label.id
# format the display property
if not self.display:
self.display = self._format_display()
def _format_quantity_for_display(self) -> str:
"""How the quantity should be displayed"""
qty: float | Fraction
# decimal
if not self.unit or not self.unit.fraction:
qty = round(self.quantity, INGREDIENT_QTY_PRECISION)
if qty.is_integer():
return str(int(qty))
else:
return str(qty)
# fraction
qty = Fraction(self.quantity).limit_denominator(MAX_INGREDIENT_DENOMINATOR)
if qty.denominator == 1:
return str(qty.numerator)
if qty.numerator <= qty.denominator:
return display_fraction(qty)
# convert an improper fraction into a mixed fraction (e.g. 11/4 --> 2 3/4)
whole_number = 0
while qty.numerator > qty.denominator:
whole_number += 1
qty -= 1
return f"{whole_number} {display_fraction(qty)}"
def _format_display(self) -> str:
components = []
# ingredients with no food come across with a qty of 1, which looks weird
# e.g. "1 2 tbsp of olive oil"
if self.quantity and (self.is_food or self.quantity != 1):
components.append(self._format_quantity_for_display())
if not self.is_food:
components.append(self.note or "")
else:
if self.quantity and self.unit:
components.append(self.unit.abbreviation if self.unit.use_abbreviation else self.unit.name)
if self.food:
components.append(self.food.name)
if self.note:
components.append(self.note)
return " ".join(components)
class Config:
orm_mode = True
getter_dict = ExtrasGetterDict