diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 54217aaa3..edb02e9b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: node-version: ["18"] steps: - uses: actions/checkout@v4 - - uses: awalsh128/cache-apt-pkgs-action@v1.4.1 + - uses: awalsh128/cache-apt-pkgs-action@v1.4.2 with: packages: libsasl2-dev python3-dev libldap2-dev libssl-dev version: 1.0 diff --git a/Dockerfile b/Dockerfile index 3f17d0f11..c5856bcfa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,8 @@ RUN apk add --no-cache postgresql-libs postgresql-client gettext zlib libjpeg li #Print all logs without buffering it. ENV PYTHONUNBUFFERED 1 +ENV DOCKER true + #This port will be used by gunicorn. EXPOSE 8080 @@ -33,6 +35,12 @@ RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev zlib-de #Copy project and execute it. COPY . ./ +# collect the static files +RUN /opt/recipes/venv/bin/python manage.py collectstatic_js_reverse +RUN /opt/recipes/venv/bin/python manage.py collectstatic --noinput +# copy the collected static files to a different location, so they can be moved into a potentially mounted volume +RUN mv /opt/recipes/staticfiles /opt/recipes/staticfiles-collect + # collect information from git repositories RUN /opt/recipes/venv/bin/python version.py # delete git repositories to reduce image size diff --git a/boot.sh b/boot.sh index ae3dbb51d..ab5d7fddd 100644 --- a/boot.sh +++ b/boot.sh @@ -67,12 +67,21 @@ echo "Migrating database" python manage.py migrate -echo "Generating static files" +if [[ "${DOCKER}" == "true" ]]; then + echo "Copying cached static files from docker build" -python manage.py collectstatic_js_reverse -python manage.py collectstatic --noinput + mkdir -p /opt/recipes/staticfiles + rm -rf /opt/recipes/staticfiles/* + mv /opt/recipes/staticfiles-collect/* /opt/recipes/staticfiles + rm -rf /opt/recipes/staticfiles-collect +else + echo "Collecting static files, this may take a while..." -echo "Done" + python manage.py collectstatic_js_reverse + python manage.py collectstatic --noinput + + echo "Done" +fi chmod -R 755 /opt/recipes/mediafiles diff --git a/cookbook/connectors/homeassistant.py b/cookbook/connectors/homeassistant.py index e653c3f3d..f824a4262 100644 --- a/cookbook/connectors/homeassistant.py +++ b/cookbook/connectors/homeassistant.py @@ -1,26 +1,35 @@ import logging from logging import Logger +from typing import Dict, Tuple +from urllib.parse import urljoin -from homeassistant_api import Client, HomeassistantAPIError, Domain +from aiohttp import ClientError, request from cookbook.connectors.connector import Connector from cookbook.models import ShoppingListEntry, ConnectorConfig, Space class HomeAssistant(Connector): - _domains_cache: dict[str, Domain] _config: ConnectorConfig _logger: Logger - _client: Client def __init__(self, config: ConnectorConfig): if not config.token or not config.url or not config.todo_entity: raise ValueError("config for HomeAssistantConnector in incomplete") - self._domains_cache = dict() + if config.url[-1] != "/": + config.url += "/" self._config = config self._logger = logging.getLogger("connector.HomeAssistant") - self._client = Client(self._config.url, self._config.token, async_cache_session=False, use_async=True) + + async def homeassistant_api_call(self, method: str, path: str, data: Dict) -> str: + headers = { + "Authorization": f"Bearer {self._config.token}", + "Content-Type": "application/json" + } + async with request(method, urljoin(self._config.url, path), headers=headers, json=data) as response: + response.raise_for_status() + return await response.json() async def on_shopping_list_entry_created(self, space: Space, shopping_list_entry: ShoppingListEntry) -> None: if not self._config.on_shopping_list_entry_created_enabled: @@ -28,15 +37,17 @@ class HomeAssistant(Connector): item, description = _format_shopping_list_entry(shopping_list_entry) - todo_domain = self._domains_cache.get('todo') - try: - if todo_domain is None: - todo_domain = await self._client.async_get_domain('todo') - self._domains_cache['todo'] = todo_domain + logging.debug(f"adding {item=} to {self._config.name}") - logging.debug(f"pushing {item} to {self._config.name}") - await todo_domain.add_item(entity_id=self._config.todo_entity, item=item) - except HomeassistantAPIError as err: + data = { + "entity_id": self._config.todo_entity, + "item": item, + "description": description, + } + + try: + await self.homeassistant_api_call("POST", "services/todo/add_item", data) + except ClientError as err: self._logger.warning(f"[HomeAssistant {self._config.name}] Received an exception from the api: {err=}, {type(err)=}") async def on_shopping_list_entry_updated(self, space: Space, shopping_list_entry: ShoppingListEntry) -> None: @@ -48,24 +59,31 @@ class HomeAssistant(Connector): if not self._config.on_shopping_list_entry_deleted_enabled: return - item, description = _format_shopping_list_entry(shopping_list_entry) + if not hasattr(shopping_list_entry._state.fields_cache, "food"): + # Sometimes the food foreign key is not loaded, and we cant load it from an async process + self._logger.debug("required property was not present in ShoppingListEntry") + return + + item, _ = _format_shopping_list_entry(shopping_list_entry) + + logging.debug(f"removing {item=} from {self._config.name}") + + data = { + "entity_id": self._config.todo_entity, + "item": item, + } - todo_domain = self._domains_cache.get('todo') try: - if todo_domain is None: - todo_domain = await self._client.async_get_domain('todo') - self._domains_cache['todo'] = todo_domain - - logging.debug(f"deleting {item} from {self._config.name}") - await todo_domain.remove_item(entity_id=self._config.todo_entity, item=item) - except HomeassistantAPIError as err: - self._logger.warning(f"[HomeAssistant {self._config.name}] Received an exception from the api: {err=}, {type(err)=}") + await self.homeassistant_api_call("POST", "services/todo/remove_item", data) + except ClientError as err: + # This error will always trigger if the item is not present/found + self._logger.debug(f"[HomeAssistant {self._config.name}] Received an exception from the api: {err=}, {type(err)=}") async def close(self) -> None: - await self._client.async_cache_session.close() + pass -def _format_shopping_list_entry(shopping_list_entry: ShoppingListEntry): +def _format_shopping_list_entry(shopping_list_entry: ShoppingListEntry) -> Tuple[str, str]: item = shopping_list_entry.food.name if shopping_list_entry.amount > 0: item += f" ({shopping_list_entry.amount:.2f}".rstrip('0').rstrip('.') @@ -76,10 +94,10 @@ def _format_shopping_list_entry(shopping_list_entry: ShoppingListEntry): else: item += ")" - description = "Imported by TandoorRecipes" + description = "From TandoorRecipes" if shopping_list_entry.created_by.first_name and len(shopping_list_entry.created_by.first_name) > 0: - description += f", created by {shopping_list_entry.created_by.first_name}" + description += f", by {shopping_list_entry.created_by.first_name}" else: - description += f", created by {shopping_list_entry.created_by.username}" + description += f", by {shopping_list_entry.created_by.username}" return item, description diff --git a/cookbook/locale/es/LC_MESSAGES/django.po b/cookbook/locale/es/LC_MESSAGES/django.po index c217d727c..408afd2dd 100644 --- a/cookbook/locale/es/LC_MESSAGES/django.po +++ b/cookbook/locale/es/LC_MESSAGES/django.po @@ -14,8 +14,8 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2024-03-21 14:39+0100\n" -"PO-Revision-Date: 2023-09-25 09:59+0000\n" -"Last-Translator: Matias Laporte \n" +"PO-Revision-Date: 2024-03-27 19:02+0000\n" +"Last-Translator: Axel Breiterman \n" "Language-Team: Spanish \n" "Language: es\n" @@ -23,7 +23,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.15\n" +"X-Generator: Weblate 5.4.2\n" #: .\cookbook\forms.py:45 msgid "" @@ -97,14 +97,16 @@ msgid "" "Long Lived Access Token for your HomeAssistant instance" msgstr "" +"Token de larga duraciónpara tu instancia de HomeAssistant" #: .\cookbook\forms.py:193 msgid "Something like http://homeassistant.local:8123/api" -msgstr "" +msgstr "Algo similar a http://homeassistant.local:8123/api" #: .\cookbook\forms.py:205 msgid "http://homeassistant.local:8123/api for example" -msgstr "" +msgstr "por ejemplo http://homeassistant.local:8123/api for example" #: .\cookbook\forms.py:222 .\cookbook\views\edit.py:117 msgid "Storage" @@ -279,7 +281,7 @@ msgstr "Ha alcanzado el número máximo de recetas para su espacio." #: .\cookbook\helper\permission_helper.py:414 msgid "You have more users than allowed in your space." -msgstr "" +msgstr "Tenés mas usuarios que los permitidos en tu espacio" #: .\cookbook\helper\recipe_url_import.py:304 #, fuzzy @@ -309,7 +311,7 @@ msgstr "fermentar" #: .\cookbook\helper\recipe_url_import.py:310 msgid "sous-vide" -msgstr "" +msgstr "sous-vide" #: .\cookbook\helper\shopping_helper.py:150 msgid "You must supply a servings size" @@ -318,7 +320,7 @@ msgstr "Debe proporcionar un tamaño de porción" #: .\cookbook\helper\template_helper.py:95 #: .\cookbook\helper\template_helper.py:97 msgid "Could not parse template code." -msgstr "" +msgstr "No se pudo parsear el código de la planitlla." #: .\cookbook\integration\copymethat.py:44 #: .\cookbook\integration\melarecipes.py:37 @@ -342,6 +344,8 @@ msgid "" "An unexpected error occurred during the import. Please make sure you have " "uploaded a valid file." msgstr "" +"Ocurrió un error inesperado al importar. Por favor asegurate de haber subido " +"un archivo válido." #: .\cookbook\integration\integration.py:217 msgid "The following recipes were ignored because they already existed:" @@ -457,7 +461,7 @@ msgstr "Calorías" #: .\cookbook\migrations\0190_auto_20230525_1506.py:20 msgid "kcal" -msgstr "" +msgstr "kcal" #: .\cookbook\models.py:325 msgid "" diff --git a/cookbook/locale/tr/LC_MESSAGES/django.po b/cookbook/locale/tr/LC_MESSAGES/django.po index 1dd15200b..5fdea7ddc 100644 --- a/cookbook/locale/tr/LC_MESSAGES/django.po +++ b/cookbook/locale/tr/LC_MESSAGES/django.po @@ -11,8 +11,8 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2024-03-21 14:39+0100\n" -"PO-Revision-Date: 2024-03-03 23:19+0000\n" -"Last-Translator: M Ugur \n" +"PO-Revision-Date: 2024-04-01 22:04+0000\n" +"Last-Translator: atom karinca \n" "Language-Team: Turkish \n" "Language: tr\n" @@ -20,7 +20,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -"X-Generator: Weblate 4.15\n" +"X-Generator: Weblate 5.4.2\n" #: .\cookbook\forms.py:45 msgid "" @@ -63,42 +63,48 @@ msgid "" "To prevent duplicates recipes with the same name as existing ones are " "ignored. Check this box to import everything." msgstr "" +"Varolan tariflerden benzer isimli olanlar mükerrerliği engellemek için " +"gözardı edilecektir. Tümünü içeri aktarmak için bu kutucuğu işaretleyin." #: .\cookbook\forms.py:143 msgid "Add your comment: " -msgstr "" +msgstr "Yorum ekleyin: " #: .\cookbook\forms.py:151 msgid "Leave empty for dropbox and enter app password for nextcloud." -msgstr "" +msgstr "Dropbox için boş bırakın ve Nextcloud için uygulama şifresini girin." #: .\cookbook\forms.py:154 msgid "Leave empty for nextcloud and enter api token for dropbox." -msgstr "" +msgstr "Nextcloud için boş bırakın ve Dropbox için API anahtarını girin." #: .\cookbook\forms.py:160 msgid "" "Leave empty for dropbox and enter only base url for nextcloud (/remote." "php/webdav/ is added automatically)" msgstr "" +"Dropbox için boş bırakın ve Nextcloud için yalnızca ana URL'yi " +"girin(/remote.php/webdav/ otomatik olarak eklenir)" #: .\cookbook\forms.py:188 msgid "" "Long Lived Access Token for your HomeAssistant instance" msgstr "" +"HomeAssistant uygulamanız için Uzun Süreli Erişim Anahtarı" #: .\cookbook\forms.py:193 msgid "Something like http://homeassistant.local:8123/api" -msgstr "" +msgstr "Örneğin http://homeassistant.local:8123/api" #: .\cookbook\forms.py:205 msgid "http://homeassistant.local:8123/api for example" -msgstr "" +msgstr "http://homeassistant.local:8123/api örneğin" #: .\cookbook\forms.py:222 .\cookbook\views\edit.py:117 msgid "Storage" -msgstr "" +msgstr "Depolama" #: .\cookbook\forms.py:222 msgid "Active" @@ -106,51 +112,60 @@ msgstr "Aktif" #: .\cookbook\forms.py:226 msgid "Search String" -msgstr "" +msgstr "Arama Sorgusu" #: .\cookbook\forms.py:246 msgid "File ID" -msgstr "" +msgstr "Dosya ID" #: .\cookbook\forms.py:262 msgid "Maximum number of users for this space reached." -msgstr "" +msgstr "Bu alan için maksimum kullanıcı sayısına ulaşıldı." #: .\cookbook\forms.py:268 msgid "Email address already taken!" -msgstr "" +msgstr "Email adresi zaten alınmış!" #: .\cookbook\forms.py:275 msgid "" "An email address is not required but if present the invite link will be sent " "to the user." msgstr "" +"Email adresi zorunlu değildir fakat verilmesi halinde davet linki " +"kullanıcıya gönderilecektir." #: .\cookbook\forms.py:287 msgid "Name already taken." -msgstr "" +msgstr "İsim zaten alınmış." #: .\cookbook\forms.py:298 msgid "Accept Terms and Privacy" -msgstr "" +msgstr "Koşulları ve Gizliliği Onayla" #: .\cookbook\forms.py:332 msgid "" "Determines how fuzzy a search is if it uses trigram similarity matching (e." "g. low values mean more typos are ignored)." msgstr "" +"Trigram benzerlik eşleşmesi kullanılması halinde aramanın ne kadar bulanık " +"olduğunu belirler (ör. düşük değerler daha fazla yazım hatasını gözardı " +"eder)." #: .\cookbook\forms.py:340 msgid "" "Select type method of search. Click here for " "full description of choices." msgstr "" +"Arama tipi metodunu seçin. Seçeneklerin tam açıklamasını görmek için buraya tıklayın." #: .\cookbook\forms.py:341 msgid "" "Use fuzzy matching on units, keywords and ingredients when editing and " "importing recipes." msgstr "" +"Tarifleri düzenlerken ve içeri aktarırken birimler, anahtar kelimeler ve " +"malzemelerde bulanık eşleştirme kullan." #: .\cookbook\forms.py:342 msgid "" diff --git a/docs/system/backup.md b/docs/system/backup.md index 39f4a302f..480266a82 100644 --- a/docs/system/backup.md +++ b/docs/system/backup.md @@ -63,6 +63,8 @@ Modify the below to match your environment and add it to your `docker-compose.ym ``` yaml pgbackup: container_name: pgbackup + env_file: + - ./.env environment: BACKUP_KEEP_DAYS: "8" BACKUP_KEEP_MONTHS: "6" diff --git a/requirements.txt b/requirements.txt index 9a852e811..c73569f1b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ Django==4.2.11 cryptography===42.0.5 django-annoying==0.10.6 django-cleanup==8.0.0 -django-crispy-forms==2.0 +django-crispy-forms==2.1 crispy-bootstrap4==2022.1 django-tables2==2.7.0 djangorestframework==3.14.0 @@ -10,12 +10,12 @@ drf-writable-nested==0.7.0 drf-spectacular==0.27.1 drf-spectacular-sidecar==2024.2.1 django-oauth-toolkit==2.3.0 -django-debug-toolbar==4.2.0 +django-debug-toolbar==4.3.0 bleach==6.0.0 gunicorn==21.2.0 lxml==5.1.0 Markdown==3.5.1 -Pillow==10.2.0 +Pillow==10.3.0 psycopg2-binary==2.9.9 python-dotenv==1.0.0 requests==2.31.0 @@ -25,14 +25,14 @@ whitenoise==6.6.0 icalendar==5.0.11 pyyaml==6.0.1 uritemplate==4.1.1 -beautifulsoup4==4.12.2 +beautifulsoup4==4.12.3 microdata==0.8.0 mock==5.1.0 Jinja2==3.1.3 django-webpack-loader==3.0.1 git+https://github.com/BITSOLVER/django-js-reverse@071e304fd600107bc64bbde6f2491f1fe049ec82 django-allauth==0.61.1 -recipe-scrapers==14.52.0 +recipe-scrapers==14.55.0 django-scopes==2.0.0 django-treebeard==4.7 django-cors-headers==4.3.1 @@ -45,13 +45,13 @@ django-auth-ldap==4.6.0 pyppeteer==2.0.0 validators==0.20.0 pytube==15.0.0 -homeassistant-api==4.1.1.post2 django-vite==3.0.3 +aiohttp==3.9.3 # Development pytest==8.0.0 pytest-django==4.8.0 -pytest-cov===4.1.0 +pytest-cov===5.0.0 pytest-factoryboy==2.6.0 pytest-html==4.1.1 pytest-asyncio==0.23.5 diff --git a/vue/src/components/ShoppingLineItem.vue b/vue/src/components/ShoppingLineItem.vue index b11f2ce72..f95c97410 100644 --- a/vue/src/components/ShoppingLineItem.vue +++ b/vue/src/components/ShoppingLineItem.vue @@ -54,7 +54,7 @@ text-field="name" value-field="id" v-model="food.supermarket_category" - @change="detail_modal_visible = false; updateFoodCategory(food)" + @input="detail_modal_visible = false; updateFoodCategory(food)" >