Feature/user seedable foods (#1176)

* remove odd ingredients

* UI Elements for food

* update translated percentage

* spek -> speck

* generate types

* seeder api endpoints + tests

* implement foods seeder UI

* localize some food strings
This commit is contained in:
Hayden
2022-05-01 12:45:50 -08:00
committed by GitHub
parent 67178f9b74
commit d6e2b4ab85
60 changed files with 478 additions and 172 deletions

View File

@@ -13,7 +13,7 @@ def fix_slug_food_names(db: AllRepositories):
logger = root_logger.get_logger("init_db")
if not food:
logger.info("No food found with slug: '{}' skipping fix".format(check_for_food))
logger.info(f"No food found with slug: '{check_for_food}' skipping fix")
return
all_foods = db.ingredient_foods.get_all()
@@ -23,5 +23,5 @@ def fix_slug_food_names(db: AllRepositories):
for food in all_foods:
if food.name in seed_foods:
food.name = seed_foods[food.name]
logger.info("Updating food: {}".format(food.name))
logger.info(f"Updating food: {food.name}")
db.ingredient_foods.update(food.id, food)

View File

@@ -14,7 +14,7 @@ from mealie.db.fixes.fix_slug_foods import fix_slug_food_names
from mealie.repos.all_repositories import get_repositories
from mealie.repos.repository_factory import AllRepositories
from mealie.repos.seed.init_users import default_user_init
from mealie.repos.seed.seeders import IngredientFoodsSeeder, IngredientUnitsSeeder, MultiPurposeLabelSeeder
from mealie.repos.seed.seeders import IngredientUnitsSeeder, MultiPurposeLabelSeeder
from mealie.schema.user.user import GroupBase
from mealie.services.group_services.group_service import GroupService
@@ -32,7 +32,6 @@ def init_db(db: AllRepositories) -> None:
seeders = [
MultiPurposeLabelSeeder(db, group_id=group_id),
IngredientFoodsSeeder(db, group_id=group_id),
IngredientUnitsSeeder(db, group_id=group_id),
]
@@ -63,11 +62,11 @@ def db_is_at_head(alembic_cfg: config.Config) -> bool:
return set(context.get_current_heads()) == set(directory.get_heads())
def safe_try(name: str, func: Callable):
def safe_try(func: Callable):
try:
func()
except Exception as e:
logger.error(f"Error calling '{name}': {e}")
logger.error(f"Error calling '{func.__name__}': {e}")
def connect(session: orm.Session) -> bool:
@@ -108,14 +107,13 @@ def main():
db = get_repositories(session)
init_user = db.users.get_all()
if init_user:
if db.users.get_all():
logger.info("Database exists")
else:
logger.info("Database contains no users, initializing...")
init_db(db)
safe_try("fix slug food names", lambda: fix_slug_food_names(db))
safe_try(lambda: fix_slug_food_names(db))
if __name__ == "__main__":

View File

@@ -25,5 +25,5 @@ class AbstractSeeder(ABC):
self.resources = Path(__file__).parent / "resources"
@abstractmethod
def seed(self):
def seed(self, locale: str | None = None) -> None:
...

View File

@@ -80,7 +80,7 @@
"eggplant": "eggplant",
"endive": "endive",
"fats": "fats",
"spek": "spek",
"speck": "speck",
"fava-beans": "fava beans",
"fiddlehead": "fiddlehead",
"fish": "fish",

View File

@@ -80,7 +80,7 @@
"eggplant": "eggplant",
"endive": "endive",
"fats": "fats",
"spek": "spek",
"speck": "speck",
"fava-beans": "fava beans",
"fiddlehead": "fiddlehead",
"fish": "fish",

View File

@@ -80,7 +80,7 @@
"eggplant": "eggplant",
"endive": "endive",
"fats": "fats",
"spek": "spek",
"speck": "speck",
"fava-beans": "fava beans",
"fiddlehead": "fiddlehead",
"fish": "fish",

View File

@@ -80,7 +80,7 @@
"eggplant": "eggplant",
"endive": "endive",
"fats": "fats",
"spek": "spek",
"speck": "speck",
"fava-beans": "fava beans",
"fiddlehead": "fiddlehead",
"fish": "fish",

View File

@@ -80,7 +80,7 @@
"eggplant": "eggplant",
"endive": "endive",
"fats": "fats",
"spek": "spek",
"speck": "speck",
"fava-beans": "fava beans",
"fiddlehead": "fiddlehead",
"fish": "fish",

View File

@@ -80,7 +80,7 @@
"eggplant": "Aubergine",
"endive": "Endivie",
"fats": "Fette",
"spek": "spek",
"speck": "speck",
"fava-beans": "Ackerbohnen",
"fiddlehead": "Farnspitzen",
"fish": "Fisch",

View File

@@ -80,7 +80,7 @@
"eggplant": "eggplant",
"endive": "endive",
"fats": "fats",
"spek": "spek",
"speck": "speck",
"fava-beans": "fava beans",
"fiddlehead": "fiddlehead",
"fish": "ψάρι",

View File

@@ -80,7 +80,7 @@
"eggplant": "eggplant",
"endive": "endive",
"fats": "fats",
"spek": "spek",
"speck": "speck",
"fava-beans": "fava beans",
"fiddlehead": "fiddlehead",
"fish": "fish",

View File

@@ -80,7 +80,7 @@
"eggplant": "eggplant",
"endive": "endive",
"fats": "fats",
"spek": "spek",
"speck": "speck",
"fava-beans": "fava beans",
"fiddlehead": "fiddlehead",
"fish": "fish",
@@ -119,7 +119,6 @@
"oregano": "oregano",
"parsley": "parsley",
"honey": "honey",
"horse": "horse",
"icing-sugar": "icing sugar",
"isomalt": "isomalt",
"jackfruit": "jackfruit",
@@ -176,7 +175,6 @@
"pumpkin": "pumpkin",
"pumpkin-seeds": "pumpkin seeds",
"radish": "radish",
"rape": "rape",
"raw-sugar": "raw sugar",
"refined-sugar": "refined sugar",
"rice-flour": "rice flour",

View File

@@ -80,7 +80,7 @@
"eggplant": "eggplant",
"endive": "endive",
"fats": "fats",
"spek": "spek",
"speck": "speck",
"fava-beans": "fava beans",
"fiddlehead": "fiddlehead",
"fish": "fish",

View File

@@ -80,7 +80,7 @@
"eggplant": "eggplant",
"endive": "endive",
"fats": "fats",
"spek": "spek",
"speck": "speck",
"fava-beans": "fava beans",
"fiddlehead": "fiddlehead",
"fish": "fish",

View File

@@ -80,7 +80,7 @@
"eggplant": "aubergine",
"endive": "endive",
"fats": "matières grasses",
"spek": "spek",
"speck": "speck",
"fava-beans": "fèves",
"fiddlehead": "crosse de fougère",
"fish": "poisson",

View File

@@ -80,7 +80,7 @@
"eggplant": "aubergine",
"endive": "endive",
"fats": "matières grasses",
"spek": "spek",
"speck": "speck",
"fava-beans": "fèves",
"fiddlehead": "crosse de fougère",
"fish": "poisson",

View File

@@ -80,7 +80,7 @@
"eggplant": "eggplant",
"endive": "endive",
"fats": "fats",
"spek": "spek",
"speck": "speck",
"fava-beans": "fava beans",
"fiddlehead": "fiddlehead",
"fish": "fish",

View File

@@ -80,7 +80,7 @@
"eggplant": "padlizsán",
"endive": "endívia",
"fats": "zsírok",
"spek": "speck sonka",
"speck": "speck sonka",
"fava-beans": "lóbab",
"fiddlehead": "hegedűfej",
"fish": "hal",

View File

@@ -80,7 +80,7 @@
"eggplant": "melanzana",
"endive": "endive",
"fats": "grassi",
"spek": "spek",
"speck": "speck",
"fava-beans": "fave",
"fiddlehead": "fiddlehead",
"fish": "pesce",

View File

@@ -80,7 +80,7 @@
"eggplant": "eggplant",
"endive": "endive",
"fats": "fats",
"spek": "spek",
"speck": "speck",
"fava-beans": "fava beans",
"fiddlehead": "fiddlehead",
"fish": "fish",

View File

@@ -80,7 +80,7 @@
"eggplant": "eggplant",
"endive": "endive",
"fats": "fats",
"spek": "spek",
"speck": "speck",
"fava-beans": "fava beans",
"fiddlehead": "fiddlehead",
"fish": "fish",

View File

@@ -80,7 +80,7 @@
"eggplant": "aubergine",
"endive": "andijvie",
"fats": "vetten",
"spek": "spek",
"speck": "speck",
"fava-beans": "tuinbonen",
"fiddlehead": "varenkrul",
"fish": "vis",

View File

@@ -80,7 +80,7 @@
"eggplant": "eggplant",
"endive": "endive",
"fats": "fats",
"spek": "spek",
"speck": "speck",
"fava-beans": "fava beans",
"fiddlehead": "fiddlehead",
"fish": "fish",

View File

@@ -80,7 +80,7 @@
"eggplant": "bakłażan",
"endive": "endywia",
"fats": "tłuszcze",
"spek": "boczek",
"speck": "boczek",
"fava-beans": "bób",
"fiddlehead": "pędy paproci",
"fish": "ryba",

View File

@@ -80,7 +80,7 @@
"eggplant": "eggplant",
"endive": "endive",
"fats": "fats",
"spek": "spek",
"speck": "speck",
"fava-beans": "fava beans",
"fiddlehead": "fiddlehead",
"fish": "fish",

View File

@@ -80,7 +80,7 @@
"eggplant": "eggplant",
"endive": "endive",
"fats": "fats",
"spek": "spek",
"speck": "speck",
"fava-beans": "fava beans",
"fiddlehead": "fiddlehead",
"fish": "fish",

View File

@@ -80,7 +80,7 @@
"eggplant": "eggplant",
"endive": "endive",
"fats": "fats",
"spek": "spek",
"speck": "speck",
"fava-beans": "fava beans",
"fiddlehead": "fiddlehead",
"fish": "fish",

View File

@@ -80,7 +80,7 @@
"eggplant": "eggplant",
"endive": "endive",
"fats": "fats",
"spek": "spek",
"speck": "speck",
"fava-beans": "fava beans",
"fiddlehead": "fiddlehead",
"fish": "fish",

View File

@@ -80,7 +80,7 @@
"eggplant": "eggplant",
"endive": "endive",
"fats": "fats",
"spek": "spek",
"speck": "speck",
"fava-beans": "fava beans",
"fiddlehead": "fiddlehead",
"fish": "fish",

View File

@@ -80,7 +80,7 @@
"eggplant": "eggplant",
"endive": "endive",
"fats": "fats",
"spek": "spek",
"speck": "speck",
"fava-beans": "fava beans",
"fiddlehead": "fiddlehead",
"fish": "fish",

View File

@@ -80,7 +80,7 @@
"eggplant": "äggplanta",
"endive": "endive",
"fats": "fetter",
"spek": "spek",
"speck": "speck",
"fava-beans": "fava beans",
"fiddlehead": "fiddlehead",
"fish": "fisk",

View File

@@ -80,7 +80,7 @@
"eggplant": "eggplant",
"endive": "endive",
"fats": "fats",
"spek": "spek",
"speck": "speck",
"fava-beans": "fava beans",
"fiddlehead": "fiddlehead",
"fish": "fish",

View File

@@ -80,7 +80,7 @@
"eggplant": "баклажан",
"endive": "ендивій (салатний цикорій)",
"fats": "жири",
"spek": "шпек",
"speck": "шпек",
"fava-beans": "біб кінський",
"fiddlehead": "рахіси папороті",
"fish": "риба",

View File

@@ -80,7 +80,7 @@
"eggplant": "eggplant",
"endive": "endive",
"fats": "fats",
"spek": "spek",
"speck": "speck",
"fava-beans": "fava beans",
"fiddlehead": "fiddlehead",
"fish": "fish",

View File

@@ -80,7 +80,7 @@
"eggplant": "eggplant",
"endive": "endive",
"fats": "fats",
"spek": "spek",
"speck": "speck",
"fava-beans": "fava beans",
"fiddlehead": "fiddlehead",
"fish": "fish",

View File

@@ -80,7 +80,7 @@
"eggplant": "茄子",
"endive": "endive",
"fats": "脂肪",
"spek": "spek",
"speck": "speck",
"fava-beans": "fava beans",
"fiddlehead": "fiddlehead",
"fish": "魚",

View File

@@ -1,4 +1,5 @@
import json
import pathlib
from collections.abc import Generator
from mealie.schema.labels import MultiPurposeLabelSave
@@ -9,8 +10,12 @@ from .resources import foods, labels, units
class MultiPurposeLabelSeeder(AbstractSeeder):
def load_data(self) -> Generator[MultiPurposeLabelSave, None, None]:
file = labels.en_US
def get_file(self, locale: str | None = None) -> pathlib.Path:
locale_path = self.resources / "labels" / "locales" / f"{locale}.json"
return locale_path if locale_path.exists() else labels.en_US
def load_data(self, locale: str | None = None) -> Generator[MultiPurposeLabelSave, None, None]:
file = self.get_file(locale)
for label in json.loads(file.read_text()):
yield MultiPurposeLabelSave(
@@ -18,9 +23,9 @@ class MultiPurposeLabelSeeder(AbstractSeeder):
group_id=self.group_id,
)
def seed(self) -> None:
def seed(self, locale: str | None = None) -> None:
self.logger.info("Seeding MultiPurposeLabel")
for label in self.load_data():
for label in self.load_data(locale):
try:
self.repos.group_multi_purpose_labels.create(label)
except Exception as e:
@@ -28,8 +33,13 @@ class MultiPurposeLabelSeeder(AbstractSeeder):
class IngredientUnitsSeeder(AbstractSeeder):
def load_data(self) -> Generator[SaveIngredientUnit, None, None]:
file = units.en_US
def get_file(self, locale: str | None = None) -> pathlib.Path:
locale_path = self.resources / "units" / "locales" / f"{locale}.json"
return locale_path if locale_path.exists() else units.en_US
def load_data(self, locale: str | None = None) -> Generator[SaveIngredientUnit, None, None]:
file = self.get_file(locale)
for unit in json.loads(file.read_text()).values():
yield SaveIngredientUnit(
group_id=self.group_id,
@@ -38,9 +48,9 @@ class IngredientUnitsSeeder(AbstractSeeder):
abbreviation=unit["abbreviation"],
)
def seed(self) -> None:
def seed(self, locale: str | None = None) -> None:
self.logger.info("Seeding Ingredient Units")
for unit in self.load_data():
for unit in self.load_data(locale):
try:
self.repos.ingredient_units.create(unit)
except Exception as e:
@@ -48,8 +58,13 @@ class IngredientUnitsSeeder(AbstractSeeder):
class IngredientFoodsSeeder(AbstractSeeder):
def load_data(self) -> Generator[SaveIngredientFood, None, None]:
file = foods.en_US
def get_file(self, locale: str | None = None) -> pathlib.Path:
locale_path = self.resources / "foods" / "locales" / f"{locale}.json"
return locale_path if locale_path.exists() else foods.en_US
def load_data(self, locale: str | None = None) -> Generator[SaveIngredientFood, None, None]:
file = self.get_file(locale)
seed_foods: dict[str, str] = json.loads(file.read_text())
for food in seed_foods.values():
yield SaveIngredientFood(
@@ -58,9 +73,9 @@ class IngredientFoodsSeeder(AbstractSeeder):
description="",
)
def seed(self) -> None:
def seed(self, locale: str | None = None) -> None:
self.logger.info("Seeding Ingredient Foods")
for food in self.load_data():
for food in self.load_data(locale):
try:
self.repos.ingredient_foods.create(food)
except Exception as e:

View File

@@ -11,6 +11,7 @@ from . import (
controller_mealplan_config,
controller_mealplan_rules,
controller_migrations,
controller_seeder,
controller_shopping_lists,
controller_webhooks,
)
@@ -30,3 +31,4 @@ router.include_router(controller_shopping_lists.router)
router.include_router(controller_shopping_lists.item_router)
router.include_router(controller_labels.router)
router.include_router(controller_group_notifications.router)
router.include_router(controller_seeder.router)

View File

@@ -0,0 +1,38 @@
from functools import cached_property
from fastapi import APIRouter, HTTPException
from mealie.routes._base.base_controllers import BaseUserController
from mealie.routes._base.controller import controller
from mealie.schema.group.group_seeder import SeederConfig
from mealie.schema.response.responses import ErrorResponse, SuccessResponse
from mealie.services.seeder.seeder_service import SeederService
router = APIRouter(prefix="/groups/seeders", tags=["Groups: Seeders"])
@controller(router)
class DataSeederController(BaseUserController):
@cached_property
def service(self) -> SeederService:
return SeederService(self.repos, self.user, self.group)
def _wrap(self, func):
try:
func()
except Exception as e:
raise HTTPException(status_code=500, detail=ErrorResponse.respond("Seeding Failed")) from e
return SuccessResponse.respond("Seeding Successful")
@router.post("/foods", response_model=SuccessResponse)
def seed_foods(self, data: SeederConfig) -> dict:
return self._wrap(lambda: self.service.seed_foods(data.locale))
@router.post("/labels", response_model=SuccessResponse)
def seed_labels(self, data: SeederConfig) -> dict:
return self._wrap(lambda: self.service.seed_labels(data.locale))
@router.post("/units", response_model=SuccessResponse)
def seed_units(self, data: SeederConfig) -> dict:
return self._wrap(lambda: self.service.seed_units(data.locale))

View File

@@ -5,6 +5,7 @@ from .group_exports import *
from .group_migration import *
from .group_permissions import *
from .group_preferences import *
from .group_seeder import *
from .group_shopping_list import *
from .group_statistics import *
from .invite_token import *

View File

@@ -0,0 +1,53 @@
from pydantic import validator
from mealie.schema._mealie.mealie_model import MealieModel
def validate_locale(locale: str) -> bool:
valid = {
"el-GR",
"it-IT",
"ko-KR",
"es-ES",
"ja-JP",
"zh-CN",
"tr-TR",
"ar-SA",
"hu-HU",
"pt-PT",
"no-NO",
"sv-SE",
"ro-RO",
"sk-SK",
"uk-UA",
"fr-CA",
"pl-PL",
"da-DK",
"pt-BR",
"de-DE",
"ca-ES",
"sr-SP",
"cs-CZ",
"fr-FR",
"zh-TW",
"af-ZA",
"ru-RU",
"he-IL",
"nl-NL",
"en-US",
"en-GB",
"fi-FI",
"vi-VN",
}
return locale in valid
class SeederConfig(MealieModel):
locale: str
@validator("locale")
def valid_locale(cls, v, values, **kwargs):
if not validate_locale(v):
raise ValueError("passwords do not match")
return v

View File

View File

@@ -0,0 +1,24 @@
from mealie.repos.repository_factory import AllRepositories
from mealie.repos.seed.seeders import IngredientFoodsSeeder, IngredientUnitsSeeder, MultiPurposeLabelSeeder
from mealie.schema.user.user import GroupInDB, PrivateUser
from mealie.services._base_service import BaseService
class SeederService(BaseService):
def __init__(self, repos: AllRepositories, user: PrivateUser, group: GroupInDB):
self.repos = repos
self.user = user
self.group = group
super().__init__()
def seed_foods(self, locale: str) -> None:
seeder = IngredientFoodsSeeder(self.repos, self.logger, self.group.id)
seeder.seed(locale)
def seed_labels(self, locale: str) -> None:
seeder = MultiPurposeLabelSeeder(self.repos, self.logger, self.group.id)
seeder.seed(locale)
def seed_units(self, locale: str) -> None:
seeder = IngredientUnitsSeeder(self.repos, self.logger, self.group.id)
seeder.seed(locale)