mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-01 04:10:06 -05:00
WIP search date based filters
This commit is contained in:
@@ -82,12 +82,25 @@ class RecipeSearch():
|
||||
self._num_recent = int(self._params.get('num_recent', 0))
|
||||
self._include_children = str2bool(
|
||||
self._params.get('include_children', None))
|
||||
self._timescooked = self._params.get('timescooked', None)
|
||||
self._cookedon = self._params.get('cookedon', None)
|
||||
self._timescooked = self._params.get('timescooked_gte', None)
|
||||
self._timescooked_gte = self._params.get('timescooked_gte', None)
|
||||
self._timescooked_lte = self._params.get('timescooked_lte', None)
|
||||
|
||||
self._createdon = self._params.get('createdon', None)
|
||||
self._createdby = self._params.get('createdby', None)
|
||||
self._createdon_gte = self._params.get('createdon_gte', None)
|
||||
self._createdon_lte = self._params.get('createdon_lte', None)
|
||||
|
||||
self._updatedon = self._params.get('updatedon', None)
|
||||
self._viewedon = self._params.get('viewedon', None)
|
||||
self._updatedon_gte = self._params.get('updatedon_gte', None)
|
||||
self._updatedon_lte = self._params.get('updatedon_lte', None)
|
||||
|
||||
self._viewedon_gte = self._params.get('viewedon_gte', None)
|
||||
self._viewedon_lte = self._params.get('viewedon_lte', None)
|
||||
|
||||
self._cookedon_gte = self._params.get('cookedon_gte', None)
|
||||
self._cookedon_lte = self._params.get('cookedon_lte', None)
|
||||
|
||||
self._createdby = self._params.get('createdby', None)
|
||||
self._makenow = self._params.get('makenow', None)
|
||||
# this supports hidden feature to find recipes missing X ingredients
|
||||
if isinstance(self._makenow, bool) and self._makenow == True:
|
||||
@@ -134,12 +147,14 @@ class RecipeSearch():
|
||||
|
||||
self._build_sort_order()
|
||||
self._recently_viewed(num_recent=self._num_recent)
|
||||
self._cooked_on_filter(cooked_date=self._cookedon)
|
||||
self._created_on_filter(created_date=self._createdon)
|
||||
|
||||
self._cooked_on_filter()
|
||||
self._created_on_filter()
|
||||
self._updated_on_filter()
|
||||
self._viewed_on_filter()
|
||||
|
||||
self._created_by_filter(created_by_user_id=self._createdby)
|
||||
self._updated_on_filter(updated_date=self._updatedon)
|
||||
self._viewed_on_filter(viewed_date=self._viewedon)
|
||||
self._favorite_recipes(times_cooked=self._timescooked)
|
||||
self._favorite_recipes()
|
||||
self._new_recipes()
|
||||
self.keyword_filters(**self._keywords)
|
||||
self.food_filters(**self._foods)
|
||||
@@ -232,9 +247,9 @@ class RecipeSearch():
|
||||
query_filter |= Q(**{"%s" % f: self._string})
|
||||
self._queryset = self._queryset.filter(query_filter).distinct()
|
||||
|
||||
def _cooked_on_filter(self, cooked_date=None):
|
||||
if self._sort_includes('lastcooked') or cooked_date:
|
||||
lessthan = self._sort_includes('-lastcooked') or '-' in (cooked_date or [])[:1]
|
||||
def _cooked_on_filter(self):
|
||||
if self._sort_includes('lastcooked') or self._cookedon_gte or self._cookedon_lte:
|
||||
lessthan = self._sort_includes('-lastcooked') or self._cookedon_lte
|
||||
if lessthan:
|
||||
default = timezone.now() - timedelta(days=100000)
|
||||
else:
|
||||
@@ -242,15 +257,11 @@ class RecipeSearch():
|
||||
self._queryset = self._queryset.annotate(
|
||||
lastcooked=Coalesce(Max(Case(When(cooklog__created_by=self._request.user, cooklog__space=self._request.space, then='cooklog__created_at'))), Value(default))
|
||||
)
|
||||
if cooked_date is None:
|
||||
return
|
||||
|
||||
cooked_date = date(*[int(x)for x in cooked_date.split('-') if x != ''])
|
||||
|
||||
if lessthan:
|
||||
self._queryset = self._queryset.filter(lastcooked__date__lte=cooked_date).exclude(lastcooked=default)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(lastcooked__date__gte=cooked_date).exclude(lastcooked=default)
|
||||
if self._cookedon_lte:
|
||||
self._queryset = self._queryset.filter(lastcooked__date__lte=self._cookedon_lte).exclude(lastcooked=default)
|
||||
elif self._cookedon_gte:
|
||||
self._queryset = self._queryset.filter(lastcooked__date__gte=self._cookedon_gte ).exclude(lastcooked=default)
|
||||
|
||||
def _created_on_filter(self, created_date=None):
|
||||
if created_date is None:
|
||||
@@ -317,9 +328,9 @@ class RecipeSearch():
|
||||
)
|
||||
self._queryset = self._queryset.annotate(recent=Coalesce(Max(Case(When(pk__in=num_recent_recipes.values('recipe'), then='viewlog__pk'))), Value(0)))
|
||||
|
||||
def _favorite_recipes(self, times_cooked=None):
|
||||
if self._sort_includes('favorite') or times_cooked:
|
||||
less_than = '-' in (str(times_cooked) or []) and not self._sort_includes('-favorite')
|
||||
def _favorite_recipes(self):
|
||||
if self._sort_includes('favorite') or self._timescooked_gte or self._timescooked_lte:
|
||||
less_than = self._timescooked_lte and not self._sort_includes('-favorite')
|
||||
if less_than:
|
||||
default = 1000
|
||||
else:
|
||||
@@ -331,15 +342,13 @@ class RecipeSearch():
|
||||
.values('count')
|
||||
)
|
||||
self._queryset = self._queryset.annotate(favorite=Coalesce(Subquery(favorite_recipes), default))
|
||||
if times_cooked is None:
|
||||
return
|
||||
|
||||
if times_cooked == '0':
|
||||
if (self._timescooked_lte == 0 and self._timescooked_gte is None) or (self._timescooked_gte == 0 and self._timescooked_lte is None):
|
||||
self._queryset = self._queryset.filter(favorite=0)
|
||||
elif less_than:
|
||||
self._queryset = self._queryset.filter(favorite__lte=int(times_cooked.replace('-', ''))).exclude(favorite=0)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(favorite__gte=int(times_cooked))
|
||||
elif self._timescooked_lte:
|
||||
self._queryset = self._queryset.filter(favorite__lte=int(self._timescooked_lte)).exclude(favorite=0)
|
||||
elif self._timescooked_gte:
|
||||
self._queryset = self._queryset.filter(favorite__gte=int(self._timescooked_gte))
|
||||
|
||||
def keyword_filters(self, **kwargs):
|
||||
if all([kwargs[x] is None for x in kwargs]):
|
||||
|
||||
@@ -1079,101 +1079,55 @@ class RecipePagination(PageNumberPagination):
|
||||
@extend_schema_view(retrieve=extend_schema(parameters=[
|
||||
OpenApiParameter(name='share', type=str),
|
||||
]), list=extend_schema(parameters=[
|
||||
OpenApiParameter(name='query', description=_(
|
||||
'Query string matched (fuzzy) against recipe name. In the future also fulltext search.'), type=str),
|
||||
OpenApiParameter(name='keywords', description=_(
|
||||
'ID of keyword a recipe should have. For multiple repeat parameter. Equivalent to keywords_or'), type=int,
|
||||
many=True),
|
||||
OpenApiParameter(name='keywords_or',
|
||||
description=_('Keyword IDs, repeat for multiple. Return recipes with any of the keywords'),
|
||||
type=int, many=True),
|
||||
OpenApiParameter(name='keywords_and',
|
||||
description=_('Keyword IDs, repeat for multiple. Return recipes with all of the keywords.'),
|
||||
type=int, many=True),
|
||||
OpenApiParameter(name='keywords_or_not',
|
||||
description=_('Keyword IDs, repeat for multiple. Exclude recipes with any of the keywords.'),
|
||||
type=int, many=True),
|
||||
OpenApiParameter(name='keywords_and_not',
|
||||
description=_('Keyword IDs, repeat for multiple. Exclude recipes with all of the keywords.'),
|
||||
type=int, many=True),
|
||||
OpenApiParameter(name='foods', description=_('ID of food a recipe should have. For multiple repeat parameter.'),
|
||||
type=int, many=True),
|
||||
OpenApiParameter(name='foods_or',
|
||||
description=_('Food IDs, repeat for multiple. Return recipes with any of the foods'), type=int,
|
||||
many=True),
|
||||
OpenApiParameter(name='foods_and',
|
||||
description=_('Food IDs, repeat for multiple. Return recipes with all of the foods.'), type=int,
|
||||
many=True),
|
||||
OpenApiParameter(name='foods_or_not',
|
||||
description=_('Food IDs, repeat for multiple. Exclude recipes with any of the foods.'), type=int,
|
||||
many=True),
|
||||
OpenApiParameter(name='foods_and_not',
|
||||
description=_('Food IDs, repeat for multiple. Exclude recipes with all of the foods.'), type=int,
|
||||
many=True),
|
||||
OpenApiParameter(name='books', description=_('ID of book a recipe should be in. For multiple repeat parameter.'),
|
||||
type=int, many=True),
|
||||
OpenApiParameter(name='books_or',
|
||||
description=_('Book IDs, repeat for multiple. Return recipes with any of the books'), type=int,
|
||||
many=True),
|
||||
OpenApiParameter(name='books_and',
|
||||
description=_('Book IDs, repeat for multiple. Return recipes with all of the books.'), type=int,
|
||||
many=True),
|
||||
OpenApiParameter(name='books_or_not',
|
||||
description=_('Book IDs, repeat for multiple. Exclude recipes with any of the books.'), type=int,
|
||||
many=True),
|
||||
OpenApiParameter(name='books_and_not',
|
||||
description=_('Book IDs, repeat for multiple. Exclude recipes with all of the books.'), type=int,
|
||||
many=True),
|
||||
OpenApiParameter(name='query', description=_('Query string matched (fuzzy) against recipe name. In the future also fulltext search.'), type=str),
|
||||
|
||||
OpenApiParameter(name='keywords', description=_('ID of keyword a recipe should have. For multiple repeat parameter. Equivalent to keywords_or'), type=int, many=True),
|
||||
OpenApiParameter(name='keywords_or', description=_('Keyword IDs, repeat for multiple. Return recipes with any of the keywords'), type=int, many=True),
|
||||
OpenApiParameter(name='keywords_and', description=_('Keyword IDs, repeat for multiple. Return recipes with all of the keywords.'), type=int, many=True),
|
||||
OpenApiParameter(name='keywords_or_not', description=_('Keyword IDs, repeat for multiple. Exclude recipes with any of the keywords.'), type=int, many=True),
|
||||
OpenApiParameter(name='keywords_and_not', description=_('Keyword IDs, repeat for multiple. Exclude recipes with all of the keywords.'), type=int, many=True),
|
||||
|
||||
OpenApiParameter(name='foods', description=_('ID of food a recipe should have. For multiple repeat parameter.'), type=int, many=True),
|
||||
OpenApiParameter(name='foods_or', description=_('Food IDs, repeat for multiple. Return recipes with any of the foods'), type=int, many=True),
|
||||
OpenApiParameter(name='foods_and', description=_('Food IDs, repeat for multiple. Return recipes with all of the foods.'), type=int, many=True),
|
||||
OpenApiParameter(name='foods_or_not', description=_('Food IDs, repeat for multiple. Exclude recipes with any of the foods.'), type=int, many=True),
|
||||
OpenApiParameter(name='foods_and_not', description=_('Food IDs, repeat for multiple. Exclude recipes with all of the foods.'), type=int, many=True),
|
||||
|
||||
OpenApiParameter(name='books', description=_('ID of book a recipe should be in. For multiple repeat parameter.'), type=int, many=True),
|
||||
OpenApiParameter(name='books_or', description=_('Book IDs, repeat for multiple. Return recipes with any of the books'), type=int, many=True),
|
||||
OpenApiParameter(name='books_and', description=_('Book IDs, repeat for multiple. Return recipes with all of the books.'), type=int, many=True),
|
||||
OpenApiParameter(name='books_or_not', description=_('Book IDs, repeat for multiple. Exclude recipes with any of the books.'), type=int, many=True),
|
||||
OpenApiParameter(name='books_and_not', description=_('Book IDs, repeat for multiple. Exclude recipes with all of the books.'), type=int, many=True),
|
||||
|
||||
OpenApiParameter(name='units', description=_('ID of unit a recipe should have.'), type=int),
|
||||
OpenApiParameter(name='internal',
|
||||
description=_('If only internal recipes should be returned. [''true''/''<b>false</b>'']'),
|
||||
type=bool),
|
||||
|
||||
OpenApiParameter(name='rating', description=_( 'Exact rating of recipe'), type=int),
|
||||
OpenApiParameter(name='rating_gte', description=_( 'Rating a recipe should have or greater. '), type=int),
|
||||
OpenApiParameter(name='rating_lte', description=_( 'Rating a recipe should have or smaller.'), type=int),
|
||||
OpenApiParameter(name='rating', description=_('Exact rating of recipe'), type=int),
|
||||
OpenApiParameter(name='rating_gte', description=_('Rating a recipe should have or greater.'), type=int),
|
||||
OpenApiParameter(name='rating_lte', description=_('Rating a recipe should have or smaller.'), type=int),
|
||||
|
||||
OpenApiParameter(name='random',
|
||||
description=_('Returns the results in randomized order. [''true''/''<b>false</b>'']')),
|
||||
OpenApiParameter(name='new',
|
||||
description=_('Returns new results first in search results. [''true''/''<b>false</b>'']')),
|
||||
OpenApiParameter(name='num_recent', description=_(
|
||||
'Returns the given number of recently viewed recipes before search results (if given)'), type=int),
|
||||
OpenApiParameter(name='timescooked', description=_(
|
||||
'Filter recipes cooked X times or more. Negative values returns cooked less than X times'), type=int),
|
||||
OpenApiParameter(
|
||||
name='cookedon',
|
||||
description=_('Filter recipes last cooked on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.'),
|
||||
type=str,
|
||||
examples=[DateExample, BeforeDateExample]
|
||||
),
|
||||
OpenApiParameter(
|
||||
name='createdon',
|
||||
description=_('Filter recipes created on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.'),
|
||||
type=str,
|
||||
examples=[DateExample, BeforeDateExample]
|
||||
),
|
||||
OpenApiParameter(
|
||||
name='createdby',
|
||||
description=_('Filter recipes for ones created by the given user ID'),
|
||||
type=int,
|
||||
),
|
||||
OpenApiParameter(
|
||||
name='updatedon',
|
||||
description=_('Filter recipes updated on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.'),
|
||||
type=str,
|
||||
examples=[DateExample, BeforeDateExample]
|
||||
),
|
||||
OpenApiParameter(
|
||||
name='viewedon',
|
||||
description=_(
|
||||
'Filter recipes lasts viewed on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.'),
|
||||
type=str,
|
||||
examples=[DateExample, BeforeDateExample]
|
||||
),
|
||||
OpenApiParameter(name='makenow',
|
||||
description=_('Filter recipes that can be made with OnHand food. [''true''/''<b>false</b>'']'),
|
||||
type=bool),
|
||||
OpenApiParameter(name='timescooked_gte', description=_('Filter recipes cooked X times or more.'), type=int),
|
||||
OpenApiParameter(name='timescooked_lte', description=_('Filter recipes cooked X times or less.'), type=int),
|
||||
|
||||
OpenApiParameter(name='createdon', description=_('Filter recipes created on the given date.'), type=OpenApiTypes.DATE, ),
|
||||
OpenApiParameter(name='createdon_gte', description=_('Filter recipes created on the given date or after.'), type=OpenApiTypes.DATE, ),
|
||||
OpenApiParameter(name='createdon_lte', description=_('Filter recipes created on the given date or before.'), type=OpenApiTypes.DATE, ),
|
||||
|
||||
OpenApiParameter(name='updatedon', description=_('Filter recipes updated on the given date.'), type=OpenApiTypes.DATE, ),
|
||||
OpenApiParameter(name='updatedon_gte', description=_('Filter recipes updated on the given date.'), type=OpenApiTypes.DATE, ),
|
||||
OpenApiParameter(name='updatedon_lte', description=_('Filter recipes updated on the given date.'), type=OpenApiTypes.DATE, ),
|
||||
|
||||
OpenApiParameter(name='cookedon_gte', description=_('Filter recipes last cooked on the given date or after.'), type=OpenApiTypes.DATE),
|
||||
OpenApiParameter(name='cookedon_lte', description=_('Filter recipes last cooked on the given date or before.'), type=OpenApiTypes.DATE),
|
||||
|
||||
OpenApiParameter(name='viewedon_gte', description=_('Filter recipes lasts viewed on the given date.'), type=OpenApiTypes.DATE, ),
|
||||
OpenApiParameter(name='viewedon_lte', description=_('Filter recipes lasts viewed on the given date.'), type=OpenApiTypes.DATE, ),
|
||||
|
||||
OpenApiParameter(name='createdby', description=_('Filter recipes for ones created by the given user ID'), type=int),
|
||||
OpenApiParameter(name='internal', description=_('If only internal recipes should be returned. [''true''/''<b>false</b>'']'), type=bool),
|
||||
OpenApiParameter(name='random', description=_('Returns the results in randomized order. [''true''/''<b>false</b>'']')),
|
||||
OpenApiParameter(name='new', description=_('Returns new results first in search results. [''true''/''<b>false</b>'']')),
|
||||
OpenApiParameter(name='num_recent', description=_('Returns the given number of recently viewed recipes before search results (if given)'), type=int),
|
||||
OpenApiParameter(name='makenow', description=_('Filter recipes that can be made with OnHand food. [''true''/''<b>false</b>'']'), type=bool),
|
||||
]))
|
||||
class RecipeViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
queryset = Recipe.objects
|
||||
@@ -1944,7 +1898,8 @@ class FdcSearchView(APIView):
|
||||
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
|
||||
|
||||
@extend_schema(responses=FdcQuerySerializer(many=False),
|
||||
parameters=[OpenApiParameter(name='query', type=str), OpenApiParameter(name='dataType', description='options: Branded,Foundation,Survey (FNDDS),SR Legacy', type=str, many=True)])
|
||||
parameters=[OpenApiParameter(name='query', type=str),
|
||||
OpenApiParameter(name='dataType', description='options: Branded,Foundation,Survey (FNDDS),SR Legacy', type=str, many=True)])
|
||||
def get(self, request, format=None):
|
||||
query = self.request.query_params.get('query', None)
|
||||
if query is not None:
|
||||
@@ -1952,7 +1907,6 @@ class FdcSearchView(APIView):
|
||||
|
||||
response = requests.get(f'https://api.nal.usda.gov/fdc/v1/foods/search?api_key={FDC_API_KEY}&query={query}&dataType={",".join(data_types)}')
|
||||
|
||||
|
||||
if response.status_code == 429:
|
||||
return JsonResponse(
|
||||
{
|
||||
|
||||
@@ -1195,9 +1195,12 @@ export interface ApiRecipeListRequest {
|
||||
booksAndNot?: Array<number>;
|
||||
booksOr?: Array<number>;
|
||||
booksOrNot?: Array<number>;
|
||||
cookedon?: string;
|
||||
cookedonGte?: Date;
|
||||
cookedonLte?: Date;
|
||||
createdby?: number;
|
||||
createdon?: string;
|
||||
createdon?: Date;
|
||||
createdonGte?: Date;
|
||||
createdonLte?: Date;
|
||||
foods?: Array<number>;
|
||||
foodsAnd?: Array<number>;
|
||||
foodsAndNot?: Array<number>;
|
||||
@@ -1219,10 +1222,14 @@ export interface ApiRecipeListRequest {
|
||||
rating?: number;
|
||||
ratingGte?: number;
|
||||
ratingLte?: number;
|
||||
timescooked?: number;
|
||||
timescookedGte?: number;
|
||||
timescookedLte?: number;
|
||||
units?: number;
|
||||
updatedon?: string;
|
||||
viewedon?: string;
|
||||
updatedon?: Date;
|
||||
updatedonGte?: Date;
|
||||
updatedonLte?: Date;
|
||||
viewedonGte?: Date;
|
||||
viewedonLte?: Date;
|
||||
}
|
||||
|
||||
export interface ApiRecipePartialUpdateRequest {
|
||||
@@ -8794,8 +8801,12 @@ export class ApiApi extends runtime.BaseAPI {
|
||||
queryParameters['books_or_not'] = requestParameters['booksOrNot'];
|
||||
}
|
||||
|
||||
if (requestParameters['cookedon'] != null) {
|
||||
queryParameters['cookedon'] = requestParameters['cookedon'];
|
||||
if (requestParameters['cookedonGte'] != null) {
|
||||
queryParameters['cookedon_gte'] = (requestParameters['cookedonGte'] as any).toISOString().substring(0,10);
|
||||
}
|
||||
|
||||
if (requestParameters['cookedonLte'] != null) {
|
||||
queryParameters['cookedon_lte'] = (requestParameters['cookedonLte'] as any).toISOString().substring(0,10);
|
||||
}
|
||||
|
||||
if (requestParameters['createdby'] != null) {
|
||||
@@ -8803,7 +8814,15 @@ export class ApiApi extends runtime.BaseAPI {
|
||||
}
|
||||
|
||||
if (requestParameters['createdon'] != null) {
|
||||
queryParameters['createdon'] = requestParameters['createdon'];
|
||||
queryParameters['createdon'] = (requestParameters['createdon'] as any).toISOString().substring(0,10);
|
||||
}
|
||||
|
||||
if (requestParameters['createdonGte'] != null) {
|
||||
queryParameters['createdon_gte'] = (requestParameters['createdonGte'] as any).toISOString().substring(0,10);
|
||||
}
|
||||
|
||||
if (requestParameters['createdonLte'] != null) {
|
||||
queryParameters['createdon_lte'] = (requestParameters['createdonLte'] as any).toISOString().substring(0,10);
|
||||
}
|
||||
|
||||
if (requestParameters['foods'] != null) {
|
||||
@@ -8890,8 +8909,12 @@ export class ApiApi extends runtime.BaseAPI {
|
||||
queryParameters['rating_lte'] = requestParameters['ratingLte'];
|
||||
}
|
||||
|
||||
if (requestParameters['timescooked'] != null) {
|
||||
queryParameters['timescooked'] = requestParameters['timescooked'];
|
||||
if (requestParameters['timescookedGte'] != null) {
|
||||
queryParameters['timescooked_gte'] = requestParameters['timescookedGte'];
|
||||
}
|
||||
|
||||
if (requestParameters['timescookedLte'] != null) {
|
||||
queryParameters['timescooked_lte'] = requestParameters['timescookedLte'];
|
||||
}
|
||||
|
||||
if (requestParameters['units'] != null) {
|
||||
@@ -8899,11 +8922,23 @@ export class ApiApi extends runtime.BaseAPI {
|
||||
}
|
||||
|
||||
if (requestParameters['updatedon'] != null) {
|
||||
queryParameters['updatedon'] = requestParameters['updatedon'];
|
||||
queryParameters['updatedon'] = (requestParameters['updatedon'] as any).toISOString().substring(0,10);
|
||||
}
|
||||
|
||||
if (requestParameters['viewedon'] != null) {
|
||||
queryParameters['viewedon'] = requestParameters['viewedon'];
|
||||
if (requestParameters['updatedonGte'] != null) {
|
||||
queryParameters['updatedon_gte'] = (requestParameters['updatedonGte'] as any).toISOString().substring(0,10);
|
||||
}
|
||||
|
||||
if (requestParameters['updatedonLte'] != null) {
|
||||
queryParameters['updatedon_lte'] = (requestParameters['updatedonLte'] as any).toISOString().substring(0,10);
|
||||
}
|
||||
|
||||
if (requestParameters['viewedonGte'] != null) {
|
||||
queryParameters['viewedon_gte'] = (requestParameters['viewedonGte'] as any).toISOString().substring(0,10);
|
||||
}
|
||||
|
||||
if (requestParameters['viewedonLte'] != null) {
|
||||
queryParameters['viewedon_lte'] = (requestParameters['viewedonLte'] as any).toISOString().substring(0,10);
|
||||
}
|
||||
|
||||
const headerParameters: runtime.HTTPHeaders = {};
|
||||
|
||||
@@ -23,14 +23,16 @@
|
||||
<v-expansion-panel-text>
|
||||
<v-form :disabled="loading" class="mt-4">
|
||||
|
||||
<template v-for="filter in Object.values(filters)">
|
||||
<component :="filter" :is="filter.is" density="compact" v-model="filter.modelValue" v-if="isFilterVisible(filter)">
|
||||
<template #append>
|
||||
<v-btn icon="fa-solid fa-times" size="small" variant="plain"
|
||||
@click="filter.enabled = false; filter.modelValue = filter.default"></v-btn>
|
||||
</template>
|
||||
</component>
|
||||
</template>
|
||||
<div v-for="filter in Object.values(filters)" :key="filter.id">
|
||||
<template v-if="filter.enabled">
|
||||
<component :="filter" :is="filter.is" density="compact" v-model="filter.modelValue">
|
||||
<template #append>
|
||||
<v-btn icon="fa-solid fa-times" size="small" variant="plain"
|
||||
@click="filter.enabled = false; filter.modelValue = filter.default"></v-btn>
|
||||
</template>
|
||||
</component>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<v-divider class="mt-2 mb-2"></v-divider>
|
||||
|
||||
@@ -87,7 +89,9 @@
|
||||
>
|
||||
<template #item.image="{item}">
|
||||
<v-avatar :image="item.image" size="x-large" class="mt-1 mb-1" v-if="item.image"></v-avatar>
|
||||
<v-avatar color="primary" variant="tonal" size="x-large" class="mt-1 mb-1" v-else><random-icon></random-icon></v-avatar>
|
||||
<v-avatar color="primary" variant="tonal" size="x-large" class="mt-1 mb-1" v-else>
|
||||
<random-icon></random-icon>
|
||||
</v-avatar>
|
||||
</template>
|
||||
|
||||
<template #item.keywords="{item}">
|
||||
@@ -136,7 +140,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {computed, nextTick, onMounted, ref, watch} from "vue";
|
||||
import {computed, nextTick, onMounted, ref, toRaw, watch} from "vue";
|
||||
import {ApiApi, ApiRecipeListRequest, CustomFilter, RecipeOverview} from "@/openapi";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
|
||||
@@ -152,9 +156,9 @@ import RecipeCard from "@/components/display/RecipeCard.vue";
|
||||
import {useDisplay} from "vuetify";
|
||||
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
|
||||
import {useRouteQuery} from "@vueuse/router";
|
||||
import {toNumberArray} from "@/utils/utils";
|
||||
import {stringToBool, toNumberArray} from "@/utils/utils";
|
||||
import RandomIcon from "@/components/display/RandomIcon.vue";
|
||||
import {VRating, VSelect} from "vuetify/components";
|
||||
import {VRating, VSelect, VTextField} from "vuetify/components";
|
||||
import RatingField from "@/components/inputs/RatingField.vue";
|
||||
|
||||
const {t} = useI18n()
|
||||
@@ -170,7 +174,7 @@ const pageSize = useRouteQuery('pageSize', useUserPreferenceStore().deviceSettin
|
||||
*/
|
||||
const filters = ref({
|
||||
keywords: {
|
||||
value: 'keywords',
|
||||
id: 'keywords',
|
||||
label: 'Keyword (any)',
|
||||
hint: 'Any of the given keywords',
|
||||
enabled: false,
|
||||
@@ -183,7 +187,7 @@ const filters = ref({
|
||||
searchOnLoad: true
|
||||
},
|
||||
keywordsAnd: {
|
||||
value: 'keywordsAnd',
|
||||
id: 'keywordsAnd',
|
||||
label: 'Keyword (all)',
|
||||
hint: 'All of the given keywords',
|
||||
enabled: false,
|
||||
@@ -196,33 +200,33 @@ const filters = ref({
|
||||
searchOnLoad: true
|
||||
},
|
||||
keywordsOrNot: {
|
||||
value: 'keywordsOrNot',
|
||||
id: 'keywordsOrNot',
|
||||
label: 'Keyword exclude (any)',
|
||||
hint: 'Exclude recipes with any of the given keywords',
|
||||
enabled: false,
|
||||
default: [],
|
||||
is: ModelSelect,
|
||||
model: 'Keyword',
|
||||
modelValue: useRouteQuery('keywordsOrNot ', [], {transform: toNumberArray}),
|
||||
modelValue: useRouteQuery('keywordsOrNot', [], {transform: toNumberArray}),
|
||||
mode: 'tags',
|
||||
object: false,
|
||||
searchOnLoad: true
|
||||
},
|
||||
keywordsAndNot: {
|
||||
value: 'keywordsAndNot',
|
||||
id: 'keywordsAndNot',
|
||||
label: 'Keyword exclude (all)',
|
||||
hint: 'Exclude recipes with all of the given keywords',
|
||||
enabled: false,
|
||||
default: [],
|
||||
is: ModelSelect,
|
||||
model: 'Keyword',
|
||||
modelValue: useRouteQuery('keywordsAndNot ', [], {transform: toNumberArray}),
|
||||
modelValue: useRouteQuery('keywordsAndNot', [], {transform: toNumberArray}),
|
||||
mode: 'tags',
|
||||
object: false,
|
||||
searchOnLoad: true
|
||||
},
|
||||
foods: {
|
||||
value: 'foods',
|
||||
id: 'foods',
|
||||
label: 'Foods (any)',
|
||||
hint: 'Any of the given foods',
|
||||
enabled: false,
|
||||
@@ -235,7 +239,7 @@ const filters = ref({
|
||||
searchOnLoad: true
|
||||
},
|
||||
foodsAnd: {
|
||||
value: 'foodsAnd',
|
||||
id: 'foodsAnd',
|
||||
label: 'Food (all)',
|
||||
hint: 'All of the given foods',
|
||||
enabled: false,
|
||||
@@ -248,33 +252,33 @@ const filters = ref({
|
||||
searchOnLoad: true
|
||||
},
|
||||
foodsOrNot: {
|
||||
value: 'foodsOrNot',
|
||||
id: 'foodsOrNot',
|
||||
label: 'Food exclude (any)',
|
||||
hint: 'Exclude recipes with any of the given foods',
|
||||
enabled: false,
|
||||
default: [],
|
||||
is: ModelSelect,
|
||||
model: 'Food',
|
||||
modelValue: useRouteQuery('foodsOrNot ', [], {transform: toNumberArray}),
|
||||
modelValue: useRouteQuery('foodsOrNot', [], {transform: toNumberArray}),
|
||||
mode: 'tags',
|
||||
object: false,
|
||||
searchOnLoad: true
|
||||
},
|
||||
foodsAndNot: {
|
||||
value: 'foodsAndNot',
|
||||
id: 'foodsAndNot',
|
||||
label: 'Food exclude (all)',
|
||||
hint: 'Exclude recipes with all of the given foods',
|
||||
enabled: false,
|
||||
default: [],
|
||||
is: ModelSelect,
|
||||
model: 'Food',
|
||||
modelValue: useRouteQuery('foodsAndNot ', [], {transform: toNumberArray}),
|
||||
modelValue: useRouteQuery('foodsAndNot', [], {transform: toNumberArray}),
|
||||
mode: 'tags',
|
||||
object: false,
|
||||
searchOnLoad: true
|
||||
},
|
||||
books: {
|
||||
value: 'books',
|
||||
id: 'books',
|
||||
label: 'Book (any)',
|
||||
hint: 'Recipes that are in any of the given books',
|
||||
enabled: false,
|
||||
@@ -287,7 +291,7 @@ const filters = ref({
|
||||
searchOnLoad: true
|
||||
},
|
||||
booksAnd: {
|
||||
value: 'booksAnd',
|
||||
id: 'booksAnd',
|
||||
label: 'Book (all)',
|
||||
hint: 'Recipes that are in all of the given books',
|
||||
enabled: false,
|
||||
@@ -300,33 +304,46 @@ const filters = ref({
|
||||
searchOnLoad: true
|
||||
},
|
||||
booksOrNot: {
|
||||
value: 'booksOrNot',
|
||||
id: 'booksOrNot',
|
||||
label: 'Book exclude (any)',
|
||||
hint: 'Exclude recipes with any of the given books',
|
||||
enabled: false,
|
||||
default: [],
|
||||
is: ModelSelect,
|
||||
model: 'RecipeBook',
|
||||
modelValue: useRouteQuery('booksOrNot ', [], {transform: toNumberArray}),
|
||||
modelValue: useRouteQuery('booksOrNot', [], {transform: toNumberArray}),
|
||||
mode: 'tags',
|
||||
object: false,
|
||||
searchOnLoad: true
|
||||
},
|
||||
booksAndNot: {
|
||||
value: 'booksAndNot',
|
||||
id: 'booksAndNot',
|
||||
label: 'Book exclude (all)',
|
||||
hint: 'Exclude recipes with all of the given books',
|
||||
enabled: false,
|
||||
default: [],
|
||||
is: ModelSelect,
|
||||
model: 'RecipeBook',
|
||||
modelValue: useRouteQuery('booksAndNot ', [], {transform: toNumberArray}),
|
||||
modelValue: useRouteQuery('booksAndNot', [], {transform: toNumberArray}),
|
||||
mode: 'tags',
|
||||
object: false,
|
||||
searchOnLoad: true
|
||||
},
|
||||
createdby: {
|
||||
id: 'createdby',
|
||||
label: 'Created By',
|
||||
hint: 'Recipes created by the selected user',
|
||||
enabled: false,
|
||||
default: undefined,
|
||||
is: ModelSelect,
|
||||
model: 'User',
|
||||
modelValue: useRouteQuery('createdby', undefined, {transform: Number}),
|
||||
mode: 'single',
|
||||
object: false,
|
||||
searchOnLoad: true
|
||||
},
|
||||
units: {
|
||||
value: 'units',
|
||||
id: 'units',
|
||||
label: 'Unit (any)',
|
||||
hint: 'Recipes that contain any of the given units',
|
||||
enabled: false,
|
||||
@@ -339,53 +356,101 @@ const filters = ref({
|
||||
searchOnLoad: true
|
||||
},
|
||||
internal: {
|
||||
value: 'internal',
|
||||
id: 'internal',
|
||||
label: 'Hide External',
|
||||
hint: 'Hide external recipes',
|
||||
enabled: false,
|
||||
default: [],
|
||||
default: "false",
|
||||
is: VSelect,
|
||||
items: [{value: true, title: 'Yes'}, {value: false, title: 'No'}],
|
||||
modelValue: useRouteQuery('internal ', "false"),
|
||||
items: [{value: "true", title: 'Yes'}, {value: "false", title: 'No'}],
|
||||
modelValue: useRouteQuery('internal', "false"),
|
||||
},
|
||||
rating: {
|
||||
value: 'rating',
|
||||
id: 'rating',
|
||||
label: 'Rating (exact)',
|
||||
hint: 'Recipes with the exact rating',
|
||||
enabled: false,
|
||||
default: 0,
|
||||
default: undefined,
|
||||
is: RatingField,
|
||||
modelValue: useRouteQuery('rating ', 0),
|
||||
modelValue: useRouteQuery('rating', undefined, {transform: Number}),
|
||||
},
|
||||
rating_gte: {
|
||||
value: 'rating_gte',
|
||||
ratingGte: {
|
||||
id: 'ratingGte',
|
||||
label: 'Rating (>=)',
|
||||
hint: 'Recipes with the given or a greater rating',
|
||||
enabled: false,
|
||||
default: 0,
|
||||
default: undefined,
|
||||
is: RatingField,
|
||||
modelValue: useRouteQuery('rating_gte ', 0),
|
||||
modelValue: useRouteQuery('ratingGte', undefined, {transform: Number}),
|
||||
},
|
||||
rating_lte: {
|
||||
value: 'rating_lte',
|
||||
ratingLte: {
|
||||
id: 'ratingLte',
|
||||
label: 'Rating (<=)',
|
||||
hint: 'Recipes with the given or a smaller rating',
|
||||
enabled: false,
|
||||
default: 0,
|
||||
default: undefined,
|
||||
is: RatingField,
|
||||
modelValue: useRouteQuery('rating_lte ', 0),
|
||||
modelValue: useRouteQuery('ratingLte', undefined, {transform: Number}),
|
||||
},
|
||||
timescookedGte: {
|
||||
id: 'timescookedGte',
|
||||
label: 'Times Cooked (>=)',
|
||||
hint: 'Recipes that were cooked at least X times',
|
||||
enabled: false,
|
||||
default: undefined,
|
||||
is: VNumberInput,
|
||||
modelValue: useRouteQuery('timescookedGte', undefined, {transform: Number}),
|
||||
},
|
||||
timescookedLte: {
|
||||
id: 'timescookedLte',
|
||||
label: 'Times Cooked (<=)',
|
||||
hint: 'Recipes that were cooked at most X times',
|
||||
enabled: false,
|
||||
default: undefined,
|
||||
is: VNumberInput,
|
||||
modelValue: useRouteQuery('timescookedLte', undefined, {transform: Number}),
|
||||
},
|
||||
makenow: {
|
||||
id: 'makenow',
|
||||
label: 'Foods on Hand',
|
||||
hint: 'Only recipes were all foods (or its substitutes) are marked as on hand',
|
||||
enabled: false,
|
||||
default: "false",
|
||||
is: VSelect,
|
||||
items: [{value: "true", title: 'Yes'}, {value: "false", title: 'No'}],
|
||||
modelValue: useRouteQuery('makenow', "false"),
|
||||
},
|
||||
|
||||
// cookedonGte: {
|
||||
// id: 'cookedonGte',
|
||||
// label: 'Cooked after',
|
||||
// hint: 'Only recipes that were cooked on or after the given date.',
|
||||
// enabled: false,
|
||||
// default: null,
|
||||
// is: VTextField,
|
||||
// type: "date",
|
||||
// modelValue: useRouteQuery('cookedonGte', null, {transform: Date}),
|
||||
// },
|
||||
// cookedonLte: {
|
||||
// id: 'cookedonLte',
|
||||
// label: 'Cooked before',
|
||||
// hint: 'Only recipes that were cooked on or before the given date.',
|
||||
// enabled: false,
|
||||
// default: null,
|
||||
// is: VDateInput,
|
||||
// modelValue: useRouteQuery('cookedonLte', null, {transform: Date}),
|
||||
// },
|
||||
})
|
||||
|
||||
/**
|
||||
* filters that are not yet enabled
|
||||
*/
|
||||
const availableFilters = computed(() => {
|
||||
let f: Array<{value: string, title: string}> = []
|
||||
let f: Array<{ value: string, title: string }> = []
|
||||
Object.entries(filters.value).forEach((entry) => {
|
||||
let [key, filter] = entry
|
||||
if (!isFilterVisible(filter)) {
|
||||
f.push({value: filter.value, title: filter.label})
|
||||
if (!filter.enabled) {
|
||||
f.push({value: filter.id, title: filter.label})
|
||||
}
|
||||
})
|
||||
return f
|
||||
@@ -404,7 +469,6 @@ const tableHeaders = computed(() => {
|
||||
if (mdAndUp.value) {
|
||||
headers.push({title: t('Keywords'), key: 'keywords',},)
|
||||
}
|
||||
|
||||
headers.push({title: t('Actions'), key: 'action', width: '1%', noBreak: true, align: 'end'},)
|
||||
|
||||
return headers
|
||||
@@ -428,6 +492,7 @@ watch(() => query.value, () => {
|
||||
* perform initial search on mounted
|
||||
*/
|
||||
onMounted(() => {
|
||||
enableFiltersWithValues()
|
||||
searchRecipes({page: page.value})
|
||||
})
|
||||
|
||||
@@ -446,7 +511,9 @@ function searchRecipes(options: VDataTableUpdateOptions) {
|
||||
} as ApiRecipeListRequest
|
||||
|
||||
Object.values(filters.value).forEach((filter) => {
|
||||
searchParameters[filter.value] = filter.modelValue
|
||||
if (!isFilterDefaultValue(filter)) {
|
||||
searchParameters[filter.id] = filter.modelValue
|
||||
}
|
||||
})
|
||||
|
||||
api.apiRecipeList(searchParameters).then((r) => {
|
||||
@@ -483,6 +550,35 @@ function handleRowClick(event: PointerEvent, data: any) {
|
||||
router.push({name: 'RecipeViewPage', params: {id: recipes.value[data.index].id}})
|
||||
}
|
||||
|
||||
/**
|
||||
* enable UI of filters that have a value that is not the default for the given filter
|
||||
*/
|
||||
function enableFiltersWithValues() {
|
||||
Object.values(filters.value).forEach((filter) => {
|
||||
if (!isFilterDefaultValue(filter)) {
|
||||
filter.enabled = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* determines if the current value of a filter is its default value
|
||||
* @param filter
|
||||
*/
|
||||
function isFilterDefaultValue(filter: any) {
|
||||
if (Array.isArray(filter.default) && Array.isArray(filter.modelValue)) {
|
||||
return filter.default.length == filter.modelValue.length
|
||||
} else if (isNaN(filter.default) && isNaN(filter.modelValue)) {
|
||||
return true
|
||||
} else {
|
||||
return toRaw(filter.default) === filter.modelValue
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// --------- Logic for saved filters ---------
|
||||
// -------------------------------------------
|
||||
|
||||
/**
|
||||
* triggered by save button, if filter exists update it, if not open dialog to create a new filter
|
||||
*/
|
||||
@@ -504,19 +600,6 @@ function saveCustomFilter() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* determines if the filter should be visible because its either enabled or not the default value
|
||||
* @param filter
|
||||
*/
|
||||
function isFilterVisible(filter: any) {
|
||||
if (!filter.enabled && filter.modelValue.length > 0) {
|
||||
filter.enabled = true
|
||||
}
|
||||
return filter.enabled
|
||||
}
|
||||
|
||||
// TODO temporary function to convert old saved search format, either make proper db table or convert to native new format
|
||||
|
||||
/**
|
||||
* create new filter
|
||||
*/
|
||||
@@ -542,6 +625,8 @@ function loadCustomFilter() {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// TODO temporary function to convert old saved search format, either make proper db table or convert to native new format
|
||||
/**
|
||||
* turn data in the format of a CustomFilter into the format needed for api request
|
||||
* @param customFilterParams
|
||||
|
||||
@@ -39,7 +39,7 @@ export function uploadRecipeImage(recipeId: number, file: File) {
|
||||
body: formData
|
||||
}).then(r => {
|
||||
r.json().then(r => {
|
||||
return RecipeImageFromJSON(r)
|
||||
return RecipeImageFromJSON(r)
|
||||
})
|
||||
|
||||
}).catch(err => {
|
||||
@@ -54,6 +54,20 @@ export function uploadRecipeImage(recipeId: number, file: File) {
|
||||
* useful for query parameter transformation
|
||||
* @param param
|
||||
*/
|
||||
export function toNumberArray(param: string | string[]): number[]{
|
||||
export function toNumberArray(param: string | string[]): number[] {
|
||||
return Array.isArray(param) ? param.map(Number) : [parseInt(param)];
|
||||
}
|
||||
|
||||
/**
|
||||
* convert a string to a bool if its either "true" or "false", return undefined otherwise
|
||||
* @param param
|
||||
*/
|
||||
export function stringToBool(param: string): boolean | undefined {
|
||||
if (param == "true") {
|
||||
return true
|
||||
} else if (param == "false") {
|
||||
return false
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user