ExtendedRecipeMixin

This commit is contained in:
smilerz 2021-10-05 06:28:25 -05:00
parent 2fc27d7c36
commit 613b618533
20 changed files with 249 additions and 205 deletions

View File

@ -876,7 +876,7 @@ class SearchPreference(models.Model, PermissionModelMixin):
unaccent = models.ManyToManyField(SearchFields, related_name="unaccent_fields", blank=True, default=allSearchFields)
icontains = models.ManyToManyField(SearchFields, related_name="icontains_fields", blank=True, default=nameSearchField)
istartswith = models.ManyToManyField(SearchFields, related_name="istartswith_fields", blank=True)
trigram = models.ManyToManyField(SearchFields, related_name="trigram_fields", blank=True,default=nameSearchField)
trigram = models.ManyToManyField(SearchFields, related_name="trigram_fields", blank=True, default=nameSearchField)
fulltext = models.ManyToManyField(SearchFields, related_name="fulltext_fields", blank=True)
trigram_threshold = models.DecimalField(default=0.1, decimal_places=2, max_digits=3)

View File

@ -21,6 +21,45 @@ from cookbook.models import (Comment, CookLog, Food, Ingredient, Keyword,
from cookbook.templatetags.custom_tags import markdown
class ExtendedRecipeMixin(serializers.ModelSerializer):
# adds image and recipe count to serializer when query param extended=1
image = serializers.SerializerMethodField('get_image')
numrecipe = serializers.SerializerMethodField('count_recipes')
recipe_filter = None
def get_fields(self, *args, **kwargs):
fields = super().get_fields(*args, **kwargs)
try:
api_serializer = self.context['view'].serializer_class
except KeyError:
api_serializer = None
# extended values are computationally expensive and not needed in normal circumstances
if bool(int(self.context['request'].query_params.get('extended', False))) and self.__class__ == api_serializer:
return fields
else:
del fields['image']
del fields['numrecipe']
return fields
def get_image(self, obj):
# TODO add caching
recipes = Recipe.objects.filter(**{self.recipe_filter: obj}, space=obj.space).exclude(image__isnull=True).exclude(image__exact='')
try:
if recipes.count() == 0 and obj.has_children():
obj__in = self.recipe_filter + '__in'
recipes = Recipe.objects.filter(**{obj__in: obj.get_descendants()}, space=obj.space).exclude(image__isnull=True).exclude(image__exact='') # if no recipes found - check whole tree
except AttributeError:
# probably not a tree
pass
if recipes.count() != 0:
return random.choice(recipes).image.url
else:
return None
def count_recipes(self, obj):
return Recipe.objects.filter(**{self.recipe_filter: obj}, space=obj.space).count()
class CustomDecimalField(serializers.Field):
"""
Custom decimal field to normalize useless decimal places
@ -28,10 +67,9 @@ class CustomDecimalField(serializers.Field):
"""
def to_representation(self, value):
if isinstance(value, Decimal):
return value.normalize()
else:
return Decimal(value).normalize()
if not isinstance(value, Decimal):
value = Decimal(value)
return round(value, 2).normalize()
def to_internal_value(self, data):
if type(data) == int or type(data) == float:
@ -206,25 +244,26 @@ class KeywordLabelSerializer(serializers.ModelSerializer):
read_only_fields = ('id', 'label')
class KeywordSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
class KeywordSerializer(UniqueFieldsMixin, ExtendedRecipeMixin):
label = serializers.SerializerMethodField('get_label')
image = serializers.SerializerMethodField('get_image')
numrecipe = serializers.SerializerMethodField('count_recipes')
# image = serializers.SerializerMethodField('get_image')
# numrecipe = serializers.SerializerMethodField('count_recipes')
recipe_filter = 'keywords'
def get_label(self, obj):
return str(obj)
def get_image(self, obj):
recipes = obj.recipe_set.all().filter(space=obj.space).exclude(image__isnull=True).exclude(image__exact='')
if recipes.count() == 0 and obj.has_children():
recipes = Recipe.objects.filter(keywords__in=obj.get_descendants(), space=obj.space).exclude(image__isnull=True).exclude(image__exact='') # if no recipes found - check whole tree
if recipes.count() != 0:
return random.choice(recipes).image.url
else:
return None
# def get_image(self, obj):
# recipes = obj.recipe_set.all().filter(space=obj.space).exclude(image__isnull=True).exclude(image__exact='')
# if recipes.count() == 0 and obj.has_children():
# recipes = Recipe.objects.filter(keywords__in=obj.get_descendants(), space=obj.space).exclude(image__isnull=True).exclude(image__exact='') # if no recipes found - check whole tree
# if recipes.count() != 0:
# return random.choice(recipes).image.url
# else:
# return None
def count_recipes(self, obj):
return obj.recipe_set.filter(space=self.context['request'].space).all().count()
# def count_recipes(self, obj):
# return obj.recipe_set.filter(space=self.context['request'].space).all().count()
def create(self, validated_data):
# since multi select tags dont have id's
@ -242,20 +281,21 @@ class KeywordSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
read_only_fields = ('id', 'numchild', 'parent', 'image')
class UnitSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
image = serializers.SerializerMethodField('get_image')
numrecipe = serializers.SerializerMethodField('count_recipes')
class UnitSerializer(UniqueFieldsMixin, ExtendedRecipeMixin):
# image = serializers.SerializerMethodField('get_image')
# numrecipe = serializers.SerializerMethodField('count_recipes')
recipe_filter = 'steps__ingredients__unit'
def get_image(self, obj):
recipes = Recipe.objects.filter(steps__ingredients__unit=obj, space=obj.space).exclude(image__isnull=True).exclude(image__exact='')
# def get_image(self, obj):
# recipes = Recipe.objects.filter(steps__ingredients__unit=obj, space=obj.space).exclude(image__isnull=True).exclude(image__exact='')
if recipes.count() != 0:
return random.choice(recipes).image.url
else:
return None
# if recipes.count() != 0:
# return random.choice(recipes).image.url
# else:
# return None
def count_recipes(self, obj):
return Recipe.objects.filter(steps__ingredients__unit=obj, space=obj.space).count()
# def count_recipes(self, obj):
# return Recipe.objects.filter(steps__ingredients__unit=obj, space=obj.space).count()
def create(self, validated_data):
validated_data['name'] = validated_data['name'].strip()
@ -317,29 +357,30 @@ class RecipeSimpleSerializer(serializers.ModelSerializer):
read_only_fields = ['id', 'name', 'url']
class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedRecipeMixin):
supermarket_category = SupermarketCategorySerializer(allow_null=True, required=False)
recipe = RecipeSimpleSerializer(allow_null=True, required=False)
image = serializers.SerializerMethodField('get_image')
numrecipe = serializers.SerializerMethodField('count_recipes')
# image = serializers.SerializerMethodField('get_image')
# numrecipe = serializers.SerializerMethodField('count_recipes')
recipe_filter = 'steps__ingredients__food'
def get_image(self, obj):
if obj.recipe and obj.space == obj.recipe.space:
if obj.recipe.image and obj.recipe.image != '':
return obj.recipe.image.url
# if food is not also a recipe, look for recipe images that use the food
recipes = Recipe.objects.filter(steps__ingredients__food=obj, space=obj.space).exclude(image__isnull=True).exclude(image__exact='')
# if no recipes found - check whole tree
if recipes.count() == 0 and obj.has_children():
recipes = Recipe.objects.filter(steps__ingredients__food__in=obj.get_descendants(), space=obj.space).exclude(image__isnull=True).exclude(image__exact='')
# def get_image(self, obj):
# if obj.recipe and obj.space == obj.recipe.space:
# if obj.recipe.image and obj.recipe.image != '':
# return obj.recipe.image.url
# # if food is not also a recipe, look for recipe images that use the food
# recipes = Recipe.objects.filter(steps__ingredients__food=obj, space=obj.space).exclude(image__isnull=True).exclude(image__exact='')
# # if no recipes found - check whole tree
# if recipes.count() == 0 and obj.has_children():
# recipes = Recipe.objects.filter(steps__ingredients__food__in=obj.get_descendants(), space=obj.space).exclude(image__isnull=True).exclude(image__exact='')
if recipes.count() != 0:
return random.choice(recipes).image.url
else:
return None
# if recipes.count() != 0:
# return random.choice(recipes).image.url
# else:
# return None
def count_recipes(self, obj):
return Recipe.objects.filter(steps__ingredients__food=obj, space=obj.space).count()
# def count_recipes(self, obj):
# return Recipe.objects.filter(steps__ingredients__food=obj, space=obj.space).count()
def create(self, validated_data):
validated_data['name'] = validated_data['name'].strip()
@ -561,7 +602,7 @@ class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
recipe = RecipeOverviewSerializer(required=False, allow_null=True)
recipe_name = serializers.ReadOnlyField(source='recipe.name')
meal_type = MealTypeSerializer()
meal_type_name = serializers.ReadOnlyField(source='meal_type.name') # TODO deprecate once old meal plan was removed
meal_type_name = serializers.ReadOnlyField(source='meal_type.name') # TODO deprecate once old meal plan was removed
note_markdown = serializers.SerializerMethodField('get_note_markdown')
servings = CustomDecimalField()

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

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

View File

@ -101,7 +101,7 @@ class FuzzyFilterMixin(ViewSetMixin):
schema = FilterSchema()
def get_queryset(self):
self.queryset = self.queryset.filter(space=self.request.space)
self.queryset = self.queryset.filter(space=self.request.space).order_by('name')
query = self.request.query_params.get('query', None)
fuzzy = self.request.user.searchpreference.lookup
@ -111,14 +111,14 @@ class FuzzyFilterMixin(ViewSetMixin):
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")
.order_by('-exact', '-trigram')
)
else:
# TODO have this check unaccent search settings or other search preferences?
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')
.filter(name__icontains=query).order_by('-exact', 'name')
)
updated_at = self.request.query_params.get('updated_at', None)
@ -139,7 +139,7 @@ class FuzzyFilterMixin(ViewSetMixin):
return self.queryset
class MergeMixin(ViewSetMixin): # TODO update Units to use merge API
class MergeMixin(ViewSetMixin):
@decorators.action(detail=True, url_path='merge/(?P<target>[^/.]+)', methods=['PUT'], )
@decorators.renderer_classes((TemplateHTMLRenderer, JSONRenderer))
def merge(self, request, pk, target):

View File

@ -252,6 +252,7 @@ export default {
},
getItems: function (params, col) {
let column = col || 'left'
params.options = {'query':{'extended': 1}} // returns extended values in API response
this.genericAPI(this.this_model, this.Actions.LIST, params).then((result) => {
let results = result.data?.results ?? result.data
@ -399,11 +400,13 @@ export default {
},
getChildren: function (col, item) {
let parent = {}
let options = {
let params = {
'root': item.id,
'pageSize': 200
'pageSize': 200,
'query': {'extended': 1},
'options': {'query':{'extended': 1}}
}
this.genericAPI(this.this_model, this.Actions.LIST, options).then((result) => {
this.genericAPI(this.this_model, this.Actions.LIST, params).then((result) => {
parent = this.findCard(item.id, this['items_' + col])
if (parent) {
Vue.set(parent, 'children', result.data.results)
@ -418,10 +421,10 @@ export default {
getRecipes: function (col, item) {
let parent = {}
// TODO: make this generic
let options = {'pageSize': 200}
options[this.this_recipe_param] = item.id
let params = {'pageSize': 50}
params[this.this_recipe_param] = item.id
this.genericAPI(this.Models.RECIPE, this.Actions.LIST, options).then((result) => {
this.genericAPI(this.Models.RECIPE, this.Actions.LIST, params).then((result) => {
parent = this.findCard(item.id, this['items_' + col])
if (parent) {
Vue.set(parent, 'recipes', result.data.results)

View File

@ -13,7 +13,7 @@ export class Models {
// MODEL_TYPES - inherited by MODELS, inherits and takes precedence over ACTIONS
static TREE = {
'list': {
'params': ['query', 'root', 'tree', 'page', 'pageSize'],
'params': ['query', 'root', 'tree', 'page', 'pageSize', 'options'],
'config': {
'root': {
'default': {
@ -471,7 +471,7 @@ export class Actions {
static LIST = {
"function": "list",
"suffix": "s",
"params": ['query', 'page', 'pageSize'],
"params": ['query', 'page', 'pageSize', 'options'],
"config": {
'query': {'default': undefined},
'page': {'default': 1},

View File

@ -159,7 +159,7 @@ export function roundDecimals(num) {
/*
* Utility functions to use OpenAPIs generically
* */
import {ApiApiFactory} from "@/utils/openapi/api.ts"; // TODO: is it possible to only import inside the Mixin?
import {ApiApiFactory} from "@/utils/openapi/api.ts";
import axios from "axios";
axios.defaults.xsrfCookieName = 'csrftoken'

View File

@ -3,87 +3,87 @@
"assets": {
"../../templates/sw.js": {
"name": "../../templates/sw.js",
"path": "..\\..\\templates\\sw.js"
"path": "../../templates/sw.js"
},
"js/checklist_view.js": {
"name": "js/checklist_view.js",
"path": "js\\checklist_view.js"
"path": "js/checklist_view.js"
},
"js/chunk-2d0da313.js": {
"name": "js/chunk-2d0da313.js",
"path": "js\\chunk-2d0da313.js"
"path": "js/chunk-2d0da313.js"
},
"css/chunk-vendors.css": {
"name": "css/chunk-vendors.css",
"path": "css\\chunk-vendors.css"
"path": "css/chunk-vendors.css"
},
"js/chunk-vendors.js": {
"name": "js/chunk-vendors.js",
"path": "js\\chunk-vendors.js"
"path": "js/chunk-vendors.js"
},
"css/cookbook_view.css": {
"name": "css/cookbook_view.css",
"path": "css\\cookbook_view.css"
"path": "css/cookbook_view.css"
},
"js/cookbook_view.js": {
"name": "js/cookbook_view.js",
"path": "js\\cookbook_view.js"
"path": "js/cookbook_view.js"
},
"css/edit_internal_recipe.css": {
"name": "css/edit_internal_recipe.css",
"path": "css\\edit_internal_recipe.css"
"path": "css/edit_internal_recipe.css"
},
"js/edit_internal_recipe.js": {
"name": "js/edit_internal_recipe.js",
"path": "js\\edit_internal_recipe.js"
"path": "js/edit_internal_recipe.js"
},
"js/import_response_view.js": {
"name": "js/import_response_view.js",
"path": "js\\import_response_view.js"
"path": "js/import_response_view.js"
},
"css/meal_plan_view.css": {
"name": "css/meal_plan_view.css",
"path": "css\\meal_plan_view.css"
"path": "css/meal_plan_view.css"
},
"js/meal_plan_view.js": {
"name": "js/meal_plan_view.js",
"path": "js\\meal_plan_view.js"
"path": "js/meal_plan_view.js"
},
"css/model_list_view.css": {
"name": "css/model_list_view.css",
"path": "css\\model_list_view.css"
"path": "css/model_list_view.css"
},
"js/model_list_view.js": {
"name": "js/model_list_view.js",
"path": "js\\model_list_view.js"
"path": "js/model_list_view.js"
},
"js/offline_view.js": {
"name": "js/offline_view.js",
"path": "js\\offline_view.js"
"path": "js/offline_view.js"
},
"css/recipe_search_view.css": {
"name": "css/recipe_search_view.css",
"path": "css\\recipe_search_view.css"
"path": "css/recipe_search_view.css"
},
"js/recipe_search_view.js": {
"name": "js/recipe_search_view.js",
"path": "js\\recipe_search_view.js"
"path": "js/recipe_search_view.js"
},
"css/recipe_view.css": {
"name": "css/recipe_view.css",
"path": "css\\recipe_view.css"
"path": "css/recipe_view.css"
},
"js/recipe_view.js": {
"name": "js/recipe_view.js",
"path": "js\\recipe_view.js"
"path": "js/recipe_view.js"
},
"js/supermarket_view.js": {
"name": "js/supermarket_view.js",
"path": "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"
"path": "js/user_file_view.js"
},
"recipe_search_view.html": {
"name": "recipe_search_view.html",