diff --git a/.env.template b/.env.template index aa13a7a96..2bdecc834 100644 --- a/.env.template +++ b/.env.template @@ -45,7 +45,8 @@ SHOPPING_MIN_AUTOSYNC_INTERVAL=5 # Default for user setting sticky navbar # STICKY_NAV_PREF_DEFAULT=1 -# If base URL is something other than just / (you are serving a subfolder in your proxy for instance http://recipe_app/recipes/) +# If base URL is something other than just / (you are serving a subfolder in your proxy for instance http://recipe_app/recipes/) +# Be sure to not have a trailing slash: e.g. '/recipes' instead of '/recipes/' # SCRIPT_NAME=/recipes # If staticfiles are stored at a different location uncomment and change accordingly, MUST END IN / @@ -148,4 +149,4 @@ REVERSE_PROXY_AUTH=0 # Enables exporting PDF (see export docs) # Disabled by default, uncomment to enable -# ENABLE_PDF_EXPORT=1 \ No newline at end of file +# ENABLE_PDF_EXPORT=1 diff --git a/cookbook/admin.py b/cookbook/admin.py index 73e86ecf5..625b2a6bc 100644 --- a/cookbook/admin.py +++ b/cookbook/admin.py @@ -15,7 +15,7 @@ from .models import (BookmarkletImport, Comment, CookLog, Food, FoodInheritField Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchPreference, ShareLink, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage, Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog, - TelegramBot, Unit, UserFile, UserPreference, ViewLog) + TelegramBot, Unit, UserFile, UserPreference, ViewLog, Automation) class CustomUserAdmin(UserAdmin): @@ -29,11 +29,52 @@ admin.site.register(User, CustomUserAdmin) admin.site.unregister(Group) +@admin.action(description='Delete all data from a space') +def delete_space_action(modeladmin, request, queryset): + for space in queryset: + CookLog.objects.filter(space=space).delete() + ViewLog.objects.filter(space=space).delete() + ImportLog.objects.filter(space=space).delete() + BookmarkletImport.objects.filter(space=space).delete() + + Comment.objects.filter(recipe__space=space).delete() + Keyword.objects.filter(space=space).delete() + Ingredient.objects.filter(space=space).delete() + Food.objects.filter(space=space).delete() + Unit.objects.filter(space=space).delete() + Step.objects.filter(space=space).delete() + NutritionInformation.objects.filter(space=space).delete() + RecipeBookEntry.objects.filter(book__space=space).delete() + RecipeBook.objects.filter(space=space).delete() + MealType.objects.filter(space=space).delete() + MealPlan.objects.filter(space=space).delete() + ShareLink.objects.filter(space=space).delete() + Recipe.objects.filter(space=space).delete() + + RecipeImport.objects.filter(space=space).delete() + SyncLog.objects.filter(sync__space=space).delete() + Sync.objects.filter(space=space).delete() + Storage.objects.filter(space=space).delete() + + ShoppingListEntry.objects.filter(shoppinglist__space=space).delete() + ShoppingListRecipe.objects.filter(shoppinglist__space=space).delete() + ShoppingList.objects.filter(space=space).delete() + + SupermarketCategoryRelation.objects.filter(supermarket__space=space).delete() + SupermarketCategory.objects.filter(space=space).delete() + Supermarket.objects.filter(space=space).delete() + + InviteLink.objects.filter(space=space).delete() + UserFile.objects.filter(space=space).delete() + Automation.objects.filter(space=space).delete() + + class SpaceAdmin(admin.ModelAdmin): list_display = ('name', 'created_by', 'max_recipes', 'max_users', 'max_file_storage_mb', 'allow_sharing') search_fields = ('name', 'created_by__username') list_filter = ('max_recipes', 'max_users', 'max_file_storage_mb', 'allow_sharing') date_hierarchy = 'created_at' + actions = [delete_space_action] admin.site.register(Space, SpaceAdmin) @@ -128,7 +169,7 @@ def sort_tree(modeladmin, request, queryset): class KeywordAdmin(TreeAdmin): form = movenodeform_factory(Keyword) ordering = ('space', 'path',) - search_fields = ('name', ) + search_fields = ('name',) actions = [sort_tree, enable_tree_sorting, disable_tree_sorting] @@ -171,13 +212,15 @@ class RecipeAdmin(admin.ModelAdmin): admin.site.register(Recipe, RecipeAdmin) admin.site.register(Unit) + + # admin.site.register(FoodInheritField) class FoodAdmin(TreeAdmin): form = movenodeform_factory(Keyword) ordering = ('space', 'path',) - search_fields = ('name', ) + search_fields = ('name',) actions = [sort_tree, enable_tree_sorting, disable_tree_sorting] diff --git a/cookbook/helper/recipe_search.py b/cookbook/helper/recipe_search.py index 673b7d0d4..35581b59b 100644 --- a/cookbook/helper/recipe_search.py +++ b/cookbook/helper/recipe_search.py @@ -172,6 +172,8 @@ class RecipeSearch(): def keyword_filters(self, keywords=None, operator=True): if not keywords: return + if not isinstance(keywords, list): + keywords = [keywords] if operator == True: # TODO creating setting to include descendants of keywords a setting self._queryset = self._queryset.filter(keywords__in=Keyword.include_descendants(Keyword.objects.filter(pk__in=keywords))) @@ -184,6 +186,8 @@ class RecipeSearch(): def food_filters(self, foods=None, operator=True): if not foods: return + if not isinstance(foods, list): + foods = [foods] if operator == True: # TODO creating setting to include descendants of food a setting self._queryset = self._queryset.filter(steps__ingredients__food__in=Food.include_descendants(Food.objects.filter(pk__in=foods))) @@ -198,7 +202,9 @@ class RecipeSearch(): raise NotImplementedError if not units: return - self._queryset = self._queryset.filter(steps__ingredients__unit__id=units) + if not isinstance(units, list): + units = [units] + self._queryset = self._queryset.filter(steps__ingredients__unit__in=units) def rating_filter(self, rating=None): if rating is None: @@ -217,6 +223,8 @@ class RecipeSearch(): def book_filters(self, books=None, operator=True): if not books: return + if not isinstance(books, list): + books = [books] if operator == True: self._queryset = self._queryset.filter(recipebookentry__book__id__in=books) else: @@ -228,6 +236,8 @@ class RecipeSearch(): raise NotImplementedError if not steps: return + if not isinstance(steps, list): + steps = [unistepsts] self._queryset = self._queryset.filter(steps__id__in=steps) def build_fulltext_filters(self, string=None): @@ -490,7 +500,7 @@ class RecipeFacet(): 'space': self._request.space, } elif self.hash_key is not None: - self._recipe_list = self._cache.get('recipe_list', None) + self._recipe_list = self._cache.get('recipe_list', []) self._search_params = { 'keyword_list': self._cache.get('keyword_list', None), 'food_list': self._cache.get('food_list', None), @@ -637,7 +647,7 @@ class RecipeFacet(): depth = getattr(keyword, 'depth', 0) + 1 steplen = depth * Keyword.steplen - return queryset.annotate(count=Coalesce(Subquery(self._recipe_count_queryset('keywords', depth, steplen)), 0) + return queryset.annotate(count=Coalesce(1, 0) ).filter(depth=depth, count__gt=0 ).values('id', 'name', 'count', 'numchild').order_by('name') @@ -645,7 +655,7 @@ class RecipeFacet(): depth = getattr(food, 'depth', 0) + 1 steplen = depth * Food.steplen - return queryset.annotate(count=Coalesce(Subquery(self._recipe_count_queryset('steps__ingredients__food', depth, steplen)), 0) + return queryset.annotate(count=Coalesce(1, 0) ).filter(depth__lte=depth, count__gt=0 ).values('id', 'name', 'count', 'numchild').order_by('name') diff --git a/cookbook/integration/default.py b/cookbook/integration/default.py index 7adec0508..39c0bc666 100644 --- a/cookbook/integration/default.py +++ b/cookbook/integration/default.py @@ -2,7 +2,6 @@ import json from io import BytesIO, StringIO from re import match from zipfile import ZipFile -from django.utils.text import get_valid_filename from rest_framework.renderers import JSONRenderer @@ -57,8 +56,7 @@ class Default(Integration): pass recipe_zip_obj.close() - - export_zip_obj.writestr(get_valid_filename(r.name) + '.zip', recipe_zip_stream.getvalue()) + export_zip_obj.writestr(str(r.pk) + '.zip', recipe_zip_stream.getvalue()) export_zip_obj.close() return [[ 'export.zip', export_zip_stream.getvalue() ]] \ No newline at end of file diff --git a/cookbook/integration/integration.py b/cookbook/integration/integration.py index 3b8034e37..6fee602c6 100644 --- a/cookbook/integration/integration.py +++ b/cookbook/integration/integration.py @@ -42,7 +42,7 @@ class Integration: try: last_kw = Keyword.objects.filter(name__regex=r'^(Import [0-9]+)', space=request.space).latest('created_at') name = f'Import {int(last_kw.name.replace("Import ", "")) + 1}' - except ObjectDoesNotExist: + except (ObjectDoesNotExist, ValueError): name = 'Import 1' parent, created = Keyword.objects.get_or_create(name='Import', space=request.space) @@ -53,7 +53,7 @@ class Integration: icon=icon, space=request.space ) - except IntegrityError: # in case, for whatever reason, the name does exist append UUID to it. Not nice but works for now. + except (IntegrityError, ValueError): # in case, for whatever reason, the name does exist append UUID to it. Not nice but works for now. self.keyword = parent.add_child( name=f'{name} {str(uuid.uuid4())[0:8]}', description=description, @@ -86,12 +86,10 @@ class Integration: export_obj.close() export_file = export_stream.getvalue() - response = HttpResponse(export_file, content_type='application/force-download') - response['Content-Disposition'] = 'attachment; filename="'+export_filename+'"' + response['Content-Disposition'] = 'attachment; filename="' + export_filename + '"' return response - def import_file_name_filter(self, zip_info_object): """ Since zipfile.namelist() returns all files in all subdirectories this function allows filtering of files @@ -262,7 +260,6 @@ class Integration: """ raise NotImplementedError('Method not implemented in integration') - def get_files_from_recipes(self, recipes, cookie): """ Takes a list of recipe object and converts it to a array containing each file. diff --git a/cookbook/provider/nextcloud.py b/cookbook/provider/nextcloud.py index d67c02446..743ad8214 100644 --- a/cookbook/provider/nextcloud.py +++ b/cookbook/provider/nextcloud.py @@ -29,7 +29,11 @@ class Nextcloud(Provider): client = Nextcloud.get_client(monitor.storage) files = client.list(monitor.path) - files.pop(0) # remove first element because its the folder itself + + try: + files.pop(0) # remove first element because its the folder itself + except IndexError: + pass # folder is emtpy, no recipes will be imported import_count = 0 for file in files: diff --git a/cookbook/serializer.py b/cookbook/serializer.py index fb1c3a224..3584138c2 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -165,9 +165,10 @@ class FoodInheritFieldSerializer(WritableNestedModelSerializer): read_only_fields = ['id'] -class UserPreferenceSerializer(serializers.ModelSerializer): +class UserPreferenceSerializer(WritableNestedModelSerializer): food_inherit_default = FoodInheritFieldSerializer(source='space.food_inherit', many=True, allow_null=True, required=False, read_only=True) plan_share = UserNameSerializer(many=True, allow_null=True, required=False, read_only=True) + shopping_share = UserNameSerializer(many=True, allow_null=True, required=False) def create(self, validated_data): if not validated_data.get('user', None): @@ -475,7 +476,7 @@ class StepSerializer(WritableNestedModelSerializer, ExtendedRecipeMixin): # check if root type is recipe to prevent infinite recursion # can be improved later to allow multi level embedding if obj.step_recipe and type(self.parent.root) == RecipeSerializer: - return StepRecipeSerializer(obj.step_recipe).data + return StepRecipeSerializer(obj.step_recipe, context={'request': self.context['request']}).data class Meta: model = Step @@ -496,6 +497,11 @@ class StepRecipeSerializer(WritableNestedModelSerializer): class NutritionInformationSerializer(serializers.ModelSerializer): + carbohydrates = CustomDecimalField() + fats = CustomDecimalField() + proteins = CustomDecimalField() + calories = CustomDecimalField() + def create(self, validated_data): validated_data['space'] = self.context['request'].space diff --git a/cookbook/templates/account/login.html b/cookbook/templates/account/login.html index fee09f1cf..2d5be9a2f 100644 --- a/cookbook/templates/account/login.html +++ b/cookbook/templates/account/login.html @@ -19,7 +19,7 @@