diff --git a/cookbook/helper/image_processing.py b/cookbook/helper/image_processing.py index 06d022d70..63ece3d27 100644 --- a/cookbook/helper/image_processing.py +++ b/cookbook/helper/image_processing.py @@ -35,6 +35,20 @@ def get_filetype(name): return '.jpeg' +def is_file_type_allowed(filename, image_only=False): + is_file_allowed = False + allowed_file_types = ['.pdf','.docx', '.xlsx'] + allowed_image_types = ['.png', '.jpg', '.jpeg', '.gif'] + check_list = allowed_image_types + if not image_only: + check_list += allowed_file_types + + for file_type in check_list: + if filename.endswith(file_type): + is_file_allowed = True + + return is_file_allowed + # TODO this whole file needs proper documentation, refactoring, and testing # TODO also add env variable to define which images sizes should be compressed # filetype argument can not be optional, otherwise this function will treat all images as if they were a jpeg diff --git a/cookbook/locale/uk/LC_MESSAGES/django.po b/cookbook/locale/uk/LC_MESSAGES/django.po index f65e1734d..968dd464f 100644 --- a/cookbook/locale/uk/LC_MESSAGES/django.po +++ b/cookbook/locale/uk/LC_MESSAGES/django.po @@ -8,8 +8,8 @@ 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-22 07:58+0000\n" -"Last-Translator: Oleh Hudyma \n" +"PO-Revision-Date: 2025-01-16 18:58+0000\n" +"Last-Translator: Anton Shevtsov \n" "Language-Team: Ukrainian \n" "Language: uk\n" @@ -18,7 +18,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && " "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" -"X-Generator: Weblate 5.6.2\n" +"X-Generator: Weblate 5.8.4\n" #: .\cookbook\forms.py:45 msgid "" @@ -32,7 +32,7 @@ msgstr "" #: .\cookbook\forms.py:62 .\cookbook\forms.py:246 .\cookbook\views\lists.py:103 msgid "Keywords" -msgstr "" +msgstr "Ключові слова" #: .\cookbook\forms.py:62 msgid "Preparation time in minutes" @@ -941,13 +941,13 @@ msgstr "" #: .\cookbook\templates\ingredient_editor.html:7 #: .\cookbook\templates\ingredient_editor.html:13 msgid "Ingredient Editor" -msgstr "" +msgstr "Редактор Інгредієнтів" #: .\cookbook\templates\base.html:275 #: .\cookbook\templates\export_response.html:7 #: .\cookbook\templates\test2.html:14 .\cookbook\templates\test2.html:20 msgid "Export" -msgstr "" +msgstr "Експорт" #: .\cookbook\templates\base.html:287 msgid "Properties" diff --git a/cookbook/provider/local.py b/cookbook/provider/local.py index 9f3d21005..54ce50ab8 100644 --- a/cookbook/provider/local.py +++ b/cookbook/provider/local.py @@ -12,21 +12,25 @@ class Local(Provider): @staticmethod def import_all(monitor): + if '/etc/' in monitor.path or '/root/' in monitor.path or '/mediafiles/' in monitor.path or '/usr/' in monitor.path: + return False + files = [f for f in listdir(monitor.path) if isfile(join(monitor.path, f))] import_count = 0 for file in files: - 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] - new_recipe = RecipeImport( - name=name, - file_path=path, - storage=monitor.storage, - space=monitor.space, - ) - new_recipe.save() - import_count += 1 + if file.endswith('.pdf') or file.endswith('.png') or file.endswith('.jpg') or file.endswith('.jpeg') or file.endswith('.gif'): + 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] + new_recipe = RecipeImport( + name=name, + file_path=path, + storage=monitor.storage, + space=monitor.space, + ) + new_recipe.save() + import_count += 1 log_entry = SyncLog( status='SUCCESS', diff --git a/cookbook/serializer.py b/cookbook/serializer.py index b348ae473..4cf2d8ce7 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -24,6 +24,7 @@ from rest_framework.fields import IntegerField from cookbook.helper.CustomStorageClass import CachedS3Boto3Storage from cookbook.helper.HelperFunctions import str2bool +from cookbook.helper.image_processing import is_file_type_allowed from cookbook.helper.permission_helper import above_space_limit from cookbook.helper.property_helper import FoodPropertyHelper from cookbook.helper.shopping_helper import RecipeShoppingEditor @@ -267,12 +268,17 @@ class UserFileSerializer(serializers.ModelSerializer): raise ValidationError(_('You have reached your file upload limit.')) def create(self, validated_data): + if not is_file_type_allowed(validated_data['file'].name): + return None + self.check_file_limit(validated_data) validated_data['created_by'] = self.context['request'].user validated_data['space'] = self.context['request'].space return super().create(validated_data) def update(self, instance, validated_data): + if not is_file_type_allowed(validated_data['file'].name): + return None self.check_file_limit(validated_data) return super().update(instance, validated_data) @@ -1032,6 +1038,16 @@ class RecipeImageSerializer(WritableNestedModelSerializer): image = serializers.ImageField(required=False, allow_null=True) image_url = serializers.CharField(max_length=4096, required=False, allow_null=True) + def create(self, validated_data): + if not is_file_type_allowed(validated_data['image'].name, image_only=True): + return None + return super().create( validated_data) + + def update(self, instance, validated_data): + if not is_file_type_allowed(validated_data['image'].name, image_only=True): + return None + return super().update(instance, validated_data) + class Meta: model = Recipe fields = ['image', 'image_url', ] diff --git a/cookbook/tests/edits/test_edits_storage.py b/cookbook/tests/edits/test_edits_storage.py index 9c4e08e1c..a8564ef9c 100644 --- a/cookbook/tests/edits/test_edits_storage.py +++ b/cookbook/tests/edits/test_edits_storage.py @@ -31,12 +31,12 @@ def test_edit_storage(storage_obj, a1_s1, a1_s2): } ) storage_obj.refresh_from_db() - assert r.status_code == 200 - r_messages = [m for m in get_messages(r.wsgi_request)] - assert not any(m.level > messages.SUCCESS for m in r_messages) + assert r.status_code == 302 + #r_messages = [m for m in get_messages(r.wsgi_request)] + #assert not any(m.level > messages.SUCCESS for m in r_messages) - assert storage_obj.password == '1234_pw' - assert storage_obj.token == '1234_token' + #assert storage_obj.password == '1234_pw' + #assert storage_obj.token == '1234_token' r = a1_s2.post( reverse('edit_storage', args={storage_obj.pk}), @@ -54,7 +54,7 @@ def test_edit_storage(storage_obj, a1_s1, a1_s2): ['a_u', 302], ['g1_s1', 302], ['u1_s1', 302], - ['a1_s1', 200], + ['a1_s1', 302], ['g1_s2', 302], ['u1_s2', 302], ['a1_s2', 404], diff --git a/cookbook/views/edit.py b/cookbook/views/edit.py index bcd874d83..aee28e34f 100644 --- a/cookbook/views/edit.py +++ b/cookbook/views/edit.py @@ -80,7 +80,7 @@ class SyncUpdate(GroupRequiredMixin, UpdateView, SpaceFormMixing): def edit_storage(request, pk): instance: Storage = get_object_or_404(Storage, pk=pk, space=request.space) - if not (instance.created_by == request.user or request.user.is_superuser): + if not request.user.is_superuser: messages.add_message(request, messages.ERROR, _('You cannot edit this storage!')) return HttpResponseRedirect(reverse('list_storage')) diff --git a/cookbook/views/new.py b/cookbook/views/new.py index 8a6f7bab4..44e18997a 100644 --- a/cookbook/views/new.py +++ b/cookbook/views/new.py @@ -58,10 +58,16 @@ class StorageCreate(GroupRequiredMixin, CreateView): obj = form.save(commit=False) obj.created_by = self.request.user obj.space = self.request.space - obj.save() + if self.request.space.demo or settings.HOSTED: messages.add_message(self.request, messages.ERROR, _('This feature is not yet available in the hosted version of tandoor!')) return redirect('index') + + if not self.request.user.is_superuser: + messages.add_message(self.request, messages.ERROR, _('This feature is only available for the instance administrator (superuser)')) + return redirect('index') + + obj.save() return HttpResponseRedirect(reverse('edit_storage', kwargs={'pk': obj.pk})) def get_context_data(self, **kwargs): diff --git a/nginx/conf.d/Recipes.conf b/nginx/conf.d/Recipes.conf index 9cda0f745..e3b4456da 100644 --- a/nginx/conf.d/Recipes.conf +++ b/nginx/conf.d/Recipes.conf @@ -8,6 +8,7 @@ server { # serve media files location /media/ { alias /media/; + add_header Content-Disposition 'attachment; filename="$args"'; } # pass requests for dynamic content to gunicorn location / { diff --git a/requirements.txt b/requirements.txt index def75155d..382cd264b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Django==4.2.17 +Django==4.2.18 cryptography===44.0.0 django-annoying==0.10.6 django-cleanup==8.0.0 @@ -32,7 +32,7 @@ Jinja2==3.1.5 django-webpack-loader==3.0.1 git+https://github.com/BITSOLVER/django-js-reverse@071e304fd600107bc64bbde6f2491f1fe049ec82 django-allauth==0.61.1 -recipe-scrapers==15.2.1 +recipe-scrapers==15.4.0 django-scopes==2.0.0 django-treebeard==4.7 django-cors-headers==4.6.0 diff --git a/vue/src/locales/uk.json b/vue/src/locales/uk.json index 462e05f7e..b095923f9 100644 --- a/vue/src/locales/uk.json +++ b/vue/src/locales/uk.json @@ -20,7 +20,7 @@ "all_fields_optional": "Всі поля опціональні і можна залишити їх пустими.", "convert_internal": "Конвертувати у внутрішній рецепт", "show_only_internal": "Показати тільки внутрішні рецепти", - "show_split_screen": "", + "show_split_screen": "Розділений перегляд", "Log_Recipe_Cooking": "", "External_Recipe_Image": "Зображення Зовнішнього Рецепту", "Add_to_Shopping": "Додати до Покупок", @@ -437,5 +437,8 @@ "Use_Fractions_Help": "Автоматично конвертувати десятки в дроби, коли дивитесь рецепт.", "Copy Link": "Скопіювати Посилання", "Original_Text": "Оригінальний текст", - "Default_Unit": "Одиниця замовчуванням" + "Default_Unit": "Одиниця замовчуванням", + "recipe_property_info": "Ви також можете додати властивості до продуктів, щоб розрахувати їх автоматично на основі вашого рецепту!", + "per_serving": "на порцію", + "err_importing_recipe": "Виникла помилка при імпортуванні рецепту!" }