Compare commits

..

8 Commits

37 changed files with 306 additions and 233 deletions

View File

@@ -31,7 +31,7 @@ To deploy mealie on your local network, it is highly recommended to use Docker t
We've gone through a few versions of Mealie v1 deployment targets. We have settled on a single container deployment, and we've begun publishing the nightly container on github containers. If you're looking to move from the old nightly (split containers _or_ the omni image) to the new nightly, there are a few things you need to do:
1. Take a backup just in case!
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v3.17.0`
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v3.18.0`
3. Take the external port from the frontend container and set that as the port mapped to port `9000` on the new container. The frontend is now served on port 9000 from the new container, so it will need to be mapped for you to have access.
4. Restart the container

View File

@@ -10,7 +10,7 @@ PostgreSQL might be considered if you need to support many concurrent users. In
```yaml
services:
mealie:
image: ghcr.io/mealie-recipes/mealie:v3.17.0 # (3)
image: ghcr.io/mealie-recipes/mealie:v3.18.0 # (3)
container_name: mealie
restart: always
ports:

View File

@@ -11,7 +11,7 @@ SQLite is a popular, open source, self-contained, zero-configuration database th
```yaml
services:
mealie:
image: ghcr.io/mealie-recipes/mealie:v3.17.0 # (3)
image: ghcr.io/mealie-recipes/mealie:v3.18.0 # (3)
container_name: mealie
restart: always
ports:

View File

@@ -38,7 +38,7 @@ export const LOCALES = [
{
name: "Svenska (Swedish)",
value: "sv-SE",
progress: 74,
progress: 75,
dir: "ltr",
pluralFoodHandling: "always",
},
@@ -101,14 +101,14 @@ export const LOCALES = [
{
name: "Norsk (Norwegian)",
value: "no-NO",
progress: 59,
progress: 60,
dir: "ltr",
pluralFoodHandling: "always",
},
{
name: "Nederlands (Dutch)",
value: "nl-NL",
progress: 97,
progress: 98,
dir: "ltr",
pluralFoodHandling: "always",
},

View File

@@ -51,7 +51,7 @@
"category": "Kategoria"
},
"events": {
"apprise-url": "Apprise URL",
"apprise-url": "Apprise-url",
"database": "Tietokanta",
"delete-event": "Poista tapahtuma",
"event-delete-confirmation": "Haluatko varmasti poistaa tämän tapahtuman?",
@@ -98,7 +98,7 @@
"dashboard": "Hallintanäkymä",
"delete": "Poista",
"disabled": "Poistettu käytöstä",
"done": "Done",
"done": "Valmis",
"download": "Lataa",
"duplicate": "Monista",
"edit": "Muokkaa",
@@ -169,7 +169,7 @@
"token": "Tunniste",
"tuesday": "Tiistai",
"type": "Tyyppi",
"undo": "Undo",
"undo": "Peru",
"update": "Päivitä",
"updated": "Päivitetty",
"upload": "Lähetä",
@@ -333,8 +333,8 @@
"any-household": "Mikä tahansa kotitalous",
"no-meal-plan-defined-yet": "Ateriasuunnitelmaa ei ole vielä määritelty",
"no-meal-planned-for-today": "Ei ateriasuunnitelmaa tälle päivälle",
"numberOfDaysPast-hint": "Number of days in the past on page load",
"numberOfDaysPast-label": "Default Days in the Past",
"numberOfDaysPast-hint": "Menneisyydestä ladattujen päivien määrä",
"numberOfDaysPast-label": "Oletusarvo menneiden päivien lataukselle",
"numberOfDays-hint": "Sivun latauspäivien lukumäärä",
"numberOfDays-label": "Oletuspäivät",
"only-recipes-with-these-categories-will-be-used-in-meal-plans": "Vain näiden luokkien reseptejä käytetään ateriasuunnitelmissa",
@@ -392,7 +392,7 @@
"nextcloud": {
"description": "Tuo tiedot Nextcloudin Cookbookista",
"description-long": "Nextcloud reseptejä voidaan tuoda zip-tiedostosta, joka sisältää Nextcloudin tallennetut tiedot. Katso esimerkkikansiorakenne alla varmistaaksesi, että reseptisi voidaan tuoda.",
"title": "Nextcloud Cookbook"
"title": "Nextcloud-keittokirja"
},
"copymethat": {
"description-long": "Mealie voi tuoda reseptejä Copy Me That -sovelluksesta. Vie reseptisi HTML-muodossa ja lataa sitten zip-tiedosto.",
@@ -702,7 +702,7 @@
"confidence-score": "Varmuuspisteet",
"ingredient-parser-description": "Ainesosat on haettu onnistuneesti. Ole hyvä ja tarkista ainesosat joista emme ole varmoja.",
"ingredient-parser-final-review-description": "Kun kaikki ainesosat on tarkistettu, sinulla on vielä yksi mahdollisuus tarkistaa kaikki ainesosat ennen kuin muokkaat reseptiäsi.",
"add-text-as-alias-for-item": "Add \"{text}\" as alias for {item}",
"add-text-as-alias-for-item": "Lisää \"{text}\" kohteen {item} aliakseksi",
"delete-item": "Poista kohde"
},
"reset-servings-count": "Palauta Annoksien Määrä",
@@ -893,17 +893,17 @@
"server-side-base-url-error-text": "`BASE_URL` on API-palvelimen oletusarvo. Tämä aiheuttaa ongelmia ilmoitusten linkkien kanssa, jotka on luotu palvelimella sähköposteja varten jne.",
"server-side-base-url-success-text": "Palvelimen nettiosoite ei vastaa oletusta",
"ldap-ready": "LDAP Valmis",
"ldap-not-ready": "LDAP Not Ready",
"ldap-not-ready": "LDAP ei valmis",
"ldap-ready-error-text": "Kaikkia LDAP-arvoja ei ole määritetty. Tämä voidaan ohittaa, jos et käytä LDAP-todennusta.",
"ldap-ready-success-text": "Kaikki vaaditut LDAP-muuttujat on asetettu.",
"build": "Koonti",
"recipe-scraper-version": "Reseptikaappaimen versio",
"oidc-ready": "OIDC valmis",
"oidc-not-ready": "OIDC Not Ready",
"oidc-not-ready": "OIDC ei ole valmis",
"oidc-ready-error-text": "Kaikkia OIDC-arvoja ei ole määritelty. Jos et käytä OIDC-todennusta, voidaan asia jättää huomiotta.",
"oidc-ready-success-text": "Kaikki vaaditut OIDC-muuttujat asetettu.",
"openai-ready": "OpenAI valmis",
"openai-not-ready": "OpenAI Not Ready",
"openai-not-ready": "OpenAI ei ole valmis",
"openai-ready-error-text": "Kaikkia OpenAI:n arvoja ei ole määritelty. Tämä voidaan sivuuttaa, mikäli et käytä OpenAI:n toimintoja.",
"openai-ready-success-text": "Vaadittavat OpenAI-muuttujat ovat asetetut."
},
@@ -917,7 +917,7 @@
"quantity": "Määrä: {0}",
"shopping-list": "Ostoslista",
"shopping-lists": "Ostoslistat",
"add-item": "Add item",
"add-item": "Lisää kohde",
"food": "Elintarvikkeet",
"note": "Muistiinpano",
"label": "Tunnus",
@@ -962,7 +962,7 @@
"language": "Kieli",
"maintenance": "Ylläpito",
"background-tasks": "Taustatehtävät",
"parser": "Parser",
"parser": "Jäsentäjä",
"developer": "Kehittäjä",
"cookbook": "Keittokirja",
"create-cookbook": "Luo uusi keittokirja"
@@ -1351,7 +1351,7 @@
"ingredient-text": "Ainesosan Teksti",
"average-confident": "{0} Luottamus",
"try-an-example": "Kokeile esimerkkiä",
"parser": "Parser",
"parser": "Jäsentäjä",
"background-tasks": "Taustatehtävät",
"background-tasks-description": "Täältä voit tarkastella kaikkia käynnissä olevia taustatehtäviä ja niiden tilaa",
"no-logs-found": "Lokeja Ei Löytynyt",
@@ -1481,7 +1481,7 @@
"announcements": "Announcements",
"all-announcements": "All announcements",
"mark-all-as-read": "Mark All as Read",
"show-announcements-from-mealie": "Show announcements from Mealie",
"show-announcements-from-mealie": "Näytä Mealien ilmoitukset",
"show-announcements-setting-description": "Whether or not you want to allow users to see announcements from Mealie. When enabled users can still opt-out from seeing them in their user settings"
}
}

View File

@@ -943,7 +943,7 @@
"are-you-sure-you-want-to-uncheck-all-items": "Biztos, hogy minden elem kijelölését visszavonja?",
"are-you-sure-you-want-to-delete-checked-items": "Biztosan törölni akarja az összes bejelölt elemet?",
"no-shopping-lists-found": "Nem találhatók bevásárlólisták",
"item-checked-off": "Checked off {item}"
"item-checked-off": "{item} leellenőrzve"
},
"sidebar": {
"all-recipes": "Minden recept",

View File

@@ -333,8 +333,8 @@
"any-household": "Öll heimili",
"no-meal-plan-defined-yet": "Ekkert matarplan hefur verið skilgreint",
"no-meal-planned-for-today": "Ekkert matarplan skipulagt í dag",
"numberOfDaysPast-hint": "Number of days in the past on page load",
"numberOfDaysPast-label": "Default Days in the Past",
"numberOfDaysPast-hint": "Fjöldi liðina daga við síðuhleðslu",
"numberOfDaysPast-label": "Sjálfgefnir liðnir dagar",
"numberOfDays-hint": "Fjöldi daga við síðuhleðslu",
"numberOfDays-label": "Sjálfgefnir dagar",
"only-recipes-with-these-categories-will-be-used-in-meal-plans": "Aðeins uppskriftir í þessum flokkum verða notaðir í matarplan",
@@ -640,8 +640,8 @@
"create-a-recipe-by-providing-the-name-all-recipes-must-have-unique-names": "Stofnaðu uppskrift með því að gefa henni nafn, allar uppskriftir þurfa að hafa einstakt nafn.",
"new-recipe-names-must-be-unique": "Nöfn uppskrifta þurfa að vera einstök",
"scrape-recipe": "Vinna uppskrift",
"scrape-recipe-description": "Scrape a recipe by url. Provide the url for the site you want to scrape, and Mealie will attempt to scrape the recipe from that site and add it to your collection.",
"scrape-recipe-description-transcription": "You can also provide the url to a video and Mealie will attempt to transcribe it into a recipe.",
"scrape-recipe-description": "Sækja uppskrift af vefslóð. Settu inn vefslóð fyrir síðuna þar sem þú vilt sækja uppskrift og Mealie mun reyna að vinna uppskriftina þaðan og bæta henni við safnið þitt.",
"scrape-recipe-description-transcription": "Þú getur einnig sett inn slóð á video og Mealie mun reyna að umrita það yfir í uppskrift.",
"scrape-recipe-have-a-lot-of-recipes": "Ertu með margar uppskriftir sem þú villt setja inn í einu?",
"scrape-recipe-suggest-bulk-importer": "Prófaðu að setja inn margar uppskriftir í einu",
"scrape-recipe-have-raw-html-or-json-data": "Ertu með hrá HTML eða JSON gögn?",
@@ -893,17 +893,17 @@
"server-side-base-url-error-text": "'BASE_URL' er enn sjálfgefið gildi á API netþjóns. Þetta getur valdið vandræðum með tilkynninga tengla sem netþjónninn býr til fyrir tölvupósta og annað.",
"server-side-base-url-success-text": "Slóð netþjóns samsvarar ekki sjálfgefnu gildi",
"ldap-ready": "LDAP klár",
"ldap-not-ready": "LDAP Not Ready",
"ldap-not-ready": "LDAP er ekki tilbúið",
"ldap-ready-error-text": "Ekki öll LDAP-gildi eru stillt. Þetta má hunsa ef þú notar ekki LDAP-auðkenningu.",
"ldap-ready-success-text": "Öll nauðsynleg LDAP-gildi eru stillt.",
"build": "Build",
"recipe-scraper-version": "Recipe Scraper útgáfa",
"oidc-ready": "OIDC klár",
"oidc-not-ready": "OIDC Not Ready",
"oidc-not-ready": "OIDC er ekki tilbúið",
"oidc-ready-error-text": "Ekki öll OIDC gildi eru stillt. Þetta má hunsa ef þú notar ekki OIDC-auðkenningu.",
"oidc-ready-success-text": "Öll nauðsynleg OIDC-gildi eru stillt.",
"openai-ready": "OpenAI klár",
"openai-not-ready": "OpenAI Not Ready",
"openai-not-ready": "OpenAI er ekki tilbúið",
"openai-ready-error-text": "Ekki öll OpenAI gildi eru stillt. Þetta má hunsa ef þú notar ekki OpenAI.",
"openai-ready-success-text": "Öll nauðsynleg OpenAI-gildi eru stillt."
},
@@ -917,7 +917,7 @@
"quantity": "Fjöldi: {0}",
"shopping-list": "Innkaupalisti",
"shopping-lists": "Innkaupalistar",
"add-item": "Add item",
"add-item": "Bæta við vöru",
"food": "Matvara",
"note": "Minnispunktur",
"label": "Merkimiði",

View File

@@ -51,7 +51,7 @@
"category": "Categorie"
},
"events": {
"apprise-url": "Apprise URL",
"apprise-url": "Kennisgevings-url",
"database": "Database",
"delete-event": "Gebeurtenis verwijderen",
"event-delete-confirmation": "Weet je zeker dat je deze gebeurtenis wilt verwijderen?",
@@ -98,7 +98,7 @@
"dashboard": "Dashboard",
"delete": "Verwijderen",
"disabled": "Uitgeschakeld",
"done": "Done",
"done": "Gereed",
"download": "Downloaden",
"duplicate": "Dupliceren",
"edit": "Bewerken",
@@ -169,7 +169,7 @@
"token": "Token",
"tuesday": "dinsdag",
"type": "Soort",
"undo": "Undo",
"undo": "Ongedaan maken",
"update": "Bijwerken",
"updated": "Bijgewerkt",
"upload": "Uploaden",
@@ -333,8 +333,8 @@
"any-household": "Elk huishouden",
"no-meal-plan-defined-yet": "Nog geen maaltijdplan opgesteld",
"no-meal-planned-for-today": "Geen maaltijd gepland voor vandaag",
"numberOfDaysPast-hint": "Number of days in the past on page load",
"numberOfDaysPast-label": "Default Days in the Past",
"numberOfDaysPast-hint": "Aantal dagen in het verleden bij laden pagina",
"numberOfDaysPast-label": "Standaard dagen in het verleden",
"numberOfDays-hint": "Aantal dagen bij laden van de pagina",
"numberOfDays-label": "Standaard aantal dagen",
"only-recipes-with-these-categories-will-be-used-in-meal-plans": "Alleen recepten met deze categorieën zullen worden gebruikt in maaltijdplannen",
@@ -443,7 +443,7 @@
"error-details": "Alleen websites met ld+json of microdata kunnen worden geïmporteerd door Mealie. De meeste grote receptenwebsites ondersteunen deze gegevensstructuur. Als je site niet kan worden geïmporteerd, maar er zijn json-gegevens in de log, maak dan een github issue aan met de URL en gegevens.",
"error-title": "Het lijkt erop dat we niets konden vinden",
"from-url": "Recept importeren",
"github-issues": "GitHub Issues",
"github-issues": "GitHubproblemen",
"google-ld-json-info": "Google ld+json Info",
"must-be-a-valid-url": "Moet een geldige URL zijn",
"paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list": "Plak je receptgegevens. Elke regel wordt behandeld als een item in een lijst",
@@ -893,17 +893,17 @@
"server-side-base-url-error-text": "`BASE_URL` is nog steeds de standaard waarde op de API Server. Dit geeft problemen met notificatielinks in e-mails etc.",
"server-side-base-url-success-text": "Server-side URL komt niet overeen met de standaard",
"ldap-ready": "LDAP klaar",
"ldap-not-ready": "LDAP Not Ready",
"ldap-not-ready": "LDAP niet gereed",
"ldap-ready-error-text": "Niet alle LDAP-waarden zijn geconfigureerd. Dit kan worden genegeerd als je geen LDAP-authenticatie gebruikt.",
"ldap-ready-success-text": "Vereiste LDAP variabelen zijn helemaal ingesteld.",
"build": "Build",
"recipe-scraper-version": "Versie van de receptenscraper",
"oidc-ready": "OIDC klaar",
"oidc-not-ready": "OIDC Not Ready",
"oidc-not-ready": "OIDC niet gereed",
"oidc-ready-error-text": "Niet alle OIDC-waarden zijn geconfigureerd. Dit kan worden genegeerd als je geen OIDC-authenticatie gebruikt.",
"oidc-ready-success-text": "Vereiste OIDC-variabelen zijn allemaal ingesteld.",
"openai-ready": "OpenAI staat klaar",
"openai-not-ready": "OpenAI Not Ready",
"openai-not-ready": "OpenAI niet gereed",
"openai-ready-error-text": "Niet alle tekstvakken voor OpenAI zijn ingevuld. Als je geen OpenAI gebruikt kun je dit leeg laten.",
"openai-ready-success-text": "Verplichte tekstvakken voor OpenAI zijn ingevuld."
},
@@ -917,7 +917,7 @@
"quantity": "Hoeveelheid: {0}",
"shopping-list": "Boodschappenlijst",
"shopping-lists": "Boodschappenlijsten",
"add-item": "Add item",
"add-item": "Item toevoegen",
"food": "Levensmiddelen",
"note": "Notitie",
"label": "Label",
@@ -943,7 +943,7 @@
"are-you-sure-you-want-to-uncheck-all-items": "Weet je zeker dat je alle items wilt deselecteren?",
"are-you-sure-you-want-to-delete-checked-items": "Weet je zeker dat je de geselecteerde items wilt verwijderen?",
"no-shopping-lists-found": "Geen boodschappenlijsten gevonden",
"item-checked-off": "Checked off {item}"
"item-checked-off": "Uitgevinkt {item}"
},
"sidebar": {
"all-recipes": "Alle Recepten",
@@ -1283,7 +1283,7 @@
"split-by-block": "Splits per tekstblok",
"flatten": "Plat maken ongeacht originele opmaak",
"help": {
"help": "Help",
"help": "Hulp",
"mouse-modes": "Muismodus",
"selection-mode": "Selectiemodus (standaard)",
"selection-mode-desc": "De selectiemodus is de hoofdmodus die gebruikt kan worden om gegevens in te voeren:",
@@ -1478,10 +1478,10 @@
"max-length": "Moet maximaal {max} tekens bevatten|Moet maximaal {max} tekens bevatten"
},
"announcements": {
"announcements": "Announcements",
"all-announcements": "All announcements",
"mark-all-as-read": "Mark All as Read",
"show-announcements-from-mealie": "Show announcements from Mealie",
"show-announcements-setting-description": "Whether or not you want to allow users to see announcements from Mealie. When enabled users can still opt-out from seeing them in their user settings"
"announcements": "Aankondigingen",
"all-announcements": "Alle aankondigingen",
"mark-all-as-read": "Alles markeren als gelezen",
"show-announcements-from-mealie": "Aankondigingen van Mealie weergeven",
"show-announcements-setting-description": "Of je gebruikers wel of niet meldingen van Mealie wilt laten zien. Wanneer ingeschakeld kunnen gebruikers nog steeds afzien van het bekijken van hen in hun gebruikersinstellingen"
}
}

View File

@@ -98,7 +98,7 @@
"dashboard": "Kontrollpanel",
"delete": "Slett",
"disabled": "Deaktivert",
"done": "Done",
"done": "Ferdig",
"download": "Last ned",
"duplicate": "Dupliser",
"edit": "Rediger",
@@ -169,7 +169,7 @@
"token": "Token",
"tuesday": "Tirsdag",
"type": "Type",
"undo": "Undo",
"undo": "Angre",
"update": "Oppdater",
"updated": "Oppdatert",
"upload": "Last opp",
@@ -334,7 +334,7 @@
"no-meal-plan-defined-yet": "Ingen måltidsplan er definert ennå",
"no-meal-planned-for-today": "Ingen måltid planlagt i dag",
"numberOfDaysPast-hint": "Number of days in the past on page load",
"numberOfDaysPast-label": "Default Days in the Past",
"numberOfDaysPast-label": "Standard antall dager tilbake",
"numberOfDays-hint": "Antall dager på sideinnlasting",
"numberOfDays-label": "Standard antall dager",
"only-recipes-with-these-categories-will-be-used-in-meal-plans": "Kun oppskrifter med disse kategoriene vil bli brukt i måltidsplaner",
@@ -392,7 +392,7 @@
"nextcloud": {
"description": "Overfør data fra en Nextcloud Cookbook-instans",
"description-long": "Oppskrifter fra Nextcloud kan importeres fra en zip-fil som inneholder dataene lagret i Nextcloud. Se eksempelet på mappestrukture nedenfor for å sikre at oppskriftene kan importeres.",
"title": "Nextcloud Cookbook"
"title": "Nextcloud kokebok"
},
"copymethat": {
"description-long": "Mealie kan importere oppskrifter fra Copy Me That. Eksporter oppskrifter i HTML-format, last deretter opp .zip-filen under.",
@@ -917,7 +917,7 @@
"quantity": "Antall: {0}",
"shopping-list": "Handleliste",
"shopping-lists": "Handlelister",
"add-item": "Add item",
"add-item": "Legg til produkt",
"food": "Matvare",
"note": "Notat",
"label": "Etikett",
@@ -943,7 +943,7 @@
"are-you-sure-you-want-to-uncheck-all-items": "Er du sikker på at du vil fjerne valg av alle elementer?",
"are-you-sure-you-want-to-delete-checked-items": "Er du sikker på at du vil slette alle valgte elementer?",
"no-shopping-lists-found": "Ingen handlelister funnet",
"item-checked-off": "Checked off {item}"
"item-checked-off": "Avkrysset av {item}"
},
"sidebar": {
"all-recipes": "Alle oppskrifter",
@@ -1478,10 +1478,10 @@
"max-length": "Må være minst minst {max} tegn må bestå av maks {max} tegn"
},
"announcements": {
"announcements": "Announcements",
"all-announcements": "All announcements",
"mark-all-as-read": "Mark All as Read",
"show-announcements-from-mealie": "Show announcements from Mealie",
"announcements": "Kunngjøringer",
"all-announcements": "Alle kunngjøringer",
"mark-all-as-read": "Marker alle som lest",
"show-announcements-from-mealie": "Vis kunngjøringer fra Mealie",
"show-announcements-setting-description": "Whether or not you want to allow users to see announcements from Mealie. When enabled users can still opt-out from seeing them in their user settings"
}
}

View File

@@ -51,7 +51,7 @@
"category": "Kategoria"
},
"events": {
"apprise-url": "Apprise URL",
"apprise-url": "URL Apprise",
"database": "Baza danych",
"delete-event": "Usuń wydarzenie",
"event-delete-confirmation": "Czy na pewno chcesz usunąć to zdarzenie?",
@@ -98,7 +98,7 @@
"dashboard": "Panel główny",
"delete": "Usuń",
"disabled": "Wyłączone",
"done": "Done",
"done": "Gotowe",
"download": "Pobierz",
"duplicate": "Duplikuj",
"edit": "Edytuj",
@@ -169,7 +169,7 @@
"token": "Token",
"tuesday": "Wtorek",
"type": "Typ",
"undo": "Undo",
"undo": "Cofnij",
"update": "Zaktualizuj",
"updated": "Zaktualizowano",
"upload": "Prześlij",
@@ -917,7 +917,7 @@
"quantity": "Ilość: {0}",
"shopping-list": "Lista zakupów",
"shopping-lists": "Listy zakupów",
"add-item": "Add item",
"add-item": "Dodaj element",
"food": "Jedzenie",
"note": "Notatka",
"label": "Etykieta",
@@ -943,7 +943,7 @@
"are-you-sure-you-want-to-uncheck-all-items": "Czy na pewno chcesz odznaczyć wszystkie elementy?",
"are-you-sure-you-want-to-delete-checked-items": "Czy jesteś pewien, że chcesz usunąć wszystkie zaznaczone elementy?",
"no-shopping-lists-found": "Nie znaleziono list zakupów",
"item-checked-off": "Checked off {item}"
"item-checked-off": "Zaznaczono {item}"
},
"sidebar": {
"all-recipes": "Wszystkie",
@@ -1478,10 +1478,10 @@
"max-length": "Może zawierać co najwyżej {max} znak|Może zawierać co najwyżej {max} znaki|Może zawierać co najwyżej {max} znaków"
},
"announcements": {
"announcements": "Announcements",
"all-announcements": "All announcements",
"mark-all-as-read": "Mark All as Read",
"show-announcements-from-mealie": "Show announcements from Mealie",
"show-announcements-setting-description": "Whether or not you want to allow users to see announcements from Mealie. When enabled users can still opt-out from seeing them in their user settings"
"announcements": "Ogłoszenia",
"all-announcements": "Wszystkie ogłoszenia",
"mark-all-as-read": "Oznacz wszystkie jako przeczytane",
"show-announcements-from-mealie": "Pokazuj ogłoszenia z Mealie",
"show-announcements-setting-description": "Czy chcesz by użytkownicy widzieli ogłoszenia z Mealie? Użytkownicy będą w dalszym ciągu mogli wyłączyć ogłoszenia w swoich ustawieniach użytkownika"
}
}

View File

@@ -943,7 +943,7 @@
"are-you-sure-you-want-to-uncheck-all-items": "Ali res ne želite izbrati vseh elementov?",
"are-you-sure-you-want-to-delete-checked-items": "Ali ste prepričani, da želite izbrisati vse izbrane elemente?",
"no-shopping-lists-found": "Ni nakupovalnih seznamov",
"item-checked-off": "Checked off {item}"
"item-checked-off": "Odkljukano {item}"
},
"sidebar": {
"all-recipes": "Vsi recepti",

View File

@@ -169,7 +169,7 @@
"token": "Token",
"tuesday": "Tisdag",
"type": "Typ",
"undo": "Undo",
"undo": "Ångra",
"update": "Uppdatera",
"updated": "Uppdaterad",
"upload": "Ladda upp",
@@ -333,8 +333,8 @@
"any-household": "Valfritt hushåll",
"no-meal-plan-defined-yet": "Ingen måltidsplan definierad ännu",
"no-meal-planned-for-today": "Ingen måltidsplan för idag",
"numberOfDaysPast-hint": "Number of days in the past on page load",
"numberOfDaysPast-label": "Default Days in the Past",
"numberOfDaysPast-hint": "Antal förflutna dagar vid sidhämtning",
"numberOfDaysPast-label": "Förvalda förflutna dagar",
"numberOfDays-hint": "Antal dagar vid sidhämtning",
"numberOfDays-label": "Förvalda dagar",
"only-recipes-with-these-categories-will-be-used-in-meal-plans": "Endast recept med dessa kategorier kommer att användas i måltidsplaner",
@@ -812,7 +812,7 @@
"settings-updated": "Inställningar uppdaterade",
"site-settings": "Systeminställningar",
"theme": {
"accent": "Accent",
"accent": "Accentfärg",
"dark": "Mörkt",
"default-to-system": "Standard",
"error": "Fel",
@@ -893,17 +893,17 @@
"server-side-base-url-error-text": "`BASE_URL` är fortfarande standardvärdet på API-servern. Detta kommer att orsaka problem med meddelanden som genereras på servern för e-postmeddelanden, etc.",
"server-side-base-url-success-text": "Serversidans URL matchar inte standard",
"ldap-ready": "LDAP Redo",
"ldap-not-ready": "LDAP Not Ready",
"ldap-not-ready": "LDAP ej tillgängligt",
"ldap-ready-error-text": "Alla LDAP-värden är inte konfigurerade. Detta kan ignoreras om du inte använder LDAP-autentisering.",
"ldap-ready-success-text": "Alla obligatoriska LDAP-variabler är satta.",
"build": "Bygge",
"recipe-scraper-version": "Version av Recept-scraper",
"oidc-ready": "OIDC Klar",
"oidc-not-ready": "OIDC Not Ready",
"oidc-not-ready": "OIDC ej tillgängligt",
"oidc-ready-error-text": "Alla OIDC-värden är inte konfigurerade. Detta kan ignoreras om du inte använder OIDC-autentisering.",
"oidc-ready-success-text": "Alla obligatoriska OIDC-variabler är satta.",
"openai-ready": "OpenAI redo",
"openai-not-ready": "OpenAI Not Ready",
"openai-not-ready": "OpenAI ej tillgängligt",
"openai-ready-error-text": "Alla OpenAI-värden är inte konfigurerade. Detta kan ignoreras om du inte använder OpenAI-funktioner.",
"openai-ready-success-text": "Alla obligatoriska OpenAI-variabler är satta."
},
@@ -917,7 +917,7 @@
"quantity": "Antal {0}",
"shopping-list": "Inköpslista",
"shopping-lists": "Inköpslistor",
"add-item": "Add item",
"add-item": "Lägg till vara",
"food": "Mat",
"note": "Anteckning",
"label": "Etikett",
@@ -943,7 +943,7 @@
"are-you-sure-you-want-to-uncheck-all-items": "Är du säker på att du vill avmarkera alla objekt?",
"are-you-sure-you-want-to-delete-checked-items": "Är du säker på att du vill ta bort alla markerade objekt?",
"no-shopping-lists-found": "Inga inköpslistor hittades",
"item-checked-off": "Checked off {item}"
"item-checked-off": "Kryssat av {item}"
},
"sidebar": {
"all-recipes": "Recept",
@@ -1478,10 +1478,10 @@
"max-length": "Måste Vara Som Mest {max} Tecken|Måste Vara Som Mest {max} Tecken"
},
"announcements": {
"announcements": "Announcements",
"all-announcements": "All announcements",
"mark-all-as-read": "Mark All as Read",
"show-announcements-from-mealie": "Show announcements from Mealie",
"show-announcements-setting-description": "Whether or not you want to allow users to see announcements from Mealie. When enabled users can still opt-out from seeing them in their user settings"
"announcements": "Meddelanden",
"all-announcements": "Alla meddelanden",
"mark-all-as-read": "Markera alla som lästa",
"show-announcements-from-mealie": "Visa meddelanden från Mealie",
"show-announcements-setting-description": "Om du vill tillåta användare att se meddelanden från Mealie eller inte. När funktionen är aktiverad kan användarna fortfarande välja att inte se dem i sina användarinställningar"
}
}

View File

@@ -911,7 +911,7 @@
"all-lists": "Всі списки",
"create-shopping-list": "Сторити список покупок",
"from-recipe": "З рецепту",
"ingredient-of-recipe": "Ingredient of {recipe}",
"ingredient-of-recipe": "Інгредієнт з {recipe}",
"list-name": "Назва списку",
"new-list": "Новий список",
"quantity": "Кількість: {0}",

View File

@@ -1,6 +1,6 @@
{
"name": "mealie",
"version": "3.17.0",
"version": "3.18.0",
"private": true,
"scripts": {
"dev": "nuxt dev",

View File

@@ -35,7 +35,7 @@ class OpenIDProvider(AuthProvider[UserInfo]):
self._logger.debug("[OIDC] %s: %s", key, value)
if not self.required_claims.issubset(claims.keys()):
self._logger.error(
self._logger.debug(
"[OIDC] Required claims not present. Expected: %s Actual: %s",
self.required_claims,
claims.keys(),
@@ -45,7 +45,7 @@ class OpenIDProvider(AuthProvider[UserInfo]):
# Check for empty required claims
for claim in self.required_claims:
if not claims.get(claim):
self._logger.error("[OIDC] Required claim '%s' is empty", claim)
self._logger.debug("[OIDC] Required claim '%s' is empty", claim)
raise MissingClaimException()
repos = get_repositories(self.session, group_id=None, household_id=None)

View File

@@ -5,13 +5,6 @@
"recipe": {
"unique-name-error": "Recipe names must be unique",
"recipe-created": "Recipe Created",
"made-this-as-side": "{name} made this as a side",
"made-this-for-breakfast": "{name} made this for breakfast",
"made-this-for-lunch": "{name} made this for lunch",
"made-this-for-dinner": "{name} made this for dinner",
"made-this-for-snack": "{name} made this for a snack",
"made-this-for-drink": "{name} made this for a drink",
"made-this-for-dessert": "{name} made this for dessert",
"recipe-image-deleted": "Recipe image deleted",
"recipe-defaults": {
"ingredient-note": "1 Cup Flour",

View File

@@ -23,7 +23,7 @@
"create-progress": {
"creating-recipe-with-ai": "Recept maken met AI...",
"creating-recipe-from-transcript-with-ai": "Maak recept van een transcript met AI...",
"creating-recipe-from-webpage-data": "Creëren recept van webpagina-gegevens...",
"creating-recipe-from-webpage-data": "Creëren recept van webpaginagegevens...",
"downloading-image": "Afbeeldingen downloaden...",
"downloading-video": "Video downloaden...",
"extracting-recipe-data": "Receptgegevens ophalen...",

View File

@@ -340,7 +340,7 @@
"aliases": [],
"description": "",
"name": "acelga",
"plural_name": "chard"
"plural_name": "acelga"
},
"pimiento": {
"aliases": [],
@@ -979,8 +979,8 @@
"chestnut purée": {
"aliases": [],
"description": "",
"name": "chestnut purée",
"plural_name": "chestnut purée"
"name": "puré de castañas",
"plural_name": "puré de castañas"
},
"prickly pear": {
"aliases": [],
@@ -1295,7 +1295,7 @@
"black fungu": {
"aliases": [],
"description": "",
"name": "black fungus",
"name": "hongos negros",
"plural_name": "hongos negros"
},
"black truffle": {
@@ -1361,7 +1361,7 @@
"white fungu": {
"aliases": [],
"description": "",
"name": "white fungus",
"name": "hongos blancos",
"plural_name": "hongos blancos"
},
"pioppini": {
@@ -1373,7 +1373,7 @@
"snow fungu": {
"aliases": [],
"description": "",
"name": "snow fungus",
"name": "hongos de nieve",
"plural_name": "hongos de nieve"
},
"white beech mushroom": {

View File

@@ -571,8 +571,8 @@
"delicata squash": {
"aliases": [],
"description": "",
"name": "delicata squash",
"plural_name": "delicata squashes"
"name": "delikat squash",
"plural_name": "delikate squasher"
},
"Frisée": {
"aliases": [
@@ -980,7 +980,7 @@
"aliases": [],
"description": "",
"name": "kastanjepuré",
"plural_name": "chestnut purée"
"plural_name": "kastanjepuré"
},
"prickly pear": {
"aliases": [],
@@ -1045,7 +1045,7 @@
"sweet lime": {
"aliases": [],
"description": "",
"name": "sweet lime",
"name": "søt lime",
"plural_name": "sweet limes"
},
"custard-apple": {
@@ -1873,8 +1873,8 @@
"melon seed": {
"aliases": [],
"description": "",
"name": "melon seed",
"plural_name": "melon seeds"
"name": "melonfrø",
"plural_name": "melonfrø"
},
"lotus seed": {
"aliases": [],
@@ -2003,48 +2003,48 @@
"parmesan cheese": {
"aliases": [],
"description": "",
"name": "parmesan cheese",
"plural_name": "parmesan cheese"
"name": "parmesanost",
"plural_name": "parmesanost"
},
"cheddar cheese": {
"aliases": [
"cheddar cheese"
"cheddarost"
],
"description": "",
"name": "cheddar cheese",
"plural_name": "cheddar cheese"
"name": "cheddarost",
"plural_name": "cheddarost"
},
"cream cheese": {
"aliases": [],
"description": "",
"name": "cream cheese",
"plural_name": "cream cheese"
"name": "kremost",
"plural_name": "kremost"
},
"sharp cheddar cheese": {
"aliases": [
"sharp cheddar"
"skarp cheddarost"
],
"description": "",
"name": "sharp cheddar cheese",
"plural_name": "sharp cheddar cheese"
"name": "skarp cheddarost",
"plural_name": "skarp cheddarost"
},
"cheese": {
"aliases": [],
"description": "",
"name": "cheese",
"plural_name": "cheese"
"name": "ost",
"plural_name": "ost"
},
"mozzarella cheese": {
"aliases": [],
"description": "",
"name": "mozzarella cheese",
"plural_name": "mozzarella cheese"
"name": "mozzarellaost",
"plural_name": "mozzarellaost"
},
"feta cheese": {
"aliases": [],
"description": "",
"name": "feta cheese",
"plural_name": "feta cheese"
"name": "fetaost",
"plural_name": "fetaost"
},
"ricotta cheese": {
"aliases": [],
@@ -2073,14 +2073,14 @@
"goat cheese": {
"aliases": [],
"description": "",
"name": "goat cheese",
"plural_name": "goat cheese"
"name": "geitost",
"plural_name": "geitost"
},
"fresh mozzarella cheese": {
"aliases": [],
"description": "",
"name": "fresh mozzarella cheese",
"plural_name": "fresh mozzarella cheese"
"name": "fersk mozzarellaost",
"plural_name": "fersk mozzarellaost"
},
"swis cheese": {
"aliases": [],

View File

@@ -919,7 +919,7 @@
"jackfruit": {
"aliases": [],
"description": "",
"name": "jackfruit",
"name": "owoc chlebowca",
"plural_name": "jackfruity"
},
"dragon fruit": {

View File

@@ -134,6 +134,7 @@ async def oauth_callback(request: Request, session: Session = Depends(generate_s
auth_provider = OpenIDProvider(session, userinfo, use_default_groups=True)
auth = auth_provider.authenticate()
except MissingClaimException:
logger.error("[OIDC] Required claims not present in ID token or userinfo endpoint")
auth = None
if not auth:

View File

@@ -54,7 +54,11 @@ class MultiPurposeLabelsController(BaseCrudController):
@router.post("", response_model=MultiPurposeLabelOut)
def create_one(self, data: MultiPurposeLabelCreate):
new_label = self.service.create_one(data)
try:
new_label = self.service.create_one(data)
except Exception as ex:
self.mixins.handle_exception(ex)
raise # handle_exception always raises; this satisfies static analysis
self.publish_event(
event_type=EventTypes.label_created,
document_data=EventLabelData(operation=EventOperation.create, label_id=new_label.id),

View File

@@ -58,6 +58,6 @@ async def get_recipe_asset(recipe_id: UUID4, file_name: str):
raise HTTPException(status.HTTP_400_BAD_REQUEST)
if file.exists():
return FileResponse(file)
return FileResponse(file, filename=file.name, content_disposition_type="attachment")
else:
raise HTTPException(status.HTTP_404_NOT_FOUND)

View File

@@ -52,7 +52,7 @@ class TagController(BaseCrudController):
def create_one(self, tag: TagIn):
"""Creates a Tag in the database"""
save_data = mapper.cast(tag, TagSave, group_id=self.group_id)
new_tag = self.repo.create(save_data)
new_tag = self.mixins.create_one(save_data)
if new_tag:
self.publish_event(

View File

@@ -80,6 +80,8 @@ from mealie.services.scraper.scraper_strategies import (
from ._base import BaseRecipeController, JSONBytes
ASSET_ALLOWED_EXTENSIONS = {"pdf", "jpg", "jpeg", "png", "gif", "webp", "bmp", "avif", "txt", "md", "csv", "json"}
router = UserAPIRouter(prefix="/recipes", route_class=MealieCrudRoute)
@@ -660,6 +662,10 @@ class RecipeController(BaseRecipeController):
if "." in extension:
extension = extension.split(".")[-1]
extension = extension.lower()
if extension not in ASSET_ALLOWED_EXTENSIONS:
raise HTTPException(status_code=400, detail="Unsupported file extension")
file_slug = slugify(name)
if not extension or not file_slug:
raise HTTPException(status_code=400, detail="Missing required fields")

View File

@@ -4,7 +4,6 @@ from functools import cached_property
from fastapi import Depends, File, Form, HTTPException
from pydantic import UUID4
from mealie.lang.providers import get_locale_provider
from mealie.repos.all_repositories import get_repositories
from mealie.routes._base import BaseCrudController, controller
from mealie.routes._base.mixins import HttpRepo
@@ -45,21 +44,6 @@ class RecipeTimelineEventsController(BaseCrudController):
self.registered_exceptions,
)
def _translate_event_subject(self, event: RecipeTimelineEventOut) -> None:
"""Translate auto-generated event subjects stored as i18n key references.
Subjects are stored as ``<i18n-key>|<name>`` (e.g. ``recipe.made-this-for-dinner|Alice``).
Falls back to en-US when the requested locale has not yet been translated.
"""
if event.event_type == TimelineEventType.info.value and "|" in event.subject:
key, _, name = event.subject.partition("|")
if key.startswith("recipe."):
translated = self.t(key, name=name)
if translated == key:
translated = get_locale_provider("en-US").t(key, name=name)
if translated != key:
event.subject = translated
@router.get("", response_model=RecipeTimelineEventPagination)
def get_all(self, q: PaginationQuery = Depends(PaginationQuery)):
response = self.repo.page_all(
@@ -68,7 +52,8 @@ class RecipeTimelineEventsController(BaseCrudController):
)
for event in response.items:
self._translate_event_subject(event)
if event.event_type == TimelineEventType.system.value:
event.subject = self.t(event.subject)
response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump())
return response
@@ -104,7 +89,8 @@ class RecipeTimelineEventsController(BaseCrudController):
@router.get("/{item_id}", response_model=RecipeTimelineEventOut)
def get_one(self, item_id: UUID4):
event = self.mixins.get_one(item_id)
self._translate_event_subject(event)
if event.event_type == TimelineEventType.system.value:
event.subject = self.t(event.subject)
return event
@router.put("/{item_id}", response_model=RecipeTimelineEventOut)

View File

@@ -38,6 +38,8 @@ from mealie.services.scraper import cleaner
from .template_service import TemplateService
RECIPE_CREATED_EVENT_SUBJECT = "recipe.recipe-created"
class RecipeServiceBase(BaseService):
def __init__(self, repos: AllRepositories, user: PrivateUser, household: HouseholdInDB, translator: Translator):
@@ -69,8 +71,19 @@ class RecipeService(RecipeServiceBase):
def can_delete(self, recipe_slugs: list[str]) -> bool:
if self.user.admin:
return True
else:
return self.can_update(recipe_slugs)
# Deletion requires ownership; collaborative editing rules (can_update) do not apply
model = self.group_recipes.model
owned_count = self.group_recipes.session.scalar(
sa.select(sa.func.count())
.select_from(model)
.where(
model.slug.in_(recipe_slugs),
model.group_id == self.user.group_id,
model.user_id == self.user.id,
)
)
return owned_count == len(recipe_slugs)
def can_update(self, recipe_slugs: list[str]) -> bool:
sql = dedent(
@@ -224,7 +237,7 @@ class RecipeService(RecipeServiceBase):
timeline_event_data = RecipeTimelineEventCreate(
user_id=new_recipe.user_id,
recipe_id=new_recipe.id,
subject=self.t("recipe.recipe-created"),
subject=RECIPE_CREATED_EVENT_SUBJECT,
event_type=TimelineEventType.system,
timestamp=new_recipe.created_at or datetime.now(UTC),
)

View File

@@ -43,10 +43,12 @@ def _create_mealplan_timeline_events_for_household(
if not user:
continue
# TODO: make this translatable
if mealplan.entry_type == PlanEntryType.side:
event_subject = f"recipe.made-this-as-side|{user.full_name}"
event_subject = f"{user.full_name} made this as a side"
else:
event_subject = f"recipe.made-this-for-{mealplan.entry_type.value}|{user.full_name}"
event_subject = f"{user.full_name} made this for {mealplan.entry_type.value}"
query_start_time = datetime.combine(datetime.now(UTC).date(), time.min)
query_end_time = query_start_time + timedelta(days=1)

View File

@@ -1,6 +1,6 @@
[project]
name = "mealie"
version = "3.17.0"
version = "3.18.0"
description = "A Recipe Manager"
authors = [{ name = "Hayden", email = "hay-kot@pm.me" }]
license = "AGPL-3.0-only"

View File

@@ -191,6 +191,24 @@ def test_organizer_association(
assert response.status_code == 200
@pytest.mark.parametrize("route", organizer_routes, ids=test_ids)
def test_organizer_create_duplicate_name_returns_400(
api_client: TestClient,
unique_user: TestUser,
route: RoutesBase,
):
# Regression test for #7582: POSTing a duplicate name to organizer endpoints
# leaked the sqlalchemy IntegrityError as an HTTP 500. The expected behavior,
# matching other organizer endpoints (foods, units, tools), is HTTP 400.
data = {"name": random_string(10)}
response = api_client.post(route.base, json=data, headers=unique_user.token)
assert response.status_code == 201
response = api_client.post(route.base, json=data, headers=unique_user.token)
assert response.status_code == 400
@pytest.mark.parametrize("route, recipe_key", association_data, ids=test_ids)
def test_organizer_get_by_slug(
api_client: TestClient,

View File

@@ -20,6 +20,21 @@ def create_labels(api_client: TestClient, unique_user: TestUser, count: int = 10
return labels
def test_label_create_duplicate_name_returns_400(api_client: TestClient, unique_user_fn_scoped: TestUser):
# Regression test for #7582: POSTing a duplicate label name leaked the
# sqlalchemy IntegrityError as an HTTP 500. The expected behavior, matching
# the other organizer endpoints (foods, units, tools, tags, categories),
# is HTTP 400. The function-scoped fixture avoids leaking the created label
# into the module-scoped `unique_user` group state used by sibling tests.
payload = {"name": random_string(), "color": "#ff0000"}
response = api_client.post(api_routes.groups_labels, json=payload, headers=unique_user_fn_scoped.token)
assert response.status_code == 200
response = api_client.post(api_routes.groups_labels, json=payload, headers=unique_user_fn_scoped.token)
assert response.status_code == 400
def test_new_list_creates_list_labels(api_client: TestClient, unique_user: TestUser):
labels = create_labels(api_client, unique_user)
response = api_client.post(

View File

@@ -201,19 +201,12 @@ def test_delete_recipes_from_other_households(
assert recipe_json["id"] == h2_recipe_id
response = api_client.delete(api_routes.recipes_slug(recipe_json["slug"]), headers=unique_user.token)
if household_lock_recipe_edits:
assert response.status_code == 403
assert response.status_code == 403
# confirm the recipe still exists
response = api_client.get(api_routes.recipes_slug(h2_recipe_id), headers=unique_user.token)
assert response.status_code == 200
assert response.json()["id"] == h2_recipe_id
else:
assert response.status_code == 200
# confirm the recipe was deleted
response = api_client.get(api_routes.recipes_slug(h2_recipe_id), headers=unique_user.token)
assert response.status_code == 404
# confirm the recipe still exists
response = api_client.get(api_routes.recipes_slug(h2_recipe_id), headers=unique_user.token)
assert response.status_code == 200
assert response.json()["id"] == h2_recipe_id
@pytest.mark.parametrize("is_private_household", [True, False])

View File

@@ -87,6 +87,23 @@ def test_recipe_asset_exploit(api_client: TestClient, unique_user: TestUser, rec
assert not (recipe.asset_dir / "test.txt").exists()
def test_recipe_asset_dangerous_extension_blocked(
api_client: TestClient, unique_user: TestUser, recipe_ingredient_only: Recipe
):
"""Ensure scriptable extensions are rejected to prevent stored XSS (GHSA-gfwc-pjx4-mg9p)."""
recipe = recipe_ingredient_only
for ext in ("html", "svg", "js", "htm", "xhtml"):
payload = {"name": random_string(10), "icon": "mdi-file", "extension": ext}
file_payload = {"file": b"<script>alert(1)</script>"}
response = api_client.post(
f"/api/recipes/{recipe.slug}/assets",
data=payload,
files=file_payload,
headers=unique_user.token,
)
assert response.status_code == 400, f"expected 400 for extension={ext}, got {response.status_code}"
def test_recipe_image_upload(api_client: TestClient, unique_user: TestUser, recipe_ingredient_only: Recipe):
data_payload = {"extension": "jpg"}
file_payload = {"image": data.images_test_image_1.read_bytes()}

View File

@@ -160,6 +160,24 @@ def test_other_user_cant_delete_recipe(api_client: TestClient, user_tuple: list[
assert response.status_code == 403
def test_other_user_cant_delete_unlocked_recipe(api_client: TestClient, user_tuple: list[TestUser]):
"""Non-owner must not delete an unlocked recipe — BOLA regression (GHSA-x5v9-9jvh-7c7q)."""
slug = random_string(10)
unique_user, other_user = user_tuple
unique_user.repos.recipes.create(
Recipe(
user_id=unique_user.user_id,
group_id=unique_user.group_id,
name=slug,
settings=RecipeSettings(locked=False),
)
)
response = api_client.delete(api_routes.recipes_slug(slug), headers=other_user.token)
assert response.status_code == 403
def test_other_user_bulk_delete(api_client: TestClient, user_tuple: list[TestUser]):
slug_locked = random_string(10)
slug_unlocked = random_string(10)
@@ -190,6 +208,30 @@ def test_other_user_bulk_delete(api_client: TestClient, user_tuple: list[TestUse
assert response.status_code == 403
def test_other_user_cant_bulk_delete_unlocked_recipes(api_client: TestClient, user_tuple: list[TestUser]):
"""Non-owner must not bulk-delete unlocked recipes — BOLA regression (GHSA-x5v9-9jvh-7c7q)."""
slug_1 = random_string(10)
slug_2 = random_string(10)
unique_user, other_user = user_tuple
for slug in (slug_1, slug_2):
unique_user.repos.recipes.create(
Recipe(
user_id=unique_user.user_id,
group_id=unique_user.group_id,
name=slug,
settings=RecipeSettings(locked=False),
)
)
response = api_client.post(
api_routes.recipes_bulk_actions_delete,
json={"recipes": [slug_1, slug_2]},
headers=other_user.token,
)
assert response.status_code == 403
def test_admin_can_delete_locked_recipe_owned_by_another_user(
api_client: TestClient, unfiltered_database: AllRepositories, unique_user: TestUser, admin_user: TestUser
):

View File

@@ -3,18 +3,24 @@ from uuid import uuid4
import pytest
from fastapi.testclient import TestClient
from mealie.lang.providers import get_all_translations
from mealie.schema.recipe.recipe import Recipe
from mealie.schema.recipe.recipe_timeline_events import (
RecipeTimelineEventOut,
RecipeTimelineEventPagination,
TimelineEventImage,
TimelineEventType,
)
from mealie.schema.recipe.request_helpers import UpdateImageResponse
from mealie.services.recipe.recipe_service import RECIPE_CREATED_EVENT_SUBJECT
from tests.utils import api_routes
from tests.utils.factories import random_string
from tests.utils.fixture_schemas import TestUser
PERSISTED_TRANSLATION_KEYS = [RECIPE_CREATED_EVENT_SUBJECT]
@pytest.fixture(scope="function")
def recipes(api_client: TestClient, unique_user: TestUser):
recipes = []
@@ -341,6 +347,50 @@ def test_create_recipe_with_timeline_event(
assert events_pagination.items
@pytest.mark.parametrize("translation_key", PERSISTED_TRANSLATION_KEYS)
def test_persisted_translation_keys_have_translations(translation_key: str):
translations = get_all_translations(translation_key)
missing_translations = [locale for locale, translation in translations.items() if translation == translation_key]
assert missing_translations == []
def test_recipe_created_system_event_is_translated(
api_client: TestClient,
unique_user: TestUser,
recipes: list[Recipe],
):
recipe = recipes[0]
params = {"queryFilter": f"recipe_id={recipe.id}"}
# fetch events in French — the system "recipe created" event should be translated
fr_headers = {**unique_user.token, "Accept-Language": "fr-FR"}
events_response = api_client.get(api_routes.recipes_timeline_events, params=params, headers=fr_headers)
assert events_response.status_code == 200
events_pagination = RecipeTimelineEventPagination.model_validate(events_response.json())
system_events = [e for e in events_pagination.items if e.event_type == TimelineEventType.system.value]
assert system_events, "expected at least one system event for a newly created recipe"
for event in system_events:
assert event.subject == "Recette créée", f"expected French translation, got: {event.subject!r}"
# also verify the individual GET endpoint translates correctly
single_response = api_client.get(api_routes.recipes_timeline_events_item_id(event.id), headers=fr_headers)
assert single_response.status_code == 200
single_event = RecipeTimelineEventOut.model_validate(single_response.json())
assert single_event.subject == "Recette créée"
# fetch the same events in English — subject should be the English string
en_headers = {**unique_user.token, "Accept-Language": "en-US"}
events_response = api_client.get(api_routes.recipes_timeline_events, params=params, headers=en_headers)
events_pagination = RecipeTimelineEventPagination.model_validate(events_response.json())
system_events = [e for e in events_pagination.items if e.event_type == TimelineEventType.system.value]
for event in system_events:
assert event.subject == "Recipe Created", f"expected English string, got: {event.subject!r}"
@pytest.mark.parametrize("use_other_household_user", [True, False])
def test_invalid_recipe_id(
api_client: TestClient, unique_user: TestUser, h2_user: TestUser, use_other_household_user: bool

View File

@@ -13,49 +13,6 @@ from tests.utils.factories import random_int, random_string
from tests.utils.fixture_schemas import TestUser
def _create_recipe_and_mealplan(api_client: TestClient, user: TestUser, entry_type: str) -> tuple[RecipeSummary, int]:
recipe_name = random_string(length=25)
response = api_client.post(api_routes.recipes, json={"name": recipe_name}, headers=user.token)
assert response.status_code == 201
response = api_client.get(api_routes.recipes_slug(recipe_name), headers=user.token)
recipe = RecipeSummary.model_validate(response.json())
params = {"queryFilter": f"recipe_id={recipe.id}"}
response = api_client.get(api_routes.recipes_timeline_events, params=params, headers=user.token)
initial_event_count = len(response.json()["items"])
new_plan = CreatePlanEntry(date=datetime.now(UTC).date(), entry_type=entry_type, recipe_id=recipe.id).model_dump(
by_alias=True
)
new_plan["date"] = datetime.now(UTC).date().isoformat()
new_plan["recipeId"] = str(recipe.id)
response = api_client.post(api_routes.households_mealplans, json=new_plan, headers=user.token)
assert response.status_code == 201
return recipe, initial_event_count
def _get_mealplan_event(
api_client: TestClient, user: TestUser, recipe: RecipeSummary, initial_count: int, extra_headers: dict
) -> dict:
create_mealplan_timeline_events()
params = {
"page": "1",
"perPage": "-1",
"orderBy": "created_at",
"orderDirection": "desc",
"queryFilter": f"recipe_id={recipe.id}",
}
response = api_client.get(
api_routes.recipes_timeline_events, headers={**user.token, **extra_headers}, params=params
)
items = response.json()["items"]
assert len(items) == initial_count + 1
return items[0]
def test_no_mealplans():
# make sure this task runs successfully even if it doesn't do anything
create_mealplan_timeline_events()
@@ -294,27 +251,3 @@ def test_preserve_future_made_date(api_client: TestClient, unique_user: TestUser
response = api_client.get(api_routes.households_self_recipes_recipe_slug(recipe.slug), headers=h2_user.token)
household_recipe = HouseholdRecipeSummary.model_validate(response.json())
assert household_recipe.last_made is None
def test_mealplan_event_subject_is_translated(api_client: TestClient, unique_user: TestUser):
"""Mealplan timeline event subjects are stored as i18n keys and translated at serve time."""
# --- dinner entry type ---
recipe, initial_count = _create_recipe_and_mealplan(api_client, unique_user, "dinner")
event = _get_mealplan_event(api_client, unique_user, recipe, initial_count, {"Accept-Language": "en-US"})
expected = f"{unique_user.full_name} made this for dinner"
assert event["subject"] == expected, f"expected {expected!r}, got {event['subject']!r}"
# --- side entry type uses a distinct phrase ---
recipe2, initial_count2 = _create_recipe_and_mealplan(api_client, unique_user, "side")
event2 = _get_mealplan_event(api_client, unique_user, recipe2, initial_count2, {"Accept-Language": "en-US"})
expected2 = f"{unique_user.full_name} made this as a side"
assert event2["subject"] == expected2, f"expected {expected2!r}, got {event2['subject']!r}"
# --- locale fallback: fr-FR doesn't have these keys yet, should fall back to en-US ---
recipe3, initial_count3 = _create_recipe_and_mealplan(api_client, unique_user, "lunch")
event3 = _get_mealplan_event(api_client, unique_user, recipe3, initial_count3, {"Accept-Language": "fr-FR"})
expected3 = f"{unique_user.full_name} made this for lunch"
assert event3["subject"] == expected3, f"expected en-US fallback {expected3!r}, got {event3['subject']!r}"

2
uv.lock generated
View File

@@ -898,7 +898,7 @@ wheels = [
[[package]]
name = "mealie"
version = "3.17.0"
version = "3.18.0"
source = { editable = "." }
dependencies = [
{ name = "aiofiles" },