add missing from rebase

This commit is contained in:
Mikhail Epifanov 2024-01-11 22:14:22 +01:00
parent e5f0c19cdc
commit bf0462cd74
No known key found for this signature in database

View File

@ -76,7 +76,7 @@ from cookbook.models import (Automation, BookmarkletImport, CookLog, CustomFilte
ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync,
SyncLog, Unit, UnitConversion, UserFile, UserPreference, UserSpace, SyncLog, Unit, UnitConversion, UserFile, UserPreference, UserSpace,
ViewLog) ViewLog, HomeAssistantConfig)
from cookbook.provider.dropbox import Dropbox from cookbook.provider.dropbox import Dropbox
from cookbook.provider.local import Local from cookbook.provider.local import Local
from cookbook.provider.nextcloud import Nextcloud from cookbook.provider.nextcloud import Nextcloud
@ -102,7 +102,7 @@ from cookbook.serializer import (AccessTokenSerializer, AutomationSerializer,
SupermarketCategorySerializer, SupermarketSerializer, SupermarketCategorySerializer, SupermarketSerializer,
SyncLogSerializer, SyncSerializer, UnitConversionSerializer, SyncLogSerializer, SyncSerializer, UnitConversionSerializer,
UnitSerializer, UserFileSerializer, UserPreferenceSerializer, UnitSerializer, UserFileSerializer, UserPreferenceSerializer,
UserSerializer, UserSpaceSerializer, ViewLogSerializer) UserSerializer, UserSpaceSerializer, ViewLogSerializer, HomeAssistantConfigSerializer)
from cookbook.views.import_export import get_integration from cookbook.views.import_export import get_integration
from recipes import settings from recipes import settings
from recipes.settings import FDC_API_KEY, DRF_THROTTLE_RECIPE_URL_IMPORT from recipes.settings import FDC_API_KEY, DRF_THROTTLE_RECIPE_URL_IMPORT
@ -181,7 +181,8 @@ class FuzzyFilterMixin(ViewSetMixin, ExtendedRecipeMixin):
self.queryset = self.queryset.filter(space=self.request.space).order_by(Lower('name').asc()) self.queryset = self.queryset.filter(space=self.request.space).order_by(Lower('name').asc())
query = self.request.query_params.get('query', None) query = self.request.query_params.get('query', None)
if self.request.user.is_authenticated: if self.request.user.is_authenticated:
fuzzy = self.request.user.searchpreference.lookup or any([self.model.__name__.lower() in x for x in fuzzy = self.request.user.searchpreference.lookup or any(
[self.model.__name__.lower() in x for x in
self.request.user.searchpreference.trigram.values_list( self.request.user.searchpreference.trigram.values_list(
'field', flat=True)]) 'field', flat=True)])
else: else:
@ -203,7 +204,9 @@ class FuzzyFilterMixin(ViewSetMixin, ExtendedRecipeMixin):
filter |= Q(name__unaccent__icontains=query) filter |= Q(name__unaccent__icontains=query)
self.queryset = ( self.queryset = (
self.queryset.annotate(starts=Case(When(name__istartswith=query, then=(Value(100))), self.queryset.annotate(
starts=Case(
When(name__istartswith=query, then=(Value(100))),
default=Value(0))) # put exact matches at the top of the result set default=Value(0))) # put exact matches at the top of the result set
.filter(filter).order_by('-starts', Lower('name').asc()) .filter(filter).order_by('-starts', Lower('name').asc())
) )
@ -326,7 +329,8 @@ class TreeMixin(MergeMixin, FuzzyFilterMixin, ExtendedRecipeMixin):
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()) 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, return self.annotate_recipe(
queryset=self.queryset, request=self.request, serializer=self.serializer_class,
tree=True) tree=True)
@decorators.action(detail=True, url_path='move/(?P<parent>[^/.]+)', methods=['PUT'], ) @decorators.action(detail=True, url_path='move/(?P<parent>[^/.]+)', methods=['PUT'], )
@ -572,7 +576,8 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
pass pass
self.queryset = super().get_queryset() self.queryset = super().get_queryset()
shopping_status = ShoppingListEntry.objects.filter(space=self.request.space, food=OuterRef('id'), shopping_status = ShoppingListEntry.objects.filter(
space=self.request.space, food=OuterRef('id'),
checked=False).values('id') checked=False).values('id')
# onhand_status = self.queryset.annotate(onhand_status=Exists(onhand_users_set__in=[shared_users])) # onhand_status = self.queryset.annotate(onhand_status=Exists(onhand_users_set__in=[shared_users]))
return self.queryset \ return self.queryset \
@ -594,7 +599,8 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
shared_users = list(self.request.user.get_shopping_share()) shared_users = list(self.request.user.get_shopping_share())
shared_users.append(request.user) shared_users.append(request.user)
if request.data.get('_delete', False) == 'true': if request.data.get('_delete', False) == 'true':
ShoppingListEntry.objects.filter(food=obj, checked=False, space=request.space, ShoppingListEntry.objects.filter(
food=obj, checked=False, space=request.space,
created_by__in=shared_users).delete() created_by__in=shared_users).delete()
content = {'msg': _(f'{obj.name} was removed from the shopping list.')} content = {'msg': _(f'{obj.name} was removed from the shopping list.')}
return Response(content, status=status.HTTP_204_NO_CONTENT) return Response(content, status=status.HTTP_204_NO_CONTENT)
@ -603,7 +609,8 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
unit = request.data.get('unit', None) unit = request.data.get('unit', None)
content = {'msg': _(f'{obj.name} was added to the shopping list.')} content = {'msg': _(f'{obj.name} was added to the shopping list.')}
ShoppingListEntry.objects.create(food=obj, amount=amount, unit=unit, space=request.space, ShoppingListEntry.objects.create(
food=obj, amount=amount, unit=unit, space=request.space,
created_by=request.user) created_by=request.user)
return Response(content, status=status.HTTP_204_NO_CONTENT) return Response(content, status=status.HTTP_204_NO_CONTENT)
@ -617,7 +624,10 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
response = requests.get(f'https://api.nal.usda.gov/fdc/v1/food/{food.fdc_id}?api_key={FDC_API_KEY}') response = requests.get(f'https://api.nal.usda.gov/fdc/v1/food/{food.fdc_id}?api_key={FDC_API_KEY}')
if response.status_code == 429: if response.status_code == 429:
return JsonResponse({'msg', 'API Key Rate Limit reached/exceeded, see https://api.data.gov/docs/rate-limits/ for more information. Configure your key in Tandoor using environment FDC_API_KEY variable.'}, status=429, return JsonResponse(
{'msg',
'API Key Rate Limit reached/exceeded, see https://api.data.gov/docs/rate-limits/ for more information. Configure your key in Tandoor using environment FDC_API_KEY variable.'},
status=429,
json_dumps_params={'indent': 4}) json_dumps_params={'indent': 4})
try: try:
@ -634,7 +644,8 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
if pt.fdc_id: if pt.fdc_id:
for fn in data['foodNutrients']: for fn in data['foodNutrients']:
if fn['nutrient']['id'] == pt.fdc_id: if fn['nutrient']['id'] == pt.fdc_id:
food_property_list.append(Property( food_property_list.append(
Property(
property_type_id=pt.id, property_type_id=pt.id,
property_amount=round(fn['amount'], 2), property_amount=round(fn['amount'], 2),
import_food_id=food.id, import_food_id=food.id,
@ -874,7 +885,9 @@ class RecipePagination(PageNumberPagination):
return super().paginate_queryset(queryset, request, view) return super().paginate_queryset(queryset, request, view)
def get_paginated_response(self, data): def get_paginated_response(self, data):
return Response(OrderedDict([ return Response(
OrderedDict(
[
('count', self.page.paginator.count), ('count', self.page.paginator.count),
('next', self.get_next_link()), ('next', self.get_next_link()),
('previous', self.get_previous_link()), ('previous', self.get_previous_link()),
@ -965,7 +978,8 @@ class RecipeViewSet(viewsets.ModelViewSet):
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
if self.request.GET.get('debug', False): if self.request.GET.get('debug', False):
return JsonResponse({ return JsonResponse(
{
'new': str(self.get_queryset().query), 'new': str(self.get_queryset().query),
}) })
return super().list(request, *args, **kwargs) return super().list(request, *args, **kwargs)
@ -1137,7 +1151,9 @@ class ShoppingListEntryViewSet(viewsets.ModelViewSet):
permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope] permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope]
query_params = [ query_params = [
QueryParam(name='id', description=_('Returns the shopping list entry with a primary key of id. Multiple values allowed.'), qtype='int'), QueryParam(name='id', description=_('Returns the shopping list entry with a primary key of id. Multiple values allowed.'), qtype='int'),
QueryParam(name='checked', description=_('Filter shopping list entries on checked. [''true'', ''false'', ''both'', ''<b>recent</b>'']<br> - ''recent'' includes unchecked items and recently completed items.') QueryParam(
name='checked', description=_(
'Filter shopping list entries on checked. [''true'', ''false'', ''both'', ''<b>recent</b>'']<br> - ''recent'' includes unchecked items and recently completed items.')
), ),
QueryParam(name='supermarket', description=_('Returns the shopping list entries sorted by supermarket category order.'), qtype='int'), QueryParam(name='supermarket', description=_('Returns the shopping list entries sorted by supermarket category order.'), qtype='int'),
] ]
@ -1329,7 +1345,8 @@ class CustomAuthToken(ObtainAuthToken):
throttle_classes = [AuthTokenThrottle] throttle_classes = [AuthTokenThrottle]
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
serializer = self.serializer_class(data=request.data, serializer = self.serializer_class(
data=request.data,
context={'request': request}) context={'request': request})
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
user = serializer.validated_data['user'] user = serializer.validated_data['user']
@ -1337,10 +1354,12 @@ class CustomAuthToken(ObtainAuthToken):
scope__contains='write').first(): scope__contains='write').first():
access_token = token access_token = token
else: else:
access_token = AccessToken.objects.create(user=user, token=f'tda_{str(uuid.uuid4()).replace("-", "_")}', access_token = AccessToken.objects.create(
user=user, token=f'tda_{str(uuid.uuid4()).replace("-", "_")}',
expires=(timezone.now() + timezone.timedelta(days=365 * 5)), expires=(timezone.now() + timezone.timedelta(days=365 * 5)),
scope='read write app') scope='read write app')
return Response({ return Response(
{
'id': access_token.id, 'id': access_token.id,
'token': access_token.token, 'token': access_token.token,
'scope': access_token.scope, 'scope': access_token.scope,
@ -1376,7 +1395,8 @@ class RecipeUrlImportView(APIView):
url = serializer.validated_data.get('url', None) url = serializer.validated_data.get('url', None)
data = unquote(serializer.validated_data.get('data', None)) data = unquote(serializer.validated_data.get('data', None))
if not url and not data: if not url and not data:
return Response({ return Response(
{
'error': True, 'error': True,
'msg': _('Nothing to do.') 'msg': _('Nothing to do.')
}, status=status.HTTP_400_BAD_REQUEST) }, status=status.HTTP_400_BAD_REQUEST)
@ -1384,7 +1404,8 @@ class RecipeUrlImportView(APIView):
elif url and not data: 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): if validators.url(url, public=True):
return Response({ return Response(
{
'recipe_json': get_from_youtube_scraper(url, request), 'recipe_json': get_from_youtube_scraper(url, request),
'recipe_images': [], 'recipe_images': [],
}, status=status.HTTP_200_OK) }, status=status.HTTP_200_OK)
@ -1392,7 +1413,8 @@ class RecipeUrlImportView(APIView):
'^(.)*/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}$', '^(.)*/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): url):
recipe_json = requests.get( recipe_json = requests.get(
url.replace('/view/recipe/', '/api/recipe/').replace(re.split('/view/recipe/[0-9]+', url)[1], url.replace('/view/recipe/', '/api/recipe/').replace(
re.split('/view/recipe/[0-9]+', url)[1],
'') + '?share=' + '') + '?share=' +
re.split('/view/recipe/[0-9]+', url)[1].replace('/', '')).json() re.split('/view/recipe/[0-9]+', url)[1].replace('/', '')).json()
recipe_json = clean_dict(recipe_json, 'id') recipe_json = clean_dict(recipe_json, 'id')
@ -1400,13 +1422,17 @@ class RecipeUrlImportView(APIView):
if serialized_recipe.is_valid(): if serialized_recipe.is_valid():
recipe = serialized_recipe.save() recipe = serialized_recipe.save()
if validators.url(recipe_json['image'], public=True): if validators.url(recipe_json['image'], public=True):
recipe.image = File(handle_image(request, recipe.image = File(
File(io.BytesIO(requests.get(recipe_json['image']).content), handle_image(
request,
File(
io.BytesIO(requests.get(recipe_json['image']).content),
name='image'), name='image'),
filetype=pathlib.Path(recipe_json['image']).suffix), filetype=pathlib.Path(recipe_json['image']).suffix),
name=f'{uuid.uuid4()}_{recipe.pk}{pathlib.Path(recipe_json["image"]).suffix}') name=f'{uuid.uuid4()}_{recipe.pk}{pathlib.Path(recipe_json["image"]).suffix}')
recipe.save() recipe.save()
return Response({ return Response(
{
'link': request.build_absolute_uri(reverse('view_recipe', args={recipe.pk})) 'link': request.build_absolute_uri(reverse('view_recipe', args={recipe.pk}))
}, status=status.HTTP_201_CREATED) }, status=status.HTTP_201_CREATED)
else: else:
@ -1415,19 +1441,22 @@ class RecipeUrlImportView(APIView):
scrape = scrape_me(url_path=url, wild_mode=True) scrape = scrape_me(url_path=url, wild_mode=True)
else: else:
return Response({ return Response(
{
'error': True, 'error': True,
'msg': _('Invalid Url') 'msg': _('Invalid Url')
}, status=status.HTTP_400_BAD_REQUEST) }, status=status.HTTP_400_BAD_REQUEST)
except NoSchemaFoundInWildMode: except NoSchemaFoundInWildMode:
pass pass
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
return Response({ return Response(
{
'error': True, 'error': True,
'msg': _('Connection Refused.') 'msg': _('Connection Refused.')
}, status=status.HTTP_400_BAD_REQUEST) }, status=status.HTTP_400_BAD_REQUEST)
except requests.exceptions.MissingSchema: except requests.exceptions.MissingSchema:
return Response({ return Response(
{
'error': True, 'error': True,
'msg': _('Bad URL Schema.') 'msg': _('Bad URL Schema.')
}, status=status.HTTP_400_BAD_REQUEST) }, status=status.HTTP_400_BAD_REQUEST)
@ -1446,13 +1475,15 @@ class RecipeUrlImportView(APIView):
scrape = text_scraper(text=data, url=found_url) scrape = text_scraper(text=data, url=found_url)
if scrape: if scrape:
return Response({ return Response(
{
'recipe_json': helper.get_from_scraper(scrape, request), 'recipe_json': helper.get_from_scraper(scrape, request),
'recipe_images': list(dict.fromkeys(get_images_from_soup(scrape.soup, url))), 'recipe_images': list(dict.fromkeys(get_images_from_soup(scrape.soup, url))),
}, status=status.HTTP_200_OK) }, status=status.HTTP_200_OK)
else: else:
return Response({ return Response(
{
'error': True, 'error': True,
'msg': _('No usable data could be found.') 'msg': _('No usable data could be found.')
}, status=status.HTTP_400_BAD_REQUEST) }, status=status.HTTP_400_BAD_REQUEST)
@ -1547,7 +1578,8 @@ def import_files(request):
return Response({'import_id': il.pk}, status=status.HTTP_200_OK) return Response({'import_id': il.pk}, status=status.HTTP_200_OK)
except NotImplementedError: except NotImplementedError:
return Response({'error': True, 'msg': _('Importing is not implemented for this provider')}, return Response(
{'error': True, 'msg': _('Importing is not implemented for this provider')},
status=status.HTTP_400_BAD_REQUEST) status=status.HTTP_400_BAD_REQUEST)
else: else:
return Response({'error': True, 'msg': form.errors}, status=status.HTTP_400_BAD_REQUEST) return Response({'error': True, 'msg': form.errors}, status=status.HTTP_400_BAD_REQUEST)
@ -1624,7 +1656,8 @@ def get_recipe_file(request, recipe_id):
@group_required('user') @group_required('user')
def sync_all(request): def sync_all(request):
if request.space.demo or settings.HOSTED: if request.space.demo or settings.HOSTED:
messages.add_message(request, messages.ERROR, messages.add_message(
request, messages.ERROR,
_('This feature is not yet available in the hosted version of tandoor!')) _('This feature is not yet available in the hosted version of tandoor!'))
return redirect('index') return redirect('index')
@ -1664,7 +1697,8 @@ def share_link(request, pk):
if request.space.allow_sharing and has_group_permission(request.user, ('user',)): if request.space.allow_sharing and has_group_permission(request.user, ('user',)):
recipe = get_object_or_404(Recipe, pk=pk, space=request.space) recipe = get_object_or_404(Recipe, pk=pk, space=request.space)
link = ShareLink.objects.create(recipe=recipe, created_by=request.user, space=request.space) link = ShareLink.objects.create(recipe=recipe, created_by=request.user, space=request.space)
return JsonResponse({'pk': pk, 'share': link.uuid, return JsonResponse(
{'pk': pk, 'share': link.uuid,
'link': request.build_absolute_uri(reverse('view_recipe', args=[pk, link.uuid]))}) 'link': request.build_absolute_uri(reverse('view_recipe', args=[pk, link.uuid]))})
else: else:
return JsonResponse({'error': 'sharing_disabled'}, status=403) return JsonResponse({'error': 'sharing_disabled'}, status=403)