Merge branch 'develop' into url_import_recipes

# Conflicts:
#	cookbook/helper/recipe_url_import.py
#	cookbook/tests/api/test_api_keyword.py
#	cookbook/tests/other/test_edits_recipe.py
#	cookbook/views/api.py
#	requirements.txt
This commit is contained in:
vabene1111
2021-03-18 20:38:51 +01:00
137 changed files with 10564 additions and 6777 deletions

View File

@ -4,6 +4,7 @@ import re
import uuid
import requests
from PIL import Image
from annoying.decorators import ajax_request
from annoying.functions import get_object_or_None
from django.contrib import messages
@ -13,16 +14,11 @@ from django.core.exceptions import FieldError, ValidationError
from django.core.files import File
from django.db.models import Q
from django.http import FileResponse, HttpResponse, JsonResponse
from django.shortcuts import redirect
from django.utils import timezone
from django.utils.formats import date_format
from django.shortcuts import redirect, get_object_or_404
from django.utils.translation import gettext as _
from icalendar import Calendar, Event
from PIL import Image
from rest_framework import decorators, permissions, viewsets
from rest_framework.exceptions import APIException, PermissionDenied
from rest_framework.mixins import (ListModelMixin, RetrieveModelMixin,
UpdateModelMixin, CreateModelMixin)
from rest_framework import decorators, viewsets, status
from rest_framework.exceptions import APIException, PermissionDenied, NotFound, MethodNotAllowed
from rest_framework.parsers import MultiPartParser
from rest_framework.response import Response
from rest_framework.viewsets import ViewSetMixin
@ -37,7 +33,7 @@ from cookbook.models import (CookLog, Food, Ingredient, Keyword, MealPlan,
MealType, Recipe, RecipeBook, ShoppingList,
ShoppingListEntry, ShoppingListRecipe, Step,
Storage, Sync, SyncLog, Unit, UserPreference,
ViewLog, RecipeBookEntry, Supermarket)
ViewLog, RecipeBookEntry, Supermarket, ImportLog)
from cookbook.provider.dropbox import Dropbox
from cookbook.provider.local import Local
from cookbook.provider.nextcloud import Nextcloud
@ -52,7 +48,7 @@ from cookbook.serializer import (FoodSerializer, IngredientSerializer,
StorageSerializer, SyncLogSerializer,
SyncSerializer, UnitSerializer,
UserNameSerializer, UserPreferenceSerializer,
ViewLogSerializer, CookLogSerializer, RecipeBookEntrySerializer, RecipeOverviewSerializer, SupermarketSerializer)
ViewLogSerializer, CookLogSerializer, RecipeBookEntrySerializer, RecipeOverviewSerializer, SupermarketSerializer, ImportLogSerializer)
from recipes.settings import DEMO
from recipe_scrapers import scrape_me, WebsiteNotImplementedError, NoSchemaFoundInWildMode
@ -71,12 +67,14 @@ class StandardFilterMixin(ViewSetMixin):
queryset = queryset.filter(updated_at__gte=updated_at)
except FieldError:
pass
except ValidationError:
raise APIException(_('Parameter updated_at incorrectly formatted'))
limit = self.request.query_params.get('limit', None)
random = self.request.query_params.get('random', False)
if limit is not None:
if random:
queryset = queryset.random(int(limit))
queryset = queryset.order_by("?")[:int(limit)]
else:
queryset = queryset[:int(limit)]
return queryset
@ -89,65 +87,69 @@ class UserNameViewSet(viewsets.ReadOnlyModelViewSet):
- **filter_list**: array of user id's to get names for
"""
queryset = User.objects.all()
queryset = User.objects
serializer_class = UserNameSerializer
permission_classes = [CustomIsGuest]
http_method_names = ['get']
def get_queryset(self):
queryset = self.queryset
queryset = self.queryset.filter(userpreference__space=self.request.space)
try:
filter_list = self.request.query_params.get('filter_list', None)
if filter_list is not None:
queryset = queryset.filter(pk__in=json.loads(filter_list))
except ValueError:
raise APIException(
_('Parameter filter_list incorrectly formatted')
)
raise APIException('Parameter filter_list incorrectly formatted')
return queryset
class UserPreferenceViewSet(viewsets.ModelViewSet):
queryset = UserPreference.objects.all()
queryset = UserPreference.objects
serializer_class = UserPreferenceSerializer
permission_classes = [CustomIsOwner, ]
def perform_create(self, serializer):
if UserPreference.objects.filter(user=self.request.user).exists():
raise APIException(_('Preference for given user already exists'))
serializer.save(user=self.request.user)
def get_queryset(self):
if self.request.user.is_superuser:
return self.queryset
return self.queryset.filter(user=self.request.user)
class StorageViewSet(viewsets.ModelViewSet):
# TODO handle delete protect error and adjust test
queryset = Storage.objects.all()
queryset = Storage.objects
serializer_class = StorageSerializer
permission_classes = [CustomIsAdmin, ]
def get_queryset(self):
return self.queryset.filter(space=self.request.space)
class SyncViewSet(viewsets.ModelViewSet):
queryset = Sync.objects.all()
queryset = Sync.objects
serializer_class = SyncSerializer
permission_classes = [CustomIsAdmin, ]
def get_queryset(self):
return self.queryset.filter(space=self.request.space)
class SyncLogViewSet(viewsets.ReadOnlyModelViewSet):
queryset = SyncLog.objects.all()
queryset = SyncLog.objects
serializer_class = SyncLogSerializer
permission_classes = [CustomIsAdmin, ]
def get_queryset(self):
return self.queryset.filter(sync__space=self.request.space)
class SupermarketViewSet(viewsets.ModelViewSet, StandardFilterMixin):
queryset = Supermarket.objects.all()
queryset = Supermarket.objects
serializer_class = SupermarketSerializer
permission_classes = [CustomIsUser]
def get_queryset(self):
self.queryset = self.queryset.filter(space=self.request.space)
return super().get_queryset()
class KeywordViewSet(viewsets.ModelViewSet, StandardFilterMixin):
"""
@ -158,44 +160,52 @@ class KeywordViewSet(viewsets.ModelViewSet, StandardFilterMixin):
in the keyword name (case in-sensitive)
- **limit**: limits the amount of returned results
"""
queryset = Keyword.objects.all()
queryset = Keyword.objects
serializer_class = KeywordSerializer
permission_classes = [CustomIsUser]
def get_queryset(self):
self.queryset = self.queryset.filter(space=self.request.space)
return super().get_queryset()
class UnitViewSet(viewsets.ModelViewSet, StandardFilterMixin):
queryset = Unit.objects.all()
queryset = Unit.objects
serializer_class = UnitSerializer
permission_classes = [CustomIsUser]
def get_queryset(self):
self.queryset = self.queryset.filter(space=self.request.space)
return super().get_queryset()
class FoodViewSet(viewsets.ModelViewSet, StandardFilterMixin):
queryset = Food.objects.all()
queryset = Food.objects
serializer_class = FoodSerializer
permission_classes = [CustomIsUser]
def get_queryset(self):
self.queryset = self.queryset.filter(space=self.request.space)
return super().get_queryset()
class RecipeBookViewSet(viewsets.ModelViewSet, StandardFilterMixin):
queryset = RecipeBook.objects.all()
queryset = RecipeBook.objects
serializer_class = RecipeBookSerializer
permission_classes = [CustomIsOwner]
def get_queryset(self):
self.queryset = super(RecipeBookViewSet, self).get_queryset()
if self.request.user.is_superuser:
return self.queryset
return self.queryset.filter(created_by=self.request.user)
self.queryset = self.queryset.filter(created_by=self.request.user).filter(space=self.request.space)
return super().get_queryset()
class RecipeBookEntryViewSet(viewsets.ModelViewSet, viewsets.GenericViewSet):
queryset = RecipeBookEntry.objects.all()
queryset = RecipeBookEntry.objects
serializer_class = RecipeBookEntrySerializer
permission_classes = [CustomIsOwner]
def get_queryset(self):
if self.request.user.is_superuser:
return self.queryset
return self.queryset.filter(created_by=self.request.user)
return self.queryset.filter(book__created_by=self.request.user).filter(book__space=self.request.space)
class MealPlanViewSet(viewsets.ModelViewSet):
@ -207,15 +217,15 @@ class MealPlanViewSet(viewsets.ModelViewSet):
- **to_date**: filter upward to (inclusive) certain date
"""
queryset = MealPlan.objects.all()
queryset = MealPlan.objects
serializer_class = MealPlanSerializer
permission_classes = [CustomIsOwner]
def get_queryset(self):
queryset = MealPlan.objects.filter(
queryset = self.queryset.filter(
Q(created_by=self.request.user) |
Q(shared=self.request.user)
).distinct().all()
).filter(space=self.request.space).distinct().all()
from_date = self.request.query_params.get('from_date', None)
if from_date is not None:
@ -232,26 +242,32 @@ class MealTypeViewSet(viewsets.ModelViewSet):
returns list of meal types created by the
requesting user ordered by the order field.
"""
queryset = MealType.objects.order_by('order').all()
queryset = MealType.objects
serializer_class = MealTypeSerializer
permission_classes = [CustomIsOwner]
def get_queryset(self):
queryset = MealType.objects.order_by('order', 'id').filter(created_by=self.request.user).all()
queryset = self.queryset.order_by('order', 'id').filter(created_by=self.request.user).filter(space=self.request.space).all()
return queryset
class IngredientViewSet(viewsets.ModelViewSet):
queryset = Ingredient.objects.all()
queryset = Ingredient.objects
serializer_class = IngredientSerializer
permission_classes = [CustomIsUser]
def get_queryset(self):
return self.queryset.filter(step__recipe__space=self.request.space)
class StepViewSet(viewsets.ModelViewSet):
queryset = Step.objects.all()
queryset = Step.objects
serializer_class = StepSerializer
permission_classes = [CustomIsUser]
def get_queryset(self):
return self.queryset.filter(recipe__space=self.request.space)
class RecipeViewSet(viewsets.ModelViewSet, StandardFilterMixin):
"""
@ -262,12 +278,13 @@ class RecipeViewSet(viewsets.ModelViewSet, StandardFilterMixin):
in the recipe name (case in-sensitive)
- **limit**: limits the amount of returned results
"""
queryset = Recipe.objects.all()
queryset = Recipe.objects
serializer_class = RecipeSerializer
# TODO split read and write permission for meal plan guest
permission_classes = [CustomIsShare | CustomIsGuest]
def get_queryset(self):
self.queryset = self.queryset.filter(space=self.request.space)
internal = self.request.query_params.get('internal', None)
if internal:
@ -290,6 +307,10 @@ class RecipeViewSet(viewsets.ModelViewSet, StandardFilterMixin):
)
def image(self, request, pk):
obj = self.get_object()
if obj.get_space() != request.space:
raise PermissionDenied(detail='You do not have the required permission to perform this action', code=403)
serializer = self.serializer_class(
obj, data=request.data, partial=True
)
@ -316,34 +337,30 @@ class RecipeViewSet(viewsets.ModelViewSet, StandardFilterMixin):
class ShoppingListRecipeViewSet(viewsets.ModelViewSet):
queryset = ShoppingListRecipe.objects.all()
queryset = ShoppingListRecipe.objects
serializer_class = ShoppingListRecipeSerializer
permission_classes = [CustomIsOwner, ]
permission_classes = [CustomIsOwner | CustomIsShared]
def get_queryset(self):
return self.queryset.filter(shoppinglist__created_by=self.request.user).all()
return self.queryset.filter(Q(shoppinglist__created_by=self.request.user) | Q(shoppinglist__shared=self.request.user)).filter(shoppinglist__space=self.request.space).all()
class ShoppingListEntryViewSet(viewsets.ModelViewSet):
queryset = ShoppingListEntry.objects.all()
queryset = ShoppingListEntry.objects
serializer_class = ShoppingListEntrySerializer
permission_classes = [CustomIsOwner, ]
permission_classes = [CustomIsOwner | CustomIsShared]
def get_queryset(self):
return self.queryset.filter(shoppinglist__created_by=self.request.user).all()
return self.queryset.filter(Q(shoppinglist__created_by=self.request.user) | Q(shoppinglist__shared=self.request.user)).filter(shoppinglist__space=self.request.space).all()
class ShoppingListViewSet(viewsets.ModelViewSet):
queryset = ShoppingList.objects.all()
queryset = ShoppingList.objects
serializer_class = ShoppingListSerializer
permission_classes = [CustomIsOwner | CustomIsShared]
def get_queryset(self):
if self.request.user.is_superuser:
return self.queryset
return self.queryset.filter(
Q(created_by=self.request.user) | Q(shared=self.request.user)
).all()
return self.queryset.filter(Q(created_by=self.request.user) | Q(shared=self.request.user)).filter(space=self.request.space).distinct()
def get_serializer_class(self):
autosync = self.request.query_params.get('autosync', None)
@ -353,22 +370,38 @@ class ShoppingListViewSet(viewsets.ModelViewSet):
class ViewLogViewSet(viewsets.ModelViewSet):
queryset = ViewLog.objects.all()
queryset = ViewLog.objects
serializer_class = ViewLogSerializer
permission_classes = [CustomIsOwner]
def get_queryset(self):
return CookLog.objects.filter(created_by=self.request.user).all()[:5]
self.queryset = self.queryset.filter(created_by=self.request.user).filter(space=self.request.space).all()
if self.request.method == 'GET':
return self.queryset[:5]
else:
return self.queryset
class CookLogViewSet(viewsets.ModelViewSet):
queryset = CookLog.objects.all()
queryset = CookLog.objects
serializer_class = CookLogSerializer
permission_classes = [CustomIsOwner]
def get_queryset(self):
queryset = CookLog.objects.filter(created_by=self.request.user).all()[:5]
return queryset
self.queryset = self.queryset.filter(created_by=self.request.user).filter(space=self.request.space).all()
if self.request.method == 'GET':
return self.queryset[:5]
else:
return self.queryset
class ImportLogViewSet(viewsets.ModelViewSet):
queryset = ImportLog.objects
serializer_class = ImportLogSerializer
permission_classes = [CustomIsUser]
def get_queryset(self):
return self.queryset.filter(space=self.request.space).all()
# -------------- non django rest api views --------------------
@ -394,7 +427,7 @@ def update_recipe_links(recipe):
@group_required('user')
def get_external_file_link(request, recipe_id):
recipe = Recipe.objects.get(id=recipe_id)
recipe = get_object_or_404(Recipe, pk=recipe_id, space=request.space)
if not recipe.link:
update_recipe_links(recipe)
@ -403,7 +436,7 @@ def get_external_file_link(request, recipe_id):
@group_required('guest')
def get_recipe_file(request, recipe_id):
recipe = Recipe.objects.get(id=recipe_id)
recipe = get_object_or_404(Recipe, pk=recipe_id, space=request.space)
if recipe.storage:
return FileResponse(get_recipe_provider(recipe).get_file(recipe))
else:
@ -418,7 +451,7 @@ def sync_all(request):
)
return redirect('index')
monitors = Sync.objects.filter(active=True)
monitors = Sync.objects.filter(active=True).filter(space=request.user.userpreference.space)
error = False
for monitor in monitors:
@ -470,7 +503,7 @@ def log_cooking(request, recipe_id):
def get_plan_ical(request, from_date, to_date):
queryset = MealPlan.objects.filter(
Q(created_by=request.user) | Q(shared=request.user)
).distinct().all()
).filter(space=request.user.userpreference.space).distinct().all()
if from_date is not None:
queryset = queryset.filter(date__gte=from_date)
@ -548,24 +581,25 @@ def recipe_from_url_old(request):
},
status=400
)
return get_from_html(response.text, url)
return get_from_html(response.text, url, request.space)
@group_required('admin')
def get_backup(request):
if not request.user.is_superuser:
return HttpResponse('', status=403)
@group_required('user')
def recipe_from_json(request):
mjson = request.POST['json']
buf = io.StringIO()
management.call_command(
'dumpdata', exclude=['contenttypes', 'auth'], stdout=buf
md_json = json.loads(mjson)
if ('@type' in md_json
and md_json['@type'] == 'Recipe'):
return JsonResponse(find_recipe_json(md_json, '', request.space))
return JsonResponse(
{
'error': True,
'msg': _('Could not parse correctly...')
},
status=400
)
response = FileResponse(buf.getvalue())
response["Content-Disposition"] = f'attachment; filename=backup{date_format(timezone.now(), format="SHORT_DATETIME_FORMAT", use_l10n=True)}.json' # noqa: E501
return response
@group_required('user')
def ingredient_from_string(request):