From f796258529fda0a0e6d115c1d9c7b5945aedfc3d Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Thu, 12 Sep 2024 04:43:23 -0500 Subject: [PATCH 1/4] fix: Broken Social Preview Links (#4183) --- mealie/routes/spa/__init__.py | 14 +-- tests/integration_tests/test_spa.py | 131 ++++++++++++++++++++++++++-- 2 files changed, 133 insertions(+), 12 deletions(-) diff --git a/mealie/routes/spa/__init__.py b/mealie/routes/spa/__init__.py index a673969c7b8..77af4747dc3 100644 --- a/mealie/routes/spa/__init__.py +++ b/mealie/routes/spa/__init__.py @@ -165,13 +165,13 @@ def serve_recipe_with_meta_public( public_repos = AllRepositories(session) group = public_repos.groups.get_by_slug_or_id(group_slug) - if not group or group.preferences.private_group: # type: ignore + if not (group and group.preferences) or group.preferences.private_group: return response_404() - group_repos = AllRepositories(session, group_id=group.id) + group_repos = AllRepositories(session, group_id=group.id, household_id=None) recipe = group_repos.recipes.get_one(recipe_slug) - if not recipe or not recipe.settings.public: # type: ignore + if not (recipe and recipe.settings) or not recipe.settings.public: return response_404() # Inject meta tags @@ -190,9 +190,9 @@ async def serve_recipe_with_meta( return serve_recipe_with_meta_public(group_slug, recipe_slug, session) try: - repos = AllRepositories(session, group_id=user.group_id) + group_repos = AllRepositories(session, group_id=user.group_id, household_id=None) - recipe = repos.recipes.get_one(recipe_slug, "slug") + recipe = group_repos.recipes.get_one(recipe_slug, "slug") if recipe is None: return response_404() @@ -204,8 +204,8 @@ async def serve_recipe_with_meta( async def serve_shared_recipe_with_meta(group_slug: str, token_id: str, session: Session = Depends(generate_session)): try: - repos = AllRepositories(session) - token_summary = repos.recipe_share_tokens.get_one(token_id) + public_repos = AllRepositories(session, group_id=None) + token_summary = public_repos.recipe_share_tokens.get_one(token_id) if token_summary is None: raise Exception("Token Not Found") diff --git a/tests/integration_tests/test_spa.py b/tests/integration_tests/test_spa.py index f4c3dc190c8..68373f79ab8 100644 --- a/tests/integration_tests/test_spa.py +++ b/tests/integration_tests/test_spa.py @@ -1,8 +1,46 @@ +import pytest from bs4 import BeautifulSoup -from mealie.routes.spa import MetaTag, inject_meta, inject_recipe_json +from mealie.routes import spa +from mealie.schema.recipe.recipe import Recipe +from mealie.schema.recipe.recipe_share_token import RecipeShareTokenSave from tests import data as test_data from tests.utils.factories import random_string +from tests.utils.fixture_schemas import TestUser + + +@pytest.fixture(autouse=True) +def set_spa_contents(): + """Inject a simple HTML string into the SPA module to enable metadata injection""" + + spa.__contents = "" + + +def set_group_is_private(unique_user: TestUser, *, is_private: bool): + group = unique_user.repos.groups.get_by_slug_or_id(unique_user.group_id) + assert group and group.preferences + group.preferences.private_group = is_private + unique_user.repos.group_preferences.update(group.id, group.preferences) + + +def set_recipe_is_public(unique_user: TestUser, recipe: Recipe, *, is_public: bool): + assert recipe.settings + recipe.settings.public = is_public + unique_user.repos.recipes.update(recipe.slug, recipe) + + +def create_recipe(user: TestUser) -> Recipe: + recipe = user.repos.recipes.create( + Recipe( + user_id=user.user_id, + group_id=user.group_id, + name=random_string(), + ) + ) + set_group_is_private(user, is_private=False) + set_recipe_is_public(user, recipe, is_public=True) + + return recipe def test_spa_metadata_injection(): @@ -22,9 +60,9 @@ def test_spa_metadata_injection(): assert title_tag and title_tag["content"] - new_title_tag = MetaTag(hid="og:title", property_name="og:title", content=random_string()) - new_arbitrary_tag = MetaTag(hid=random_string(), property_name=random_string(), content=random_string()) - new_html = inject_meta(str(soup), [new_title_tag, new_arbitrary_tag]) + new_title_tag = spa.MetaTag(hid="og:title", property_name="og:title", content=random_string()) + new_arbitrary_tag = spa.MetaTag(hid=random_string(), property_name=random_string(), content=random_string()) + new_html = spa.inject_meta(str(soup), [new_title_tag, new_arbitrary_tag]) # verify changes were injected soup = BeautifulSoup(new_html, "lxml") @@ -63,8 +101,91 @@ def test_spa_recipe_json_injection(): soup = BeautifulSoup(f, "lxml") assert "https://schema.org" not in str(soup) - html = inject_recipe_json(str(soup), schema) + html = spa.inject_recipe_json(str(soup), schema) assert "@context" in html assert "https://schema.org" in html assert recipe_name in html + + +@pytest.mark.parametrize("use_public_user", [True, False]) +@pytest.mark.asyncio +async def test_spa_serve_recipe_with_meta(unique_user: TestUser, use_public_user: bool): + recipe = create_recipe(unique_user) + user = unique_user.repos.users.get_by_username(unique_user.username) + assert user + + response = await spa.serve_recipe_with_meta( + user.group_slug, recipe.slug, user=None if use_public_user else user, session=unique_user.repos.session + ) + assert response.status_code == 200 + assert "https://schema.org" in response.body.decode() + + +@pytest.mark.parametrize("use_public_user", [True, False]) +@pytest.mark.asyncio +async def test_spa_serve_recipe_with_meta_invalid_data(unique_user: TestUser, use_public_user: bool): + recipe = create_recipe(unique_user) + user = unique_user.repos.users.get_by_username(unique_user.username) + assert user + + response = await spa.serve_recipe_with_meta( + random_string(), recipe.slug, user=None if use_public_user else user, session=unique_user.repos.session + ) + assert response.status_code == 404 + + response = await spa.serve_recipe_with_meta( + user.group_slug, random_string(), user=None if use_public_user else user, session=unique_user.repos.session + ) + assert response.status_code == 404 + + set_recipe_is_public(unique_user, recipe, is_public=False) + response = await spa.serve_recipe_with_meta( + user.group_slug, recipe.slug, user=None if use_public_user else user, session=unique_user.repos.session + ) + if use_public_user: + assert response.status_code == 404 + else: + assert response.status_code == 200 + + set_group_is_private(unique_user, is_private=True) + set_recipe_is_public(unique_user, recipe, is_public=True) + response = await spa.serve_recipe_with_meta( + user.group_slug, recipe.slug, user=None if use_public_user else user, session=unique_user.repos.session + ) + if use_public_user: + assert response.status_code == 404 + else: + assert response.status_code == 200 + + +@pytest.mark.parametrize("use_private_group", [True, False]) +@pytest.mark.parametrize("use_public_recipe", [True, False]) +@pytest.mark.asyncio +async def test_spa_service_shared_recipe_with_meta( + unique_user: TestUser, use_private_group: bool, use_public_recipe: bool +): + group = unique_user.repos.groups.get_by_slug_or_id(unique_user.group_id) + assert group + recipe = create_recipe(unique_user) + + # visibility settings shouldn't matter for shared recipes + set_group_is_private(unique_user, is_private=use_private_group) + set_recipe_is_public(unique_user, recipe, is_public=use_public_recipe) + + token = unique_user.repos.recipe_share_tokens.create( + RecipeShareTokenSave(recipe_id=recipe.id, group_id=unique_user.group_id) + ) + + response = await spa.serve_shared_recipe_with_meta(group.slug, token.id, session=unique_user.repos.session) + assert response.status_code == 200 + assert "https://schema.org" in response.body.decode() + + +@pytest.mark.asyncio +async def test_spa_service_shared_recipe_with_meta_invalid_data(unique_user: TestUser): + group = unique_user.repos.groups.get_by_slug_or_id(unique_user.group_id) + assert group + + response = await spa.serve_shared_recipe_with_meta(group.slug, random_string(), session=unique_user.repos.session) + assert response.status_code == 404 From c97053ef83b03729853c52c8d61af157995925e0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Sep 2024 21:59:37 +1000 Subject: [PATCH 2/4] chore(deps): update dependency pydantic-to-typescript2 to v1.0.6 (#4199) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 49180e7aaad..1dcd8c8ced1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2202,13 +2202,13 @@ yaml = ["pyyaml (>=6.0.1)"] [[package]] name = "pydantic-to-typescript2" -version = "1.0.4" +version = "1.0.6" description = "Convert pydantic v1 and pydantic v2 models to typescript interfaces" optional = false python-versions = "*" files = [ - {file = "pydantic-to-typescript2-1.0.4.tar.gz", hash = "sha256:1bd25dea0e1ce4220c495ad408d028ddd905376cbed1a856b2caa6f31c344371"}, - {file = "pydantic_to_typescript2-1.0.4-py3-none-any.whl", hash = "sha256:597f1848e918d0b95879c9a371ff664c8f300894d43aafb0b7f693f6c84f3657"}, + {file = "pydantic-to-typescript2-1.0.6.tar.gz", hash = "sha256:19cc0fb03802abcb508b02fbc334f1667ff50e0853a782b58df9dd0409290163"}, + {file = "pydantic_to_typescript2-1.0.6-py3-none-any.whl", hash = "sha256:89bbdd4b84b72d9f8ada33fd4d7d6605457be302dd6d4c6d48faa9310841bb69"}, ] [package.dependencies] From 6f1df3a95e71d1f4481ccf98b1c7739f96061181 Mon Sep 17 00:00:00 2001 From: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com> Date: Thu, 12 Sep 2024 16:07:26 +0200 Subject: [PATCH 3/4] feat: Reorder ShoppingListItemEditor (#4200) --- .../ShoppingList/ShoppingListItemEditor.vue | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/frontend/components/Domain/ShoppingList/ShoppingListItemEditor.vue b/frontend/components/Domain/ShoppingList/ShoppingListItemEditor.vue index a707924723a..da9ca4427ea 100644 --- a/frontend/components/Domain/ShoppingList/ShoppingListItemEditor.vue +++ b/frontend/components/Domain/ShoppingList/ShoppingListItemEditor.vue @@ -3,14 +3,9 @@
- +
+ +
+ +
+
+ +
-
- -
+
Date: Thu, 12 Sep 2024 09:49:08 -0500 Subject: [PATCH 4/4] chore(l10n): New Crowdin updates (#4201) --- frontend/lang/messages/es-ES.json | 42 +++++++++++++++---------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/frontend/lang/messages/es-ES.json b/frontend/lang/messages/es-ES.json index 7366526be58..258fbe541c2 100644 --- a/frontend/lang/messages/es-ES.json +++ b/frontend/lang/messages/es-ES.json @@ -283,14 +283,14 @@ "manage-households": "Administrar Casas", "admin-household-management": "Administración de la Casa", "admin-household-management-text": "Changes to this household will be reflected immediately.", - "household-id-value": "Household Id: {0}", + "household-id-value": "Id del hogar: {0}", "private-household": "Casa Privada", - "private-household-description": "Setting your household to private will default all public view options to default. This overrides any individual recipes public view settings.", - "household-recipe-preferences": "Household Recipe Preferences", - "default-recipe-preferences-description": "These are the default settings when a new recipe is created in your household. These can be changed for individual recipes in the recipe settings menu.", - "allow-users-outside-of-your-household-to-see-your-recipes": "Allow users outside of your household to see your recipes", - "allow-users-outside-of-your-household-to-see-your-recipes-description": "When enabled you can use a public share link to share specific recipes without authorizing the user. When disabled, you can only share recipes with users who are in your household or with a pre-generated private link", - "household-preferences": "Household Preferences" + "private-household-description": "Configurar tu hogar a privado predeterminará todas las opciones de vista pública por defecto. Esto anula cualquier receta individual de configuración de vista pública.", + "household-recipe-preferences": "Preferencias de la Receta de la Casa", + "default-recipe-preferences-description": "Estas son las configuraciones por defecto cuando se crea una nueva receta en su hogar. Estos se pueden cambiar para recetas individuales en el menú de ajustes de receta.", + "allow-users-outside-of-your-household-to-see-your-recipes": "Permite a los usuarios fuera de tu hogar ver tus recetas", + "allow-users-outside-of-your-household-to-see-your-recipes-description": "Cuando está habilitado puede utilizar un enlace público para compartir recetas específicas sin autorizar al usuario. Cuando está desactivado, sólo puedes compartir recetas con usuarios que estén en tu hogar o con un enlace privado generado previamente", + "household-preferences": "Preferencias de la Casa" }, "meal-plan": { "create-a-new-meal-plan": "Crear un nuevo menú", @@ -583,12 +583,12 @@ "create-recipe-description": "Crear nueva receta desde cero.", "create-recipes": "Crear Recetas", "import-with-zip": "Importar desde .zip", - "create-recipe-from-an-image": "Create Recipe from an Image", - "create-recipe-from-an-image-description": "Create a recipe by uploading an image of it. Mealie will attempt to extract the text from the image using AI and create a recipe from it.", - "crop-and-rotate-the-image": "Crop and rotate the image so that only the text is visible, and it's in the correct orientation.", - "create-from-image": "Create from Image", - "should-translate-description": "Translate the recipe into my language", - "please-wait-image-procesing": "Please wait, the image is processing. This may take some time.", + "create-recipe-from-an-image": "Crear receta a partir de una imagen", + "create-recipe-from-an-image-description": "Crea una receta cargando una imagen de ella. Mealie intentará extraer el texto de la imagen usando IA y crear una receta de ella.", + "crop-and-rotate-the-image": "Recortar y rotar la imagen de manera que sólo el texto sea visible, y esté en la orientación correcta.", + "create-from-image": "Crear desde imagen", + "should-translate-description": "Traducir la receta a mi idioma", + "please-wait-image-procesing": "Por favor, espere, la imagen se está procesando. Esto puede tardar un tiempo.", "bulk-url-import": "Importación masiva desde URL", "debug-scraper": "Depurar analizador", "create-a-recipe-by-providing-the-name-all-recipes-must-have-unique-names": "Crear una receta proporcionando el nombre. Todas las recetas deben tener nombres únicos.", @@ -1252,8 +1252,8 @@ "account-summary-description": "Aquí hay un resumen de la información del grupo.", "group-statistics": "Estadísticas del grupo", "group-statistics-description": "Tus estadísticas de grupo proporcionan información sobre cómo utilizas Mealie.", - "household-statistics": "Household Statistics", - "household-statistics-description": "Your Household Statistics provide some insight how you're using Mealie.", + "household-statistics": "Estadísticas de la Casa", + "household-statistics-description": "Tus estadísticas de la casa proporcionan información sobre cómo utilizas Mealie.", "storage-capacity": "Capacidad de almacenamiento", "storage-capacity-description": "Tu capacidad de almacenamiento es el cálculo de las imágenes y recursos que has subido.", "personal": "Personal", @@ -1263,13 +1263,13 @@ "api-tokens-description": "Administra tus API tokens para el acceso desde apps externas.", "group-description": "Estos elementos se comparten dentro del grupo. ¡Editar cualquiera de ellos lo modificará para todo el grupo!", "group-settings": "Ajustes de grupo", - "group-settings-description": "Manage your common group settings, like privacy settings.", - "household-description": "These items are shared within your household. Editing one of them will change it for the whole household!", - "household-settings": "Household Settings", - "household-settings-description": "Manage your household settings, like mealplan and privacy settings.", + "group-settings-description": "Administra tu configuración común de grupo, como la configuración de privacidad.", + "household-description": "Estos elementos se comparten dentro de la casa. ¡Editar cualquiera de ellos lo modificará para todos en la casa!", + "household-settings": "Configuración de la Casa", + "household-settings-description": "Administre los ajustes de su hogar, como el plan de comidas y la configuración de privacidad.", "cookbooks-description": "Gestiona un a colección de categorías de receta y genera páginas para estas.", "members": "Miembros", - "members-description": "See who's in your household and manage their permissions.", + "members-description": "Mira quién está en tu hogar y administra sus permisos.", "webhooks-description": "Setup webhooks that trigger on days that you have have mealplan scheduled.", "notifiers": "Notificaciones", "notifiers-description": "Setup email and push notifications that trigger on specific events.", @@ -1304,7 +1304,7 @@ "require-all-tools": "Requiere todos los utensilios", "cookbook-name": "Nombre del recetario", "cookbook-with-name": "Recetario {0}", - "household-cookbook-name": "{0} Cookbook {1}", + "household-cookbook-name": "{0} Libro de Cocina {1}", "create-a-cookbook": "Crear Recetario", "cookbook": "Recetario" }