@ -4,13 +4,15 @@ from recipes import settings
|
|||||||
from django.contrib.postgres.search import (
|
from django.contrib.postgres.search import (
|
||||||
SearchQuery, SearchRank, TrigramSimilarity
|
SearchQuery, SearchRank, TrigramSimilarity
|
||||||
)
|
)
|
||||||
from django.db.models import Count, Q, Subquery, Case, When, Value
|
from django.db.models import Count, Max, Q, Subquery, Case, When, Value
|
||||||
from django.utils import translation
|
from django.utils import translation
|
||||||
|
|
||||||
from cookbook.managers import DICTIONARY
|
from cookbook.managers import DICTIONARY
|
||||||
from cookbook.models import Food, Keyword, ViewLog
|
from cookbook.models import Food, Keyword, Recipe, ViewLog
|
||||||
|
|
||||||
|
|
||||||
|
# TODO create extensive tests to make sure ORs ANDs and various filters, sorting, etc work as expected
|
||||||
|
# TODO consider creating a simpleListRecipe API that only includes minimum of recipe info and minimal filtering
|
||||||
def search_recipes(request, queryset, params):
|
def search_recipes(request, queryset, params):
|
||||||
search_prefs = request.user.searchpreference
|
search_prefs = request.user.searchpreference
|
||||||
search_string = params.get('query', '')
|
search_string = params.get('query', '')
|
||||||
@ -19,6 +21,7 @@ def search_recipes(request, queryset, params):
|
|||||||
search_foods = params.getlist('foods', [])
|
search_foods = params.getlist('foods', [])
|
||||||
search_books = params.getlist('books', [])
|
search_books = params.getlist('books', [])
|
||||||
|
|
||||||
|
# TODO I think default behavior should be 'AND' which is how most sites operate with facet/filters based on results
|
||||||
search_keywords_or = params.get('keywords_or', True)
|
search_keywords_or = params.get('keywords_or', True)
|
||||||
search_foods_or = params.get('foods_or', True)
|
search_foods_or = params.get('foods_or', True)
|
||||||
search_books_or = params.get('books_or', True)
|
search_books_or = params.get('books_or', True)
|
||||||
@ -27,24 +30,30 @@ def search_recipes(request, queryset, params):
|
|||||||
search_random = params.get('random', False)
|
search_random = params.get('random', False)
|
||||||
search_new = params.get('new', False)
|
search_new = params.get('new', False)
|
||||||
search_last_viewed = int(params.get('last_viewed', 0))
|
search_last_viewed = int(params.get('last_viewed', 0))
|
||||||
|
orderby = []
|
||||||
|
|
||||||
# TODO update this to concat with full search queryset qs1 | qs2
|
# TODO update this to concat with full search queryset qs1 | qs2
|
||||||
if search_last_viewed > 0:
|
if search_last_viewed > 0:
|
||||||
last_viewed_recipes = ViewLog.objects.filter(
|
last_viewed_recipes = ViewLog.objects.filter(
|
||||||
created_by=request.user, space=request.space,
|
created_by=request.user, space=request.space,
|
||||||
created_at__gte=datetime.now() - timedelta(days=14)
|
created_at__gte=datetime.now() - timedelta(days=14) # TODO make recent days a setting
|
||||||
).order_by('pk').values_list('recipe__pk', flat=True).distinct()
|
).order_by('-pk').values_list('recipe__pk', flat=True)
|
||||||
|
last_viewed_recipes = list(dict.fromkeys(last_viewed_recipes))[:search_last_viewed] # removes duplicates from list prior to slicing
|
||||||
|
|
||||||
return queryset.filter(pk__in=last_viewed_recipes[len(last_viewed_recipes) - min(len(last_viewed_recipes), search_last_viewed):])
|
# return queryset.annotate(last_view=Max('viewlog__pk')).annotate(new=Case(When(pk__in=last_viewed_recipes, then=('last_view')), default=Value(0))).filter(new__gt=0).order_by('-new')
|
||||||
|
# queryset that only annotates most recent view (higher pk = lastest view)
|
||||||
|
queryset = queryset.annotate(last_view=Max('viewlog__pk')).annotate(recent=Case(When(pk__in=last_viewed_recipes, then=('last_view')), default=Value(0)))
|
||||||
|
orderby += ['-recent']
|
||||||
|
|
||||||
orderby = []
|
# TODO create setting for default ordering - most cooked, rating,
|
||||||
|
# TODO create options for live sorting
|
||||||
|
# TODO make days of new recipe a setting
|
||||||
if search_new == 'true':
|
if search_new == 'true':
|
||||||
queryset = queryset.annotate(
|
queryset = (
|
||||||
new_recipe=Case(When(created_at__gte=(datetime.now() - timedelta(days=7)), then=Value(100)),
|
queryset.annotate(new_recipe=Case(
|
||||||
default=Value(0), ))
|
When(created_at__gte=(datetime.now() - timedelta(days=7)), then=('pk')), default=Value(0),))
|
||||||
orderby += ['new_recipe']
|
)
|
||||||
else:
|
orderby += ['-new_recipe']
|
||||||
queryset = queryset
|
|
||||||
|
|
||||||
search_type = search_prefs.search or 'plain'
|
search_type = search_prefs.search or 'plain'
|
||||||
if len(search_string) > 0:
|
if len(search_string) > 0:
|
||||||
@ -124,12 +133,12 @@ def search_recipes(request, queryset, params):
|
|||||||
queryset = queryset.filter(query_filter)
|
queryset = queryset.filter(query_filter)
|
||||||
|
|
||||||
if len(search_keywords) > 0:
|
if len(search_keywords) > 0:
|
||||||
# TODO creating setting to include descendants of keywords a setting
|
|
||||||
if search_keywords_or == 'true':
|
if search_keywords_or == 'true':
|
||||||
# when performing an 'or' search all descendants are included in the OR condition
|
# when performing an 'or' search all descendants are included in the OR condition
|
||||||
# so descendants are appended to filter all at once
|
# so descendants are appended to filter all at once
|
||||||
for kw in Keyword.objects.filter(pk__in=search_keywords):
|
# TODO creating setting to include descendants of keywords a setting
|
||||||
search_keywords += list(kw.get_descendants().values_list('pk', flat=True))
|
# for kw in Keyword.objects.filter(pk__in=search_keywords):
|
||||||
|
# search_keywords += list(kw.get_descendants().values_list('pk', flat=True))
|
||||||
queryset = queryset.filter(keywords__id__in=search_keywords)
|
queryset = queryset.filter(keywords__id__in=search_keywords)
|
||||||
else:
|
else:
|
||||||
# when performing an 'and' search returned recipes should include a parent OR any of its descedants
|
# when performing an 'and' search returned recipes should include a parent OR any of its descedants
|
||||||
@ -160,32 +169,42 @@ def search_recipes(request, queryset, params):
|
|||||||
queryset = queryset.order_by("?")
|
queryset = queryset.order_by("?")
|
||||||
else:
|
else:
|
||||||
# TODO add order by user settings
|
# TODO add order by user settings
|
||||||
orderby += ['name']
|
# orderby += ['name']
|
||||||
queryset = queryset.order_by(*orderby)
|
queryset = queryset.order_by(*orderby)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
def get_facet(qs, params):
|
def get_facet(qs, request):
|
||||||
|
# NOTE facet counts for tree models include self AND descendants
|
||||||
facets = {}
|
facets = {}
|
||||||
ratings = params.getlist('ratings', [])
|
ratings = request.query_params.getlist('ratings', [])
|
||||||
keyword_list = params.getlist('keywords', [])
|
keyword_list = request.query_params.getlist('keywords', [])
|
||||||
ingredient_list = params.getlist('ingredient', [])
|
food_list = request.query_params.getlist('foods', [])
|
||||||
book_list = params.getlist('book', [])
|
book_list = request.query_params.getlist('book', [])
|
||||||
|
search_keywords_or = request.query_params.get('keywords_or', True)
|
||||||
|
search_foods_or = request.query_params.get('foods_or', True)
|
||||||
|
search_books_or = request.query_params.get('books_or', True)
|
||||||
|
|
||||||
# this returns a list of keywords in the queryset and how many times it appears
|
# if using an OR search, will annotate all keywords, otherwise, just those that appear in results
|
||||||
kws = Keyword.objects.filter(recipe__in=qs).annotate(kw_count=Count('recipe'))
|
if search_keywords_or:
|
||||||
|
keywords = Keyword.objects.filter(space=request.space).annotate(recipe_count=Count('recipe'))
|
||||||
|
else:
|
||||||
|
keywords = Keyword.objects.filter(recipe__in=qs, space=request.space).annotate(recipe_count=Count('recipe'))
|
||||||
# custom django-tree function annotates a queryset to make building a tree easier.
|
# custom django-tree function annotates a queryset to make building a tree easier.
|
||||||
# see https://django-treebeard.readthedocs.io/en/latest/api.html#treebeard.models.Node.get_annotated_list_qs for details
|
# see https://django-treebeard.readthedocs.io/en/latest/api.html#treebeard.models.Node.get_annotated_list_qs for details
|
||||||
kw_a = annotated_qs(kws, root=True, fill=True)
|
kw_a = annotated_qs(keywords, root=True, fill=True)
|
||||||
|
|
||||||
# TODO add rating facet
|
# TODO add rating facet
|
||||||
facets['Ratings'] = []
|
facets['Ratings'] = []
|
||||||
facets['Keywords'] = fill_annotated_parents(kw_a, keyword_list)
|
facets['Keywords'] = fill_annotated_parents(kw_a, keyword_list)
|
||||||
# TODO add food facet
|
# TODO add food facet
|
||||||
facets['Ingredients'] = []
|
facets['Foods'] = []
|
||||||
# TODO add book facet
|
# TODO add book facet
|
||||||
facets['Books'] = []
|
facets['Books'] = []
|
||||||
|
facets['Recent'] = ViewLog.objects.filter(
|
||||||
|
created_by=request.user, space=request.space,
|
||||||
|
created_at__gte=datetime.now() - timedelta(days=14) # TODO make days of recent recipe a setting
|
||||||
|
).values_list('recipe__pk', flat=True)
|
||||||
return facets
|
return facets
|
||||||
|
|
||||||
|
|
||||||
@ -199,7 +218,7 @@ def fill_annotated_parents(annotation, filters):
|
|||||||
|
|
||||||
annotation[i][1]['id'] = r[0].id
|
annotation[i][1]['id'] = r[0].id
|
||||||
annotation[i][1]['name'] = r[0].name
|
annotation[i][1]['name'] = r[0].name
|
||||||
annotation[i][1]['count'] = getattr(r[0], 'kw_count', 0)
|
annotation[i][1]['count'] = getattr(r[0], 'recipe_count', 0)
|
||||||
annotation[i][1]['isDefaultExpanded'] = False
|
annotation[i][1]['isDefaultExpanded'] = False
|
||||||
|
|
||||||
if str(r[0].id) in filters:
|
if str(r[0].id) in filters:
|
||||||
@ -217,7 +236,7 @@ def fill_annotated_parents(annotation, filters):
|
|||||||
|
|
||||||
while j < level:
|
while j < level:
|
||||||
# this causes some double counting when a recipe has both a child and an ancestor
|
# this causes some double counting when a recipe has both a child and an ancestor
|
||||||
annotation[parent[j]][1]['count'] += getattr(r[0], 'kw_count', 0)
|
annotation[parent[j]][1]['count'] += getattr(r[0], 'recipe_count', 0)
|
||||||
if expand:
|
if expand:
|
||||||
annotation[parent[j]][1]['isDefaultExpanded'] = True
|
annotation[parent[j]][1]['isDefaultExpanded'] = True
|
||||||
j += 1
|
j += 1
|
||||||
|
@ -5,7 +5,6 @@ import uuid
|
|||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
|
|
||||||
from annoying.fields import AutoOneToOneField
|
from annoying.fields import AutoOneToOneField
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib import auth
|
from django.contrib import auth
|
||||||
from django.contrib.auth.models import Group, User
|
from django.contrib.auth.models import Group, User
|
||||||
from django.contrib.postgres.indexes import GinIndex
|
from django.contrib.postgres.indexes import GinIndex
|
||||||
@ -745,7 +744,7 @@ class CookLog(ExportModelOperationsMixin('cook_log'), models.Model, PermissionMo
|
|||||||
return self.recipe.name
|
return self.recipe.name
|
||||||
|
|
||||||
class Meta():
|
class Meta():
|
||||||
indexes = (Index(fields=['id', 'recipe', '-created_at', 'rating']),)
|
indexes = (Index(fields=['id', 'recipe', '-created_at', 'rating', 'created_by']),)
|
||||||
|
|
||||||
|
|
||||||
class ViewLog(ExportModelOperationsMixin('view_log'), models.Model, PermissionModelMixin):
|
class ViewLog(ExportModelOperationsMixin('view_log'), models.Model, PermissionModelMixin):
|
||||||
@ -760,7 +759,7 @@ class ViewLog(ExportModelOperationsMixin('view_log'), models.Model, PermissionMo
|
|||||||
return self.recipe.name
|
return self.recipe.name
|
||||||
|
|
||||||
class Meta():
|
class Meta():
|
||||||
indexes = (Index(fields=['recipe', '-created_at']),)
|
indexes = (Index(fields=['recipe', '-created_at', 'created_by']),)
|
||||||
|
|
||||||
|
|
||||||
class ImportLog(models.Model, PermissionModelMixin):
|
class ImportLog(models.Model, PermissionModelMixin):
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import random
|
import random
|
||||||
|
from datetime import timedelta
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from gettext import gettext as _
|
from gettext import gettext as _
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.db.models import QuerySet, Sum, Avg
|
from django.db.models import QuerySet, Sum, Avg
|
||||||
|
from django.utils import timezone
|
||||||
from drf_writable_nested import (UniqueFieldsMixin,
|
from drf_writable_nested import (UniqueFieldsMixin,
|
||||||
WritableNestedModelSerializer)
|
WritableNestedModelSerializer)
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import ValidationError, NotFound
|
from rest_framework.exceptions import ValidationError, NotFound
|
||||||
from treebeard.mp_tree import MP_NodeQuerySet
|
|
||||||
|
|
||||||
from cookbook.models import (Comment, CookLog, Food, Ingredient, Keyword,
|
from cookbook.models import (Comment, CookLog, Food, Ingredient, Keyword,
|
||||||
MealPlan, MealType, NutritionInformation, Recipe,
|
MealPlan, MealType, NutritionInformation, Recipe,
|
||||||
@ -287,7 +287,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
|
|||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
validated_data['name'] = validated_data['name'].strip()
|
validated_data['name'] = validated_data['name'].strip()
|
||||||
validated_data['space'] = self.context['request'].space
|
validated_data['space'] = self.context['request'].space
|
||||||
obj, created = Food.objects.get_or_create(validated_data)
|
obj, created = Food.objects.get_or_create(**validated_data)
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
@ -387,11 +387,19 @@ class RecipeBaseSerializer(WritableNestedModelSerializer):
|
|||||||
pass
|
pass
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# TODO make days of new recipe a setting
|
||||||
|
def is_recipe_new(self, obj):
|
||||||
|
if obj.created_at > (timezone.now() - timedelta(days=7)):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class RecipeOverviewSerializer(RecipeBaseSerializer):
|
class RecipeOverviewSerializer(RecipeBaseSerializer):
|
||||||
keywords = KeywordLabelSerializer(many=True)
|
keywords = KeywordLabelSerializer(many=True)
|
||||||
rating = serializers.SerializerMethodField('get_recipe_rating')
|
rating = serializers.SerializerMethodField('get_recipe_rating')
|
||||||
last_cooked = serializers.SerializerMethodField('get_recipe_last_cooked')
|
last_cooked = serializers.SerializerMethodField('get_recipe_last_cooked')
|
||||||
|
new = serializers.SerializerMethodField('is_recipe_new')
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
pass
|
pass
|
||||||
@ -404,7 +412,7 @@ class RecipeOverviewSerializer(RecipeBaseSerializer):
|
|||||||
fields = (
|
fields = (
|
||||||
'id', 'name', 'description', 'image', 'keywords', 'working_time',
|
'id', 'name', 'description', 'image', 'keywords', 'working_time',
|
||||||
'waiting_time', 'created_by', 'created_at', 'updated_at',
|
'waiting_time', 'created_by', 'created_at', 'updated_at',
|
||||||
'internal', 'servings', 'servings_text', 'rating', 'last_cooked',
|
'internal', 'servings', 'servings_text', 'rating', 'last_cooked', 'new'
|
||||||
)
|
)
|
||||||
read_only_fields = ['image', 'created_by', 'created_at']
|
read_only_fields = ['image', 'created_by', 'created_at']
|
||||||
|
|
||||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,10 +1,11 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from cookbook.models import Recipe
|
from cookbook.models import Recipe
|
||||||
|
from cookbook.tests.conftest import get_random_json_recipe, validate_recipe
|
||||||
|
|
||||||
LIST_URL = 'api:recipe-list'
|
LIST_URL = 'api:recipe-list'
|
||||||
DETAIL_URL = 'api:recipe-detail'
|
DETAIL_URL = 'api:recipe-detail'
|
||||||
@ -49,18 +50,19 @@ def test_list_space(recipe_1_s1, u1_s1, u1_s2, space_2):
|
|||||||
def test_update(arg, request, recipe_1_s1):
|
def test_update(arg, request, recipe_1_s1):
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
c = request.getfixturevalue(arg[0])
|
c = request.getfixturevalue(arg[0])
|
||||||
|
j = get_random_json_recipe()
|
||||||
r = c.patch(
|
r = c.patch(
|
||||||
reverse(
|
reverse(
|
||||||
DETAIL_URL,
|
DETAIL_URL,
|
||||||
args={recipe_1_s1.id}
|
args={recipe_1_s1.id}
|
||||||
),
|
),
|
||||||
{'name': 'new'},
|
j,
|
||||||
content_type='application/json'
|
content_type='application/json'
|
||||||
)
|
)
|
||||||
response = json.loads(r.content)
|
response = json.loads(r.content)
|
||||||
assert r.status_code == arg[1]
|
assert r.status_code == arg[1]
|
||||||
if r.status_code == 200:
|
if r.status_code == 200:
|
||||||
assert response['name'] == 'new'
|
validate_recipe(j, json.loads(r.content))
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("arg", [
|
@pytest.mark.parametrize("arg", [
|
||||||
@ -70,22 +72,24 @@ def test_update(arg, request, recipe_1_s1):
|
|||||||
['a1_s1', 201],
|
['a1_s1', 201],
|
||||||
])
|
])
|
||||||
def test_add(arg, request, u1_s2):
|
def test_add(arg, request, u1_s2):
|
||||||
c = request.getfixturevalue(arg[0])
|
x = 0
|
||||||
r = c.post(
|
while x < 2:
|
||||||
reverse(LIST_URL),
|
c = request.getfixturevalue(arg[0])
|
||||||
{'name': 'test', 'waiting_time': 0, 'working_time': 0, 'keywords': [], 'steps': []},
|
j = get_random_json_recipe()
|
||||||
content_type='application/json'
|
r = c.post(
|
||||||
)
|
reverse(LIST_URL), j, content_type='application/json'
|
||||||
response = json.loads(r.content)
|
)
|
||||||
print(r.content)
|
response = json.loads(r.content)
|
||||||
assert r.status_code == arg[1]
|
print(r.content)
|
||||||
if r.status_code == 201:
|
assert r.status_code == arg[1]
|
||||||
# id can change when running multiple tests, changed to validate name
|
if r.status_code == 201:
|
||||||
assert response['name'] == 'test'
|
# id can change when running multiple tests, changed to validate name
|
||||||
r = c.get(reverse(DETAIL_URL, args={response['id']}))
|
validate_recipe(j, json.loads(r.content))
|
||||||
assert r.status_code == 200
|
r = c.get(reverse(DETAIL_URL, args={response['id']}))
|
||||||
r = u1_s2.get(reverse(DETAIL_URL, args={response['id']}))
|
assert r.status_code == 200
|
||||||
assert r.status_code == 404
|
r = u1_s2.get(reverse(DETAIL_URL, args={response['id']}))
|
||||||
|
assert r.status_code == 404
|
||||||
|
x += 1
|
||||||
|
|
||||||
|
|
||||||
def test_delete(u1_s1, u1_s2, recipe_1_s1):
|
def test_delete(u1_s1, u1_s2, recipe_1_s1):
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import copy
|
import copy
|
||||||
import inspect
|
import inspect
|
||||||
|
import random
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@ -81,6 +82,74 @@ def get_random_recipe(space_1, u1_s1):
|
|||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def get_random_json_recipe():
|
||||||
|
return {
|
||||||
|
"name": str(uuid.uuid4()),
|
||||||
|
"description": str(uuid.uuid4()),
|
||||||
|
"keywords": [{"name": str(uuid.uuid4())}, {"name": str(uuid.uuid4())}],
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"instruction": str(uuid.uuid4()),
|
||||||
|
"ingredients": [
|
||||||
|
{"food": {"name": str(uuid.uuid4())}, "unit": {"name": str(uuid.uuid4())}, "amount": random.randint(0, 10)},
|
||||||
|
{"food": {"name": str(uuid.uuid4())}, "unit": {"name": str(uuid.uuid4())}, "amount": random.randint(0, 10)},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"working_time": random.randint(0, 120),
|
||||||
|
"waiting_time": random.randint(0, 120),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def validate_recipe(expected, recipe):
|
||||||
|
expected_lists = {}
|
||||||
|
target_lists = {}
|
||||||
|
# file and url are metadata not related to the recipe
|
||||||
|
[expected.pop(k) for k in ['file', 'url'] if k in expected]
|
||||||
|
# if a key is a list remove it to deal with later
|
||||||
|
lists = [k for k, v in expected.items() if type(v) == list]
|
||||||
|
for k in lists:
|
||||||
|
expected_lists[k] = expected.pop(k)
|
||||||
|
target_lists[k] = recipe.pop(k)
|
||||||
|
try:
|
||||||
|
# recipe dicts will have additional keys (IDs, default values, etc)
|
||||||
|
# this will check for an exact match from expected key:value to a superset of key:value pairs
|
||||||
|
assert expected.items() <= recipe.items()
|
||||||
|
except AssertionError:
|
||||||
|
for key in expected:
|
||||||
|
if expected[key] != recipe[key]:
|
||||||
|
print('Expected : ', expected[key], ' got: ', recipe[key])
|
||||||
|
|
||||||
|
# this is later, it may or may not work with keys that have list values
|
||||||
|
# it also may or may not work on complex nested dicts
|
||||||
|
for key in expected_lists:
|
||||||
|
for k in expected_lists[key]:
|
||||||
|
try:
|
||||||
|
assert any([dict_compare(k, i) for i in target_lists[key]])
|
||||||
|
except AssertionError:
|
||||||
|
for result in [dict_compare(k, i, details=True) for i in target_lists[key]]:
|
||||||
|
print('Added Keys: ', result[0])
|
||||||
|
print('Removed Keys', result[1])
|
||||||
|
print('Modified Value Keys', result[2])
|
||||||
|
print('Modified Dictionary Keys', result[3])
|
||||||
|
|
||||||
|
|
||||||
|
def dict_compare(d1, d2, details=False):
|
||||||
|
d1_keys = set(d1.keys())
|
||||||
|
d2_keys = set(d2.keys())
|
||||||
|
shared = d1_keys.intersection(d2_keys)
|
||||||
|
sub_dicts = [i for i, j in d1.items() if type(j) == dict]
|
||||||
|
not_dicts = shared - set(sub_dicts)
|
||||||
|
added = d1_keys - d2_keys
|
||||||
|
removed = d2_keys - d1_keys
|
||||||
|
modified = {o: (d1[o], d2[o]) for o in not_dicts if d1[o] != d2[o]}
|
||||||
|
modified_dicts = {o: (d1[o], d2[o]) for o in sub_dicts if not d1[o].items() <= d2[o].items()}
|
||||||
|
if details:
|
||||||
|
return added, removed, modified, modified_dicts
|
||||||
|
else:
|
||||||
|
return any([not added, not removed, not modified, not modified_dicts])
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def recipe_1_s1(space_1, u1_s1):
|
def recipe_1_s1(space_1, u1_s1):
|
||||||
return get_random_recipe(space_1, u1_s1)
|
return get_random_recipe(space_1, u1_s1)
|
||||||
|
@ -9,6 +9,7 @@ from ._recipes import (
|
|||||||
ALLRECIPES, AMERICAS_TEST_KITCHEN, CHEF_KOCH, CHEF_KOCH2, COOKPAD,
|
ALLRECIPES, AMERICAS_TEST_KITCHEN, CHEF_KOCH, CHEF_KOCH2, COOKPAD,
|
||||||
COOKS_COUNTRY, DELISH, FOOD_NETWORK, GIALLOZAFFERANO, JOURNAL_DES_FEMMES,
|
COOKS_COUNTRY, DELISH, FOOD_NETWORK, GIALLOZAFFERANO, JOURNAL_DES_FEMMES,
|
||||||
MADAME_DESSERT, MARMITON, TASTE_OF_HOME, THE_SPRUCE_EATS, TUDOGOSTOSO)
|
MADAME_DESSERT, MARMITON, TASTE_OF_HOME, THE_SPRUCE_EATS, TUDOGOSTOSO)
|
||||||
|
from cookbook.tests.conftest import validate_recipe
|
||||||
|
|
||||||
IMPORT_SOURCE_URL = 'api_recipe_from_source'
|
IMPORT_SOURCE_URL = 'api_recipe_from_source'
|
||||||
DATA_DIR = "cookbook/tests/other/test_data/"
|
DATA_DIR = "cookbook/tests/other/test_data/"
|
||||||
@ -56,7 +57,8 @@ def test_import_permission(arg, request):
|
|||||||
TUDOGOSTOSO,
|
TUDOGOSTOSO,
|
||||||
])
|
])
|
||||||
def test_recipe_import(arg, u1_s1):
|
def test_recipe_import(arg, u1_s1):
|
||||||
for f in arg['file']:
|
url = arg['url']
|
||||||
|
for f in list(arg['file']) : # url and files get popped later
|
||||||
if 'cookbook' in os.getcwd():
|
if 'cookbook' in os.getcwd():
|
||||||
test_file = os.path.join(os.getcwd(), 'other', 'test_data', f)
|
test_file = os.path.join(os.getcwd(), 'other', 'test_data', f)
|
||||||
else:
|
else:
|
||||||
@ -66,33 +68,10 @@ def test_recipe_import(arg, u1_s1):
|
|||||||
reverse(IMPORT_SOURCE_URL),
|
reverse(IMPORT_SOURCE_URL),
|
||||||
{
|
{
|
||||||
'data': d.read(),
|
'data': d.read(),
|
||||||
'url': arg['url'],
|
'url': url,
|
||||||
'mode': 'source'
|
'mode': 'source'
|
||||||
},
|
},
|
||||||
files={'foo': 'bar'}
|
files={'foo': 'bar'}
|
||||||
)
|
)
|
||||||
recipe = json.loads(response.content)['recipe_json']
|
recipe = json.loads(response.content)['recipe_json']
|
||||||
for key in list(set(arg) - set(['file', 'url'])):
|
validate_recipe(arg, recipe)
|
||||||
if type(arg[key]) == list:
|
|
||||||
assert len(recipe[key]) == len(arg[key])
|
|
||||||
if key == 'keywords':
|
|
||||||
valid_keywords = [i['text'] for i in arg[key]]
|
|
||||||
for k in recipe[key]:
|
|
||||||
assert k['text'] in valid_keywords
|
|
||||||
elif key == 'recipeIngredient':
|
|
||||||
valid_ing = ["{:g}{}{}{}{}".format(
|
|
||||||
i['amount'],
|
|
||||||
i['unit']['text'],
|
|
||||||
i['ingredient']['text'],
|
|
||||||
i['note'],
|
|
||||||
i['original'])
|
|
||||||
for i in arg[key]]
|
|
||||||
for i in recipe[key]:
|
|
||||||
assert "{:g}{}{}{}{}".format(
|
|
||||||
i['amount'],
|
|
||||||
i['unit']['text'],
|
|
||||||
i['ingredient']['text'],
|
|
||||||
i['note'],
|
|
||||||
i['original']) in valid_ing
|
|
||||||
else:
|
|
||||||
assert recipe[key] == arg[key]
|
|
||||||
|
@ -12,7 +12,7 @@ from django.contrib.auth.models import User
|
|||||||
from django.contrib.postgres.search import TrigramSimilarity
|
from django.contrib.postgres.search import TrigramSimilarity
|
||||||
from django.core.exceptions import FieldError, ValidationError
|
from django.core.exceptions import FieldError, ValidationError
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
from django.db.models import Q
|
from django.db.models import Case, Q, Value, When
|
||||||
from django.http import FileResponse, HttpResponse, JsonResponse
|
from django.http import FileResponse, HttpResponse, JsonResponse
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
from django.shortcuts import redirect, get_object_or_404
|
from django.shortcuts import redirect, get_object_or_404
|
||||||
@ -98,7 +98,6 @@ class DefaultPagination(PageNumberPagination):
|
|||||||
|
|
||||||
|
|
||||||
class FuzzyFilterMixin(ViewSetMixin):
|
class FuzzyFilterMixin(ViewSetMixin):
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
self.queryset = self.queryset.filter(space=self.request.space)
|
self.queryset = self.queryset.filter(space=self.request.space)
|
||||||
query = self.request.query_params.get('query', None)
|
query = self.request.query_params.get('query', None)
|
||||||
@ -106,10 +105,19 @@ class FuzzyFilterMixin(ViewSetMixin):
|
|||||||
|
|
||||||
if query is not None and query not in ["''", '']:
|
if query is not None and query not in ["''", '']:
|
||||||
if fuzzy:
|
if fuzzy:
|
||||||
self.queryset = self.queryset.annotate(trigram=TrigramSimilarity('name', query)).filter(trigram__gt=0.2).order_by("-trigram")
|
self.queryset = (
|
||||||
|
self.queryset
|
||||||
|
.annotate(exact=Case(When(name__iexact=query, then=(Value(100))), default=Value(0))) # put exact matches at the top of the result set
|
||||||
|
.annotate(trigram=TrigramSimilarity('name', query)).filter(trigram__gt=0.2)
|
||||||
|
.order_by('-exact').order_by("-trigram")
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# TODO have this check unaccent search settings?
|
# TODO have this check unaccent search settings or other search preferences?
|
||||||
self.queryset = self.queryset.filter(name__icontains=query)
|
self.queryset = (
|
||||||
|
self.queryset
|
||||||
|
.annotate(exact=Case(When(name__iexact=query, then=(Value(100))), default=Value(0))) # put exact matches at the top of the result set
|
||||||
|
.filter(name__icontains=query).order_by('-exact')
|
||||||
|
)
|
||||||
|
|
||||||
updated_at = self.request.query_params.get('updated_at', None)
|
updated_at = self.request.query_params.get('updated_at', None)
|
||||||
if updated_at is not None:
|
if updated_at is not None:
|
||||||
@ -356,10 +364,6 @@ class FoodViewSet(viewsets.ModelViewSet, FuzzyFilterMixin):
|
|||||||
serializer_class = FoodSerializer
|
serializer_class = FoodSerializer
|
||||||
permission_classes = [CustomIsUser]
|
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):
|
class RecipeBookViewSet(viewsets.ModelViewSet, StandardFilterMixin):
|
||||||
queryset = RecipeBook.objects
|
queryset = RecipeBook.objects
|
||||||
@ -466,7 +470,7 @@ class RecipePagination(PageNumberPagination):
|
|||||||
max_page_size = 100
|
max_page_size = 100
|
||||||
|
|
||||||
def paginate_queryset(self, queryset, request, view=None):
|
def paginate_queryset(self, queryset, request, view=None):
|
||||||
self.facets = get_facet(queryset, request.query_params)
|
self.facets = get_facet(queryset, request)
|
||||||
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):
|
||||||
|
@ -126,11 +126,6 @@
|
|||||||
<a :href="resolveDjangoUrl('view_settings') + '#search'">{{ $t('Advanced Search Settings') }}</a>
|
<a :href="resolveDjangoUrl('view_settings') + '#search'">{{ $t('Advanced Search Settings') }}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row" style="margin-top: 1vh">
|
|
||||||
<div class="col-12">
|
|
||||||
<a :href="resolveDjangoUrl('view_settings') + '#search'">{{ $t('Advanced Search Settings') }}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row" style="margin-top: 1vh">
|
<div class="row" style="margin-top: 1vh">
|
||||||
<div class="col-12" style="text-align: right">
|
<div class="col-12" style="text-align: right">
|
||||||
<b-button size="sm" variant="secondary" style="margin-right:8px"
|
<b-button size="sm" variant="secondary" style="margin-right:8px"
|
||||||
@ -143,12 +138,6 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<b-input-group class="mt-2">
|
<b-input-group class="mt-2">
|
||||||
<!-- <generic-multiselect @change="genericSelectChanged" parent_variable="search_keywords"
|
|
||||||
:initial_selection="settings.search_keywords"
|
|
||||||
search_function="listKeywords" label="label"
|
|
||||||
:tree_api="true"
|
|
||||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
|
||||||
v-bind:placeholder="$t('Keywords')"></generic-multiselect> -->
|
|
||||||
<treeselect v-model="settings.search_keywords" :options="facets.Keywords" :flat="true"
|
<treeselect v-model="settings.search_keywords" :options="facets.Keywords" :flat="true"
|
||||||
searchNested multiple :placeholder="$t('Keywords')" :normalizer="normalizer"
|
searchNested multiple :placeholder="$t('Keywords')" :normalizer="normalizer"
|
||||||
@input="refreshData(false)"
|
@input="refreshData(false)"
|
||||||
@ -238,12 +227,11 @@
|
|||||||
<recipe-card v-bind:key="`mp_${m.id}`" v-for="m in meal_plans" :recipe="m.recipe"
|
<recipe-card v-bind:key="`mp_${m.id}`" v-for="m in meal_plans" :recipe="m.recipe"
|
||||||
:meal_plan="m" :footer_text="m.meal_type_name"
|
:meal_plan="m" :footer_text="m.meal_type_name"
|
||||||
footer_icon="far fa-calendar-alt"></recipe-card>
|
footer_icon="far fa-calendar-alt"></recipe-card>
|
||||||
|
|
||||||
<recipe-card v-for="r in last_viewed_recipes" v-bind:key="`rv_${r.id}`" :recipe="r"
|
|
||||||
v-bind:footer_text="$t('Recently_Viewed')" footer_icon="fas fa-eye"></recipe-card>
|
|
||||||
</template>
|
</template>
|
||||||
|
<recipe-card v-for="r in recipes" v-bind:key="r.id" :recipe="r"
|
||||||
<recipe-card v-for="r in recipes" v-bind:key="r.id" :recipe="r"></recipe-card>
|
:footer_text="isRecentOrNew(r)[0]"
|
||||||
|
:footer_icon="isRecentOrNew(r)[1]">
|
||||||
|
</recipe-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -333,22 +321,9 @@ export default {
|
|||||||
mounted() {
|
mounted() {
|
||||||
this.$nextTick(function () {
|
this.$nextTick(function () {
|
||||||
if (this.$cookies.isKey(SETTINGS_COOKIE_NAME)) {
|
if (this.$cookies.isKey(SETTINGS_COOKIE_NAME)) {
|
||||||
let cookie_val = this.$cookies.get(SETTINGS_COOKIE_NAME)
|
this.settings = Object.assign({}, this.settings, this.$cookies.get(SETTINGS_COOKIE_NAME))
|
||||||
for (let i of Object.keys(cookie_val)) {
|
|
||||||
this.$set(this.settings, i, cookie_val[i])
|
|
||||||
}
|
|
||||||
//TODO i have no idea why the above code does not suffice to update the
|
|
||||||
//TODO pagination UI element as $set should update all values reactively but it does not
|
|
||||||
setTimeout(function () {
|
|
||||||
this.$set(this.settings, 'pagination_page', 0)
|
|
||||||
}.bind(this), 50)
|
|
||||||
setTimeout(function () {
|
|
||||||
this.$set(this.settings, 'pagination_page', cookie_val['pagination_page'])
|
|
||||||
}.bind(this), 51)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let urlParams = new URLSearchParams(window.location.search);
|
let urlParams = new URLSearchParams(window.location.search);
|
||||||
let apiClient = new ApiApiFactory()
|
let apiClient = new ApiApiFactory()
|
||||||
|
|
||||||
@ -364,8 +339,8 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.loadMealPlan()
|
this.loadMealPlan()
|
||||||
this.loadRecentlyViewed()
|
// this.loadRecentlyViewed()
|
||||||
this.refreshData(false)
|
// this.refreshData(false) // this gets triggered when the cookies get loaded
|
||||||
})
|
})
|
||||||
|
|
||||||
this.$i18n.locale = window.CUSTOM_LOCALE
|
this.$i18n.locale = window.CUSTOM_LOCALE
|
||||||
@ -381,9 +356,12 @@ export default {
|
|||||||
this.loadMealPlan()
|
this.loadMealPlan()
|
||||||
},
|
},
|
||||||
'settings.recently_viewed': function () {
|
'settings.recently_viewed': function () {
|
||||||
this.loadRecentlyViewed()
|
// this.loadRecentlyViewed()
|
||||||
|
this.refreshData(false)
|
||||||
},
|
},
|
||||||
'settings.search_input': _debounce(function () {
|
'settings.search_input': _debounce(function () {
|
||||||
|
this.settings.pagination_page = 1
|
||||||
|
this.pagination_count = 0
|
||||||
this.refreshData(false)
|
this.refreshData(false)
|
||||||
}, 300),
|
}, 300),
|
||||||
'settings.page_count': _debounce(function () {
|
'settings.page_count': _debounce(function () {
|
||||||
@ -412,12 +390,15 @@ export default {
|
|||||||
random,
|
random,
|
||||||
this.settings.sort_by_new,
|
this.settings.sort_by_new,
|
||||||
this.settings.pagination_page,
|
this.settings.pagination_page,
|
||||||
this.settings.page_count
|
this.settings.page_count,
|
||||||
|
{query: {last_viewed: this.settings.recently_viewed}}
|
||||||
).then(result => {
|
).then(result => {
|
||||||
|
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
this.pagination_count = result.data.count
|
this.pagination_count = result.data.count
|
||||||
this.recipes = result.data.results
|
this.recipes = result.data.results
|
||||||
this.facets = result.data.facets
|
this.facets = result.data.facets
|
||||||
|
console.log(this.recipes)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
openRandom: function () {
|
openRandom: function () {
|
||||||
@ -438,20 +419,19 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
this.meal_plans = []
|
this.meal_plans = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
},
|
|
||||||
loadRecentlyViewed: function () {
|
|
||||||
let apiClient = new ApiApiFactory()
|
|
||||||
if (this.settings.recently_viewed > 0) {
|
|
||||||
apiClient.listRecipes(undefined, undefined, undefined, undefined, undefined, undefined,
|
|
||||||
undefined, undefined, undefined, this.settings.sort_by_new, 1, this.settings.recently_viewed, {query: {last_viewed: this.settings.recently_viewed}}).then(result => {
|
|
||||||
this.last_viewed_recipes = result.data.results
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this.last_viewed_recipes = []
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
// DEPRECATED: intergrated into standard FTS queryset
|
||||||
|
// loadRecentlyViewed: function () {
|
||||||
|
// let apiClient = new ApiApiFactory()
|
||||||
|
// if (this.settings.recently_viewed > 0) {
|
||||||
|
// apiClient.listRecipes(undefined, undefined, undefined, undefined, undefined, undefined,
|
||||||
|
// undefined, undefined, undefined, this.settings.sort_by_new, 1, this.settings.recently_viewed, {query: {last_viewed: this.settings.recently_viewed}}).then(result => {
|
||||||
|
// this.last_viewed_recipes = result.data.results
|
||||||
|
// })
|
||||||
|
// } else {
|
||||||
|
// this.last_viewed_recipes = []
|
||||||
|
// }
|
||||||
|
// },
|
||||||
genericSelectChanged: function (obj) {
|
genericSelectChanged: function (obj) {
|
||||||
this.settings[obj.var] = obj.val
|
this.settings[obj.var] = obj.val
|
||||||
this.refreshData(false)
|
this.refreshData(false)
|
||||||
@ -479,6 +459,17 @@ export default {
|
|||||||
children: node.children,
|
children: node.children,
|
||||||
isDefaultExpanded: node.isDefaultExpanded
|
isDefaultExpanded: node.isDefaultExpanded
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
isRecentOrNew: function(x) {
|
||||||
|
let recent_recipe = [this.$t('Recently_Viewed'), "fas fa-eye"]
|
||||||
|
let new_recipe = [this.$t('New_Recipe'), "fas fa-splotch"]
|
||||||
|
if (x.new) {
|
||||||
|
return new_recipe
|
||||||
|
} else if (this.facets.Recent.includes(x.id)) {
|
||||||
|
return recent_recipe
|
||||||
|
} else {
|
||||||
|
return [undefined, undefined]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,38 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<b-dropdown variant="link" toggle-class="text-decoration-none" no-caret>
|
|
||||||
<template #button-content>
|
|
||||||
<i class="fas fa-ellipsis-v" ></i>
|
|
||||||
</template>
|
|
||||||
<b-dropdown-item v-on:click="$emit('item-action', 'edit')" v-if="show_edit">
|
|
||||||
<i class="fas fa-pencil-alt fa-fw"></i> {{ $t('Edit') }}
|
|
||||||
</b-dropdown-item>
|
|
||||||
|
|
||||||
<b-dropdown-item v-on:click="$emit('item-action', 'delete')" v-if="show_delete">
|
|
||||||
<i class="fas fa-trash-alt fa-fw"></i> {{ $t('Delete') }}
|
|
||||||
</b-dropdown-item>
|
|
||||||
|
|
||||||
<b-dropdown-item v-on:click="$emit('item-action', 'move')" v-if="show_move">
|
|
||||||
<i class="fas fa-trash-alt fa-fw"></i> {{ $t('Move') }}
|
|
||||||
</b-dropdown-item>
|
|
||||||
|
|
||||||
<b-dropdown-item v-if="show_merge" v-on:click="$emit('item-action', 'merge')">
|
|
||||||
<i class="fas fa-trash-alt fa-fw"></i> {{ $t('Merge') }}
|
|
||||||
</b-dropdown-item>
|
|
||||||
|
|
||||||
</b-dropdown>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'KeywordContextMenu',
|
|
||||||
props: {
|
|
||||||
show_edit: {type: Boolean, default: true},
|
|
||||||
show_delete: {type: Boolean, default: true},
|
|
||||||
show_move: {type: Boolean, default: false},
|
|
||||||
show_merge: {type: Boolean, default: false},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -1 +1 @@
|
|||||||
{"status":"done","chunks":{"recipe_search_view":["css/chunk-vendors.css","js/chunk-vendors.js","js/recipe_search_view.js"],"recipe_view":["css/chunk-vendors.css","js/chunk-vendors.js","js/recipe_view.js"],"offline_view":["css/chunk-vendors.css","js/chunk-vendors.js","js/offline_view.js"],"import_response_view":["css/chunk-vendors.css","js/chunk-vendors.js","js/import_response_view.js"],"supermarket_view":["css/chunk-vendors.css","js/chunk-vendors.js","js/supermarket_view.js"],"user_file_view":["css/chunk-vendors.css","js/chunk-vendors.js","js/user_file_view.js"],"keyword_list_view":["css/chunk-vendors.css","js/chunk-vendors.js","css/keyword_list_view.css","js/keyword_list_view.js"]},"assets":{"../../templates/sw.js":{"name":"../../templates/sw.js","path":"..\\..\\templates\\sw.js"},"css/chunk-vendors.css":{"name":"css/chunk-vendors.css","path":"css\\chunk-vendors.css"},"js/chunk-vendors.js":{"name":"js/chunk-vendors.js","path":"js\\chunk-vendors.js"},"js/import_response_view.js":{"name":"js/import_response_view.js","path":"js\\import_response_view.js"},"css/keyword_list_view.css":{"name":"css/keyword_list_view.css","path":"css\\keyword_list_view.css"},"js/keyword_list_view.js":{"name":"js/keyword_list_view.js","path":"js\\keyword_list_view.js"},"js/offline_view.js":{"name":"js/offline_view.js","path":"js\\offline_view.js"},"js/recipe_search_view.js":{"name":"js/recipe_search_view.js","path":"js\\recipe_search_view.js"},"js/recipe_view.js":{"name":"js/recipe_view.js","path":"js\\recipe_view.js"},"js/supermarket_view.js":{"name":"js/supermarket_view.js","path":"js\\supermarket_view.js"},"js/user_file_view.js":{"name":"js/user_file_view.js","path":"js\\user_file_view.js"},"recipe_search_view.html":{"name":"recipe_search_view.html","path":"recipe_search_view.html"},"recipe_view.html":{"name":"recipe_view.html","path":"recipe_view.html"},"offline_view.html":{"name":"offline_view.html","path":"offline_view.html"},"import_response_view.html":{"name":"import_response_view.html","path":"import_response_view.html"},"supermarket_view.html":{"name":"supermarket_view.html","path":"supermarket_view.html"},"user_file_view.html":{"name":"user_file_view.html","path":"user_file_view.html"},"keyword_list_view.html":{"name":"keyword_list_view.html","path":"keyword_list_view.html"},"manifest.json":{"name":"manifest.json","path":"manifest.json"}}}
|
{"status":"done","chunks":{"recipe_search_view":["css/chunk-vendors.css","js/chunk-vendors.js","js/recipe_search_view.js"],"recipe_view":["css/chunk-vendors.css","js/chunk-vendors.js","js/recipe_view.js"],"offline_view":["css/chunk-vendors.css","js/chunk-vendors.js","js/offline_view.js"],"import_response_view":["css/chunk-vendors.css","js/chunk-vendors.js","js/import_response_view.js"],"supermarket_view":["css/chunk-vendors.css","js/chunk-vendors.js","js/supermarket_view.js"],"user_file_view":["css/chunk-vendors.css","js/chunk-vendors.js","js/user_file_view.js"],"keyword_list_view":["css/chunk-vendors.css","js/chunk-vendors.js","css/keyword_list_view.css","js/keyword_list_view.js"]},"assets":{"../../templates/sw.js":{"name":"../../templates/sw.js","path":"../../templates/sw.js"},"css/chunk-vendors.css":{"name":"css/chunk-vendors.css","path":"css/chunk-vendors.css"},"js/chunk-vendors.js":{"name":"js/chunk-vendors.js","path":"js/chunk-vendors.js"},"js/import_response_view.js":{"name":"js/import_response_view.js","path":"js/import_response_view.js"},"css/keyword_list_view.css":{"name":"css/keyword_list_view.css","path":"css/keyword_list_view.css"},"js/keyword_list_view.js":{"name":"js/keyword_list_view.js","path":"js/keyword_list_view.js"},"js/offline_view.js":{"name":"js/offline_view.js","path":"js/offline_view.js"},"js/recipe_search_view.js":{"name":"js/recipe_search_view.js","path":"js/recipe_search_view.js"},"js/recipe_view.js":{"name":"js/recipe_view.js","path":"js/recipe_view.js"},"js/supermarket_view.js":{"name":"js/supermarket_view.js","path":"js/supermarket_view.js"},"js/user_file_view.js":{"name":"js/user_file_view.js","path":"js/user_file_view.js"},"recipe_search_view.html":{"name":"recipe_search_view.html","path":"recipe_search_view.html"},"recipe_view.html":{"name":"recipe_view.html","path":"recipe_view.html"},"offline_view.html":{"name":"offline_view.html","path":"offline_view.html"},"import_response_view.html":{"name":"import_response_view.html","path":"import_response_view.html"},"supermarket_view.html":{"name":"supermarket_view.html","path":"supermarket_view.html"},"user_file_view.html":{"name":"user_file_view.html","path":"user_file_view.html"},"keyword_list_view.html":{"name":"keyword_list_view.html","path":"keyword_list_view.html"},"manifest.json":{"name":"manifest.json","path":"manifest.json"}}}
|
Reference in New Issue
Block a user