diff --git a/cookbook/serializer.py b/cookbook/serializer.py index 6dc269b6b0..9e6558b798 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -1,4 +1,3 @@ -import random import traceback import uuid from datetime import datetime, timedelta @@ -7,34 +6,34 @@ from html import escape from smtplib import SMTPException -from django.contrib.auth.models import Group, User, AnonymousUser +from django.contrib.auth.models import AnonymousUser, Group, User from django.core.cache import caches from django.core.mail import send_mail -from django.db.models import Avg, Q, QuerySet, Sum +from django.db.models import Q, QuerySet, Sum from django.http import BadHeaderError from django.urls import reverse from django.utils import timezone from django_scopes import scopes_disabled from drf_writable_nested import UniqueFieldsMixin, WritableNestedModelSerializer -from PIL import Image from oauth2_provider.models import AccessToken +from PIL import Image from rest_framework import serializers from rest_framework.exceptions import NotFound, ValidationError from cookbook.helper.CustomStorageClass import CachedS3Boto3Storage from cookbook.helper.HelperFunctions import str2bool -from cookbook.helper.property_helper import FoodPropertyHelper from cookbook.helper.permission_helper import above_space_limit +from cookbook.helper.property_helper import FoodPropertyHelper from cookbook.helper.shopping_helper import RecipeShoppingEditor from cookbook.helper.unit_conversion_helper import UnitConversionHelper from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, CustomFilter, ExportLog, Food, FoodInheritField, ImportLog, Ingredient, InviteLink, - Keyword, MealPlan, MealType, NutritionInformation, Recipe, RecipeBook, - RecipeBookEntry, RecipeImport, ShareLink, ShoppingList, - ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage, - Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, - SyncLog, Unit, UserFile, UserPreference, UserSpace, ViewLog, UnitConversion, Property, - PropertyType, Property) + Keyword, MealPlan, MealType, NutritionInformation, Property, + PropertyType, Recipe, RecipeBook, RecipeBookEntry, RecipeImport, + ShareLink, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space, + Step, Storage, Supermarket, SupermarketCategory, + SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion, + UserFile, UserPreference, UserSpace, ViewLog) from cookbook.templatetags.custom_tags import markdown from recipes.settings import AWS_ENABLED, MEDIA_URL @@ -57,10 +56,9 @@ def get_fields(self, *args, **kwargs): api_serializer = None # extended values are computationally expensive and not needed in normal circumstances try: - if str2bool( - self.context['request'].query_params.get('extended', False)) and self.__class__ == api_serializer: + if str2bool(self.context['request'].query_params.get('extended', False)) and self.__class__ == api_serializer: return fields - except (AttributeError, KeyError) as e: + except (AttributeError, KeyError): pass try: del fields['image'] @@ -104,9 +102,9 @@ def to_representation(self, value): return round(value, 2).normalize() def to_internal_value(self, data): - if type(data) == int or type(data) == float: + if isinstance(data, int) or isinstance(data, float): return data - elif type(data) == str: + elif isinstance(data, str): if data == '': return 0 try: @@ -146,11 +144,11 @@ class SpaceFilterSerializer(serializers.ListSerializer): def to_representation(self, data): if self.context.get('request', None) is None: return - if (type(data) == QuerySet and data.query.is_sliced): + if (isinstance(data, QuerySet) and data.query.is_sliced): # if query is sliced it came from api request not nested serializer return super().to_representation(data) if self.child.Meta.model == User: - if type(self.context['request'].user) == AnonymousUser: + if isinstance(self.context['request'].user, AnonymousUser): data = [] else: data = data.filter(userspace__space=self.context['request'].user.get_active_space()).all() @@ -449,7 +447,7 @@ def create(self, validated_data): class Meta: model = Keyword fields = ( - 'id', 'name', 'label', 'description', 'image', 'parent', 'numchild', 'numrecipe', 'created_at', + 'id', 'name', 'label', 'description', 'image', 'parent', 'numchild', 'numrecipe', 'created_at', 'updated_at', 'full_name') read_only_fields = ('id', 'label', 'numchild', 'parent', 'image') @@ -528,7 +526,7 @@ def create(self, validated_data): class Meta: model = PropertyType - fields = ('id', 'name', 'unit', 'description', 'order', 'open_data_slug') + fields = ('id', 'name', 'unit', 'description', 'order', 'open_data_slug') class PropertySerializer(UniqueFieldsMixin, WritableNestedModelSerializer): @@ -636,7 +634,7 @@ def create(self, validated_data): validated_data['recipe'] = Recipe.objects.get(**recipe) # assuming if on hand for user also onhand for shopping_share users - if not onhand is None: + if onhand is not None: shared_users = [user := self.context['request'].user] + list(user.userpreference.shopping_share.all()) if self.instance: onhand_users = self.instance.onhand_users.all() @@ -669,7 +667,7 @@ def update(self, instance, validated_data): # assuming if on hand for user also onhand for shopping_share users onhand = validated_data.get('food_onhand', None) reset_inherit = self.initial_data.get('reset_inherit', False) - if not onhand is None: + if onhand is not None: shared_users = [user := self.context['request'].user] + list(user.userpreference.shopping_share.all()) if onhand: validated_data['onhand_users'] = list(self.instance.onhand_users.all()) + shared_users @@ -764,7 +762,7 @@ def get_step_recipes(self, obj): def get_step_recipe_data(self, obj): # 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: + if obj.step_recipe and isinstance(self.parent.root, RecipeSerializer): return StepRecipeSerializer(obj.step_recipe, context={'request': self.context['request']}).data class Meta: @@ -957,7 +955,7 @@ 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(): + '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 @@ -1023,10 +1021,10 @@ def get_name(self, obj): 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 + 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): diff --git a/cookbook/views/api.py b/cookbook/views/api.py index 16a885a016..0ef0d2f4cc 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -3,7 +3,6 @@ import json import mimetypes import pathlib -import random import re import threading import traceback @@ -15,7 +14,6 @@ import requests import validators -from PIL import UnidentifiedImageError from annoying.decorators import ajax_request from annoying.functions import get_object_or_None from django.contrib import messages @@ -24,7 +22,7 @@ from django.core.cache import caches from django.core.exceptions import FieldError, ValidationError from django.core.files import File -from django.db.models import Case, Count, Exists, OuterRef, ProtectedError, Q, Subquery, Value, When, Avg, Max +from django.db.models import Case, Count, Exists, OuterRef, ProtectedError, Q, Subquery, Value, When from django.db.models.fields.related import ForeignObjectRel from django.db.models.functions import Coalesce, Lower from django.db.models.signals import post_save @@ -36,6 +34,7 @@ from django_scopes import scopes_disabled from icalendar import Calendar, Event from oauth2_provider.models import AccessToken +from PIL import UnidentifiedImageError from recipe_scrapers import scrape_me from recipe_scrapers._exceptions import NoSchemaFoundInWildMode from requests.exceptions import MissingSchema @@ -58,35 +57,41 @@ from cookbook.helper.image_processing import handle_image from cookbook.helper.ingredient_parser import IngredientParser from cookbook.helper.open_data_importer import OpenDataImporter -from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsOwner, - CustomIsOwnerReadOnly, CustomIsShared, - CustomIsSpaceOwner, CustomIsUser, group_required, - is_space_owner, switch_user_active_space, above_space_limit, - CustomRecipePermission, CustomUserPermission, - CustomTokenHasReadWriteScope, CustomTokenHasScope, has_group_permission, IsReadOnlyDRF) +from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsOwner, CustomIsOwnerReadOnly, + CustomIsShared, CustomIsSpaceOwner, CustomIsUser, + CustomRecipePermission, CustomTokenHasReadWriteScope, + CustomTokenHasScope, CustomUserPermission, + IsReadOnlyDRF, above_space_limit, group_required, + has_group_permission, is_space_owner, + switch_user_active_space) from cookbook.helper.recipe_search import RecipeFacet, RecipeSearch -from cookbook.helper.recipe_url_import import get_from_youtube_scraper, get_images_from_soup, clean_dict +from cookbook.helper.recipe_url_import import (clean_dict, get_from_youtube_scraper, + get_images_from_soup) from cookbook.helper.scrapers.scrapers import text_scraper from cookbook.helper.shopping_helper import RecipeShoppingEditor, shopping_helper from cookbook.models import (Automation, BookmarkletImport, CookLog, CustomFilter, ExportLog, Food, FoodInheritField, ImportLog, Ingredient, InviteLink, Keyword, MealPlan, - MealType, Recipe, RecipeBook, RecipeBookEntry, ShareLink, ShoppingList, - ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage, - Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, - SyncLog, Unit, UserFile, UserPreference, UserSpace, ViewLog, UnitConversion, PropertyType, Property) + MealType, Property, PropertyType, Recipe, RecipeBook, RecipeBookEntry, + ShareLink, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space, + Step, Storage, Supermarket, SupermarketCategory, + SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion, + UserFile, UserPreference, UserSpace, ViewLog) from cookbook.provider.dropbox import Dropbox from cookbook.provider.local import Local from cookbook.provider.nextcloud import Nextcloud from cookbook.schemas import FilterSchema, QueryParam, QueryParamAutoSchema, TreeSchema -from cookbook.serializer import (AutomationSerializer, BookmarkletImportListSerializer, +from cookbook.serializer import (AccessTokenSerializer, AutomationSerializer, + AutoMealPlanSerializer, BookmarkletImportListSerializer, BookmarkletImportSerializer, CookLogSerializer, CustomFilterSerializer, ExportLogSerializer, FoodInheritFieldSerializer, FoodSerializer, - FoodShoppingUpdateSerializer, GroupSerializer, ImportLogSerializer, - IngredientSerializer, IngredientSimpleSerializer, - InviteLinkSerializer, KeywordSerializer, MealPlanSerializer, - MealTypeSerializer, RecipeBookEntrySerializer, - RecipeBookSerializer, RecipeFromSourceSerializer, + FoodShoppingUpdateSerializer, FoodSimpleSerializer, + GroupSerializer, ImportLogSerializer, IngredientSerializer, + IngredientSimpleSerializer, InviteLinkSerializer, + KeywordSerializer, MealPlanSerializer, MealTypeSerializer, + PropertySerializer, PropertyTypeSerializer, + RecipeBookEntrySerializer, RecipeBookSerializer, + RecipeExportSerializer, RecipeFromSourceSerializer, RecipeImageSerializer, RecipeOverviewSerializer, RecipeSerializer, RecipeShoppingUpdateSerializer, RecipeSimpleSerializer, ShoppingListAutoSyncSerializer, ShoppingListEntrySerializer, @@ -94,11 +99,9 @@ SpaceSerializer, StepSerializer, StorageSerializer, SupermarketCategoryRelationSerializer, SupermarketCategorySerializer, SupermarketSerializer, - SyncLogSerializer, SyncSerializer, UnitSerializer, - UserFileSerializer, UserSerializer, UserPreferenceSerializer, - UserSpaceSerializer, ViewLogSerializer, AccessTokenSerializer, FoodSimpleSerializer, - RecipeExportSerializer, UnitConversionSerializer, PropertyTypeSerializer, - PropertySerializer, AutoMealPlanSerializer) + SyncLogSerializer, SyncSerializer, UnitConversionSerializer, + UnitSerializer, UserFileSerializer, UserPreferenceSerializer, + UserSerializer, UserSpaceSerializer, ViewLogSerializer) from cookbook.views.import_export import get_integration from recipes import settings @@ -150,8 +153,7 @@ def annotate_recipe(self, queryset=None, request=None, serializer=None, tree=Fal # add a recipe count annotation to the query # explanation on construction https://stackoverflow.com/a/43771738/15762829 - recipe_count = Recipe.objects.filter(**{recipe_filter: OuterRef('id')}, space=space).values( - recipe_filter).annotate(count=Count('pk')).values('count') + recipe_count = Recipe.objects.filter(**{recipe_filter: OuterRef('id')}, space=space).values(recipe_filter).annotate(count=Count('pk', distinct=True)).values('count') queryset = queryset.annotate(recipe_count=Coalesce(Subquery(recipe_count), 0)) # add a recipe image annotation to the query @@ -186,7 +188,8 @@ def get_queryset(self): if query is not None and query not in ["''", '']: if fuzzy and (settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']): - if self.request.user.is_authenticated and any([self.model.__name__.lower() in x for x in self.request.user.searchpreference.unaccent.values_list('field', flat=True)]): + if self.request.user.is_authenticated and any( + [self.model.__name__.lower() in x for x in self.request.user.searchpreference.unaccent.values_list('field', flat=True)]): self.queryset = self.queryset.annotate(trigram=TrigramSimilarity('name__unaccent', query)) else: self.queryset = self.queryset.annotate(trigram=TrigramSimilarity('name', query)) @@ -319,8 +322,7 @@ def get_queryset(self): except self.model.DoesNotExist: self.queryset = self.model.objects.none() else: - return self.annotate_recipe(queryset=super().get_queryset(), request=self.request, - serializer=self.serializer_class, tree=True) + return self.annotate_recipe(queryset=super().get_queryset(), request=self.request, serializer=self.serializer_class, tree=True) self.queryset = self.queryset.filter(space=self.request.space).order_by(Lower('name').asc()) return self.annotate_recipe(queryset=self.queryset, request=self.request, serializer=self.serializer_class, @@ -367,7 +369,7 @@ def move(self, request, pk, parent): child.move(parent, f'{node_location}-child') content = {'msg': _(f'{child.name} was moved successfully to parent {parent.name}')} return Response(content, status=status.HTTP_200_OK) - except (PathOverflow, InvalidMoveToDescendant, InvalidPosition) as e: + except (PathOverflow, InvalidMoveToDescendant, InvalidPosition): content = {'error': True, 'msg': _('An error occurred attempting to move ') + child.name} return Response(content, status=status.HTTP_400_BAD_REQUEST) @@ -824,7 +826,7 @@ class RecipeViewSet(viewsets.ModelViewSet): 'Query string matched (fuzzy) against recipe name. In the future also fulltext search.')), QueryParam(name='keywords', description=_( 'ID of keyword a recipe should have. For multiple repeat parameter. Equivalent to keywords_or'), - qtype='int'), + qtype='int'), QueryParam(name='keywords_or', description=_('Keyword IDs, repeat for multiple. Return recipes with any of the keywords'), qtype='int'), @@ -1344,7 +1346,7 @@ def recipe_from_source(request): }, status=status.HTTP_400_BAD_REQUEST) elif url and not data: - if re.match('^(https?://)?(www\.youtube\.com|youtu\.be)/.+$', url): + if re.match('^(https?://)?(www\\.youtube\\.com|youtu\\.be)/.+$', url): if validators.url(url, public=True): return Response({ 'recipe_json': get_from_youtube_scraper(url, request),