food inherit attributes

This commit is contained in:
smilerz 2021-10-19 08:35:33 -05:00
parent 4377505b14
commit fbe748db62
13 changed files with 252 additions and 336 deletions

View File

@ -45,8 +45,7 @@ class UserPreferenceForm(forms.ModelForm):
fields = (
'default_unit', 'use_fractions', 'use_kj', 'theme', 'nav_color',
'sticky_navbar', 'default_page', 'show_recent', 'search_style',
'plan_share', 'shopping_share', 'ingredient_decimals', 'shopping_auto_sync',
'comments'
'plan_share', 'ingredient_decimals', 'comments',
)
labels = {
@ -93,7 +92,8 @@ class UserPreferenceForm(forms.ModelForm):
widgets = {
'plan_share': MultiSelectWidget,
'shopping_share': MultiSelectWidget
'shopping_share': MultiSelectWidget,
}

View File

@ -1,13 +1,14 @@
# Generated by Django 3.2.7 on 2021-10-01 22:34
import datetime
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
from django.utils.timezone import utc
from django_scopes import scopes_disabled
from cookbook.models import ShoppingListEntry
from cookbook.models import FoodInheritField, ShoppingListEntry
def delete_orphaned_sle(apps, schema_editor):
@ -16,6 +17,15 @@ def delete_orphaned_sle(apps, schema_editor):
ShoppingListEntry.objects.filter(shoppinglist=None).delete()
def create_inheritfields(apps, schema_editor):
FoodInheritField.objects.create(name='Supermarket Category', field='supermarket_category')
FoodInheritField.objects.create(name='Ignore Shopping', field='ignore_shopping')
FoodInheritField.objects.create(name='Diet', field='diet')
FoodInheritField.objects.create(name='Substitute', field='substitute')
FoodInheritField.objects.create(name='Substitute Children', field='substitute_children')
FoodInheritField.objects.create(name='Substitute Siblings', field='substitute_siblings')
class Migration(migrations.Migration):
dependencies = [
@ -25,4 +35,5 @@ class Migration(migrations.Migration):
operations = [
migrations.RunPython(delete_orphaned_sle),
migrations.RunPython(create_inheritfields),
]

View File

@ -1,43 +0,0 @@
# Generated by Django 3.2.7 on 2021-10-14 22:36
import cookbook.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0161_alter_shoppinglistentry_list_recipe'),
]
operations = [
migrations.CreateModel(
name='FoodParentIgnore',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('field', models.CharField(max_length=32, unique=True)),
('name', models.CharField(max_length=64, unique=True)),
],
bases=(models.Model, cookbook.models.PermissionModelMixin),
),
migrations.AddField(
model_name='food',
name='child_inherit',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='userpreference',
name='food_inherit',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='userpreference',
name='mealplan_autoinclude_related',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='food',
name='ignore_parent',
field=models.ManyToManyField(blank=True, related_name='ignore_parent', to='cookbook.FoodParentIgnore'),
),
]

View File

@ -1,24 +0,0 @@
from cookbook.models import FoodParentIgnore
from django.db import migrations
def create_ignorefields(apps, schema_editor):
FoodParentIgnore.objects.create(name='Supermarket Category', field='name')
FoodParentIgnore.objects.create(name='Ignore Shopping', field='ignore_shopping')
FoodParentIgnore.objects.create(name='Diet', field='diet')
FoodParentIgnore.objects.create(name='Substitute', field='substitute')
FoodParentIgnore.objects.create(name='Substitute Children', field='substitute_children')
FoodParentIgnore.objects.create(name='Substitute Siblings', field='substitute_siblings')
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0162_food_inherit'),
]
operations = [
migrations.RunPython(
create_ignorefields
),
]

View File

@ -15,9 +15,7 @@ from django.core.files.uploadedfile import InMemoryUploadedFile, UploadedFile
from django.core.validators import MinLengthValidator
from django.db import IntegrityError, models
from django.db.models import Index, ProtectedError, Q, Subquery
from django.db.models.fields.related import ManyToManyField
from django.db.models.functions import Substr
from django.db.transaction import atomic
from django.utils import timezone
from django.utils.translation import gettext as _
from django_prometheus.models import ExportModelOperationsMixin
@ -329,7 +327,6 @@ class UserPreference(models.Model, PermissionModelMixin):
mealplan_autoadd_shopping = models.BooleanField(default=False)
mealplan_autoexclude_onhand = models.BooleanField(default=True)
mealplan_autoinclude_related = models.BooleanField(default=True)
food_inherit = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE, null=True)
@ -473,18 +470,6 @@ class Unit(ExportModelOperationsMixin('unit'), models.Model, PermissionModelMixi
]
class FoodParentIgnore(models.Model, PermissionModelMixin):
field = models.CharField(max_length=32, unique=True)
name = models.CharField(max_length=64, unique=True)
def __str__(self):
return _(self.name)
@staticmethod
def get_name(self):
return _(self.name)
class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
# exclude fields not implemented yet
inherit_fields = FoodInheritField.objects.exclude(field__in=['diet', 'substitute', 'substitute_children', 'substitute_siblings'])
@ -498,8 +483,8 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
ignore_shopping = models.BooleanField(default=False)
description = models.TextField(default='', blank=True)
on_hand = models.BooleanField(default=False)
child_inherit = models.BooleanField(default=False)
ignore_parent = models.ManyToManyField(FoodParentIgnore, related_name="ignore_parent", blank=True)
inherit = models.BooleanField(default=False)
ignore_inherit = models.ManyToManyField(FoodInheritField, blank=True) # is this better as inherit instead of ignore inherit? which is more intuitive?
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space', _manager_class=TreeManager)
@ -857,8 +842,17 @@ class ShoppingListEntry(ExportModelOperationsMixin('shopping_list_entry'), model
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
@classmethod
def list_from_recipe(self, recipe=None, mealplan=None, servings=None, ingredients=None, created_by=None, space=None):
@ classmethod
def list_from_recipe(self, list_recipe=None, recipe=None, mealplan=None, servings=None, ingredients=None, created_by=None, space=None):
"""
Creates ShoppingListRecipe and associated ShoppingListEntrys from a recipe or a meal plan with a recipe
:param list_recipe: Modify an existing ShoppingListRecipe
:param recipe: Recipe to use as list of ingredients. One of [recipe, mealplan] are required
:param mealplan: alternatively use a mealplan recipe as source of ingredients
:param servings: Optional: Number of servings to use to scale shoppinglist. If servings = 0 an existing recipe list will be deleted
:param ingredients: Ingredients, list of ingredient IDs to include on the shopping list. When not provided all ingredients will be used
"""
# TODO cascade to associated recipes
try:
r = recipe or mealplan.recipe
except AttributeError:

View File

@ -12,12 +12,13 @@ from drf_writable_nested import UniqueFieldsMixin, WritableNestedModelSerializer
from rest_framework import serializers
from rest_framework.exceptions import NotFound, ValidationError
from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, Food, ImportLog,
Ingredient, Keyword, MealPlan, MealType, NutritionInformation, Recipe,
RecipeBook, RecipeBookEntry, RecipeImport, ShareLink, ShoppingList,
ShoppingListEntry, ShoppingListRecipe, Step, Storage, Supermarket,
SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog, Unit,
UserFile, UserPreference, ViewLog)
from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, Food,
FoodInheritField, ImportLog, Ingredient, Keyword, MealPlan, MealType,
NutritionInformation, Recipe, RecipeBook, RecipeBookEntry,
RecipeImport, ShareLink, ShoppingList, ShoppingListEntry,
ShoppingListRecipe, Step, Storage, Supermarket, SupermarketCategory,
SupermarketCategoryRelation, Sync, SyncLog, Unit, UserFile,
UserPreference, ViewLog)
from cookbook.templatetags.custom_tags import markdown
@ -129,19 +130,12 @@ class UserNameSerializer(WritableNestedModelSerializer):
fields = ('id', 'username')
class FoodInheritFieldSerializer(UniqueFieldsMixin):
def create(self, validated_data):
# don't allow writing to FoodInheritField via API
return FoodInheritField.objects.get(**validated_data)
def update(self, instance, validated_data):
# don't allow writing to FoodInheritField via API
return FoodInheritField.objects.get(**validated_data)
class FoodInheritFieldSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
class Meta:
model = FoodInheritField
fields = ['id', 'name', 'field', ]
read_only_fields = ('id', 'name', 'field', )
class UserPreferenceSerializer(serializers.ModelSerializer):
@ -161,7 +155,7 @@ class UserPreferenceSerializer(serializers.ModelSerializer):
fields = (
'user', 'theme', 'nav_color', 'default_unit', 'default_page',
'search_style', 'show_recent', 'plan_share', 'ingredient_decimals',
'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping'
'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'food_ignore_default'
)
@ -367,7 +361,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
supermarket_category = SupermarketCategorySerializer(allow_null=True, required=False)
recipe = RecipeSimpleSerializer(allow_null=True, required=False)
shopping = serializers.SerializerMethodField('get_shopping_status')
ignore_inherit = FoodInheritFieldSerializer(allow_null=True, many=True, required=False)
ignore_inherit = FoodInheritFieldSerializer(allow_null=True, required=False, many=True)
recipe_filter = 'steps__ingredients__food'
@ -403,7 +397,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
model = Food
fields = (
'id', 'name', 'description', 'shopping', 'recipe', 'ignore_shopping', 'supermarket_category',
'image', 'parent', 'numchild', 'numrecipe', 'on_hand', 'child_inherit', 'ignore_parent'
'image', 'parent', 'numchild', 'numrecipe', 'on_hand', 'inherit', 'ignore_inherit'
)
read_only_fields = ('id', 'numchild', 'parent', 'image', 'numrecipe')
@ -930,13 +924,3 @@ class FoodShoppingUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = Recipe
fields = ['id', 'amount', 'unit', 'delete', ]
class FoodParentIgnoreSerializer(serializers.ModelSerializer):
field = serializers.CharField()
name = serializers.CharField()
class Meta:
model = Recipe
fields = ['id', 'name', 'field', ]
read_only_fields = ('id', 'name', 'field', )

View File

@ -48,7 +48,6 @@ def update_step_search_vector(sender, instance=None, created=False, **kwargs):
@receiver(post_save, sender=Food)
@skip_signal
def update_food_inheritance(sender, instance=None, created=False, **kwargs):
if not instance:
return

View File

@ -19,7 +19,7 @@ router.register(r'automation', api.AutomationViewSet)
router.register(r'bookmarklet-import', api.BookmarkletImportViewSet)
router.register(r'cook-log', api.CookLogViewSet)
router.register(r'food', api.FoodViewSet)
router.register(r'food-inherit-ignore', api.FoodParentIgnoreViewSet)
router.register(r'food-inherit-field', api.FoodInheritFieldViewSet)
router.register(r'import-log', api.ImportLogViewSet)
router.register(r'ingredient', api.IngredientViewSet)
router.register(r'keyword', api.KeywordViewSet)

View File

@ -39,7 +39,7 @@ from cookbook.helper.recipe_html_import import get_recipe_from_source
from cookbook.helper.recipe_search import get_facet, old_search, search_recipes
from cookbook.helper.recipe_url_import import get_from_scraper
from cookbook.helper.shopping_helper import shopping_helper
from cookbook.models import (Automation, BookmarkletImport, CookLog, Food, FoodParentIgnore,
from cookbook.models import (Automation, BookmarkletImport, CookLog, Food, FoodInheritField,
ImportLog, Ingredient, Keyword, MealPlan, MealType, Recipe, RecipeBook,
RecipeBookEntry, ShareLink, ShoppingList, ShoppingListEntry,
ShoppingListRecipe, Step, Storage, Supermarket, SupermarketCategory,
@ -50,7 +50,7 @@ from cookbook.provider.local import Local
from cookbook.provider.nextcloud import Nextcloud
from cookbook.schemas import FilterSchema, QueryParam, QueryParamAutoSchema, TreeSchema
from cookbook.serializer import (AutomationSerializer, BookmarkletImportSerializer,
CookLogSerializer, FoodParentIgnoreSerializer, FoodSerializer,
CookLogSerializer, FoodInheritFieldSerializer, FoodSerializer,
FoodShoppingUpdateSerializer, ImportLogSerializer,
IngredientSerializer, KeywordSerializer, MealPlanSerializer,
MealTypeSerializer, RecipeBookEntrySerializer,
@ -393,14 +393,14 @@ class UnitViewSet(viewsets.ModelViewSet, MergeMixin, FuzzyFilterMixin):
pagination_class = DefaultPagination
class FoodParentIgnoreViewSet(viewsets.ReadOnlyModelViewSet):
queryset = FoodParentIgnore.objects
serializer_class = FoodParentIgnoreSerializer
class FoodInheritFieldViewSet(viewsets.ReadOnlyModelViewSet):
queryset = FoodInheritField.objects
serializer_class = FoodInheritFieldSerializer
permission_classes = [CustomIsUser]
def get_queryset(self):
# exclude fields not yet implemented
return self.queryset.exclude(field__in=['diet', 'substitute', 'substitute_children', 'substitute_siblings'])
return Food.inherit_fields
class FoodViewSet(viewsets.ModelViewSet, TreeMixin):

View File

@ -64,7 +64,7 @@ import { BootstrapVue } from "bootstrap-vue"
import "bootstrap-vue/dist/bootstrap-vue.css"
import { CardMixin, ApiMixin, getConfig, StandardToasts } from "@/utils/utils"
import { CardMixin, ApiMixin, getConfig, StandardToasts, getUserPreference } from "@/utils/utils"
import GenericInfiniteCards from "@/components/GenericInfiniteCards"
import GenericHorizontalCard from "@/components/GenericHorizontalCard"

View File

@ -74,7 +74,18 @@ export class Models {
// REQUIRED: unordered array of fields that can be set during create
create: {
// if not defined partialUpdate will use the same parameters, prepending 'id'
params: [["name", "description", "recipe", "ignore_shopping", "supermarket_category", "on_hand", "inherit", "ignore_inherit"]],
params: [
[
"name",
"description",
"recipe",
"ignore_shopping",
"supermarket_category",
"on_hand",
"inherit",
"ignore_inherit",
],
],
form: {
name: {
@ -404,16 +415,35 @@ export class Models {
}
static RECIPE = {
'name': i18n.t('Recipe'),
'apiName': 'Recipe',
'list': {
'params': ['query', 'keywords', 'foods', 'units', 'rating', 'books', 'keywordsOr', 'foodsOr', 'booksOr', 'internal', 'random', '_new', 'page', 'pageSize', 'options'],
name: i18n.t("Recipe"),
apiName: "Recipe",
list: {
params: [
"query",
"keywords",
"foods",
"units",
"rating",
"books",
"keywordsOr",
"foodsOr",
"booksOr",
"internal",
"random",
"_new",
"page",
"pageSize",
"options",
],
// 'config': {
// 'foods': {'type': 'string'},
// 'keywords': {'type': 'string'},
// 'books': {'type': 'string'},
// }
},
shopping: {
params: ["id", ["id", "list_recipe", "ingredients", "servings"]],
},
}
static USER_NAME = {

View File

@ -239,6 +239,68 @@ export interface Food {
* @memberof Food
*/
on_hand?: boolean;
/**
*
* @type {boolean}
* @memberof Food
*/
inherit?: boolean;
/**
*
* @type {Array<FoodIgnoreInherit>}
* @memberof Food
*/
ignore_inherit?: Array<FoodIgnoreInherit> | null;
}
/**
*
* @export
* @interface FoodIgnoreInherit
*/
export interface FoodIgnoreInherit {
/**
*
* @type {number}
* @memberof FoodIgnoreInherit
*/
id?: number;
/**
*
* @type {string}
* @memberof FoodIgnoreInherit
*/
name?: string;
/**
*
* @type {string}
* @memberof FoodIgnoreInherit
*/
field?: string;
}
/**
*
* @export
* @interface FoodInheritField
*/
export interface FoodInheritField {
/**
*
* @type {number}
* @memberof FoodInheritField
*/
id?: number;
/**
*
* @type {string}
* @memberof FoodInheritField
*/
name?: string;
/**
*
* @type {string}
* @memberof FoodInheritField
*/
field?: string;
}
/**
*
@ -736,10 +798,10 @@ export interface InlineResponse2004 {
previous?: string | null;
/**
*
* @type {Array<Step>}
* @type {Array<RecipeOverview>}
* @memberof InlineResponse2004
*/
results?: Array<Step>;
results?: Array<RecipeOverview>;
}
/**
*
@ -896,6 +958,37 @@ export interface InlineResponse2009 {
*/
results?: Array<ViewLog>;
}
/**
*
* @export
* @interface InlineResponse2009
*/
export interface InlineResponse2009 {
/**
*
* @type {number}
* @memberof InlineResponse2009
*/
count?: number;
/**
*
* @type {string}
* @memberof InlineResponse2009
*/
next?: string | null;
/**
*
* @type {string}
* @memberof InlineResponse2009
*/
previous?: string | null;
/**
*
* @type {Array<ViewLog>}
* @memberof InlineResponse2009
*/
results?: Array<ViewLog>;
}
/**
*
* @export
@ -1379,10 +1472,10 @@ export interface RecipeBook {
icon?: string | null;
/**
*
* @type {Array<ShoppingListCreatedBy>}
* @type {Array<RecipeBookShared>}
* @memberof RecipeBook
*/
shared: Array<ShoppingListCreatedBy>;
shared: Array<RecipeBookShared>;
/**
*
* @type {string}
@ -1539,6 +1632,61 @@ export interface RecipeIngredients {
*/
no_amount?: boolean;
}
/**
*
* @export
* @interface RecipeKeywords
*/
export interface RecipeIngredients {
/**
*
* @type {number}
* @memberof RecipeIngredients
*/
id?: number;
/**
*
* @type {IngredientFood}
* @memberof RecipeIngredients
*/
food: IngredientFood | null;
/**
*
* @type {FoodSupermarketCategory}
* @memberof RecipeIngredients
*/
unit: FoodSupermarketCategory | null;
/**
*
* @type {string}
* @memberof RecipeIngredients
*/
amount: string;
/**
*
* @type {string}
* @memberof RecipeIngredients
*/
note?: string | null;
/**
*
* @type {number}
* @memberof RecipeIngredients
*/
order?: number;
/**
*
* @type {boolean}
* @memberof RecipeIngredients
*/
is_header?: boolean;
/**
*
* @type {boolean}
* @memberof RecipeIngredients
*/
no_amount?: boolean;
}
/**
*
* @export
@ -1919,10 +2067,10 @@ export interface ShoppingList {
entries: Array<ShoppingListEntries> | null;
/**
*
* @type {Array<ShoppingListCreatedBy>}
* @type {Array<RecipeBookShared>}
* @memberof ShoppingList
*/
shared: Array<ShoppingListCreatedBy>;
shared: Array<RecipeBookShared>;
/**
*
* @type {boolean}
@ -2003,18 +2151,6 @@ export interface ShoppingListEntries {
* @memberof ShoppingListEntries
*/
ingredient?: number | null;
/**
*
* @type {FoodSupermarketCategory}
* @memberof ShoppingListEntries
*/
unit?: FoodSupermarketCategory | null;
/**
*
* @type {number}
* @memberof ShoppingListEntries
*/
ingredient?: number | null;
/**
*
* @type {string}
@ -2326,49 +2462,6 @@ export interface ShoppingListRecipeMealplan {
*/
mealplan_note?: string;
}
/**
*
* @export
* @interface ShoppingListRecipes
*/
export interface ShoppingListRecipes {
/**
*
* @type {number}
* @memberof ShoppingListRecipes
*/
id?: number;
/**
*
* @type {number}
* @memberof ShoppingListRecipes
*/
recipe?: number | null;
/**
*
* @type {number}
* @memberof ShoppingListRecipes
*/
mealplan?: number | null;
/**
*
* @type {string}
* @memberof ShoppingListRecipes
*/
recipe_name?: string;
/**
*
* @type {string}
* @memberof ShoppingListRecipes
*/
servings: string;
/**
*
* @type {string}
* @memberof ShoppingListRecipes
*/
mealplan_note?: string;
}
/**
*
* @export
@ -2559,147 +2652,6 @@ export enum StepTypeEnum {
Recipe = 'RECIPE'
}
/**
*
* @export
* @interface StepFile
*/
export interface StepFile {
/**
*
* @type {string}
* @memberof StepFile
*/
name: string;
/**
*
* @type {any}
* @memberof StepFile
*/
file?: any;
/**
*
* @type {number}
* @memberof StepFile
*/
id?: number;
}
/**
*
* @export
* @interface StepFood
*/
export interface StepFood {
/**
*
* @type {number}
* @memberof StepFood
*/
id?: number;
/**
*
* @type {string}
* @memberof StepFood
*/
name: string;
/**
*
* @type {string}
* @memberof StepFood
*/
description?: string;
/**
*
* @type {FoodRecipe}
* @memberof StepFood
*/
recipe?: FoodRecipe | null;
/**
*
* @type {boolean}
* @memberof StepFood
*/
ignore_shopping?: boolean;
/**
*
* @type {FoodSupermarketCategory}
* @memberof StepFood
*/
supermarket_category?: FoodSupermarketCategory | null;
/**
*
* @type {string}
* @memberof StepFood
*/
parent?: string;
/**
*
* @type {number}
* @memberof StepFood
*/
numchild?: number;
/**
*
* @type {boolean}
* @memberof StepFood
*/
on_hand?: boolean;
}
/**
*
* @export
* @interface StepIngredients
*/
export interface StepIngredients {
/**
*
* @type {number}
* @memberof StepIngredients
*/
id?: number;
/**
*
* @type {StepFood}
* @memberof StepIngredients
*/
food: StepFood | null;
/**
*
* @type {FoodSupermarketCategory}
* @memberof StepIngredients
*/
unit: FoodSupermarketCategory | null;
/**
*
* @type {string}
* @memberof StepIngredients
*/
amount: string;
/**
*
* @type {string}
* @memberof StepIngredients
*/
note?: string | null;
/**
*
* @type {number}
* @memberof StepIngredients
*/
order?: number;
/**
*
* @type {boolean}
* @memberof StepIngredients
*/
is_header?: boolean;
/**
*
* @type {boolean}
* @memberof StepIngredients
*/
no_amount?: boolean;
}
/**
*
* @export

View File

@ -220,6 +220,11 @@ export const ApiMixin = {
return {
Models: Models,
Actions: Actions,
FoodCreateDefault: function(form) {
form.inherit_ignore = getUserPreference("food_ignore_default")
form.inherit = form.supermarket_category.length > 0
return form
},
}
},
methods: {
@ -531,3 +536,11 @@ const specialCases = {
})
},
}
export const formFunctions = {
FoodCreateDefault: function(form) {
form.fields.filter((x) => x.field === "ignore_inherit")[0].value = getUserPreference("food_ignore_default")
form.fields.filter((x) => x.field === "inherit")[0].value = getUserPreference("food_ignore_default").length > 0
return form
},
}