diff --git a/.gitignore b/.gitignore index c4d38e4181..ddc9e96930 100644 --- a/.gitignore +++ b/.gitignore @@ -84,3 +84,5 @@ vue3/.vite # Configs vetur.config.js venv/ +.idea/easy-i18n.xml +cookbook/static/vue3 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 4b52f246da..d2b92c1a5e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,6 +35,12 @@ RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev zlib-de #Copy project and execute it. COPY . ./ +HEALTHCHECK --interval=30s \ + --timeout=5s \ + --start-period=10s \ + --retries=3 \ + CMD [ "/usr/bin/wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:8080/openapi" ] + # collect information from git repositories RUN /opt/recipes/venv/bin/python version.py # delete git repositories to reduce image size diff --git a/cookbook/forms.py b/cookbook/forms.py index bda48e3dd5..071431a4f0 100644 --- a/cookbook/forms.py +++ b/cookbook/forms.py @@ -1,6 +1,8 @@ from datetime import datetime + from allauth.account.forms import ResetPasswordForm, SignupForm +from allauth.socialaccount.forms import SignupForm as SocialSignupForm from django import forms from django.conf import settings from django.core.exceptions import ValidationError @@ -14,15 +16,13 @@ class SelectWidget(widgets.Select): - class Media: - js = ('custom/js/form_select.js', ) + js = ('custom/js/form_select.js',) class MultiSelectWidget(widgets.SelectMultiple): - class Media: - js = ('custom/js/form_multiselect.js', ) + js = ('custom/js/form_multiselect.js',) # Yes there are some stupid browsers that still dont support this but @@ -139,7 +139,7 @@ class CommentForm(forms.ModelForm): class Meta: model = Comment - fields = ('text', ) + fields = ('text',) labels = {'text': _('Add your comment: '), } widgets = {'text': forms.Textarea(attrs={'rows': 2, 'cols': 15}), } @@ -161,7 +161,6 @@ class Meta: help_texts = {'url': _('Leave empty for dropbox and enter only base url for nextcloud (/remote.php/webdav/ is added automatically)'), } - class ConnectorConfigForm(forms.ModelForm): enabled = forms.BooleanField( help_text="Is the connector enabled", @@ -315,6 +314,18 @@ def signup(self, request, user): pass +class AllAuthSocialSignupForm(SocialSignupForm): + terms = forms.BooleanField(label=_('Accept Terms and Privacy')) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + if settings.PRIVACY_URL == '' and settings.TERMS_URL == '': + self.fields.pop('terms') + + def signup(self, request, user): + pass + + class CustomPasswordResetForm(ResetPasswordForm): captcha = hCaptchaField() @@ -345,12 +356,13 @@ class Meta: help_texts = { 'search': _('Select type method of search. Click here for full description of choices.'), 'lookup': - _('Use fuzzy matching on units, keywords and ingredients when editing and importing recipes.'), 'unaccent': - _('Fields to search ignoring accents. Selecting this option can improve or degrade search quality depending on language'), 'icontains': - _("Fields to search for partial matches. (e.g. searching for 'Pie' will return 'pie' and 'piece' and 'soapie')"), 'istartswith': - _("Fields to search for beginning of word matches. (e.g. searching for 'sa' will return 'salad' and 'sandwich')"), 'trigram': - _("Fields to 'fuzzy' search. (e.g. searching for 'recpie' will find 'recipe'.) Note: this option will conflict with 'web' and 'raw' methods of search."), 'fulltext': - _("Fields to full text search. Note: 'web', 'phrase', and 'raw' search methods only function with fulltext fields."), + _('Use fuzzy matching on units, keywords and ingredients when editing and importing recipes.'), 'unaccent': + _('Fields to search ignoring accents. Selecting this option can improve or degrade search quality depending on language'), 'icontains': + _("Fields to search for partial matches. (e.g. searching for 'Pie' will return 'pie' and 'piece' and 'soapie')"), 'istartswith': + _("Fields to search for beginning of word matches. (e.g. searching for 'sa' will return 'salad' and 'sandwich')"), 'trigram': + _("Fields to 'fuzzy' search. (e.g. searching for 'recpie' will find 'recipe'.) Note: this option will conflict with 'web' and 'raw' methods of search."), + 'fulltext': + _("Fields to full text search. Note: 'web', 'phrase', and 'raw' search methods only function with fulltext fields."), } labels = { @@ -360,5 +372,5 @@ class Meta: widgets = { 'search': SelectWidget, 'unaccent': MultiSelectWidget, 'icontains': MultiSelectWidget, 'istartswith': MultiSelectWidget, 'trigram': MultiSelectWidget, 'fulltext': - MultiSelectWidget, + MultiSelectWidget, } diff --git a/cookbook/locale/it/LC_MESSAGES/django.po b/cookbook/locale/it/LC_MESSAGES/django.po index cc06bd4ae6..e66531254f 100644 --- a/cookbook/locale/it/LC_MESSAGES/django.po +++ b/cookbook/locale/it/LC_MESSAGES/django.po @@ -12,7 +12,7 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2024-08-01 15:04+0200\n" -"PO-Revision-Date: 2024-11-01 06:58+0000\n" +"PO-Revision-Date: 2024-12-09 00:58+0000\n" "Last-Translator: Vincenzo Reale \n" "Language-Team: Italian \n" @@ -21,7 +21,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 5.6.2\n" +"X-Generator: Weblate 5.8.4\n" #: .\cookbook\forms.py:45 msgid "" @@ -520,7 +520,7 @@ msgstr "Web" #: .\cookbook\models.py:1411 .\cookbook\templates\search_info.html:47 msgid "Raw" -msgstr "Raw" +msgstr "Crudo" #: .\cookbook\models.py:1467 msgid "Food Alias" @@ -1440,8 +1440,9 @@ msgid "" "\"noreferrer noopener\" target=\"_blank\">this one." msgstr "" "Le tabelle in markdown sono difficili da creare a mano. Si raccomanda " -"l'utilizzo di un editor di come questo." +"l'utilizzo di un editor di tabelle come questo." #: .\cookbook\templates\markdown_info.html:155 #: .\cookbook\templates\markdown_info.html:157 @@ -2203,8 +2204,8 @@ msgstr "" " Le migrazioni non andate a buon fine probabilmente causeranno il " "malfunzionamento di parti importanti dell'applicazione.\n" " Se una migrazione non riesce, assicurati di avere la versione " -"più recente e, in tal caso, pubblica il registro della migrazione e la " -"panoramica di seguito in una segnalazione di problema su GitHub.\n" +"più recente e, in tal caso, pubblica il registro della migrazione e il " +"riepilogo che segue in una segnalazione di problema su GitHub.\n" " " #: .\cookbook\templates\system.html:182 @@ -2765,7 +2766,7 @@ msgid "" "but not recommended as some features only work with postgres databases." msgstr "" "Questa applicazione non è in esecuzione con un database Postgres. Va bene, " -"ma non è consigliato perché alcune funzionalità sono disponibili solo con un " +"ma non è consigliato perché alcune funzionalità sono disponibili solo con " "database Postgres." #: .\cookbook\views\views.py:360 diff --git a/cookbook/templates/account/signup.html b/cookbook/templates/account/signup.html index baceb2cb15..fc29ca2b2f 100644 --- a/cookbook/templates/account/signup.html +++ b/cookbook/templates/account/signup.html @@ -1,6 +1,5 @@ {% extends "base.html" %} {% load crispy_forms_filters %} -{% load crispy_forms_filters %} {% load i18n %} {% block title %}{% trans 'Register' %}{% endblock %} diff --git a/cookbook/templates/socialaccount/signup.html b/cookbook/templates/socialaccount/signup.html index 6779752fa2..79608c1ffd 100644 --- a/cookbook/templates/socialaccount/signup.html +++ b/cookbook/templates/socialaccount/signup.html @@ -16,7 +16,6 @@

{% trans "Sign Up" %}

{% if redirect_field_value %} {% endif %} -
{{ form.username |as_crispy_field }}
@@ -30,7 +29,7 @@

{% trans "Sign Up" %}

{{ form.terms |as_crispy_field }} - {% trans 'I accept the follwoing' %} + {% trans 'I accept the following' %} {% if TERMS_URL != '' %} {% trans 'Terms and Conditions' %} diff --git a/cookbook/templates/system.html b/cookbook/templates/system.html index e3b7d20243..8f7d83910d 100644 --- a/cookbook/templates/system.html +++ b/cookbook/templates/system.html @@ -12,9 +12,9 @@

{% trans 'System' %}

{% blocktrans %} - Django Recipes is an open source free software application. It can be found on - GitHub. - Changelogs can be found here. + Tandoor Recipes is an open source free software application. It can be found on + GitHub. + Changelogs can be found here. {% endblocktrans %}

{% trans 'System Information' %}

diff --git a/cookbook/views/api.py b/cookbook/views/api.py index 003e7b9bd2..ab6ef27e37 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -1455,13 +1455,21 @@ def post(self, request, *args, **kwargs): url = serializer.validated_data.get('url', None) data = unquote(serializer.validated_data.get('data', None)) + + duplicate = False + if url: + # Check for existing recipes with provided url + existing_recipe = Recipe.objects.filter(source_url=url).first() + if existing_recipe: + duplicate = True + if not url and not data: return Response({'error': True, 'msg': _('Nothing to do.')}, status=status.HTTP_400_BAD_REQUEST) elif url and not data: if re.match('^(https?://)?(www\\.youtube\\.com|youtu\\.be)/.+$', url): if validate_import_url(url): - return Response({'recipe_json': get_from_youtube_scraper(url, request), 'recipe_images': [], }, status=status.HTTP_200_OK) + return Response({'recipe_json': get_from_youtube_scraper(url, request), 'recipe_images': [], 'duplicate': duplicate}, status=status.HTTP_200_OK) if re.match('^(.)*/view/recipe/[0-9]+/[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$', url): recipe_json = requests.get( url.replace('/view/recipe/', '/api/recipe/').replace(re.split('/view/recipe/[0-9]+', url)[1], '') + '?share=' @@ -1476,7 +1484,7 @@ def post(self, request, *args, **kwargs): filetype=pathlib.Path(recipe_json['image']).suffix), name=f'{uuid.uuid4()}_{recipe.pk}{pathlib.Path(recipe_json["image"]).suffix}') recipe.save() - return Response({'link': request.build_absolute_uri(reverse('view_recipe', args={recipe.pk}))}, status=status.HTTP_201_CREATED) + return Response({'link': request.build_absolute_uri(reverse('view_recipe', args={recipe.pk})), 'duplicate': duplicate}, status=status.HTTP_201_CREATED) else: try: if validate_import_url(url): @@ -1511,6 +1519,7 @@ def post(self, request, *args, **kwargs): return Response({ 'recipe_json': helper.get_from_scraper(scrape, request), 'recipe_images': list(dict.fromkeys(get_images_from_soup(scrape.soup, url))), + 'duplicate': duplicate }, status=status.HTTP_200_OK) diff --git a/docs/install/manual.md b/docs/install/manual.md index c6ec34abab..c5cd7412bb 100644 --- a/docs/install/manual.md +++ b/docs/install/manual.md @@ -3,7 +3,7 @@ These instructions are inspired from a standard django/gunicorn/postgresql instructions ([for example](https://www.digitalocean.com/community/tutorials/how-to-set-up-django-with-postgres-nginx-and-gunicorn-on-ubuntu-16-04)) !!! warning - Make sure to use Python 3.10 or higher, and ensure that `pip` is associated with Python 3. Depending on your system configuration, using `python` or `pip` might default to Python 2. Make sure your machine has at least 2048 MB of memory; otherwise, the `yarn build` process may fail with the error: `FATAL ERROR: Reached heap limit - Allocation failed: JavaScript heap out of memory`. + Make sure to use at least Python 3.10 (although 3.12 is preferred) or higher, and ensure that `pip` is associated with Python 3. Depending on your system configuration, using `python` or `pip` might default to Python 2. Make sure your machine has at least 2048 MB of memory; otherwise, the `yarn build` process may fail with the error: `FATAL ERROR: Reached heap limit - Allocation failed: JavaScript heap out of memory`. ## Prerequisites diff --git a/recipes/settings.py b/recipes/settings.py index 061add493f..1d2a72f024 100644 --- a/recipes/settings.py +++ b/recipes/settings.py @@ -66,7 +66,6 @@ }, } - # allow djangos wsgi server to server mediafiles GUNICORN_MEDIA = bool(int(os.getenv('GUNICORN_MEDIA', False))) @@ -247,14 +246,14 @@ ] if DEBUG_TOOLBAR: - MIDDLEWARE += ('debug_toolbar.middleware.DebugToolbarMiddleware', ) - INSTALLED_APPS += ('debug_toolbar', ) + MIDDLEWARE += ('debug_toolbar.middleware.DebugToolbarMiddleware',) + INSTALLED_APPS += ('debug_toolbar',) SORT_TREE_BY_NAME = bool(int(os.getenv('SORT_TREE_BY_NAME', False))) DISABLE_TREE_FIX_STARTUP = bool(int(os.getenv('DISABLE_TREE_FIX_STARTUP', False))) if bool(int(os.getenv('SQL_DEBUG', False))): - MIDDLEWARE += ('recipes.middleware.SqlPrintingMiddleware', ) + MIDDLEWARE += ('recipes.middleware.SqlPrintingMiddleware',) if ENABLE_METRICS: MIDDLEWARE += 'django_prometheus.middleware.PrometheusAfterMiddleware', @@ -294,7 +293,6 @@ "handlers": ["console"] } - AUTHENTICATION_BACKENDS += [ 'django.contrib.auth.backends.ModelBackend', 'allauth.account.auth_backends.AuthenticationBackend', @@ -564,6 +562,9 @@ def setup_database(db_url=None, db_options=None, db_engine=None, pg_host=None, p # ACCOUNT_SIGNUP_FORM_CLASS = 'cookbook.forms.AllAuthSignupForm' ACCOUNT_FORMS = {'signup': 'cookbook.forms.AllAuthSignupForm', 'reset_password': 'cookbook.forms.CustomPasswordResetForm'} +SOCIALACCOUNT_FORMS = { + 'signup': 'cookbook.forms.AllAuthSocialSignupForm', +} ACCOUNT_EMAIL_UNKNOWN_ACCOUNTS = False ACCOUNT_RATE_LIMITS = { diff --git a/requirements.txt b/requirements.txt index 5273135cb3..39c18c6187 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ -Django==4.2.16 +Django==4.2.17 cryptography===43.0.1 django-annoying==0.10.6 django-cleanup==8.0.0 django-crispy-forms==2.3 -crispy-bootstrap4==2024.1 +crispy-bootstrap4==2024.10 django-tables2==2.7.0 djangorestframework==3.15.2 drf-writable-nested==0.7.0 @@ -20,13 +20,13 @@ requests==2.32.3 six==1.16.0 webdavclient3==3.14.6 whitenoise==6.7.0 -icalendar==5.0.11 +icalendar==6.1.0 pyyaml==6.0.2 uritemplate==4.1.1 beautifulsoup4==4.12.3 microdata==0.8.0 mock==5.1.0 -Jinja2==3.1.4 +Jinja2==3.1.5 django-webpack-loader==3.0.1 git+https://github.com/BITSOLVER/django-js-reverse@071e304fd600107bc64bbde6f2491f1fe049ec82 django-allauth==0.61.1 @@ -48,9 +48,9 @@ redis==5.2.0 # Development pytest==8.0.0 -pytest-django==4.8.0 +pytest-django==4.9.0 pytest-cov===5.0.0 -pytest-factoryboy==2.6.0 +pytest-factoryboy==2.7.0 pytest-html==4.1.1 pytest-asyncio==0.23.5 pytest-xdist==3.6.1 diff --git a/vue/src/apps/ImportView/ImportView.vue b/vue/src/apps/ImportView/ImportView.vue index b1743944ce..fbd5a0402b 100644 --- a/vue/src/apps/ImportView/ImportView.vue +++ b/vue/src/apps/ImportView/ImportView.vue @@ -83,6 +83,11 @@ + + + {{ duplicateWarning }} + + @@ -463,6 +468,7 @@ export default { }, // URL import LS_IMPORT_RECENT: 'import_recent_urls', //TODO use central helper to manage all local storage keys (and maybe even access) + duplicateWarning: '', website_url: '', website_url_list: '', import_multiple: false, @@ -643,6 +649,12 @@ export default { return } + if ('duplicate' in response.data && response.data['duplicate']) { + this.duplicateWarning = "A recipe with this URL already exists."; + } else { + this.duplicateWarning = ""; + } + this.loading = false this.recipe_json = response.data['recipe_json']; @@ -763,6 +775,16 @@ export default {