ExtendedRecipeMixin
This commit is contained in:
parent
2fc27d7c36
commit
613b618533
@ -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)
|
||||
|
||||
|
@ -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
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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},
|
||||
|
@ -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'
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user