diff --git a/.github/workflows/docker-publish-beta.yml b/.github/workflows/docker-publish-beta.yml index 380edda2f..8641850f4 100644 --- a/.github/workflows/docker-publish-beta.yml +++ b/.github/workflows/docker-publish-beta.yml @@ -35,8 +35,8 @@ jobs: publish: true imageName: vabene1111/recipes tag: beta - dockerHubUser: ${{ secrets.DOCKER_USERNAME }} - dockerHubPassword: ${{ secrets.DOCKER_PASSWORD }} + dockerUser: ${{ secrets.DOCKER_USERNAME }} + dockerPassword: ${{ secrets.DOCKER_PASSWORD }} # Send discord notification - name: Discord notification env: diff --git a/.github/workflows/docker-publish-latest.yml b/.github/workflows/docker-publish-latest.yml index cf0de16ba..07ab18695 100644 --- a/.github/workflows/docker-publish-latest.yml +++ b/.github/workflows/docker-publish-latest.yml @@ -39,5 +39,5 @@ jobs: publish: true imageName: vabene1111/recipes tag: latest - dockerHubUser: ${{ secrets.DOCKER_USERNAME }} - dockerHubPassword: ${{ secrets.DOCKER_PASSWORD }} + dockerUser: ${{ secrets.DOCKER_USERNAME }} + dockerPassword: ${{ secrets.DOCKER_PASSWORD }} diff --git a/.github/workflows/docker-publish-release.yml b/.github/workflows/docker-publish-release.yml index 9cf625fa2..636ed5768 100644 --- a/.github/workflows/docker-publish-release.yml +++ b/.github/workflows/docker-publish-release.yml @@ -41,8 +41,8 @@ jobs: publish: true imageName: vabene1111/recipes tag: ${{ steps.get_version.outputs.VERSION }} - dockerHubUser: ${{ secrets.DOCKER_USERNAME }} - dockerHubPassword: ${{ secrets.DOCKER_PASSWORD }} + dockerUser: ${{ secrets.DOCKER_USERNAME }} + dockerPassword: ${{ secrets.DOCKER_PASSWORD }} # Send discord notification - name: Discord notification env: diff --git a/CONTRIBUTERS.md b/CONTRIBUTERS.md index bcb859e99..1a5c56ffd 100644 --- a/CONTRIBUTERS.md +++ b/CONTRIBUTERS.md @@ -6,11 +6,17 @@ Please have a look at the [list of pull requests](https://github.com/vabene1111/ a complete list of contributions. Below are some of the larger contributions made yet. - -- @tourn provided the serving feature and **several** other improvements! -- @l0c4lh057 provided a much improved ingredient text parser in [#277](https://github.com/vabene1111/recipes/pull/277) -- @sebimarkgraf added nutritional information [#199](https://github.com/vabene1111/recipes/pull/199) -- @cazier added reverse proxy authentication [#88](https://github.com/vabene1111/recipes/pull/88) +- [vabene1111] +- [Kaibu] +- [smilerz] +- [MaxJa4] Docker builds and other improvements +- [tourn] provided the serving feature and **several** other improvements! +- [l0c4lh057] provided a much improved ingredient text parser in [#277](https://github.com/vabene1111/recipes/pull/277) +- [sebimarkgraf] added nutritional information [#199](https://github.com/vabene1111/recipes/pull/199) +- [cazier] added reverse proxy authentication [#88](https://github.com/vabene1111/recipes/pull/88) +- [murphy83] added support for IPv6 #1490 +- [TheHaf] added custom serving size component #1411 +- [lostlont] added LDAP support #960 ## Translations @@ -30,6 +36,7 @@ Below are some of the larger contributions made yet. ### German [eTaurus](https://www.transifex.com/user/profile/eTaurus/) [l0c4lh057](https://www.transifex.com/user/profile/l0c4lh057/) +[hyperbit00] ### Hungarian [igazka](https://www.transifex.com/user/profile/igazka/) @@ -60,4 +67,4 @@ Below are some of the larger contributions made yet. ### Vietnamese -[vuongtrunghieu](https://www.transifex.com/user/profile/vuongtrunghieu/) \ No newline at end of file +[vuongtrunghieu](https://www.transifex.com/user/profile/vuongtrunghieu/) diff --git a/Dockerfile b/Dockerfile index 873a74ca9..6f2201369 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM python:3.10-alpine3.15 #Install all dependencies. -RUN apk add --no-cache postgresql-libs postgresql-client gettext zlib libjpeg libwebp libxml2-dev libxslt-dev py-cryptography +RUN apk add --no-cache postgresql-libs postgresql-client gettext zlib libjpeg libwebp libxml2-dev libxslt-dev py-cryptography openldap #Print all logs without buffering it. ENV PYTHONUNBUFFERED 1 @@ -15,11 +15,12 @@ WORKDIR /opt/recipes COPY requirements.txt ./ -RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev zlib-dev jpeg-dev libwebp-dev libressl-dev libffi-dev cargo openssl-dev openldap-dev && \ +RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev zlib-dev jpeg-dev libwebp-dev libressl-dev libffi-dev cargo openssl-dev openldap-dev python3-dev && \ echo -n "INPUT ( libldap.so )" > /usr/lib/libldap_r.so && \ python -m venv venv && \ /opt/recipes/venv/bin/python -m pip install --upgrade pip && \ - venv/bin/pip install wheel==0.36.2 && \ + venv/bin/pip install wheel==0.37.1 && \ + venv/bin/pip install setuptools_rust==1.1.2 && \ venv/bin/pip install -r requirements.txt --no-cache-dir &&\ apk --purge del .build-deps diff --git a/boot.sh b/boot.sh index 313fbd42d..985f5bd7b 100644 --- a/boot.sh +++ b/boot.sh @@ -21,18 +21,23 @@ if [ -z "${SECRET_KEY}" ]; then display_warning "The environment variable 'SECRET_KEY' is not set but REQUIRED for running Tandoor!" fi -# POSTGRES_PASSWORD must be set in .env file -if [ -z "${POSTGRES_PASSWORD}" ]; then - display_warning "The environment variable 'POSTGRES_PASSWORD' is not set but REQUIRED for running Tandoor!" -fi echo "Waiting for database to be ready..." attempt=0 max_attempts=20 -while pg_isready --host=${POSTGRES_HOST} -q; status=$?; attempt=$((attempt+1)); [ $status -ne 0 ] && [ $attempt -le $max_attempts ]; do - sleep 5 -done + +if [ "${DB_ENGINE}" != 'django.db.backends.sqlite3' ]; then + + # POSTGRES_PASSWORD must be set in .env file + if [ -z "${POSTGRES_PASSWORD}" ]; then + display_warning "The environment variable 'POSTGRES_PASSWORD' is not set but REQUIRED for running Tandoor!" + fi + + while pg_isready --host=${POSTGRES_HOST} --port=${POSTGRES_PORT} -q; status=$?; attempt=$((attempt+1)); [ $status -ne 0 ] && [ $attempt -le $max_attempts ]; do + sleep 5 + done +fi if [ $attempt -gt $max_attempts ]; then echo -e "\nDatabase not reachable. Maximum attempts exceeded." @@ -58,4 +63,4 @@ echo "Done" chmod -R 755 /opt/recipes/mediafiles -exec gunicorn -b :$TANDOOR_PORT --access-logfile - --error-logfile - --log-level INFO recipes.wsgi \ No newline at end of file +exec gunicorn -b :$TANDOOR_PORT --access-logfile - --error-logfile - --log-level INFO recipes.wsgi diff --git a/cookbook/forms.py b/cookbook/forms.py index 60237ac7e..caa129b51 100644 --- a/cookbook/forms.py +++ b/cookbook/forms.py @@ -179,6 +179,7 @@ class ImportForm(ImportExportBase): class ExportForm(ImportExportBase): recipes = forms.ModelMultipleChoiceField(widget=MultiSelectWidget, queryset=Recipe.objects.none(), required=False) all = forms.BooleanField(required=False) + custom_filter = forms.IntegerField(required=False) def __init__(self, *args, **kwargs): space = kwargs.pop('space') diff --git a/cookbook/helper/recipe_search.py b/cookbook/helper/recipe_search.py index 38518712d..49ffe062a 100644 --- a/cookbook/helper/recipe_search.py +++ b/cookbook/helper/recipe_search.py @@ -13,8 +13,8 @@ from cookbook.filters import RecipeFilter from cookbook.helper.HelperFunctions import Round, str2bool from cookbook.helper.permission_helper import has_group_permission from cookbook.managers import DICTIONARY -from cookbook.models import (CookLog, CustomFilter, Food, Keyword, Recipe, SearchFields, - SearchPreference, ViewLog, RecipeBook) +from cookbook.models import (CookLog, CustomFilter, Food, Keyword, Recipe, RecipeBook, SearchFields, + SearchPreference, ViewLog) from recipes import settings @@ -28,7 +28,7 @@ class RecipeSearch(): self._queryset = None if f := params.get('filter', None): custom_filter = CustomFilter.objects.filter(id=f, space=self._request.space).filter(Q(created_by=self._request.user) | - Q(shared=self._request.user) | Q(recipebook__shared=self._request.user)).first() + Q(shared=self._request.user) | Q(recipebook__shared=self._request.user)).first() if custom_filter: self._params = {**json.loads(custom_filter.search)} self._original_params = {**(params or {})} @@ -40,7 +40,7 @@ class RecipeSearch(): self._search_prefs = request.user.searchpreference else: self._search_prefs = SearchPreference() - self._string = params.get('query').strip() if params.get('query', None) else None + self._string = self._params.get('query').strip() if self._params.get('query', None) else None self._rating = self._params.get('rating', None) self._keywords = { 'or': self._params.get('keywords_or', None) or self._params.get('keywords', None), @@ -89,7 +89,10 @@ class RecipeSearch(): self._search_type = self._search_prefs.search or 'plain' if self._string: - self._unaccent_include = self._search_prefs.unaccent.values_list('field', flat=True) + if self._postgres: + self._unaccent_include = self._search_prefs.unaccent.values_list('field', flat=True) + else: + self._unaccent_include = [] self._icontains_include = [x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.icontains.values_list('field', flat=True)] self._istartswith_include = [x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.istartswith.values_list('field', flat=True)] self._trigram_include = None @@ -205,7 +208,7 @@ class RecipeSearch(): else: self._queryset = self._queryset.annotate(simularity=Coalesce(Subquery(simularity), 0.0)) if self._sort_includes('score') and self._fulltext_include and self._fuzzy_match is not None: - self._queryset = self._queryset.annotate(score=Sum(F('rank')+F('simularity'))) + self._queryset = self._queryset.annotate(score=F('rank')+F('simularity')) else: query_filter = Q() for f in [x + '__unaccent__iexact' if x in self._unaccent_include else x + '__iexact' for x in SearchFields.objects.all().values_list('field', flat=True)]: @@ -726,9 +729,8 @@ class RecipeFacet(): return self.get_facets() def _recipe_count_queryset(self, field, depth=1, steplen=4): - return Recipe.objects.filter(**{f'{field}__path__startswith': OuterRef('path')}, id__in=self._recipe_list, space=self._request.space - ).values(child=Substr(f'{field}__path', 1, steplen*depth) - ).annotate(count=Count('pk', distinct=True)).values('count') + return Recipe.objects.filter(**{f'{field}__path__startswith': OuterRef('path'), f'{field}__depth__gte': depth}, id__in=self._recipe_list, space=self._request.space + ).annotate(count=Coalesce(Func('pk', function='Count'), 0)).values('count') def _keyword_queryset(self, queryset, keyword=None): depth = getattr(keyword, 'depth', 0) + 1 diff --git a/cookbook/helper/recipe_url_import.py b/cookbook/helper/recipe_url_import.py index 307e5f603..10e29a42c 100644 --- a/cookbook/helper/recipe_url_import.py +++ b/cookbook/helper/recipe_url_import.py @@ -4,8 +4,10 @@ from html import unescape from unicodedata import decomposition from django.utils.dateparse import parse_duration +from django.utils.translation import gettext as _ from isodate import parse_duration as iso_parse_duration from isodate.isoerror import ISO8601Error +from recipe_scrapers._utils import get_minutes from cookbook.helper import recipe_url_import as helper from cookbook.helper.ingredient_parser import IngredientParser @@ -29,9 +31,14 @@ def get_from_scraper(scrape, request): recipe_json['name'] = '' try: - description = scrape.schema.data.get("description") or '' + description = scrape.description() or None except Exception: - description = '' + description = None + if not description: + try: + description = scrape.schema.data.get("description") or '' + except Exception: + description = '' recipe_json['description'] = parse_description(description)[:512] recipe_json['internal'] = True @@ -53,13 +60,19 @@ def get_from_scraper(scrape, request): recipe_json['servings'] = max(servings, 1) try: - recipe_json['working_time'] = get_minutes(scrape.schema.data.get("prepTime")) or 0 + recipe_json['working_time'] = get_minutes(scrape.prep_time()) or 0 except Exception: - recipe_json['working_time'] = 0 + try: + recipe_json['working_time'] = get_minutes(scrape.schema.data.get("prepTime")) or 0 + except Exception: + recipe_json['working_time'] = 0 try: - recipe_json['waiting_time'] = get_minutes(scrape.schema.data.get("cookTime")) or 0 + recipe_json['waiting_time'] = get_minutes(scrape.cook_time()) or 0 except Exception: - recipe_json['waiting_time'] = 0 + try: + recipe_json['waiting_time'] = get_minutes(scrape.schema.data.get("cookTime")) or 0 + except Exception: + recipe_json['waiting_time'] = 0 if recipe_json['working_time'] + recipe_json['waiting_time'] == 0: try: @@ -87,15 +100,23 @@ def get_from_scraper(scrape, request): except Exception: pass try: - if scrape.schema.data.get('recipeCategory'): - keywords += listify_keywords(scrape.schema.data.get("recipeCategory")) + if scrape.category(): + keywords += listify_keywords(scrape.category()) except Exception: - pass + try: + if scrape.schema.data.get('recipeCategory'): + keywords += listify_keywords(scrape.schema.data.get("recipeCategory")) + except Exception: + pass try: - if scrape.schema.data.get('recipeCuisine'): - keywords += listify_keywords(scrape.schema.data.get("recipeCuisine")) + if scrape.cuisine(): + keywords += listify_keywords(scrape.cuisine()) except Exception: - pass + try: + if scrape.schema.data.get('recipeCuisine'): + keywords += listify_keywords(scrape.schema.data.get("recipeCuisine")) + except Exception: + pass try: recipe_json['keywords'] = parse_keywords(list(set(map(str.casefold, keywords))), request.space) except AttributeError: @@ -142,8 +163,8 @@ def get_from_scraper(scrape, request): except Exception: pass - if scrape.url: - recipe_json['source_url'] = scrape.url + if scrape.canonical_url(): + recipe_json['source_url'] = scrape.canonical_url() return recipe_json @@ -307,56 +328,6 @@ def normalize_string(string): return unescaped_string -# TODO deprecate when merged into recipe_scapers - - -def get_minutes(time_text): - if time_text is None: - return 0 - TIME_REGEX = re.compile( - r"(\D*(?P\d*.?(\s\d)?\/?\d+)\s*(hours|hrs|hr|h|óra))?(\D*(?P\d+)\s*(minutes|mins|min|m|perc))?", - re.IGNORECASE, - ) - try: - return int(time_text) - except Exception: - pass - - if time_text.startswith("P") and "T" in time_text: - time_text = time_text.split("T", 2)[1] - if "-" in time_text: - time_text = time_text.split("-", 2)[ - 1 - ] # sometimes formats are like this: '12-15 minutes' - if " to " in time_text: - time_text = time_text.split("to", 2)[ - 1 - ] # sometimes formats are like this: '12 to 15 minutes' - - empty = '' - for x in time_text: - if 'fraction' in decomposition(x): - f = decomposition(x[-1:]).split() - empty += f" {f[1].replace('003', '')}/{f[3].replace('003', '')}" - else: - empty += x - time_text = empty - matched = TIME_REGEX.search(time_text) - - minutes = int(matched.groupdict().get("minutes") or 0) - - if "/" in (hours := matched.groupdict().get("hours") or ''): - number = hours.split(" ") - if len(number) == 2: - minutes += 60 * int(number[0]) - fraction = number[-1:][0].split("/") - minutes += 60 * float(int(fraction[0]) / int(fraction[1])) - else: - minutes += 60 * float(hours) - - return int(minutes) - - def iso_duration_to_minutes(string): match = re.match( r'P((?P\d+)Y)?((?P\d+)M)?((?P\d+)W)?((?P\d+)D)?T((?P\d+)H)?((?P\d+)M)?((?P\d+)S)?', diff --git a/cookbook/helper/shopping_helper.py b/cookbook/helper/shopping_helper.py index d8e8046e9..1783ca421 100644 --- a/cookbook/helper/shopping_helper.py +++ b/cookbook/helper/shopping_helper.py @@ -35,7 +35,7 @@ def shopping_helper(qs, request): qs = qs.filter(Q(checked=False) | Q(completed_at__gte=week_ago)) supermarket_order = ['checked'] + supermarket_order - return qs.order_by(*supermarket_order).select_related('unit', 'food', 'ingredient', 'created_by', 'list_recipe', 'list_recipe__mealplan', 'list_recipe__recipe') + return qs.distinct().order_by(*supermarket_order).select_related('unit', 'food', 'ingredient', 'created_by', 'list_recipe', 'list_recipe__mealplan', 'list_recipe__recipe') class RecipeShoppingEditor(): diff --git a/cookbook/integration/cheftap.py b/cookbook/integration/cheftap.py index f83203dcd..cf462d9a3 100644 --- a/cookbook/integration/cheftap.py +++ b/cookbook/integration/cheftap.py @@ -2,14 +2,14 @@ import re from cookbook.helper.ingredient_parser import IngredientParser from cookbook.integration.integration import Integration -from cookbook.models import Recipe, Step, Ingredient +from cookbook.models import Ingredient, Recipe, Step class ChefTap(Integration): def import_file_name_filter(self, zip_info_object): print("testing", zip_info_object.filename) - return re.match(r'^cheftap_export/([A-Za-z\d\w\s-])+.txt$', zip_info_object.filename) or re.match(r'^([A-Za-z\d\w\s-])+.txt$', zip_info_object.filename) + return re.match(r'^cheftap_export/([A-Za-z\d\s\-_()\[\]\u00C0-\u017F])+.txt$', zip_info_object.filename) or re.match(r'^([A-Za-z\d\s\-_()\[\]\u00C0-\u017F])+.txt$', zip_info_object.filename) def get_recipe_from_file(self, file): source_url = '' @@ -45,11 +45,11 @@ class ChefTap(Integration): ingredient_parser = IngredientParser(self.request, True) for ingredient in ingredients: if len(ingredient.strip()) > 0: - amount, unit, ingredient, note = ingredient_parser.parse(ingredient) - f = ingredient_parser.get_food(ingredient) + amount, unit, food, note = ingredient_parser.parse(ingredient) + f = ingredient_parser.get_food(food) u = ingredient_parser.get_unit(unit) step.ingredients.add(Ingredient.objects.create( - food=f, unit=u, amount=amount, note=note, space=self.request.space, + food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space, )) recipe.steps.add(step) diff --git a/cookbook/integration/chowdown.py b/cookbook/integration/chowdown.py index 8a16ae0ef..2f635436d 100644 --- a/cookbook/integration/chowdown.py +++ b/cookbook/integration/chowdown.py @@ -5,14 +5,14 @@ from zipfile import ZipFile from cookbook.helper.image_processing import get_filetype from cookbook.helper.ingredient_parser import IngredientParser from cookbook.integration.integration import Integration -from cookbook.models import Recipe, Step, Ingredient, Keyword +from cookbook.models import Ingredient, Keyword, Recipe, Step class Chowdown(Integration): def import_file_name_filter(self, zip_info_object): print("testing", zip_info_object.filename) - return re.match(r'^(_)*recipes/([A-Za-z\d\s-])+.md$', zip_info_object.filename) + return re.match(r'^(_)*recipes/([A-Za-z\d\s\-_()\[\]\u00C0-\u017F])+.md$', zip_info_object.filename) def get_recipe_from_file(self, file): ingredient_mode = False @@ -60,12 +60,13 @@ class Chowdown(Integration): ingredient_parser = IngredientParser(self.request, True) for ingredient in ingredients: - amount, unit, ingredient, note = ingredient_parser.parse(ingredient) - f = ingredient_parser.get_food(ingredient) - u = ingredient_parser.get_unit(unit) - step.ingredients.add(Ingredient.objects.create( - food=f, unit=u, amount=amount, note=note, space=self.request.space, - )) + if len(ingredient.strip()) > 0: + amount, unit, food, note = ingredient_parser.parse(ingredient) + f = ingredient_parser.get_food(food) + u = ingredient_parser.get_unit(unit) + step.ingredients.add(Ingredient.objects.create( + food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space, + )) recipe.steps.add(step) for f in self.files: diff --git a/cookbook/integration/cookbookapp.py b/cookbook/integration/cookbookapp.py index a89ff35f8..898887d30 100644 --- a/cookbook/integration/cookbookapp.py +++ b/cookbook/integration/cookbookapp.py @@ -2,6 +2,7 @@ import base64 import gzip import json import re +from gettext import gettext as _ from io import BytesIO import requests @@ -11,8 +12,7 @@ from cookbook.helper.ingredient_parser import IngredientParser from cookbook.helper.recipe_html_import import get_recipe_from_source from cookbook.helper.recipe_url_import import iso_duration_to_minutes from cookbook.integration.integration import Integration -from cookbook.models import Recipe, Step, Ingredient, Keyword -from gettext import gettext as _ +from cookbook.models import Ingredient, Keyword, Recipe, Step class CookBookApp(Integration): @@ -51,11 +51,11 @@ class CookBookApp(Integration): ingredient_parser = IngredientParser(self.request, True) for ingredient in recipe_json['recipeIngredient']: - f = ingredient_parser.get_food(ingredient['ingredient']['text']) - u = ingredient_parser.get_unit(ingredient['unit']['text']) - step.ingredients.add(Ingredient.objects.create( - food=f, unit=u, amount=ingredient['amount'], note=ingredient['note'], space=self.request.space, - )) + f = ingredient_parser.get_food(ingredient['ingredient']['text']) + u = ingredient_parser.get_unit(ingredient['unit']['text']) + step.ingredients.add(Ingredient.objects.create( + food=f, unit=u, amount=ingredient['amount'], note=ingredient['note'], space=self.request.space, + )) if len(images) > 0: try: diff --git a/cookbook/integration/copymethat.py b/cookbook/integration/copymethat.py index 4f4a217e5..c961a51a5 100644 --- a/cookbook/integration/copymethat.py +++ b/cookbook/integration/copymethat.py @@ -4,11 +4,12 @@ from zipfile import ZipFile from bs4 import BeautifulSoup +from django.utils.translation import gettext as _ from cookbook.helper.ingredient_parser import IngredientParser from cookbook.helper.recipe_html_import import get_recipe_from_source from cookbook.helper.recipe_url_import import iso_duration_to_minutes, parse_servings from cookbook.integration.integration import Integration -from cookbook.models import Recipe, Step, Ingredient, Keyword +from cookbook.models import Ingredient, Keyword, Recipe, Step from recipes.settings import DEBUG @@ -41,11 +42,11 @@ class CopyMeThat(Integration): for ingredient in file.find_all("li", {"class": "recipeIngredient"}): if ingredient.text == "": continue - amount, unit, ingredient, note = ingredient_parser.parse(ingredient.text.strip()) - f = ingredient_parser.get_food(ingredient) + amount, unit, food, note = ingredient_parser.parse(ingredient.text.strip()) + f = ingredient_parser.get_food(food) u = ingredient_parser.get_unit(unit) step.ingredients.add(Ingredient.objects.create( - food=f, unit=u, amount=amount, note=note, space=self.request.space, + food=f, unit=u, amount=amount, note=note, original_text=ingredient.text.strip(), space=self.request.space, )) for s in file.find_all("li", {"class": "instruction"}): @@ -60,7 +61,7 @@ class CopyMeThat(Integration): try: if file.find("a", {"id": "original_link"}).text != '': - step.instruction += "\n\nImported from: " + file.find("a", {"id": "original_link"}).text + step.instruction += "\n\n" + _("Imported from") + ": " + file.find("a", {"id": "original_link"}).text step.save() except AttributeError: pass diff --git a/cookbook/integration/domestica.py b/cookbook/integration/domestica.py index f580063d5..d1cc3bc19 100644 --- a/cookbook/integration/domestica.py +++ b/cookbook/integration/domestica.py @@ -4,7 +4,7 @@ from io import BytesIO from cookbook.helper.ingredient_parser import IngredientParser from cookbook.integration.integration import Integration -from cookbook.models import Recipe, Step, Ingredient +from cookbook.models import Ingredient, Recipe, Step class Domestica(Integration): @@ -37,11 +37,11 @@ class Domestica(Integration): ingredient_parser = IngredientParser(self.request, True) for ingredient in file['ingredients'].split('\n'): if len(ingredient.strip()) > 0: - amount, unit, ingredient, note = ingredient_parser.parse(ingredient) - f = ingredient_parser.get_food(ingredient) + amount, unit, food, note = ingredient_parser.parse(ingredient) + f = ingredient_parser.get_food(food) u = ingredient_parser.get_unit(unit) step.ingredients.add(Ingredient.objects.create( - food=f, unit=u, amount=amount, note=note, space=self.request.space, + food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space, )) recipe.steps.add(step) diff --git a/cookbook/integration/integration.py b/cookbook/integration/integration.py index 48899d366..d1d82c57c 100644 --- a/cookbook/integration/integration.py +++ b/cookbook/integration/integration.py @@ -172,7 +172,7 @@ class Integration: traceback.print_exc() self.handle_exception(e, log=il, message=f'-------------------- \nERROR \n{e}\n--------------------\n') import_zip.close() - elif '.json' in f['name'] or '.txt' in f['name'] or '.mmf' in f['name']: + elif '.json' in f['name'] or '.txt' in f['name'] or '.mmf' in f['name'] or '.rk' in f['name']: data_list = self.split_recipe_file(f['file']) il.total_recipes += len(data_list) for d in data_list: diff --git a/cookbook/integration/mealie.py b/cookbook/integration/mealie.py index 151175951..803cc6382 100644 --- a/cookbook/integration/mealie.py +++ b/cookbook/integration/mealie.py @@ -6,13 +6,13 @@ from zipfile import ZipFile from cookbook.helper.image_processing import get_filetype from cookbook.helper.ingredient_parser import IngredientParser from cookbook.integration.integration import Integration -from cookbook.models import Recipe, Step, Ingredient +from cookbook.models import Ingredient, Recipe, Step class Mealie(Integration): def import_file_name_filter(self, zip_info_object): - return re.match(r'^recipes/([A-Za-z\d-])+/([A-Za-z\d-])+.json$', zip_info_object.filename) + return re.match(r'^recipes/([A-Za-z\d\s\-_()\[\]\u00C0-\u017F])+/([A-Za-z\d\s\-_()\[\]\u00C0-\u017F])+.json$', zip_info_object.filename) def get_recipe_from_file(self, file): recipe_json = json.loads(file.getvalue().decode("utf-8")) @@ -45,12 +45,14 @@ class Mealie(Integration): u = ingredient_parser.get_unit(ingredient['unit']) amount = ingredient['quantity'] note = ingredient['note'] + original_text = None else: - amount, unit, ingredient, note = ingredient_parser.parse(ingredient['note']) - f = ingredient_parser.get_food(ingredient) + amount, unit, food, note = ingredient_parser.parse(ingredient['note']) + f = ingredient_parser.get_food(food) u = ingredient_parser.get_unit(unit) + original_text = ingredient['note'] step.ingredients.add(Ingredient.objects.create( - food=f, unit=u, amount=amount, note=note, space=self.request.space, + food=f, unit=u, amount=amount, note=note, original_text=original_text, space=self.request.space, )) except Exception: pass @@ -60,7 +62,8 @@ class Mealie(Integration): if '.zip' in f['name']: import_zip = ZipFile(f['file']) try: - self.import_recipe_image(recipe, BytesIO(import_zip.read(f'recipes/{recipe_json["slug"]}/images/min-original.webp')), filetype=get_filetype(f'recipes/{recipe_json["slug"]}/images/original')) + self.import_recipe_image(recipe, BytesIO(import_zip.read(f'recipes/{recipe_json["slug"]}/images/min-original.webp')), + filetype=get_filetype(f'recipes/{recipe_json["slug"]}/images/original')) except Exception: pass diff --git a/cookbook/integration/mealmaster.py b/cookbook/integration/mealmaster.py index f9a98cb3c..dcd70efa2 100644 --- a/cookbook/integration/mealmaster.py +++ b/cookbook/integration/mealmaster.py @@ -2,7 +2,7 @@ import re from cookbook.helper.ingredient_parser import IngredientParser from cookbook.integration.integration import Integration -from cookbook.models import Recipe, Step, Ingredient, Keyword +from cookbook.models import Ingredient, Keyword, Recipe, Step class MealMaster(Integration): @@ -45,11 +45,11 @@ class MealMaster(Integration): ingredient_parser = IngredientParser(self.request, True) for ingredient in ingredients: if len(ingredient.strip()) > 0: - amount, unit, ingredient, note = ingredient_parser.parse(ingredient) + amount, unit, food, note = ingredient_parser.parse(ingredient) f = ingredient_parser.get_food(ingredient) u = ingredient_parser.get_unit(unit) step.ingredients.add(Ingredient.objects.create( - food=f, unit=u, amount=amount, note=note, space=self.request.space, + food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space, )) recipe.steps.add(step) diff --git a/cookbook/integration/nextcloud_cookbook.py b/cookbook/integration/nextcloud_cookbook.py index 6b0d12882..c61a6d236 100644 --- a/cookbook/integration/nextcloud_cookbook.py +++ b/cookbook/integration/nextcloud_cookbook.py @@ -7,7 +7,7 @@ from cookbook.helper.image_processing import get_filetype from cookbook.helper.ingredient_parser import IngredientParser from cookbook.helper.recipe_url_import import iso_duration_to_minutes from cookbook.integration.integration import Integration -from cookbook.models import Recipe, Step, Ingredient, Keyword +from cookbook.models import Ingredient, Keyword, Recipe, Step class NextcloudCookbook(Integration): @@ -57,11 +57,11 @@ class NextcloudCookbook(Integration): ingredient_parser = IngredientParser(self.request, True) for ingredient in recipe_json['recipeIngredient']: - amount, unit, ingredient, note = ingredient_parser.parse(ingredient) - f = ingredient_parser.get_food(ingredient) + amount, unit, food, note = ingredient_parser.parse(ingredient) + f = ingredient_parser.get_food(food) u = ingredient_parser.get_unit(unit) step.ingredients.add(Ingredient.objects.create( - food=f, unit=u, amount=amount, note=note, space=self.request.space, + food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space, )) recipe.steps.add(step) diff --git a/cookbook/integration/openeats.py b/cookbook/integration/openeats.py index d948d90af..a8485f050 100644 --- a/cookbook/integration/openeats.py +++ b/cookbook/integration/openeats.py @@ -2,7 +2,7 @@ import json from cookbook.helper.ingredient_parser import IngredientParser from cookbook.integration.integration import Integration -from cookbook.models import Recipe, Step, Ingredient +from cookbook.models import Ingredient, Recipe, Step class OpenEats(Integration): diff --git a/cookbook/integration/paprika.py b/cookbook/integration/paprika.py index dcd5bfbea..7a1255000 100644 --- a/cookbook/integration/paprika.py +++ b/cookbook/integration/paprika.py @@ -2,12 +2,12 @@ import base64 import gzip import json import re +from gettext import gettext as _ from io import BytesIO from cookbook.helper.ingredient_parser import IngredientParser from cookbook.integration.integration import Integration -from cookbook.models import Recipe, Step, Ingredient, Keyword -from gettext import gettext as _ +from cookbook.models import Ingredient, Keyword, Recipe, Step class Paprika(Integration): @@ -70,11 +70,11 @@ class Paprika(Integration): try: for ingredient in recipe_json['ingredients'].split('\n'): if len(ingredient.strip()) > 0: - amount, unit, ingredient, note = ingredient_parser.parse(ingredient) - f = ingredient_parser.get_food(ingredient) + amount, unit, food, note = ingredient_parser.parse(ingredient) + f = ingredient_parser.get_food(food) u = ingredient_parser.get_unit(unit) step.ingredients.add(Ingredient.objects.create( - food=f, unit=u, amount=amount, note=note, space=self.request.space, + food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space, )) except AttributeError: pass diff --git a/cookbook/integration/pepperplate.py b/cookbook/integration/pepperplate.py index 4acc2d7b5..7cfafa84d 100644 --- a/cookbook/integration/pepperplate.py +++ b/cookbook/integration/pepperplate.py @@ -1,6 +1,6 @@ from cookbook.helper.ingredient_parser import IngredientParser from cookbook.integration.integration import Integration -from cookbook.models import Recipe, Step, Ingredient +from cookbook.models import Ingredient, Recipe, Step class Pepperplate(Integration): @@ -41,11 +41,11 @@ class Pepperplate(Integration): ingredient_parser = IngredientParser(self.request, True) for ingredient in ingredients: if len(ingredient.strip()) > 0: - amount, unit, ingredient, note = ingredient_parser.parse(ingredient) - f = ingredient_parser.get_food(ingredient) + amount, unit, food, note = ingredient_parser.parse(ingredient) + f = ingredient_parser.get_food(food) u = ingredient_parser.get_unit(unit) step.ingredients.add(Ingredient.objects.create( - food=f, unit=u, amount=amount, note=note, space=self.request.space, + food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space, )) recipe.steps.add(step) diff --git a/cookbook/integration/plantoeat.py b/cookbook/integration/plantoeat.py index 9644c12a1..d1004f70c 100644 --- a/cookbook/integration/plantoeat.py +++ b/cookbook/integration/plantoeat.py @@ -4,7 +4,7 @@ import requests from cookbook.helper.ingredient_parser import IngredientParser from cookbook.integration.integration import Integration -from cookbook.models import Recipe, Step, Ingredient, Keyword +from cookbook.models import Ingredient, Keyword, Recipe, Step class Plantoeat(Integration): @@ -56,11 +56,11 @@ class Plantoeat(Integration): ingredient_parser = IngredientParser(self.request, True) for ingredient in ingredients: if len(ingredient.strip()) > 0: - amount, unit, ingredient, note = ingredient_parser.parse(ingredient) - f = ingredient_parser.get_food(ingredient) + amount, unit, food, note = ingredient_parser.parse(ingredient) + f = ingredient_parser.get_food(food) u = ingredient_parser.get_unit(unit) step.ingredients.add(Ingredient.objects.create( - food=f, unit=u, amount=amount, note=note, space=self.request.space, + food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space, )) recipe.steps.add(step) diff --git a/cookbook/integration/recettetek.py b/cookbook/integration/recettetek.py index 7ae4115b8..7eb93a8e9 100644 --- a/cookbook/integration/recettetek.py +++ b/cookbook/integration/recettetek.py @@ -1,14 +1,16 @@ -import re +import imghdr import json -import requests +import re from io import BytesIO from zipfile import ZipFile -import imghdr +import requests + +from django.utils.translation import gettext as _ from cookbook.helper.image_processing import get_filetype from cookbook.helper.ingredient_parser import IngredientParser from cookbook.integration.integration import Integration -from cookbook.models import Recipe, Step, Ingredient, Keyword +from cookbook.models import Ingredient, Keyword, Recipe, Step class RecetteTek(Integration): @@ -48,7 +50,7 @@ class RecetteTek(Integration): # Append the original import url to the step (if it exists) try: if file['url'] != '': - step.instruction += '\n\nImported from: ' + file['url'] + step.instruction += '\n\n' + _('Imported from') + ': ' + file['url'] step.save() except Exception as e: print(recipe.name, ': failed to import source url ', str(e)) @@ -58,11 +60,11 @@ class RecetteTek(Integration): ingredient_parser = IngredientParser(self.request, True) for ingredient in file['ingredients'].split('\n'): if len(ingredient.strip()) > 0: - amount, unit, ingredient, note = ingredient_parser.parse(ingredient) + amount, unit, food, note = ingredient_parser.parse(food) f = ingredient_parser.get_food(ingredient) u = ingredient_parser.get_unit(unit) step.ingredients.add(Ingredient.objects.create( - food=f, unit=u, amount=amount, note=note, space=self.request.space, + food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space, )) except Exception as e: print(recipe.name, ': failed to parse recipe ingredients ', str(e)) diff --git a/cookbook/integration/recipekeeper.py b/cookbook/integration/recipekeeper.py index 0de2ff8e7..31cc326f9 100644 --- a/cookbook/integration/recipekeeper.py +++ b/cookbook/integration/recipekeeper.py @@ -1,12 +1,14 @@ import re -from bs4 import BeautifulSoup from io import BytesIO from zipfile import ZipFile +from bs4 import BeautifulSoup + +from django.utils.translation import gettext as _ from cookbook.helper.ingredient_parser import IngredientParser -from cookbook.helper.recipe_url_import import parse_servings, iso_duration_to_minutes +from cookbook.helper.recipe_url_import import iso_duration_to_minutes, parse_servings from cookbook.integration.integration import Integration -from cookbook.models import Recipe, Step, Ingredient, Keyword +from cookbook.models import Ingredient, Keyword, Recipe, Step class RecipeKeeper(Integration): @@ -45,11 +47,11 @@ class RecipeKeeper(Integration): for ingredient in file.find("div", {"itemprop": "recipeIngredients"}).findChildren("p"): if ingredient.text == "": continue - amount, unit, ingredient, note = ingredient_parser.parse(ingredient.text.strip()) - f = ingredient_parser.get_food(ingredient) + amount, unit, food, note = ingredient_parser.parse(ingredient.text.strip()) + f = ingredient_parser.get_food(food) u = ingredient_parser.get_unit(unit) step.ingredients.add(Ingredient.objects.create( - food=f, unit=u, amount=amount, note=note, space=self.request.space, + food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space, )) for s in file.find("div", {"itemprop": "recipeDirections"}).find_all("p"): @@ -58,7 +60,7 @@ class RecipeKeeper(Integration): step.instruction += s.text + ' \n' if file.find("span", {"itemprop": "recipeSource"}).text != '': - step.instruction += "\n\nImported from: " + file.find("span", {"itemprop": "recipeSource"}).text + step.instruction += "\n\n" + _("Imported from") + ": " + file.find("span", {"itemprop": "recipeSource"}).text step.save() recipe.steps.add(step) diff --git a/cookbook/integration/recipesage.py b/cookbook/integration/recipesage.py index 0bc6704be..d5292456f 100644 --- a/cookbook/integration/recipesage.py +++ b/cookbook/integration/recipesage.py @@ -5,7 +5,7 @@ import requests from cookbook.helper.ingredient_parser import IngredientParser from cookbook.integration.integration import Integration -from cookbook.models import Recipe, Step, Ingredient +from cookbook.models import Ingredient, Recipe, Step class RecipeSage(Integration): @@ -31,7 +31,7 @@ class RecipeSage(Integration): except Exception as e: print('failed to parse yield or time ', str(e)) - ingredient_parser = IngredientParser(self.request,True) + ingredient_parser = IngredientParser(self.request, True) ingredients_added = False for s in file['recipeInstructions']: step = Step.objects.create( @@ -41,11 +41,11 @@ class RecipeSage(Integration): ingredients_added = True for ingredient in file['recipeIngredient']: - amount, unit, ingredient, note = ingredient_parser.parse(ingredient) - f = ingredient_parser.get_food(ingredient) + amount, unit, food, note = ingredient_parser.parse(ingredient) + f = ingredient_parser.get_food(food) u = ingredient_parser.get_unit(unit) step.ingredients.add(Ingredient.objects.create( - food=f, unit=u, amount=amount, note=note, space=self.request.space, + food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space, )) recipe.steps.add(step) diff --git a/cookbook/integration/rezkonv.py b/cookbook/integration/rezkonv.py index f75db229d..37beb4bc4 100644 --- a/cookbook/integration/rezkonv.py +++ b/cookbook/integration/rezkonv.py @@ -1,6 +1,6 @@ from cookbook.helper.ingredient_parser import IngredientParser from cookbook.integration.integration import Integration -from cookbook.models import Recipe, Step, Ingredient, Keyword +from cookbook.models import Ingredient, Keyword, Recipe, Step class RezKonv(Integration): @@ -44,11 +44,11 @@ class RezKonv(Integration): ingredient_parser = IngredientParser(self.request, True) for ingredient in ingredients: if len(ingredient.strip()) > 0: - amount, unit, ingredient, note = ingredient_parser.parse(ingredient) - f = ingredient_parser.get_food(ingredient) + amount, unit, food, note = ingredient_parser.parse(ingredient) + f = ingredient_parser.get_food(food) u = ingredient_parser.get_unit(unit) step.ingredients.add(Ingredient.objects.create( - food=f, unit=u, amount=amount, note=note, space=self.request.space, + food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space, )) recipe.steps.add(step) @@ -60,9 +60,14 @@ class RezKonv(Integration): def split_recipe_file(self, file): recipe_list = [] current_recipe = '' - + encoding_list = ['windows-1250', 'latin-1'] #TODO build algorithm to try trough encodings and fail if none work, use for all importers + encoding = 'windows-1250' for fl in file.readlines(): - line = fl.decode("windows-1250") + try: + line = fl.decode(encoding) + except UnicodeDecodeError: + encoding = 'latin-1' + line = fl.decode(encoding) if line.startswith('=====') and 'rezkonv' in line.lower(): if current_recipe != '': recipe_list.append(current_recipe) diff --git a/cookbook/integration/saffron.py b/cookbook/integration/saffron.py index 058f2a8f7..0bfc4fbb3 100644 --- a/cookbook/integration/saffron.py +++ b/cookbook/integration/saffron.py @@ -2,7 +2,7 @@ from django.utils.translation import gettext as _ from cookbook.helper.ingredient_parser import IngredientParser from cookbook.integration.integration import Integration -from cookbook.models import Recipe, Step, Ingredient +from cookbook.models import Ingredient, Recipe, Step class Saffron(Integration): @@ -47,11 +47,11 @@ class Saffron(Integration): ingredient_parser = IngredientParser(self.request, True) for ingredient in ingredients: - amount, unit, ingredient, note = ingredient_parser.parse(ingredient) - f = ingredient_parser.get_food(ingredient) + amount, unit, food, note = ingredient_parser.parse(ingredient) + f = ingredient_parser.get_food(food) u = ingredient_parser.get_unit(unit) step.ingredients.add(Ingredient.objects.create( - food=f, unit=u, amount=amount, note=note, space=self.request.space, + food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space, )) recipe.steps.add(step) @@ -76,7 +76,7 @@ class Saffron(Integration): for i in s.ingredients.all(): recipeIngredient.append(f'{float(i.amount)} {i.unit} {i.food}') - + data += "Ingredients: \n" for ingredient in recipeIngredient: data += ingredient+"\n" @@ -91,10 +91,10 @@ class Saffron(Integration): files = [] for r in recipes: filename, data = self.get_file_from_recipe(r) - files.append([ filename, data ]) + files.append([filename, data]) el.exported_recipes += 1 el.msg += self.get_recipe_processed_msg(r) el.save() - - return files \ No newline at end of file + + return files diff --git a/cookbook/migrations/0172_ingredient_original_text.py b/cookbook/migrations/0172_ingredient_original_text.py new file mode 100644 index 000000000..1e10c55d7 --- /dev/null +++ b/cookbook/migrations/0172_ingredient_original_text.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.12 on 2022-02-25 15:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cookbook', '0171_alter_searchpreference_trigram_threshold'), + ] + + operations = [ + migrations.AddField( + model_name='ingredient', + name='original_text', + field=models.CharField(blank=True, default=None, max_length=512, null=True), + ), + ] diff --git a/cookbook/models.py b/cookbook/models.py index 5db27217b..2ed79cc51 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -62,9 +62,10 @@ class TreeManager(MP_NodeManager): # model.Manager get_or_create() is not compatible with MP_Tree def get_or_create(self, *args, **kwargs): kwargs['name'] = kwargs['name'].strip() - try: - return self.get(name__iexact=kwargs['name'], space=kwargs['space']), False - except self.model.DoesNotExist: + + if obj := self.filter(name__iexact=kwargs['name'], space=kwargs['space']).first(): + return obj, False + else: with scopes_disabled(): try: defaults = kwargs.pop('defaults', None) @@ -590,6 +591,7 @@ class Ingredient(ExportModelOperationsMixin('ingredient'), models.Model, Permiss is_header = models.BooleanField(default=False) no_amount = models.BooleanField(default=False) order = models.IntegerField(default=0) + original_text = models.CharField(max_length=512, null=True, blank=True, default=None) original_text = models.CharField(max_length=512, null=True, blank=True, default=None) diff --git a/cookbook/provider/nextcloud.py b/cookbook/provider/nextcloud.py index b1d319193..e7b100dfd 100644 --- a/cookbook/provider/nextcloud.py +++ b/cookbook/provider/nextcloud.py @@ -9,6 +9,8 @@ from cookbook.models import Recipe, RecipeImport, SyncLog from cookbook.provider.provider import Provider from requests.auth import HTTPBasicAuth +from recipes.settings import DEBUG + class Nextcloud(Provider): @@ -28,15 +30,18 @@ class Nextcloud(Provider): def import_all(monitor): client = Nextcloud.get_client(monitor.storage) + if DEBUG: + print(f'TANDOOR_PROVIDER_DEBUG checking path {monitor.path} with client {client}') + files = client.list(monitor.path) - try: - files.pop(0) # remove first element because its the folder itself - except IndexError: - pass # folder is empty, no recipes will be imported + if DEBUG: + print(f'TANDOOR_PROVIDER_DEBUG file list {files}') import_count = 0 for file in files: + if DEBUG: + print(f'TANDOOR_PROVIDER_DEBUG importing file {file}') path = monitor.path + '/' + file if not Recipe.objects.filter(file_path__iexact=path, space=monitor.space).exists() and not RecipeImport.objects.filter(file_path=path, space=monitor.space).exists(): name = os.path.splitext(file)[0] diff --git a/cookbook/serializer.py b/cookbook/serializer.py index 3c19b7be0..08c967461 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -337,7 +337,7 @@ class SupermarketCategorySerializer(UniqueFieldsMixin, WritableNestedModelSerial def create(self, validated_data): name = validated_data.pop('name').strip() space = validated_data.pop('space', self.context['request'].space) - obj, created = SupermarketCategory.objects.get_or_create(name__iexact=name, space=space, defaults=validated_data) + obj, created = SupermarketCategory.objects.get_or_create(name=name, space=space) return obj def update(self, instance, validated_data): @@ -421,9 +421,11 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR space = validated_data.pop('space', self.context['request'].space) # supermarket category needs to be handled manually as food.get or create does not create nested serializers unlike a super.create of serializer if 'supermarket_category' in validated_data and validated_data['supermarket_category']: + sm_category = validated_data['supermarket_category'] + sc_name = sm_category.pop('name', None) validated_data['supermarket_category'], sc_created = SupermarketCategory.objects.get_or_create( - name__iexact=validated_data.pop('supermarket_category')['name'], - space=self.context['request'].space) + name=sc_name, + space=space, defaults=sm_category) onhand = validated_data.pop('food_onhand', None) # assuming if on hand for user also onhand for shopping_share users @@ -479,6 +481,10 @@ class IngredientSerializer(WritableNestedModelSerializer): validated_data['space'] = self.context['request'].space return super().create(validated_data) + def update(self, instance, validated_data): + validated_data.pop('original_text', None) + return super().update(instance, validated_data) + class Meta: model = Ingredient fields = ( @@ -681,7 +687,7 @@ class RecipeBookEntrySerializer(serializers.ModelSerializer): def create(self, validated_data): book = validated_data['book'] recipe = validated_data['recipe'] - if not book.get_owner() == self.context['request'].user and not self.context['request'].user in book.get_shared(): + if not book.get_owner() == self.context['request'].user and not self.context['request'].user in book.get_shared(): raise NotFound(detail=None, code=None) obj, created = RecipeBookEntry.objects.get_or_create(book=book, recipe=recipe) return obj @@ -736,11 +742,11 @@ class ShoppingListRecipeSerializer(serializers.ModelSerializer): value = Decimal(value) value = value.quantize(Decimal(1)) if value == value.to_integral() else value.normalize() # strips trailing zero return ( - obj.name - or getattr(obj.mealplan, 'title', None) - or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)]) - or obj.recipe.name - ) + f' ({value:.2g})' + obj.name + or getattr(obj.mealplan, 'title', None) + or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)]) + or obj.recipe.name + ) + f' ({value:.2g})' def update(self, instance, validated_data): # TODO remove once old shopping list diff --git a/cookbook/templates/shoppinglist_template.html b/cookbook/templates/shoppinglist_template.html index 95e88237b..8085b544f 100644 --- a/cookbook/templates/shoppinglist_template.html +++ b/cookbook/templates/shoppinglist_template.html @@ -12,6 +12,7 @@ {% render_bundle 'shopping_list_view' %} {% endblock %} diff --git a/cookbook/templates/space.html b/cookbook/templates/space.html index 1447527af..7939b02b9 100644 --- a/cookbook/templates/space.html +++ b/cookbook/templates/space.html @@ -88,9 +88,8 @@

{% trans 'Members' %} {{ space_users|length }}/ {% if request.space.max_users > 0 %} {{ request.space.max_users }}{% else - %}∞{% endif %} + >{{ space_users|length }}/{% if request.space.max_users > 0 %} {{ request.space.max_users }}{% else %}∞{% endif %} + {% trans 'Invite User' %} diff --git a/cookbook/templates/url_import.html b/cookbook/templates/url_import.html index 8404527d8..6c82b27d9 100644 --- a/cookbook/templates/url_import.html +++ b/cookbook/templates/url_import.html @@ -30,7 +30,7 @@ style="height:50%" href="{% bookmarklet request %}" title="{% trans 'Drag me to your bookmarks to import recipes from anywhere' %}"> - {% trans 'Bookmark Me!' %} + {% trans 'Bookmark Me!' %}