diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 9b7582bd1..24b1da2e0 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -52,6 +52,15 @@ "group": "groupA" }, "problemMatcher": [] + }, + { + "label": "Run python tests", + "command": "make test", + "type": "shell", + "presentation": { + "reveal": "always" + }, + "problemMatcher": [] } ] } diff --git a/Dockerfile b/Dockerfile index 2528d200b..53025eccd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,9 +40,9 @@ RUN apk add --update --no-cache --virtual .build-deps \ cd /app/ && poetry install --no-root --no-dev && \ apk --purge del .build-deps - COPY ./mealie /app/mealie RUN poetry install --no-dev + COPY ./Caddyfile /app COPY ./dev/data/templates /app/data/templates COPY --from=build-stage /app/dist /app/dist diff --git a/docs/docs/api-usage/bulk-url-import.md b/docs/docs/api-usage/bulk-url-import.md new file mode 100644 index 000000000..130e42d9a --- /dev/null +++ b/docs/docs/api-usage/bulk-url-import.md @@ -0,0 +1,93 @@ +!!! info + This example was submitted by a user. Have an Example? Submit a PR! + + +Recipes can be imported in bulk from a file containing a list of URLs. This can be done using the following bash or python scripts with the `list` file containing one URL per line. + +#### Bash +```bash +#!/bin/bash + +function authentification () { + auth=$(curl -X 'POST' \ + "$3/api/auth/token" \ + -H 'accept: application/json' \ + -H 'Content-Type: application/x-www-form-urlencoded' \ + -d 'grant_type=&username='$1'&password='$2'&scope=&client_id=&client_secret=') + + echo $auth | sed -e 's/.*token":"\(.*\)",.*/\1/' +} + +function import_from_file () { + while IFS= read -r line + do + echo $line + curl -X 'POST' \ + "$3/api/recipes/create-url" \ + -H "Authorization: Bearer $2" \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '{"url": "'$line'" }' + echo + done < "$1" +} + +input="list" +mail="changeme@email.com" +password="MyPassword" +mealie_url=http://localhost:9000 + + +token=$(authentification $mail $password $mealie_url) +import_from_file $input $token $mealie_url + +``` + +#### Python +```python +import requests +import re + +def authentification(mail, password, mealie_url): + headers = { + 'accept': 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded', + } + data = { + 'grant_type': '', + 'username': mail, + 'password': password, + 'scope': '', + 'client_id': '', + 'client_secret': '' + } + auth = requests.post(mealie_url + "/api/auth/token", headers=headers, data=data) + token = re.sub(r'.*token":"(.*)",.*', r'\1', auth.text) + return token + +def import_from_file(input_file, token, mealie_url): + with open(input_file) as fp: + for l in fp: + line = re.sub(r'(.*)\n', r'\1', l) + print(line) + headers = { + 'Authorization': "Bearer " + token, + 'accept': 'application/json', + 'Content-Type': 'application/json' + } + data = { + 'url': line + } + response = requests.post(mealie_url + "/api/recipes/create-url", headers=headers, json=data) + print(response.text) + +input_file="list" +mail="changeme@email.com" +password="MyPassword" +mealie_url="http://localhost:9000" + + +token = authentification(mail, password, mealie_url) +import_from_file(input_file, token, mealie_url) +``` + diff --git a/docs/docs/api-usage/getting-started.md b/docs/docs/api-usage/getting-started.md new file mode 100644 index 000000000..95abfd6a1 --- /dev/null +++ b/docs/docs/api-usage/getting-started.md @@ -0,0 +1,39 @@ +# Usage + +## Getting a Token + +Currently Mealie doesn't support creating a long-live token. You can however get a token from the API. This example was pulled from the automatic API documentation provided by Mealie. + +### Curl +```bash +curl -X 'POST' \ + 'https://mealie-demo.hay-kot.dev/api/auth/token' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/x-www-form-urlencoded' \ + -d 'grant_type=&username=changeme%40email.com&password=demo&scope=&client_id=&client_secret=' + +``` + +#### Response +```json +{ + "snackbar": { + "text": "User Successfully Logged In", + "type": "success" + }, + "access_token": "your-long-token-string", + "token_type": "bearer" +} +``` + +## Key Components + +### Exploring Your Local API +On your local installation you can access interactive API documentation that provides `curl` examples and expected results. This allows you to easily test and interact with your API to identify places to include your own functionality. You can visit the documentation at `http://mealie.yourdomain.com/docs or see the example at the [Demo Site](https://mealie-demo.hay-kot.dev/docs) + +### Recipe Extras +Recipes extras are a key feature of the Mealie API. They allow you to create custom json key/value pairs within a recipe to reference from 3rd part applications. You can use these keys to contain information to trigger automation or custom messages to relay to your desired device. + +For example you could add `{"message": "Remember to thaw the chicken"}` to a recipe and use the webhooks built into mealie to send that message payload to a destination to be processed. + +![api-extras-gif](../assets/gifs/api-extras.gif) diff --git a/docs/docs/api-usage/home-assistant.md b/docs/docs/api-usage/home-assistant.md new file mode 100644 index 000000000..4de60be82 --- /dev/null +++ b/docs/docs/api-usage/home-assistant.md @@ -0,0 +1,30 @@ +In a lot of ways, Home Assistant is why this project exists! Since it Mealie has a robust API it makes it a great fit for interacting with Home Assistant and pulling information into your dashboard. + +### Get Todays Meal in Lovelace +Starting in v0.4.1 you are now able to use the uri `/api​/meal-plans​/today​/image?group_name=Home` to directly access the image to todays meal. This makes it incredible easy to include the image into your Home Assistant Dashboard using the picture entity. + +Here's an example where `sensor.mealie_todays_meal` is pulling in the meal-plan name and I'm using the url to get the image. + +![api-extras-gif](../assets/img/home-assistant-card.png) + +```yaml +type: picture-entity +entity: sensor.mealie_todays_meal +name: Dinner Tonight +show_state: true +show_name: true +image: 'http://localhost:9000/api/meal-plans/today/image?group_name=Home' +style: +.: | + ha-card { + max-height: 300px !important; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + } +``` + + +!!! tip + Due to how Home Assistant works with images, I had to include the additional styling to get the images to not appear distorted. This includes and [additional installation](https://github.com/thomasloven/lovelace-card-mod) from HACS. diff --git a/docs/docs/assets/img/home-assistant-card.png b/docs/docs/assets/img/home-assistant-card.png new file mode 100644 index 000000000..9a6e46acc Binary files /dev/null and b/docs/docs/assets/img/home-assistant-card.png differ diff --git a/docs/docs/getting-started/api-usage.md b/docs/docs/getting-started/api-usage.md index b58fc485f..f44a30831 100644 --- a/docs/docs/getting-started/api-usage.md +++ b/docs/docs/getting-started/api-usage.md @@ -1,5 +1,8 @@ # Usage +## Getting a Token +Bla Bla + ## Key Components ### Recipe Extras Recipes extras are a key feature of the Mealie API. They allow you to create custom json key/value pairs within a recipe to reference from 3rd part applications. You can use these keys to contain information to trigger automation or custom messages to relay to your desired device. @@ -8,96 +11,4 @@ For example you could add `{"message": "Remember to thaw the chicken"}` to a rec ![api-extras-gif](../assets/gifs/api-extras.gif) - -## Examples -### Bulk import -Recipes can be imported in bulk from a file containing a list of URLs. This can be done using the following bash or python scripts with the `list` file containing one URL per line. - -#### Bash -```bash -#!/bin/bash - -function authentification () { - auth=$(curl -X 'POST' \ - "$3/api/auth/token" \ - -H 'accept: application/json' \ - -H 'Content-Type: application/x-www-form-urlencoded' \ - -d 'grant_type=&username='$1'&password='$2'&scope=&client_id=&client_secret=') - - echo $auth | sed -e 's/.*token":"\(.*\)",.*/\1/' -} - -function import_from_file () { - while IFS= read -r line - do - echo $line - curl -X 'POST' \ - "$3/api/recipes/create-url" \ - -H "Authorization: Bearer $2" \ - -H 'accept: application/json' \ - -H 'Content-Type: application/json' \ - -d '{"url": "'$line'" }' - echo - done < "$1" -} - -input="list" -mail="changeme@email.com" -password="MyPassword" -mealie_url=http://localhost:9000 - - -token=$(authentification $mail $password $mealie_url) -import_from_file $input $token $mealie_url - -``` - -#### Python -```python -import requests -import re - -def authentification(mail, password, mealie_url): - headers = { - 'accept': 'application/json', - 'Content-Type': 'application/x-www-form-urlencoded', - } - data = { - 'grant_type': '', - 'username': mail, - 'password': password, - 'scope': '', - 'client_id': '', - 'client_secret': '' - } - auth = requests.post(mealie_url + "/api/auth/token", headers=headers, data=data) - token = re.sub(r'.*token":"(.*)",.*', r'\1', auth.text) - return token - -def import_from_file(input_file, token, mealie_url): - with open(input_file) as fp: - for l in fp: - line = re.sub(r'(.*)\n', r'\1', l) - print(line) - headers = { - 'Authorization': "Bearer " + token, - 'accept': 'application/json', - 'Content-Type': 'application/json' - } - data = { - 'url': line - } - response = requests.post(mealie_url + "/api/recipes/create-url", headers=headers, json=data) - print(response.text) - -input_file="list" -mail="changeme@email.com" -password="MyPassword" -mealie_url="http://localhost:9000" - - -token = authentification(mail, password, mealie_url) -import_from_file(input_file, token, mealie_url) -``` - Have Ideas? Submit a PR! diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 7d675172a..ab46a17c7 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -56,7 +56,6 @@ nav: - Organizing Recipes: "getting-started/organizing-recipes.md" - Planning Meals: "getting-started/meal-planner.md" - iOS Shortcuts: "getting-started/ios.md" - - API Usage: "getting-started/api-usage.md" - Site Administration: - User Settings: "site-administration/user-settings.md" - Site Settings: "site-administration/site-settings.md" @@ -64,6 +63,10 @@ nav: - User Management: "site-administration/user-management.md" - Backups and Restore: "site-administration/backups-and-exports.md" - Recipe Migration: "site-administration/migration-imports.md" + - API Usage: + - Getting Started: "api-usage/getting-started.md" + - Home Assistant: "api-usage/home-assistant.md" + - Bulk Url Import: "api-usage/bulk-url-import.md" - API Reference: "api/redoc.md" - Contributors Guide: - Non-Code: "contributors/non-coders.md" diff --git a/frontend/src/locales/pt-PT.json b/frontend/src/locales/pt-PT.json new file mode 100644 index 000000000..d10b43433 --- /dev/null +++ b/frontend/src/locales/pt-PT.json @@ -0,0 +1,160 @@ +{ + "404": { + "page-not-found": "404 Página não encontrada", + "take-me-home": "Voltar ao início" + }, + "new-recipe": { + "from-url": "Do URL", + "recipe-url": "URL da Receita", + "error-message": "Ocorreu um erro ao ler o URL. Verifica os registos e o debug/last_recipe.json para perceber o que correu mal." , + "bulk-add": "Adicionar Vários", + "paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list": "Insira os dados da sua receita. Cada linha será tratada como um item numa lista." + }, + "general": { + "upload": "Enviar", + "submit": "Submeter", + "name": "Nome", + "settings": "Definições", + "close": "Fechar", + "save": "Guardar", + "image-file": "Ficheiro de Imagem", + "update": "Atualizar", + "edit": "Editar", + "delete": "Eliminar", + "select": "Seleccionar", + "random": "Aleatório", + "new": "Novo", + "create": "Criar", + "cancel": "Cancelar", + "ok": "OK", + "enabled": "Ativado", + "download": "Transferir", + "import": "Importar", + "options": "Opções", + "templates": "Templates", + "recipes": "Receitas", + "themes": "Temas", + "confirm": "Confirmar" + }, + "login": { + "stay-logged-in": "Manter a sessão iniciada?", + "email": "Email", + "password": "Password", + "sign-in": "Iniciar Sessão", + "sign-up": "Criar Conta" + }, + "meal-plan": { + "shopping-list": "Lista de Compras", + "dinner-this-week": "Jantar esta semana", + "meal-planner": "Planeador de Refeições", + "dinner-today": "Jantar Hoje", + "planner": "Planeador", + "edit-meal-plan": "Editar Plano de Refeições", + "meal-plans": "Planos de Refeições", + "create-a-new-meal-plan": "Criar novo Plano de Refeições", + "start-date": "Data de Inicio", + "end-date": "Data de Fim" + }, + "recipe": { + "description": "Descrição", + "ingredients": "Ingredientes", + "categories": "Categorias", + "tags": "Etiquetas", + "instructions": "Instruções", + "step-index": "Passo: {step}", + "recipe-name": "Nome da Receita", + "servings": "Porções", + "ingredient": "Ingrediente", + "notes": "Notas", + "note": "Nota", + "original-url": "URL Original", + "view-recipe": "Ver Receita", + "title": "Título", + "total-time": "Tempo Total", + "prep-time": "Tempo de Preparação", + "perform-time": "Tempo de Cozedura", + "api-extras": "Extras API", + "object-key": "Chave do Objeto", + "object-value": "Valor do Objeto", + "new-key-name": "Novo nome da Chave", + "add-key": "Adicionar Chave", + "key-name-required": "Nome da Chave é Obrigatório", + "no-white-space-allowed": "Espaço em Branco não Permitido", + "delete-recipe": "Eliminar Receita", + "delete-confirmation": "Tem a certeza que deseja eliminar esta receita?" + }, + "search": { + "search-mealie": "Pesquisar Mealie" + }, + "settings": { + "general-settings": "Definições Gerais", + "local-api": "API Local", + "language": "Língua", + "add-a-new-theme": "Adicionar novo tema", + "set-new-time": "Definir hora", + "current": "Versão:", + "latest": "Mais Recente", + "explore-the-docs": "Explorar Documentação", + "contribute": "Contribuir", + "backup-and-exports": "Backups", + "backup-info": "Backups are exported in standard JSON format along with all the images stored on the file system. In your backup folder you'll find a .zip file that contains all of the recipe JSON and images from the database. Additionally, if you selected a markdown file, those will also be stored in the .zip file. To import a backup, it must be located in your backups folder. Automated backups are done each day at 3:00 AM.", + "available-backups": "Backups Disponíveis", + "theme": { + "theme-name": "Nome do Tema", + "theme-settings": "Definições do Tema", + "select-a-theme-from-the-dropdown-or-create-a-new-theme-note-that-the-default-theme-will-be-served-to-all-users-who-have-not-set-a-theme-preference": "Selecione um tema da lista ou crie um novo tema. Note que o tema por defeito será utilizado por todos os utilizadores que não selecionaram um tema preferido.", + "dark-mode": "Modo Escuro", + "theme-is-required": "Tema é Obrigatório", + "primary": "Primário", + "secondary": "Secondário", + "accent": "Accent", + "success": "Successo", + "info": "Info", + "warning": "Aviso", + "error": "Erro", + "default-to-system": "Mesmo do Sistema", + "light": "Claro", + "dark": "Escuro", + "theme": "Tema", + "saved-color-theme": "Cor de Tema Guardado", + "delete-theme": "Eliminar Tema", + "are-you-sure-you-want-to-delete-this-theme": "Tem a certeza que deseja eliminar este tema?", + "choose-how-mealie-looks-to-you-set-your-theme-preference-to-follow-your-system-settings-or-choose-to-use-the-light-or-dark-theme": "Escolha como o Mealie estará visivel. Escolha o Mesmo do sistema para seguir o tema do seu dispositivo, ou selecione claro ou escuro.", + "theme-name-is-required": "Nome do Tema é Obrigatório." + }, + "webhooks": { + "meal-planner-webhooks": "Webhooks do Organizador de Refeições", + "the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "Os URLs apresentados abaixo receberão webhooks que contêm os dados da receita para o plano de refeições no dia marcado. Atualmente, os webhooks serão executados a ", + "test-webhooks": "Webhooks de Teste", + "webhook-url": "Webhook URL" + }, + "new-version-available": "Uma nova versão do Mealie está disponível, Visite o Repo ", + "backup": { + "import-recipes": "Importar Receitas", + "import-themes": "Importar Temas", + "import-settings": "Importa Definições", + "create-heading": "Criar um Backup", + "backup-tag": "Etiqueta do Backup", + "full-backup": "Backup Completo", + "partial-backup": "Backup Parcial", + "backup-restore-report": "Análise do Resultado do Backup", + "successfully-imported": "Importado com Sucesso", + "failed-imports": "Importações falhadas" + } + }, + "migration": { + "recipe-migration": "Migração da Receita", + "failed-imports": "Importações Falhadas", + "migration-report": "Análise das Migrações", + "successful-imports": "Importações Bem sucedidas", + "no-migration-data-available": "Não há dados de migração disponíveis", + "nextcloud": { + "title": "Nextcloud Cookbook", + "description": "Migraar dados de uma instância do Nextcloud CookBook" + }, + "chowdown": { + "title": "Chowdown", + "description": "Migrar dados do Chowdown" + } + } +} diff --git a/frontend/src/store/modules/language.js b/frontend/src/store/modules/language.js index 8fad87b07..7eed2de85 100644 --- a/frontend/src/store/modules/language.js +++ b/frontend/src/store/modules/language.js @@ -35,6 +35,10 @@ const state = { name: "German", value: "de", }, + { + name: "Português", + value: "pt-PT", + }, ], }; @@ -48,13 +52,13 @@ const mutations = { const actions = { initLang({ getters }, { currentVueComponent }) { VueI18n.locale = getters.getActiveLang; - currentVueComponent.$vuetify.lang.current = getters.getActiveLang; + currentVueComponent.$vuetify.lang.current = getters.getActiveLang; }, }; const getters = { - getActiveLang: (state) => state.lang, - getAllLangs: (state) => state.allLangs, + getActiveLang: state => state.lang, + getAllLangs: state => state.allLangs, }; export default { diff --git a/mealie/routes/mealplans/crud.py b/mealie/routes/mealplans/crud.py index f1dcc02bb..de3085bdd 100644 --- a/mealie/routes/mealplans/crud.py +++ b/mealie/routes/mealplans/crud.py @@ -79,7 +79,7 @@ def get_today(session: Session = Depends(generate_session), current_user: UserIn @router.get("/today/image", tags=["Meal Plan"]) -def get_today(session: Session = Depends(generate_session), group_name: str = "Home"): +def get_todays_image(session: Session = Depends(generate_session), group_name: str = "Home"): """ Returns the image for todays meal-plan. """ diff --git a/mealie/run.sh b/mealie/run.sh index 12a5d142d..56ce31532 100755 --- a/mealie/run.sh +++ b/mealie/run.sh @@ -4,11 +4,11 @@ ARG1=${1:-production} # Set Script Directory - Used for running the script from a different directory. -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +# DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" # # Initialize Database Prerun -poetry run python $DIR/db/init_db.py -poetry run python $DIR/services/image/minify.py +poetry run python /app/mealie/db/init_db.py +poetry run python /app/mealie/services/image/minify.py # Migrations # TODO diff --git a/mealie/services/image/minify.py b/mealie/services/image/minify.py index 024656a50..c6f781e3a 100644 --- a/mealie/services/image/minify.py +++ b/mealie/services/image/minify.py @@ -60,7 +60,10 @@ def move_all_images(): continue new_folder = app_dirs.IMG_DIR.joinpath(image_file.stem) new_folder.mkdir(parents=True, exist_ok=True) - image_file.rename(new_folder.joinpath(f"original{image_file.suffix}")) + new_file = new_folder.joinpath(f"original{image_file.suffix}") + if new_file.is_file(): + new_file.unlink() + image_file.rename(new_file) def validate_slugs_in_database(session: Session = None): diff --git a/tests/unit_tests/test_cleaner.py b/tests/unit_tests/test_cleaner.py index b1630a132..dbe597281 100644 --- a/tests/unit_tests/test_cleaner.py +++ b/tests/unit_tests/test_cleaner.py @@ -80,7 +80,7 @@ def test_cleaner_instructions(instructions): def test_html_with_recipe_data(): path = TEST_RAW_HTML.joinpath("healthy_pasta_bake_60759.html") url = "https://www.bbc.co.uk/food/recipes/healthy_pasta_bake_60759" - recipe_data = extract_recipe_from_html(open(path).read(), url) + recipe_data = extract_recipe_from_html(open(path,encoding="utf8").read(), url) assert len(recipe_data["name"]) > 10 assert len(recipe_data["slug"]) > 10