Squashed commit of shoppinglist_v2

This commit is contained in:
smilerz
2021-12-30 15:33:34 -06:00
parent 67e4c88be7
commit 957c659a62
49 changed files with 701 additions and 1472 deletions

View File

@ -7,7 +7,9 @@ SQL_DEBUG=0
ALLOWED_HOSTS=* ALLOWED_HOSTS=*
# random secret key, use for example `base64 /dev/urandom | head -c50` to generate one # random secret key, use for example `base64 /dev/urandom | head -c50` to generate one
# ---------------------------- REQUIRED -------------------------
SECRET_KEY= SECRET_KEY=
# ---------------------------------------------------------------
# your default timezone See https://timezonedb.com/time-zones for a list of timezones # your default timezone See https://timezonedb.com/time-zones for a list of timezones
TIMEZONE=Europe/Berlin TIMEZONE=Europe/Berlin
@ -18,7 +20,9 @@ DB_ENGINE=django.db.backends.postgresql
POSTGRES_HOST=db_recipes POSTGRES_HOST=db_recipes
POSTGRES_PORT=5432 POSTGRES_PORT=5432
POSTGRES_USER=djangouser POSTGRES_USER=djangouser
# ---------------------------- REQUIRED -------------------------
POSTGRES_PASSWORD= POSTGRES_PASSWORD=
# ---------------------------------------------------------------
POSTGRES_DB=djangodb POSTGRES_DB=djangodb
# database connection string, when used overrides other database settings. # database connection string, when used overrides other database settings.

View File

@ -259,7 +259,7 @@ admin.site.register(ViewLog, ViewLogAdmin)
class InviteLinkAdmin(admin.ModelAdmin): class InviteLinkAdmin(admin.ModelAdmin):
list_display = ( list_display = (
'group', 'valid_until', 'group', 'valid_until', 'space',
'created_by', 'created_at', 'used_by' 'created_by', 'created_at', 'used_by'
) )

View File

@ -14,24 +14,24 @@ class CookbookConfig(AppConfig):
def ready(self): def ready(self):
import cookbook.signals # noqa import cookbook.signals # noqa
if not settings.DISABLE_TREE_FIX_STARTUP: # if not settings.DISABLE_TREE_FIX_STARTUP:
# when starting up run fix_tree to: # # when starting up run fix_tree to:
# a) make sure that nodes are sorted when switching between sort modes # # a) make sure that nodes are sorted when switching between sort modes
# b) fix problems, if any, with tree consistency # # b) fix problems, if any, with tree consistency
with scopes_disabled(): # with scopes_disabled():
try: # try:
from cookbook.models import Food, Keyword # from cookbook.models import Food, Keyword
Keyword.fix_tree(fix_paths=True) # Keyword.fix_tree(fix_paths=True)
Food.fix_tree(fix_paths=True) # Food.fix_tree(fix_paths=True)
except OperationalError: # except OperationalError:
if DEBUG: # if DEBUG:
traceback.print_exc() # traceback.print_exc()
pass # if model does not exist there is no need to fix it # pass # if model does not exist there is no need to fix it
except ProgrammingError: # except ProgrammingError:
if DEBUG: # if DEBUG:
traceback.print_exc() # traceback.print_exc()
pass # if migration has not been run database cannot be fixed yet # pass # if migration has not been run database cannot be fixed yet
except Exception: # except Exception:
if DEBUG: # if DEBUG:
traceback.print_exc() # traceback.print_exc()
pass # dont break startup just because fix could not run, need to investigate cases when this happens # pass # dont break startup just because fix could not run, need to investigate cases when this happens

View File

@ -7,7 +7,7 @@ from django_scopes import scopes_disabled
from django_scopes.forms import SafeModelChoiceField, SafeModelMultipleChoiceField from django_scopes.forms import SafeModelChoiceField, SafeModelMultipleChoiceField
from hcaptcha.fields import hCaptchaField from hcaptcha.fields import hCaptchaField
from .models import (Comment, InviteLink, Keyword, MealPlan, MealType, Recipe, RecipeBook, from .models import (Comment, Food, InviteLink, Keyword, MealPlan, MealType, Recipe, RecipeBook,
RecipeBookEntry, SearchPreference, Space, Storage, Sync, User, UserPreference) RecipeBookEntry, SearchPreference, Space, Storage, Sync, User, UserPreference)
@ -155,13 +155,14 @@ class ImportExportBase(forms.Form):
OPENEATS = 'OPENEATS' OPENEATS = 'OPENEATS'
PLANTOEAT = 'PLANTOEAT' PLANTOEAT = 'PLANTOEAT'
COOKBOOKAPP = 'COOKBOOKAPP' COOKBOOKAPP = 'COOKBOOKAPP'
COPYMETHAT = 'COPYMETHAT'
type = forms.ChoiceField(choices=( type = forms.ChoiceField(choices=(
(DEFAULT, _('Default')), (PAPRIKA, 'Paprika'), (NEXTCLOUD, 'Nextcloud Cookbook'), (DEFAULT, _('Default')), (PAPRIKA, 'Paprika'), (NEXTCLOUD, 'Nextcloud Cookbook'),
(MEALIE, 'Mealie'), (CHOWDOWN, 'Chowdown'), (SAFRON, 'Safron'), (CHEFTAP, 'ChefTap'), (MEALIE, 'Mealie'), (CHOWDOWN, 'Chowdown'), (SAFRON, 'Safron'), (CHEFTAP, 'ChefTap'),
(PEPPERPLATE, 'Pepperplate'), (RECETTETEK, 'RecetteTek'), (RECIPESAGE, 'Recipe Sage'), (DOMESTICA, 'Domestica'), (PEPPERPLATE, 'Pepperplate'), (RECETTETEK, 'RecetteTek'), (RECIPESAGE, 'Recipe Sage'), (DOMESTICA, 'Domestica'),
(MEALMASTER, 'MealMaster'), (REZKONV, 'RezKonv'), (OPENEATS, 'Openeats'), (RECIPEKEEPER, 'Recipe Keeper'), (MEALMASTER, 'MealMaster'), (REZKONV, 'RezKonv'), (OPENEATS, 'Openeats'), (RECIPEKEEPER, 'Recipe Keeper'),
(PLANTOEAT, 'Plantoeat'), (COOKBOOKAPP, 'CookBookApp'), (PLANTOEAT, 'Plantoeat'), (COOKBOOKAPP, 'CookBookApp'), (COPYMETHAT, 'CopyMeThat'),
)) ))
@ -524,7 +525,7 @@ class SpacePreferenceForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) # populates the post super().__init__(*args, **kwargs) # populates the post
self.fields['food_inherit'].queryset = Food.inherit_fields self.fields['food_inherit'].queryset = Food.inheritable_fields
class Meta: class Meta:
model = Space model = Space

View File

@ -1,13 +1,14 @@
import json import json
import re import re
from json import JSONDecodeError
from urllib.parse import unquote
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from bs4.element import Tag from bs4.element import Tag
from recipe_scrapers._utils import get_host_name, normalize_string
from cookbook.helper import recipe_url_import as helper from cookbook.helper import recipe_url_import as helper
from cookbook.helper.scrapers.scrapers import text_scraper from cookbook.helper.scrapers.scrapers import text_scraper
from json import JSONDecodeError
from recipe_scrapers._utils import get_host_name, normalize_string
from urllib.parse import unquote
def get_recipe_from_source(text, url, request): def get_recipe_from_source(text, url, request):
@ -58,7 +59,7 @@ def get_recipe_from_source(text, url, request):
return kid_list return kid_list
recipe_json = { recipe_json = {
'name': '', 'name': '',
'url': '', 'url': '',
'description': '', 'description': '',
'image': '', 'image': '',
@ -188,6 +189,6 @@ def remove_graph(el):
for x in el['@graph']: for x in el['@graph']:
if '@type' in x and x['@type'] == 'Recipe': if '@type' in x and x['@type'] == 'Recipe':
el = x el = x
except TypeError: except (TypeError, JSONDecodeError):
pass pass
return el return el

View File

@ -82,7 +82,7 @@ def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None
ingredients = Ingredient.objects.filter(step__recipe=r, space=space) ingredients = Ingredient.objects.filter(step__recipe=r, space=space)
if exclude_onhand := created_by.userpreference.mealplan_autoexclude_onhand: if exclude_onhand := created_by.userpreference.mealplan_autoexclude_onhand:
ingredients = ingredients.exclude(food__on_hand=True) ingredients = ingredients.exclude(food__food_onhand=True)
if related := created_by.userpreference.mealplan_autoinclude_related: if related := created_by.userpreference.mealplan_autoinclude_related:
# TODO: add levels of related recipes (related recipes of related recipes) to use when auto-adding mealplans # TODO: add levels of related recipes (related recipes of related recipes) to use when auto-adding mealplans
@ -93,7 +93,7 @@ def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None
# TODO once/if Steps can have a serving size this needs to be refactored # TODO once/if Steps can have a serving size this needs to be refactored
if exclude_onhand: if exclude_onhand:
# if steps are used more than once in a recipe or subrecipe - I don' think this results in the desired behavior # if steps are used more than once in a recipe or subrecipe - I don' think this results in the desired behavior
related_step_ing += Ingredient.objects.filter(step__recipe=x, food__on_hand=False, space=space).values_list('id', flat=True) related_step_ing += Ingredient.objects.filter(step__recipe=x, food__food_onhand=False, space=space).values_list('id', flat=True)
else: else:
related_step_ing += Ingredient.objects.filter(step__recipe=x, space=space).values_list('id', flat=True) related_step_ing += Ingredient.objects.filter(step__recipe=x, space=space).values_list('id', flat=True)
@ -101,10 +101,10 @@ def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None
if ingredients.filter(food__recipe=x).exists(): if ingredients.filter(food__recipe=x).exists():
for ing in ingredients.filter(food__recipe=x): for ing in ingredients.filter(food__recipe=x):
if exclude_onhand: if exclude_onhand:
x_ing = Ingredient.objects.filter(step__recipe=x, food__on_hand=False, space=space) x_ing = Ingredient.objects.filter(step__recipe=x, food__food_onhand=False, space=space)
else: else:
x_ing = Ingredient.objects.filter(step__recipe=x, space=space) x_ing = Ingredient.objects.filter(step__recipe=x, space=space)
for i in [x for x in x_ing if not x.food.ignore_shopping]: for i in [x for x in x_ing]:
ShoppingListEntry.objects.create( ShoppingListEntry.objects.create(
list_recipe=list_recipe, list_recipe=list_recipe,
food=i.food, food=i.food,
@ -139,7 +139,7 @@ def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None
sle.save() sle.save()
# add any missing Entrys # add any missing Entrys
for i in [x for x in add_ingredients if x.food and not x.food.ignore_shopping]: for i in [x for x in add_ingredients if x.food]:
ShoppingListEntry.objects.create( ShoppingListEntry.objects.create(
list_recipe=list_recipe, list_recipe=list_recipe,

View File

@ -0,0 +1,84 @@
import re
from io import BytesIO
from zipfile import ZipFile
from bs4 import BeautifulSoup
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.recipe_html_import import get_recipe_from_source
from cookbook.helper.recipe_url_import import iso_duration_to_minutes, parse_servings
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient, Keyword
from recipes.settings import DEBUG
class CopyMeThat(Integration):
def import_file_name_filter(self, zip_info_object):
if DEBUG:
print("testing", zip_info_object.filename, zip_info_object.filename == 'recipes.html')
return zip_info_object.filename == 'recipes.html'
def get_recipe_from_file(self, file):
# 'file' comes is as a beautifulsoup object
recipe = Recipe.objects.create(name=file.find("div", {"id": "name"}).text.strip(), created_by=self.request.user, internal=True, space=self.request.space, )
for category in file.find_all("span", {"class": "recipeCategory"}):
keyword, created = Keyword.objects.get_or_create(name=category.text, space=self.request.space)
recipe.keywords.add(keyword)
try:
recipe.servings = parse_servings(file.find("a", {"id": "recipeYield"}).text.strip())
recipe.working_time = iso_duration_to_minutes(file.find("span", {"meta": "prepTime"}).text.strip())
recipe.waiting_time = iso_duration_to_minutes(file.find("span", {"meta": "cookTime"}).text.strip())
recipe.save()
except AttributeError:
pass
step = Step.objects.create(instruction='', space=self.request.space, )
ingredient_parser = IngredientParser(self.request, True)
for ingredient in file.find_all("li", {"class": "recipeIngredient"}):
if ingredient.text == "":
continue
amount, unit, ingredient, note = ingredient_parser.parse(ingredient.text.strip())
f = ingredient_parser.get_food(ingredient)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
))
for s in file.find_all("li", {"class": "instruction"}):
if s.text == "":
continue
step.instruction += s.text.strip() + ' \n\n'
for s in file.find_all("li", {"class": "recipeNote"}):
if s.text == "":
continue
step.instruction += s.text.strip() + ' \n\n'
try:
if file.find("a", {"id": "original_link"}).text != '':
step.instruction += "\n\nImported from: " + file.find("a", {"id": "original_link"}).text
step.save()
except AttributeError:
pass
recipe.steps.add(step)
# import the Primary recipe image that is stored in the Zip
try:
for f in self.files:
if '.zip' in f['name']:
import_zip = ZipFile(f['file'])
self.import_recipe_image(recipe, BytesIO(import_zip.read(file.find("img", class_="recipeImage").get("src"))), filetype='.jpeg')
except Exception as e:
print(recipe.name, ': failed to import image ', str(e))
recipe.save()
return recipe
def split_recipe_file(self, file):
soup = BeautifulSoup(file, "html.parser")
return soup.find_all("div", {"class": "recipe"})

View File

@ -5,6 +5,7 @@ import uuid
from io import BytesIO, StringIO from io import BytesIO, StringIO
from zipfile import BadZipFile, ZipFile from zipfile import BadZipFile, ZipFile
from bs4 import Tag
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.core.files import File from django.core.files import File
from django.db import IntegrityError from django.db import IntegrityError
@ -16,7 +17,7 @@ from django_scopes import scope
from cookbook.forms import ImportExportBase from cookbook.forms import ImportExportBase
from cookbook.helper.image_processing import get_filetype, handle_image from cookbook.helper.image_processing import get_filetype, handle_image
from cookbook.models import Keyword, Recipe from cookbook.models import Keyword, Recipe
from recipes.settings import DATABASES, DEBUG from recipes.settings import DEBUG
class Integration: class Integration:
@ -153,9 +154,17 @@ class Integration:
file_list.append(z) file_list.append(z)
il.total_recipes += len(file_list) il.total_recipes += len(file_list)
import cookbook
if isinstance(self, cookbook.integration.copymethat.CopyMeThat):
file_list = self.split_recipe_file(BytesIO(import_zip.read('recipes.html')))
il.total_recipes += len(file_list)
for z in file_list: for z in file_list:
try: try:
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename))) if isinstance(z, Tag):
recipe = self.get_recipe_from_file(z)
else:
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
recipe.keywords.add(self.keyword) recipe.keywords.add(self.keyword)
il.msg += f'{recipe.pk} - {recipe.name} \n' il.msg += f'{recipe.pk} - {recipe.name} \n'
self.handle_duplicates(recipe, import_duplicates) self.handle_duplicates(recipe, import_duplicates)

View File

@ -28,11 +28,6 @@ class Migration(migrations.Migration):
] ]
operations = [ operations = [
migrations.AddField(
model_name='food',
name='on_hand',
field=models.BooleanField(default=False),
),
migrations.AddField( migrations.AddField(
model_name='shoppinglistentry', model_name='shoppinglistentry',
name='completed_at', name='completed_at',
@ -105,11 +100,6 @@ class Migration(migrations.Migration):
], ],
bases=(models.Model, PermissionModelMixin), bases=(models.Model, PermissionModelMixin),
), ),
migrations.AddField(
model_name='food',
name='inherit',
field=models.BooleanField(default=False),
),
migrations.AddField( migrations.AddField(
model_name='userpreference', model_name='userpreference',
name='mealplan_autoinclude_related', name='mealplan_autoinclude_related',
@ -117,7 +107,7 @@ class Migration(migrations.Migration):
), ),
migrations.AddField( migrations.AddField(
model_name='food', model_name='food',
name='ignore_inherit', name='inherit_fields',
field=models.ManyToManyField(blank=True, to='cookbook.FoodInheritField'), field=models.ManyToManyField(blank=True, to='cookbook.FoodInheritField'),
), ),
migrations.AddField( migrations.AddField(
@ -145,5 +135,10 @@ class Migration(migrations.Migration):
name='shopping_recent_days', name='shopping_recent_days',
field=models.PositiveIntegerField(default=7), field=models.PositiveIntegerField(default=7),
), ),
migrations.RenameField(
model_name='food',
old_name='ignore_shopping',
new_name='food_onhand',
),
migrations.RunPython(copy_values_to_sle), migrations.RunPython(copy_values_to_sle),
] ]

View File

@ -21,7 +21,7 @@ def delete_orphaned_sle(apps, schema_editor):
def create_inheritfields(apps, schema_editor): def create_inheritfields(apps, schema_editor):
FoodInheritField.objects.create(name='Supermarket Category', field='supermarket_category') FoodInheritField.objects.create(name='Supermarket Category', field='supermarket_category')
FoodInheritField.objects.create(name='Ignore Shopping', field='ignore_shopping') FoodInheritField.objects.create(name='On Hand', field='food_onhand')
FoodInheritField.objects.create(name='Diet', field='diet') FoodInheritField.objects.create(name='Diet', field='diet')
FoodInheritField.objects.create(name='Substitute', field='substitute') FoodInheritField.objects.create(name='Substitute', field='substitute')
FoodInheritField.objects.create(name='Substitute Children', field='substitute_children') FoodInheritField.objects.create(name='Substitute Children', field='substitute_children')

View File

@ -482,7 +482,7 @@ class Unit(ExportModelOperationsMixin('unit'), models.Model, PermissionModelMixi
class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin): class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
# exclude fields not implemented yet # exclude fields not implemented yet
inherit_fields = FoodInheritField.objects.exclude(field__in=['diet', 'substitute', 'substitute_children', 'substitute_siblings']) inheritable_fields = FoodInheritField.objects.exclude(field__in=['diet', 'substitute', 'substitute_children', 'substitute_siblings'])
# WARNING: Food inheritance relies on post_save signals, avoid using UPDATE to update Food objects unless you intend to bypass those signals # WARNING: Food inheritance relies on post_save signals, avoid using UPDATE to update Food objects unless you intend to bypass those signals
if SORT_TREE_BY_NAME: if SORT_TREE_BY_NAME:
@ -490,11 +490,9 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
name = models.CharField(max_length=128, validators=[MinLengthValidator(1)]) name = models.CharField(max_length=128, validators=[MinLengthValidator(1)])
recipe = models.ForeignKey('Recipe', null=True, blank=True, on_delete=models.SET_NULL) recipe = models.ForeignKey('Recipe', null=True, blank=True, on_delete=models.SET_NULL)
supermarket_category = models.ForeignKey(SupermarketCategory, null=True, blank=True, on_delete=models.SET_NULL) supermarket_category = models.ForeignKey(SupermarketCategory, null=True, blank=True, on_delete=models.SET_NULL)
ignore_shopping = models.BooleanField(default=False) # inherited field food_onhand = models.BooleanField(default=False) # inherited field
description = models.TextField(default='', blank=True) description = models.TextField(default='', blank=True)
on_hand = models.BooleanField(default=False) inherit_fields = models.ManyToManyField(FoodInheritField, blank=True) # inherited field: is this name better as inherit instead of ignore inherit? which is more intuitive?
inherit = models.BooleanField(default=False)
ignore_inherit = models.ManyToManyField(FoodInheritField, blank=True) # inherited field: is this name better as inherit instead of ignore inherit? which is more intuitive?
space = models.ForeignKey(Space, on_delete=models.CASCADE) space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space', _manager_class=TreeManager) objects = ScopedManager(space='space', _manager_class=TreeManager)
@ -512,34 +510,30 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
def reset_inheritance(space=None): def reset_inheritance(space=None):
# resets inheritted fields to the space defaults and updates all inheritted fields to root object values # resets inheritted fields to the space defaults and updates all inheritted fields to root object values
inherit = space.food_inherit.all() inherit = space.food_inherit.all()
ignore_inherit = Food.inherit_fields.difference(inherit)
# remove all inherited fields from food
Through = Food.objects.filter(space=space).first().inherit_fields.through
Through.objects.all().delete()
# food is going to inherit attributes # food is going to inherit attributes
if space.food_inherit.all().count() > 0: if space.food_inherit.all().count() > 0:
# using update to avoid creating a N*depth! save signals
Food.objects.filter(space=space).update(inherit=True)
# ManyToMany cannot be updated through an UPDATE operation # ManyToMany cannot be updated through an UPDATE operation
Through = Food.objects.first().ignore_inherit.through for i in inherit:
Through.objects.all().delete()
for i in ignore_inherit:
Through.objects.bulk_create([ Through.objects.bulk_create([
Through(food_id=x, foodinheritfield_id=i.id) Through(food_id=x, foodinheritfield_id=i.id)
for x in Food.objects.filter(space=space).values_list('id', flat=True) for x in Food.objects.filter(space=space).values_list('id', flat=True)
]) ])
inherit = inherit.values_list('field', flat=True) inherit = inherit.values_list('field', flat=True)
if 'ignore_shopping' in inherit: if 'food_onhand' in inherit:
# get food at root that have children that need updated # get food at root that have children that need updated
Food.include_descendants(queryset=Food.objects.filter(depth=1, numchild__gt=0, space=space, ignore_shopping=True)).update(ignore_shopping=True) Food.include_descendants(queryset=Food.objects.filter(depth=1, numchild__gt=0, space=space, food_onhand=True)).update(food_onhand=True)
Food.include_descendants(queryset=Food.objects.filter(depth=1, numchild__gt=0, space=space, ignore_shopping=False)).update(ignore_shopping=False) Food.include_descendants(queryset=Food.objects.filter(depth=1, numchild__gt=0, space=space, food_onhand=False)).update(food_onhand=False)
if 'supermarket_category' in inherit: if 'supermarket_category' in inherit:
# when supermarket_category is null or blank assuming it is not set and not intended to be blank for all descedants # when supermarket_category is null or blank assuming it is not set and not intended to be blank for all descedants
# find top node that has category set # find top node that has category set
category_roots = Food.exclude_descendants(queryset=Food.objects.filter(supermarket_category__isnull=False, numchild__gt=0, space=space)) category_roots = Food.exclude_descendants(queryset=Food.objects.filter(supermarket_category__isnull=False, numchild__gt=0, space=space))
for root in category_roots: for root in category_roots:
root.get_descendants().update(supermarket_category=root.supermarket_category) root.get_descendants().update(supermarket_category=root.supermarket_category)
else: # food is not going to inherit any attributes
Food.objects.filter(space=space).update(inherit=False)
class Meta: class Meta:
constraints = [ constraints = [

View File

@ -157,15 +157,9 @@ class FoodInheritFieldSerializer(WritableNestedModelSerializer):
class UserPreferenceSerializer(serializers.ModelSerializer): class UserPreferenceSerializer(serializers.ModelSerializer):
# food_inherit_default = FoodInheritFieldSerializer(source='space.food_inherit', read_only=True) food_inherit_default = FoodInheritFieldSerializer(source='space.food_inherit', many=True, allow_null=True, required=False, read_only=True)
food_ignore_default = serializers.SerializerMethodField('get_ignore_default')
plan_share = UserNameSerializer(many=True, allow_null=True, required=False, read_only=True) plan_share = UserNameSerializer(many=True, allow_null=True, required=False, read_only=True)
# TODO decide: default inherit field values for foods are being handled via VUE client through user preference
# should inherit field instead be set during the django model create?
def get_ignore_default(self, obj):
return FoodInheritFieldSerializer(Food.inherit_fields.difference(obj.space.food_inherit.all()), many=True).data
def create(self, validated_data): def create(self, validated_data):
if not validated_data.get('user', None): if not validated_data.get('user', None):
raise ValidationError(_('A user is required')) raise ValidationError(_('A user is required'))
@ -181,7 +175,7 @@ class UserPreferenceSerializer(serializers.ModelSerializer):
model = UserPreference model = UserPreference
fields = ( fields = (
'user', 'theme', 'nav_color', 'default_unit', 'default_page', 'use_kj', 'search_style', 'show_recent', 'plan_share', 'user', 'theme', 'nav_color', 'default_unit', 'default_page', 'use_kj', 'search_style', 'show_recent', 'plan_share',
'ingredient_decimals', 'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'food_ignore_default', 'default_delay', 'ingredient_decimals', 'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'food_inherit_default', 'default_delay',
'mealplan_autoinclude_related', 'mealplan_autoexclude_onhand', 'shopping_share', 'shopping_recent_days', 'csv_delim', 'csv_prefix', 'filter_to_supermarket' 'mealplan_autoinclude_related', 'mealplan_autoexclude_onhand', 'shopping_share', 'shopping_recent_days', 'csv_delim', 'csv_prefix', 'filter_to_supermarket'
) )
@ -305,7 +299,7 @@ class KeywordSerializer(UniqueFieldsMixin, ExtendedRecipeMixin):
model = Keyword model = Keyword
fields = ( fields = (
'id', 'name', 'icon', 'label', 'description', 'image', 'parent', 'numchild', 'numrecipe', 'created_at', 'id', 'name', 'icon', 'label', 'description', 'image', 'parent', 'numchild', 'numrecipe', 'created_at',
'updated_at') 'updated_at', 'full_name')
read_only_fields = ('id', 'label', 'numchild', 'parent', 'image') read_only_fields = ('id', 'label', 'numchild', 'parent', 'image')
@ -376,7 +370,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
supermarket_category = SupermarketCategorySerializer(allow_null=True, required=False) supermarket_category = SupermarketCategorySerializer(allow_null=True, required=False)
recipe = RecipeSimpleSerializer(allow_null=True, required=False) recipe = RecipeSimpleSerializer(allow_null=True, required=False)
shopping = serializers.SerializerMethodField('get_shopping_status') shopping = serializers.SerializerMethodField('get_shopping_status')
ignore_inherit = FoodInheritFieldSerializer(many=True, allow_null=True, required=False) inherit_fields = FoodInheritFieldSerializer(many=True, allow_null=True, required=False)
recipe_filter = 'steps__ingredients__food' recipe_filter = 'steps__ingredients__food'
@ -402,8 +396,8 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
class Meta: class Meta:
model = Food model = Food
fields = ( fields = (
'id', 'name', 'description', 'shopping', 'recipe', 'ignore_shopping', 'supermarket_category', 'id', 'name', 'description', 'shopping', 'recipe', 'food_onhand', 'supermarket_category',
'image', 'parent', 'numchild', 'numrecipe', 'on_hand', 'inherit', 'ignore_inherit', 'image', 'parent', 'numchild', 'numrecipe', 'inherit_fields', 'full_name'
) )
read_only_fields = ('id', 'numchild', 'parent', 'image', 'numrecipe') read_only_fields = ('id', 'numchild', 'parent', 'image', 'numrecipe')
@ -867,7 +861,7 @@ class FoodExportSerializer(FoodSerializer):
class Meta: class Meta:
model = Food model = Food
fields = ('name', 'ignore_shopping', 'supermarket_category', 'on_hand') fields = ('name', 'food_onhand', 'supermarket_category',)
class IngredientExportSerializer(WritableNestedModelSerializer): class IngredientExportSerializer(WritableNestedModelSerializer):

View File

@ -66,17 +66,17 @@ def update_food_inheritance(sender, instance=None, created=False, **kwargs):
if not instance: if not instance:
return return
inherit = Food.inherit_fields.difference(instance.ignore_inherit.all()) inherit = instance.inherit_fields.all()
# nothing to apply from parent and nothing to apply to children # nothing to apply from parent and nothing to apply to children
if (not instance.inherit or not instance.parent or inherit.count() == 0) and instance.numchild == 0: if (not instance.parent or inherit.count() == 0) and instance.numchild == 0:
return return
inherit = inherit.values_list('field', flat=True) inherit = inherit.values_list('field', flat=True)
# apply changes from parent to instance for each inheritted field # apply changes from parent to instance for each inheritted field
if instance.inherit and instance.parent and inherit.count() > 0: if instance.parent and inherit.count() > 0:
parent = instance.get_parent() parent = instance.get_parent()
if 'ignore_shopping' in inherit: if 'food_onhand' in inherit:
instance.ignore_shopping = parent.ignore_shopping instance.food_onhand = parent.food_onhand
# if supermarket_category is not set, do not cascade - if this becomes non-intuitive can change # if supermarket_category is not set, do not cascade - if this becomes non-intuitive can change
if 'supermarket_category' in inherit and parent.supermarket_category: if 'supermarket_category' in inherit and parent.supermarket_category:
instance.supermarket_category = parent.supermarket_category instance.supermarket_category = parent.supermarket_category
@ -89,13 +89,13 @@ def update_food_inheritance(sender, instance=None, created=False, **kwargs):
# TODO figure out how to generalize this # TODO figure out how to generalize this
# apply changes to direct children - depend on save signals for those objects to cascade inheritance down # apply changes to direct children - depend on save signals for those objects to cascade inheritance down
_save = [] _save = []
for child in instance.get_children().filter(inherit=True).exclude(ignore_inherit__field='ignore_shopping'): for child in instance.get_children().filter(inherit_fields__field='food_onhand'):
child.ignore_shopping = instance.ignore_shopping child.food_onhand = instance.food_onhand
_save.append(child) _save.append(child)
# don't cascade empty supermarket category # don't cascade empty supermarket category
if instance.supermarket_category: if instance.supermarket_category:
# apply changes to direct children - depend on save signals for those objects to cascade inheritance down # apply changes to direct children - depend on save signals for those objects to cascade inheritance down
for child in instance.get_children().filter(inherit=True).exclude(ignore_inherit__field='supermarket_category'): for child in instance.get_children().filter(inherit_fields__field='supermarket_category'):
child.supermarket_category = instance.supermarket_category child.supermarket_category = instance.supermarket_category
_save.append(child) _save.append(child)
for child in set(_save): for child in set(_save):

File diff suppressed because one or more lines are too long

View File

@ -67,7 +67,7 @@
</button> </button>
{% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %} {% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %}
<a class="navbar-brand p-0 me-2 justify-content-center" href="/" aria-label="Tandoor"> <a class="navbar-brand p-0 me-2 justify-content-center" href="{% base_path request 'base' %}" aria-label="Tandoor">
<img class="brand-icon" src="{% static 'assets/brand_logo.png' %}" alt="" style="height: 5vh;"> <img class="brand-icon" src="{% static 'assets/brand_logo.png' %}" alt="" style="height: 5vh;">
</a> </a>
{% endif %} {% endif %}

View File

@ -834,7 +834,7 @@
this.$http.get('{% url 'api:recipe-detail' 123456 %}'.replace('123456', recipe.id)).then((response) => { this.$http.get('{% url 'api:recipe-detail' 123456 %}'.replace('123456', recipe.id)).then((response) => {
for (let s of response.data.steps) { for (let s of response.data.steps) {
for (let i of s.ingredients) { for (let i of s.ingredients) {
if (!i.is_header && i.food !== null && i.food.ignore_shopping === false) { if (!i.is_header && i.food !== null && i.food.food_onhand === false) {
this.shopping_list.entries.push({ this.shopping_list.entries.push({
'list_recipe': slr.id, 'list_recipe': slr.id,
'food': i.food, 'food': i.food,

View File

@ -76,6 +76,7 @@
<option value="CHEFTAP">Cheftap</option> <option value="CHEFTAP">Cheftap</option>
<option value="CHOWDOWN">Chowdown</option> <option value="CHOWDOWN">Chowdown</option>
<option value="COOKBOOKAPP">CookBookApp</option> <option value="COOKBOOKAPP">CookBookApp</option>
<option value="COPYMETHAT">CopyMeThat</option>
<option value="DOMESTICA">Domestica</option> <option value="DOMESTICA">Domestica</option>
<option value="MEALIE">Mealie</option> <option value="MEALIE">Mealie</option>
<option value="MEALMASTER">Mealmaster</option> <option value="MEALMASTER">Mealmaster</option>

View File

@ -57,7 +57,19 @@ def obj_tree_1(request, space_1):
except AttributeError: except AttributeError:
params = {} params = {}
objs = [] objs = []
inherit = params.pop('inherit', False)
objs.extend(FoodFactory.create_batch(3, space=space_1, **params)) objs.extend(FoodFactory.create_batch(3, space=space_1, **params))
# set all foods to inherit everything
if inherit:
inherit = Food.inheritable_fields
Through = Food.objects.filter(space=space_1).first().inherit_fields.through
for i in inherit:
Through.objects.bulk_create([
Through(food_id=x, foodinheritfield_id=i.id)
for x in Food.objects.filter(space=space_1).values_list('id', flat=True)
])
objs[0].move(objs[1], node_location) objs[0].move(objs[1], node_location)
objs[1].move(objs[2], node_location) objs[1].move(objs[2], node_location)
return Food.objects.get(id=objs[1].id) # whenever you move/merge a tree it's safest to re-get the object return Food.objects.get(id=objs[1].id) # whenever you move/merge a tree it's safest to re-get the object
@ -471,8 +483,8 @@ def test_tree_filter(obj_tree_1, obj_2, obj_3, u1_s1):
@pytest.mark.parametrize("obj_tree_1, field, inherit, new_val", [ @pytest.mark.parametrize("obj_tree_1, field, inherit, new_val", [
({'has_category': True, 'inherit': True}, 'supermarket_category', True, 'cat_1'), ({'has_category': True, 'inherit': True}, 'supermarket_category', True, 'cat_1'),
({'has_category': True, 'inherit': False}, 'supermarket_category', False, 'cat_1'), ({'has_category': True, 'inherit': False}, 'supermarket_category', False, 'cat_1'),
({'ignore_shopping': True, 'inherit': True}, 'ignore_shopping', True, 'false'), ({'food_onhand': True, 'inherit': True}, 'food_onhand', True, 'false'),
({'ignore_shopping': True, 'inherit': False}, 'ignore_shopping', False, 'false'), ({'food_onhand': True, 'inherit': False}, 'food_onhand', False, 'false'),
], indirect=['obj_tree_1']) # indirect=True populates magic variable request.param of obj_tree_1 with the parameter ], indirect=['obj_tree_1']) # indirect=True populates magic variable request.param of obj_tree_1 with the parameter
def test_inherit(request, obj_tree_1, field, inherit, new_val, u1_s1): def test_inherit(request, obj_tree_1, field, inherit, new_val, u1_s1):
with scope(space=obj_tree_1.space): with scope(space=obj_tree_1.space):
@ -496,47 +508,17 @@ def test_inherit(request, obj_tree_1, field, inherit, new_val, u1_s1):
assert (getattr(child, field) == new_val) == inherit assert (getattr(child, field) == new_val) == inherit
@pytest.mark.parametrize("obj_tree_1, field, inherit, new_val", [
({'has_category': True, 'inherit': True, }, 'supermarket_category', True, 'cat_1'),
({'ignore_shopping': True, 'inherit': True, }, 'ignore_shopping', True, 'false'),
], indirect=['obj_tree_1'])
# This is more about the model than the API - should this be moved to a different test?
def test_ignoreinherit_field(request, obj_tree_1, field, inherit, new_val, u1_s1):
with scope(space=obj_tree_1.space):
parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0]
obj_tree_1.ignore_inherit.add(FoodInheritField.objects.get(field=field))
new_val = request.getfixturevalue(new_val)
# change parent to a new value
setattr(parent, field, new_val)
with scope(space=parent.space):
parent.save() # trigger post-save signal
# get the objects again because values are cached
obj_tree_1 = Food.objects.get(id=obj_tree_1.id)
# inheritance is blocked - should not get new value
assert getattr(obj_tree_1, field) != new_val
setattr(obj_tree_1, field, new_val)
with scope(space=parent.space):
obj_tree_1.save() # trigger post-save signal
# get the objects again because values are cached
child = Food.objects.get(id=child.id)
# inherit with child should still work
assert getattr(child, field) == new_val
@pytest.mark.parametrize("obj_tree_1", [ @pytest.mark.parametrize("obj_tree_1", [
({'has_category': True, 'inherit': False, 'ignore_shopping': True}), ({'has_category': True, 'inherit': False, 'food_onhand': True}),
], indirect=['obj_tree_1']) ], indirect=['obj_tree_1'])
def test_reset_inherit(obj_tree_1, space_1): def test_reset_inherit(obj_tree_1, space_1):
with scope(space=space_1): with scope(space=space_1):
space_1.food_inherit.add(*Food.inherit_fields.values_list('id', flat=True)) # set default inherit fields space_1.food_inherit.add(*Food.inheritable_fields.values_list('id', flat=True)) # set default inherit fields
parent = obj_tree_1.get_parent() parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0] child = obj_tree_1.get_descendants()[0]
obj_tree_1.ignore_shopping = False obj_tree_1.food_onhand = False
assert parent.ignore_shopping == child.ignore_shopping assert parent.food_onhand == child.food_onhand
assert parent.ignore_shopping != obj_tree_1.ignore_shopping assert parent.food_onhand != obj_tree_1.food_onhand
assert parent.supermarket_category != child.supermarket_category assert parent.supermarket_category != child.supermarket_category
assert parent.supermarket_category != obj_tree_1.supermarket_category assert parent.supermarket_category != obj_tree_1.supermarket_category
@ -545,5 +527,5 @@ def test_reset_inherit(obj_tree_1, space_1):
obj_tree_1 = Food.objects.get(id=obj_tree_1.id) obj_tree_1 = Food.objects.get(id=obj_tree_1.id)
parent = obj_tree_1.get_parent() parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0] child = obj_tree_1.get_descendants()[0]
assert parent.ignore_shopping == obj_tree_1.ignore_shopping == child.ignore_shopping assert parent.food_onhand == obj_tree_1.food_onhand == child.food_onhand
assert parent.supermarket_category == obj_tree_1.supermarket_category == child.supermarket_category assert parent.supermarket_category == obj_tree_1.supermarket_category == child.supermarket_category

View File

@ -3,6 +3,8 @@ from datetime import timedelta
import factory import factory
import pytest import pytest
# work around for bug described here https://stackoverflow.com/a/70312265/15762829
from django.conf import settings
from django.contrib import auth from django.contrib import auth
from django.forms import model_to_dict from django.forms import model_to_dict
from django.urls import reverse from django.urls import reverse
@ -14,6 +16,11 @@ from cookbook.models import Food, Ingredient, ShoppingListEntry, Step
from cookbook.tests.factories import (IngredientFactory, MealPlanFactory, RecipeFactory, from cookbook.tests.factories import (IngredientFactory, MealPlanFactory, RecipeFactory,
StepFactory, UserFactory) StepFactory, UserFactory)
if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2',
'django.db.backends.postgresql']:
from django.db.backends.postgresql.features import DatabaseFeatures
DatabaseFeatures.can_defer_constraint_checks = False
SHOPPING_LIST_URL = 'api:shoppinglistentry-list' SHOPPING_LIST_URL = 'api:shoppinglistentry-list'
SHOPPING_RECIPE_URL = 'api:recipe-shopping' SHOPPING_RECIPE_URL = 'api:recipe-shopping'
@ -43,7 +50,7 @@ def recipe(request, space_1, u1_s1):
# steps__food_recipe_count = params.get('steps__food_recipe_count', {}) # steps__food_recipe_count = params.get('steps__food_recipe_count', {})
params['created_by'] = params.get('created_by', auth.get_user(u1_s1)) params['created_by'] = params.get('created_by', auth.get_user(u1_s1))
params['space'] = space_1 params['space'] = space_1
return RecipeFactory.create(**params) return RecipeFactory(**params)
# return RecipeFactory.create( # return RecipeFactory.create(
# steps__recipe_count=steps__recipe_count, # steps__recipe_count=steps__recipe_count,
@ -178,27 +185,24 @@ def test_shopping_recipe_edit(request, recipe, sle_count, use_mealplan, u1_s1, u
@pytest.mark.parametrize("user2, sle_count", [ @pytest.mark.parametrize("user2, sle_count", [
({'mealplan_autoadd_shopping': False}, (0, 17)), ({'mealplan_autoadd_shopping': False}, (0, 18)),
({'mealplan_autoinclude_related': False}, (8, 8)), ({'mealplan_autoinclude_related': False}, (9, 9)),
({'mealplan_autoexclude_onhand': False}, (19, 19)), ({'mealplan_autoexclude_onhand': False}, (20, 20)),
({'mealplan_autoexclude_onhand': False, 'mealplan_autoinclude_related': False}, (9, 9)), ({'mealplan_autoexclude_onhand': False, 'mealplan_autoinclude_related': False}, (10, 10)),
], indirect=['user2']) ], indirect=['user2'])
@pytest.mark.parametrize("use_mealplan", [(False), (True), ]) @pytest.mark.parametrize("use_mealplan", [(False), (True), ])
@pytest.mark.parametrize("recipe", [({'steps__recipe_count': 1})], indirect=['recipe']) @pytest.mark.parametrize("recipe", [({'steps__recipe_count': 1})], indirect=['recipe'])
def test_shopping_recipe_userpreference(recipe, sle_count, use_mealplan, user2): def test_shopping_recipe_userpreference(recipe, sle_count, use_mealplan, user2):
with scopes_disabled(): with scopes_disabled():
user = auth.get_user(user2) user = auth.get_user(user2)
# setup recipe with 10 ingredients, 1 step recipe with 10 ingredients, 2 food onhand(from recipe and step_recipe), 1 food ignore shopping # setup recipe with 10 ingredients, 1 step recipe with 10 ingredients, 2 food onhand(from recipe and step_recipe)
ingredients = Ingredient.objects.filter(step__recipe=recipe) ingredients = Ingredient.objects.filter(step__recipe=recipe)
food = Food.objects.get(id=ingredients[2].food.id) food = Food.objects.get(id=ingredients[2].food.id)
food.on_hand = True food.food_onhand = True
food.save() food.save()
food = recipe.steps.filter(type=Step.RECIPE).first().step_recipe.steps.first().ingredients.first().food food = recipe.steps.filter(type=Step.RECIPE).first().step_recipe.steps.first().ingredients.first().food
food = Food.objects.get(id=food.id) food = Food.objects.get(id=food.id)
food.on_hand = True food.food_onhand = True
food.save()
food = Food.objects.get(id=ingredients[4].food.id)
food.ignore_shopping = True
food.save() food.save()
if use_mealplan: if use_mealplan:

View File

@ -112,30 +112,29 @@ def test_preference_delete(u1_s1, u2_s1):
def test_default_inherit_fields(u1_s1, u1_s2, space_1, space_2): def test_default_inherit_fields(u1_s1, u1_s2, space_1, space_2):
food_inherit_fields = Food.inherit_fields.all() food_inherit_fields = Food.inheritable_fields
assert len([x.field for x in food_inherit_fields]) > 0
r = u1_s1.get(
reverse(DETAIL_URL, args={auth.get_user(u1_s1).id}),
)
# by default space food will not inherit any fields, so all of them will be ignored # by default space food will not inherit any fields, so all of them will be ignored
assert space_1.food_inherit.all().count() == 0 assert space_1.food_inherit.all().count() == 0
assert len([x.field for x in food_inherit_fields]) == len([x['field'] for x in json.loads(r.content)['food_ignore_default']]) > 0
# inherit all possible fields
with scope(space=space_1):
space_1.food_inherit.add(*Food.inherit_fields.values_list('id', flat=True))
r = u1_s1.get( r = u1_s1.get(
reverse(DETAIL_URL, args={auth.get_user(u1_s1).id}), reverse(DETAIL_URL, args={auth.get_user(u1_s1).id}),
) )
assert len([x['field'] for x in json.loads(r.content)['food_inherit_default']]) == 0
assert space_1.food_inherit.all().count() == Food.inherit_fields.all().count() > 0 # inherit all possible fields
# now by default, food is not ignoring inheritance on any field with scope(space=space_1):
assert len([x['field'] for x in json.loads(r.content)['food_ignore_default']]) == 0 space_1.food_inherit.add(*Food.inheritable_fields.values_list('id', flat=True))
# other spaces and users in those spaced not effected assert space_1.food_inherit.all().count() == Food.inheritable_fields.count() > 0
# now by default, food is inheriting all of the possible fields
r = u1_s1.get(
reverse(DETAIL_URL, args={auth.get_user(u1_s1).id}),
)
assert len([x['field'] for x in json.loads(r.content)['food_inherit_default']]) == space_1.food_inherit.all().count()
# other spaces and users in those spaces not effected
r = u1_s2.get( r = u1_s2.get(
reverse(DETAIL_URL, args={auth.get_user(u1_s2).id}), reverse(DETAIL_URL, args={auth.get_user(u1_s2).id}),
) )
assert space_2.food_inherit.all().count() == 0 assert space_2.food_inherit.all().count() == 0 == len([x['field'] for x in json.loads(r.content)['food_inherit_default']])
assert len([x.field for x in food_inherit_fields]) == len([x['field'] for x in json.loads(r.content)['food_ignore_default']]) > 0

View File

@ -402,7 +402,8 @@ class FoodInheritFieldViewSet(viewsets.ReadOnlyModelViewSet):
def get_queryset(self): def get_queryset(self):
# exclude fields not yet implemented # exclude fields not yet implemented
return Food.inherit_fields self.queryset = Food.inheritable_fields
return super().get_queryset()
class FoodViewSet(viewsets.ModelViewSet, TreeMixin): class FoodViewSet(viewsets.ModelViewSet, TreeMixin):

View File

@ -11,6 +11,7 @@ from django.utils.translation import gettext as _
from cookbook.forms import ExportForm, ImportForm, ImportExportBase from cookbook.forms import ExportForm, ImportForm, ImportExportBase
from cookbook.helper.permission_helper import group_required from cookbook.helper.permission_helper import group_required
from cookbook.integration.cookbookapp import CookBookApp from cookbook.integration.cookbookapp import CookBookApp
from cookbook.integration.copymethat import CopyMeThat
from cookbook.integration.pepperplate import Pepperplate from cookbook.integration.pepperplate import Pepperplate
from cookbook.integration.cheftap import ChefTap from cookbook.integration.cheftap import ChefTap
from cookbook.integration.chowdown import Chowdown from cookbook.integration.chowdown import Chowdown
@ -65,6 +66,8 @@ def get_integration(request, export_type):
return Plantoeat(request, export_type) return Plantoeat(request, export_type)
if export_type == ImportExportBase.COOKBOOKAPP: if export_type == ImportExportBase.COOKBOOKAPP:
return CookBookApp(request, export_type) return CookBookApp(request, export_type)
if export_type == ImportExportBase.COPYMETHAT:
return CopyMeThat(request, export_type)
@group_required('user') @group_required('user')

View File

@ -201,7 +201,10 @@ class InviteLinkCreate(GroupRequiredMixin, CreateView):
def form_valid(self, form): def form_valid(self, form):
obj = form.save(commit=False) obj = form.save(commit=False)
obj.created_by = self.request.user obj.created_by = self.request.user
obj.space = self.request.space
# verify given space is actually owned by the user creating the link
if obj.space.created_by != self.request.user:
obj.space = self.request.space
obj.save() obj.save()
if obj.email: if obj.email:
try: try:

View File

@ -561,7 +561,7 @@ def space(request):
space_form = SpacePreferenceForm(instance=request.space) space_form = SpacePreferenceForm(instance=request.space)
space_form.base_fields['food_inherit'].queryset = Food.inherit_fields space_form.base_fields['food_inherit'].queryset = Food.inheritable_fields
if request.method == "POST" and 'space_form' in request.POST: if request.method == "POST" and 'space_form' in request.POST:
form = SpacePreferenceForm(request.POST, prefix='space') form = SpacePreferenceForm(request.POST, prefix='space')
if form.is_valid(): if form.is_valid():

View File

@ -37,6 +37,7 @@ Overview of the capabilities of the different integrations.
| OpenEats | ✔️ | ❌ | ⌚ | | OpenEats | ✔️ | ❌ | ⌚ |
| Plantoeat | ✔️ | ❌ | ✔ | | Plantoeat | ✔️ | ❌ | ✔ |
| CookBookApp | ✔️ | ⌚ | ✔️ | | CookBookApp | ✔️ | ⌚ | ✔️ |
| CopyMeThat | ✔️ | ❌ | ✔️ |
✔ = implemented, ❌ = not implemented and not possible/planned, ⌚ = not yet implemented ✔ = implemented, ❌ = not implemented and not possible/planned, ⌚ = not yet implemented
@ -218,3 +219,7 @@ Plan to eat allows you to export a text file containing all your recipes. Simply
## CookBookApp ## CookBookApp
CookBookApp can export .zip files containing .html files. Upload the entire ZIP to Tandoor to import all included recipes. CookBookApp can export .zip files containing .html files. Upload the entire ZIP to Tandoor to import all included recipes.
## CopyMeThat
CopyMeThat can export .zip files containing an `.html` file as well as a folder containing all the images. Upload the entire ZIP to Tandoor to import all included recipes.

View File

@ -1,6 +1,6 @@
!!! success "Recommended Installation" !!! success "Recommended Installation"
Setting up this application using Docker is recommended. This does not mean that other options are bad, just that Setting up this application using Docker is recommended. This does not mean that other options are bad, just that
support is much easier for this setup. support is much easier for this setup.
It is possible to install this application using many Docker configurations. It is possible to install this application using many Docker configurations.
@ -34,27 +34,27 @@ file in the GitHub repository to verify if additional environment variables are
### Versions ### Versions
There are different versions (tags) released on docker hub. There are different versions (tags) released on docker hub.
- **latest** Default image. The one you should use if you don't know that you need anything else. - **latest** Default image. The one you should use if you don't know that you need anything else.
- **beta** Partially stable version that gets updated every now and then. Expect to have some problems. - **beta** Partially stable version that gets updated every now and then. Expect to have some problems.
- **develop** If you want the most bleeding edge version with potentially many breaking changes feel free to use this version (I don't recommend it!). - **develop** If you want the most bleeding edge version with potentially many breaking changes feel free to use this version (I don't recommend it!).
- **X.Y.Z** each released version has its own image. If you need to revert to an old version or want to make sure you stay on one specific use these tags. - **X.Y.Z** each released version has its own image. If you need to revert to an old version or want to make sure you stay on one specific use these tags.
!!! danger "No Downgrading" !!! danger "No Downgrading"
There is currently no way to migrate back to an older version as there is no mechanism to downgrade the database. There is currently no way to migrate back to an older version as there is no mechanism to downgrade the database.
You could probably do it but I cannot help you with that. Choose wisely if you want to use the unstable images. You could probably do it but I cannot help you with that. Choose wisely if you want to use the unstable images.
That said **beta** should usually be working if you like frequent updates and new stuff. That said **beta** should usually be working if you like frequent updates and new stuff.
## Docker Compose ## Docker Compose
The main, and also recommended, installation option is to install this application using Docker Compose. The main, and also recommended, installation option is to install this application using Docker Compose.
1. Choose your `docker-compose.yml` from the examples below. 1. Choose your `docker-compose.yml` from the examples below.
2. Download the `.env` configuration file with `wget`, then **edit it accordingly**. 2. Download the `.env` configuration file with `wget`, then **edit it accordingly** (you NEED to set `SECRET_KEY` and `POSTGRES_PASSWORD`).
```shell ```shell
wget https://raw.githubusercontent.com/vabene1111/recipes/develop/.env.template -O .env wget https://raw.githubusercontent.com/vabene1111/recipes/develop/.env.template -O .env
``` ```
3. Start your container using `docker-compose up -d`. 3. Start your container using `docker-compose up -d`.
### Plain ### Plain
@ -65,29 +65,30 @@ This configuration exposes the application through an nginx web server on port 8
wget https://raw.githubusercontent.com/vabene1111/recipes/develop/docs/install/docker/plain/docker-compose.yml wget https://raw.githubusercontent.com/vabene1111/recipes/develop/docs/install/docker/plain/docker-compose.yml
``` ```
~~~yaml ```yaml
{% include "./docker/plain/docker-compose.yml" %} { % include "./docker/plain/docker-compose.yml" % }
~~~ ```
### Reverse Proxy ### Reverse Proxy
Most deployments will likely use a reverse proxy. Most deployments will likely use a reverse proxy.
#### Traefik #### Traefik
If you use traefik, this configuration is the one for you. If you use traefik, this configuration is the one for you.
!!! info !!! info
Traefik can be a little confusing to setup. Traefik can be a little confusing to setup.
Please refer to [their excellent documentation](https://doc.traefik.io/traefik/). If that does not help, Please refer to [their excellent documentation](https://doc.traefik.io/traefik/). If that does not help,
[this little example](traefik.md) might be for you. [this little example](traefik.md) might be for you.
```shell ```shell
wget https://raw.githubusercontent.com/vabene1111/recipes/develop/docs/install/docker/traefik-nginx/docker-compose.yml wget https://raw.githubusercontent.com/vabene1111/recipes/develop/docs/install/docker/traefik-nginx/docker-compose.yml
``` ```
~~~yaml ```yaml
{% include "./docker/traefik-nginx/docker-compose.yml" %} { % include "./docker/traefik-nginx/docker-compose.yml" % }
~~~ ```
#### nginx-proxy #### nginx-proxy
@ -97,6 +98,7 @@ in combination with [jrcs's letsencrypt companion](https://hub.docker.com/r/jrcs
Please refer to the appropriate documentation on how to setup the reverse proxy and networks. Please refer to the appropriate documentation on how to setup the reverse proxy and networks.
Remember to add the appropriate environment variables to `.env` file: Remember to add the appropriate environment variables to `.env` file:
``` ```
VIRTUAL_HOST= VIRTUAL_HOST=
LETSENCRYPT_HOST= LETSENCRYPT_HOST=
@ -107,27 +109,49 @@ LETSENCRYPT_EMAIL=
wget https://raw.githubusercontent.com/vabene1111/recipes/develop/docs/install/docker/nginx-proxy/docker-compose.yml wget https://raw.githubusercontent.com/vabene1111/recipes/develop/docs/install/docker/nginx-proxy/docker-compose.yml
``` ```
~~~yaml ```yaml
{% include "./docker/nginx-proxy/docker-compose.yml" %} { % include "./docker/nginx-proxy/docker-compose.yml" % }
~~~ ```
#### Nginx Swag by LinuxServer #### Nginx Swag by LinuxServer
[This container](https://github.com/linuxserver/docker-swag) is an all in one solution created by LinuxServer.io
It also contains templates for popular apps, including Tandoor Recipes, so you don't have to manually configure nginx and discard the template provided in Tandoor repo. Tandoor config is called `recipes.subdomain.conf.sample` which you can adapt for your instance [This container](https://github.com/linuxserver/docker-swag) is an all in one solution created by LinuxServer.io.
It contains templates for popular apps, including Tandoor Recipes, so you don't have to manually configure nginx and discard the template provided in Tandoor repo. Tandoor config is called `recipes.subdomain.conf.sample` which you can adapt for your instance.
If you're running Swag on the default port, you'll just need to change the container name to yours. If you're running Swag on the default port, you'll just need to change the container name to yours.
If your running Swag on a custom port, some headers must be changed. To do this, If your running Swag on a custom port, some headers must be changed:
- Create a copy of `proxy.conf` - Create a copy of `proxy.conf`
- Replace `proxy_set_header X-Forwarded-Host $host;` and `proxy_set_header Host $host;` to - Replace `proxy_set_header X-Forwarded-Host $host;` and `proxy_set_header Host $host;` to
- `proxy_set_header X-Forwarded-Host $http_host;` and `proxy_set_header Host $http_host;` - `proxy_set_header X-Forwarded-Host $http_host;` and `proxy_set_header Host $http_host;`
- Update `recipes.subdomain.conf` to use the new file - Update `recipes.subdomain.conf` to use the new file
- Restart the linuxserver/swag container and Recipes will work - Restart the linuxserver/swag container and Recipes will work correctly
More information [here](https://github.com/TandoorRecipes/recipes/issues/959#issuecomment-962648627). More information [here](https://github.com/TandoorRecipes/recipes/issues/959#issuecomment-962648627).
In both cases, also make sure to mount `/media/` in your swag container to point to your Tandoor Recipes Media directory.
Please refer to the [appropriate documentation](https://github.com/linuxserver/docker-swag#usage) for the container setup.
#### Nginx Swag by LinuxServer
[This container](https://github.com/linuxserver/docker-swag) is an all in one solution created by LinuxServer.io
It also contains templates for popular apps, including Tandoor Recipes, so you don't have to manually configure nginx and discard the template provided in Tandoor repo. Tandoor config is called `recipes.subdomain.conf.sample` which you can adapt for your instance
If you're running Swag on the default port, you'll just need to change the container name to yours.
If your running Swag on a custom port, some headers must be changed. To do this,
- Create a copy of `proxy.conf`
- Replace `proxy_set_header X-Forwarded-Host $host;` and `proxy_set_header Host $host;` to
- `proxy_set_header X-Forwarded-Host $http_host;` and `proxy_set_header Host $http_host;`
- Update `recipes.subdomain.conf` to use the new file
- Restart the linuxserver/swag container and Recipes will work
More information [here](https://github.com/TandoorRecipes/recipes/issues/959#issuecomment-962648627).
In both cases, also make sure to mount `/media/` in your swag container to point to your Tandoor Recipes Media directory. In both cases, also make sure to mount `/media/` in your swag container to point to your Tandoor Recipes Media directory.
@ -136,6 +160,7 @@ Please refer to the [appropriate documentation](https://github.com/linuxserver/d
## Additional Information ## Additional Information
### Nginx vs Gunicorn ### Nginx vs Gunicorn
All examples use an additional `nginx` container to serve mediafiles and act as the forward facing webserver. All examples use an additional `nginx` container to serve mediafiles and act as the forward facing webserver.
This is **technically not required** but **very much recommended**. This is **technically not required** but **very much recommended**.
@ -144,14 +169,14 @@ the WSGi server that handles the Python execution, explicitly state that it is n
You will also likely not see any decrease in performance or a lot of space used as nginx is a very light container. You will also likely not see any decrease in performance or a lot of space used as nginx is a very light container.
!!! info !!! info
Even if you run behind a reverse proxy as described above, using an additional nginx container is the recommended option. Even if you run behind a reverse proxy as described above, using an additional nginx container is the recommended option.
If you run a small private deployment and don't care about performance, security and whatever else feel free to run If you run a small private deployment and don't care about performance, security and whatever else feel free to run
without a ngix container. without a ngix container.
!!! warning !!! warning
When running without nginx make sure to enable `GUNICORN_MEDIA` in the `.env`. Without it, media files will be uploaded When running without nginx make sure to enable `GUNICORN_MEDIA` in the `.env`. Without it, media files will be uploaded
but not shown on the page. but not shown on the page.
For additional information please refer to the [0.9.0 Release](https://github.com/vabene1111/recipes/releases?after=0.9.0) For additional information please refer to the [0.9.0 Release](https://github.com/vabene1111/recipes/releases?after=0.9.0)
and [Issue 201](https://github.com/vabene1111/recipes/issues/201) where these topics have been discussed. and [Issue 201](https://github.com/vabene1111/recipes/issues/201) where these topics have been discussed.

View File

@ -56,6 +56,7 @@ CORS_ORIGIN_ALLOW_ALL = True
LOGIN_REDIRECT_URL = "index" LOGIN_REDIRECT_URL = "index"
LOGOUT_REDIRECT_URL = "index" LOGOUT_REDIRECT_URL = "index"
ACCOUNT_LOGOUT_REDIRECT_URL = "index"
SESSION_EXPIRE_AT_BROWSER_CLOSE = False SESSION_EXPIRE_AT_BROWSER_CLOSE = False
SESSION_COOKIE_AGE = 365 * 60 * 24 * 60 SESSION_COOKIE_AGE = 365 * 60 * 24 * 60

View File

@ -19,8 +19,10 @@
<!-- <span><b-button variant="link" size="sm" class="text-dark shadow-none"><i class="fas fa-chevron-down"></i></b-button></span> --> <!-- <span><b-button variant="link" size="sm" class="text-dark shadow-none"><i class="fas fa-chevron-down"></i></b-button></span> -->
<model-menu /> <model-menu />
<span>{{ this.this_model.name }}</span> <span>{{ this.this_model.name }}</span>
<span v-if="this_model.name !== 'Step'" <span v-if="apiName !== 'Step'">
><b-button variant="link" @click="startAction({ action: 'new' })"><i class="fas fa-plus-circle fa-2x"></i></b-button></span <b-button variant="link" @click="startAction({ action: 'new' })">
<i class="fas fa-plus-circle fa-2x"></i>
</b-button> </span
><!-- TODO add proper field to model config to determine if create should be available or not --> ><!-- TODO add proper field to model config to determine if create should be available or not -->
</h3> </h3>
</div> </div>
@ -112,6 +114,9 @@ export default {
// TODO this is not necessarily bad but maybe there are better options to do this // TODO this is not necessarily bad but maybe there are better options to do this
return () => import(/* webpackChunkName: "header-component" */ `@/components/${this.header_component_name}`) return () => import(/* webpackChunkName: "header-component" */ `@/components/${this.header_component_name}`)
}, },
apiName() {
return this.this_model?.apiName
},
}, },
mounted() { mounted() {
// value is passed from lists.py // value is passed from lists.py
@ -291,11 +296,6 @@ export default {
this.refreshCard({ ...food }, this.items_right) this.refreshCard({ ...food }, this.items_right)
}) })
}, },
addOnhand: function (item) {
item.on_hand = true
this.saveThis(item)
},
updateThis: function (item) { updateThis: function (item) {
this.refreshThis(item.id) this.refreshThis(item.id)
}, },

View File

@ -49,13 +49,13 @@
<div class="col-md-6 mt-1"> <div class="col-md-6 mt-1">
<label for="id_name"> {{ $t('Preparation') }} {{ $t('Time') }} ({{ $t('min') }})</label> <label for="id_name"> {{ $t('Preparation') }} {{ $t('Time') }} ({{ $t('min') }})</label>
<input class="form-control" id="id_prep_time" v-model="recipe.working_time"> <input class="form-control" id="id_prep_time" v-model="recipe.working_time" type="number">
<br/> <br/>
<label for="id_name"> {{ $t('Waiting') }} {{ $t('Time') }} ({{ $t('min') }})</label> <label for="id_name"> {{ $t('Waiting') }} {{ $t('Time') }} ({{ $t('min') }})</label>
<input class="form-control" id="id_wait_time" v-model="recipe.waiting_time"> <input class="form-control" id="id_wait_time" v-model="recipe.waiting_time" type="number">
<br/> <br/>
<label for="id_name"> {{ $t('Servings') }}</label> <label for="id_name"> {{ $t('Servings') }}</label>
<input class="form-control" id="id_servings" v-model="recipe.servings"> <input class="form-control" id="id_servings" v-model="recipe.servings" type="number">
<br/> <br/>
<label for="id_name"> {{ $t('Servings') }} {{ $t('Text') }}</label> <label for="id_name"> {{ $t('Servings') }} {{ $t('Text') }}</label>
<input class="form-control" id="id_servings_text" v-model="recipe.servings_text" maxlength="32"> <input class="form-control" id="id_servings_text" v-model="recipe.servings_text" maxlength="32">
@ -343,7 +343,7 @@
</div> </div>
<div class="small-padding" <div class="small-padding"
v-bind:class="{ 'col-lg-4 col-md-6': !ingredient.is_header, 'col-lg-12 col-md-12': ingredient.is_header }"> v-bind:class="{ 'col-lg-4 col-md-6': !ingredient.is_header, 'col-lg-12 col-md-12': ingredient.is_header }">
<input class="form-control" <input class="form-control" maxlength="256"
v-model="ingredient.note" v-model="ingredient.note"
v-bind:placeholder="$t('Note')" v-bind:placeholder="$t('Note')"
v-on:keydown.tab="event => {if(step.ingredients.indexOf(ingredient) === (step.ingredients.length -1)){event.preventDefault();addIngredient(step)}}"> v-on:keydown.tab="event => {if(step.ingredients.indexOf(ingredient) === (step.ingredients.length -1)){event.preventDefault();addIngredient(step)}}">
@ -623,9 +623,10 @@ export default {
this.sortIngredients(s) this.sortIngredients(s)
} }
if (this.recipe.waiting_time === ''){ this.recipe.waiting_time = 0} if (this.recipe.waiting_time === '' || isNaN(this.recipe.waiting_time)){ this.recipe.waiting_time = 0}
if (this.recipe.working_time === ''){ this.recipe.working_time = 0} if (this.recipe.working_time === ''|| isNaN(this.recipe.working_time)){ this.recipe.working_time = 0}
if (this.recipe.servings === ''){ this.recipe.servings = 0} if (this.recipe.servings === ''|| isNaN(this.recipe.servings)){ this.recipe.servings = 0}
apiFactory.updateRecipe(this.recipe_id, this.recipe, apiFactory.updateRecipe(this.recipe_id, this.recipe,
{}).then((response) => { {}).then((response) => {

View File

@ -306,7 +306,7 @@ export default {
this.settings?.search_keywords?.length === 0 && this.settings?.search_keywords?.length === 0 &&
this.settings?.search_foods?.length === 0 && this.settings?.search_foods?.length === 0 &&
this.settings?.search_books?.length === 0 && this.settings?.search_books?.length === 0 &&
this.settings?.pagination_page === 1 && // this.settings?.pagination_page === 1 &&
!this.random_search && !this.random_search &&
this.settings?.search_ratings === undefined this.settings?.search_ratings === undefined
) { ) {

View File

@ -200,6 +200,9 @@ export default {
ingredient_factor: function () { ingredient_factor: function () {
return this.servings / this.recipe.servings return this.servings / this.recipe.servings
}, },
title() {
return this.recipe?.steps?.map((x) => x?.ingredients).flat()
},
}, },
data() { data() {
return { return {
@ -212,9 +215,11 @@ export default {
share_uid: window.SHARE_UID, share_uid: window.SHARE_UID,
} }
}, },
mounted() { mounted() {
this.loadRecipe(window.RECIPE_ID) this.loadRecipe(window.RECIPE_ID)
this.$i18n.locale = window.CUSTOM_LOCALE this.$i18n.locale = window.CUSTOM_LOCALE
console.log(this.recipe)
}, },
methods: { methods: {
loadRecipe: function (recipe_id) { loadRecipe: function (recipe_id) {

View File

@ -30,22 +30,24 @@
<div class="row"> <div class="row">
<div class="col col-md-12"> <div class="col col-md-12">
<div role="tablist"> <div role="tablist">
<div class="row justify-content-md-center w-75" v-if="entrymode"> <!-- add to shopping form -->
<div class="col col-md-2"> <b-row class="row justify-content-md-center" v-if="entrymode">
<b-col cols="12" sm="4" md="2">
<b-form-input min="1" type="number" :description="$t('Amount')" v-model="new_item.amount"></b-form-input> <b-form-input min="1" type="number" :description="$t('Amount')" v-model="new_item.amount"></b-form-input>
</div> </b-col>
<div class="col col-md-3"> <b-col cols="12" sm="8" md="3">
<lookup-input :form="formUnit" :model="Models.UNIT" @change="new_item.unit = $event" :show_label="false" /> <lookup-input :form="formUnit" :model="Models.UNIT" @change="new_item.unit = $event" :show_label="false" />
</div> </b-col>
<div class="col col-md-4"> <b-col cols="12" sm="8" md="4">
<lookup-input :form="formFood" :model="Models.FOOD" @change="new_item.food = $event" :show_label="false" /> <lookup-input :form="formFood" :model="Models.FOOD" @change="new_item.food = $event" :show_label="false" />
</div> </b-col>
<div class="col col-md-1"> <b-col cols="12" sm="4" md="1">
<b-button variant="link" class="px-0"> <b-button variant="link" class="px-0">
<i class="btn fas fa-cart-plus fa-lg px-0 text-success" @click="addItem" /> <i class="btn fas fa-cart-plus fa-lg px-0 text-success" @click="addItem" />
</b-button> </b-button>
</div> </b-col>
</div> </b-row>
<!-- shopping list table -->
<div v-if="items && items.length > 0"> <div v-if="items && items.length > 0">
<div v-for="(done, x) in Sections" :key="x"> <div v-for="(done, x) in Sections" :key="x">
<div v-if="x == 'true'"> <div v-if="x == 'true'">
@ -491,15 +493,6 @@
</b-form-group> </b-form-group>
</ContextMenuItem> </ContextMenuItem>
<ContextMenuItem
@click="
$refs.menu.close()
ignoreThis(contextData)
"
>
<a class="dropdown-item p-2" href="#"><i class="fas fa-ban"></i> {{ $t("IgnoreThis", { food: foodName(contextData) }) }}</a>
</ContextMenuItem>
<ContextMenuItem <ContextMenuItem
@click=" @click="
$refs.menu.close() $refs.menu.close()
@ -746,7 +739,7 @@ export default {
} else { } else {
console.log("no data returned") console.log("no data returned")
} }
this.new_item = { amount: 1 } this.new_item = { amount: 1, unit: undefined, food: undefined }
}) })
.catch((err) => { .catch((err) => {
console.log(err) console.log(err)
@ -906,13 +899,6 @@ export default {
getThis: function (id) { getThis: function (id) {
return this.genericAPI(this.Models.SHOPPING_CATEGORY, this.Actions.FETCH, { id: id }) return this.genericAPI(this.Models.SHOPPING_CATEGORY, this.Actions.FETCH, { id: id })
}, },
ignoreThis: function (item) {
let food = {
id: item?.[0]?.food.id ?? item.food.id,
ignore_shopping: true,
}
this.updateFood(food, "ignore_shopping")
},
mergeShoppingList: function (data) { mergeShoppingList: function (data) {
this.items.map((x) => this.items.map((x) =>
data.map((y) => { data.map((y) => {
@ -939,10 +925,10 @@ export default {
let api = new ApiApiFactory() let api = new ApiApiFactory()
let food = { let food = {
id: item?.[0]?.food.id ?? item?.food?.id, id: item?.[0]?.food.id ?? item?.food?.id,
on_hand: true, food_onhand: true,
} }
this.updateFood(food) this.updateFood(food, "food_onhand")
.then((result) => { .then((result) => {
let entries = this.items.filter((x) => x.food.id == food.id).map((x) => x.id) let entries = this.items.filter((x) => x.food.id == food.id).map((x) => x.id)
this.items = this.items.filter((x) => x.food.id !== food.id) this.items = this.items.filter((x) => x.food.id !== food.id)
@ -1005,16 +991,18 @@ export default {
// when checking a sub item don't refresh the screen until all entries complete but change class to cross out // when checking a sub item don't refresh the screen until all entries complete but change class to cross out
let promises = [] let promises = []
update.entries.forEach((x) => { update.entries.forEach((x) => {
promises.push(this.saveThis({ id: x, checked: update.checked }, false)) const id = x?.id ?? x
let item = this.items.filter((entry) => entry.id == x)[0] let completed_at = undefined
Vue.set(item, "checked", update.checked)
if (update.checked) { if (update.checked) {
Vue.set(item, "completed_at", new Date().toISOString()) completed_at = new Date().toISOString()
} else {
Vue.set(item, "completed_at", undefined)
} }
promises.push(this.saveThis({ id: id, checked: update.checked }, false))
let item = this.items.filter((entry) => entry.id == id)[0]
Vue.set(item, "checked", update.checked)
Vue.set(item, "completed_at", completed_at)
}) })
Promise.all(promises).catch((err) => { Promise.all(promises).catch((err) => {
console.log(err, err.response) console.log(err, err.response)
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE) StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
@ -1024,8 +1012,8 @@ export default {
let api = new ApiApiFactory() let api = new ApiApiFactory()
let ignore_category let ignore_category
if (field) { if (field) {
ignore_category = food.ignore_inherit ignore_category = food.inherit_fields
.map((x) => food.ignore_inherit.fields) .map((x) => food.inherit_fields.fields)
.flat() .flat()
.includes(field) .includes(field)
} else { } else {
@ -1035,7 +1023,7 @@ export default {
return api return api
.partialUpdateFood(food.id, food) .partialUpdateFood(food.id, food)
.then((result) => { .then((result) => {
if (food.inherit && food.supermarket_category && !ignore_category && food.parent) { if (food.supermarket_category && !ignore_category && food.parent) {
makeToast(this.$t("Warning"), this.$t("InheritWarning", { food: food.name }), "warning") makeToast(this.$t("Warning"), this.$t("InheritWarning", { food: food.name }), "warning")
} else { } else {
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_UPDATE) StandardToasts.makeStandardToast(StandardToasts.SUCCESS_UPDATE)

View File

@ -1,52 +1,44 @@
<template> <template>
<span> <span>
<linked-recipe v-if="linkedRecipe" <linked-recipe v-if="linkedRecipe" :item="item" />
:item="item"/> <icon-badge v-if="Icon" :item="item" />
<icon-badge v-if="Icon" <on-hand-badge v-if="OnHand" :item="item" />
:item="item"/> <shopping-badge v-if="Shopping" :item="item" />
<on-hand-badge v-if="OnHand"
:item="item"/>
<shopping-badge v-if="Shopping"
:item="item"/>
</span> </span>
</template> </template>
<script> <script>
import LinkedRecipe from "@/components/Badges/LinkedRecipe"; import LinkedRecipe from "@/components/Badges/LinkedRecipe"
import IconBadge from "@/components/Badges/Icon"; import IconBadge from "@/components/Badges/Icon"
import OnHandBadge from "@/components/Badges/OnHand"; import OnHandBadge from "@/components/Badges/OnHand"
import ShoppingBadge from "@/components/Badges/Shopping"; import ShoppingBadge from "@/components/Badges/Shopping"
export default { export default {
name: 'CardBadges', name: "CardBadges",
components: {LinkedRecipe, IconBadge, OnHandBadge, ShoppingBadge}, components: { LinkedRecipe, IconBadge, OnHandBadge, ShoppingBadge },
props: { props: {
item: {type: Object}, item: { type: Object },
model: {type: Object} model: { type: Object },
},
data() {
return {
}
},
mounted() {
},
computed: {
linkedRecipe: function () {
return this.model?.badges?.linked_recipe ?? false
}, },
Icon: function () { data() {
return this.model?.badges?.icon ?? false return {}
}, },
OnHand: function () { mounted() {},
return this.model?.badges?.on_hand ?? false computed: {
linkedRecipe: function () {
return this.model?.badges?.linked_recipe ?? false
},
Icon: function () {
return this.model?.badges?.icon ?? false
},
OnHand: function () {
return this.model?.badges?.food_onhand ?? false
},
Shopping: function () {
return this.model?.badges?.shopping ?? false
},
}, },
Shopping: function () { watch: {},
return this.model?.badges?.shopping ?? false methods: {},
}
},
watch: {
},
methods: {
}
} }
</script> </script>

View File

@ -1,7 +1,7 @@
<template> <template>
<span> <span>
<b-button <b-button
class="btn text-decoration-none fas px-1 py-0 border-0" class="btn text-decoration-none fas px-1 py-0 border-0"
variant="link" variant="link"
v-b-popover.hover.html v-b-popover.hover.html
:title="[onhand ? $t('FoodOnHand', { food: item.name }) : $t('FoodNotOnHand', { food: item.name })]" :title="[onhand ? $t('FoodOnHand', { food: item.name }) : $t('FoodNotOnHand', { food: item.name })]"
@ -26,16 +26,16 @@ export default {
} }
}, },
mounted() { mounted() {
this.onhand = this.item.on_hand this.onhand = this.item.food_onhand
}, },
watch: { watch: {
"item.on_hand": function(newVal, oldVal) { "item.food_onhand": function (newVal, oldVal) {
this.onhand = newVal this.onhand = newVal
}, },
}, },
methods: { methods: {
toggleOnHand() { toggleOnHand() {
let params = { id: this.item.id, on_hand: !this.onhand } let params = { id: this.item.id, food_onhand: !this.onhand }
this.genericAPI(this.Models.FOOD, this.Actions.UPDATE, params).then(() => { this.genericAPI(this.Models.FOOD, this.Actions.UPDATE, params).then(() => {
this.onhand = !this.onhand this.onhand = !this.onhand
}) })

View File

@ -1,6 +1,6 @@
<template> <template>
<span> <span>
<b-button class="btn text-decoration-none px-1 border-0" variant="link" v-if="ShowBadge" :id="`shopping${item.id}`" @click="addShopping()"> <b-button class="btn text-decoration-none px-1 border-0" variant="link" :id="`shopping${item.id}`" @click="addShopping()">
<i <i
class="fas" class="fas"
v-b-popover.hover.html v-b-popover.hover.html
@ -8,13 +8,13 @@
:class="[shopping ? 'text-success fa-shopping-cart' : 'text-muted fa-cart-plus']" :class="[shopping ? 'text-success fa-shopping-cart' : 'text-muted fa-cart-plus']"
/> />
</b-button> </b-button>
<b-popover :target="`${ShowConfirmation}`" :ref="'shopping' + item.id" triggers="focus" placement="top"> <b-popover v-if="shopping" :target="`${ShowConfirmation}`" :ref="'shopping' + item.id" triggers="focus" placement="top">
<template #title>{{ DeleteConfirmation }}</template> <template #title>{{ DeleteConfirmation }}</template>
<b-row align-h="end"> <b-row align-h="end">
<b-col cols="auto" <b-col cols="auto">
><b-button class="btn btn-sm btn-info shadow-none px-1 border-0" @click="cancelDelete()">{{ $t("Cancel") }}</b-button> <b-button class="btn btn-sm btn-info shadow-none px-1 border-0" @click="cancelDelete()">{{ $t("Cancel") }}</b-button>
<b-button class="btn btn-sm btn-danger shadow-none px-1" @click="confirmDelete()">{{ $t("Confirm") }}</b-button></b-col <b-button class="btn btn-sm btn-danger shadow-none px-1" @click="confirmDelete()">{{ $t("Confirm") }}</b-button>
> </b-col>
</b-row> </b-row>
</b-popover> </b-popover>
</span> </span>
@ -27,7 +27,6 @@ export default {
name: "ShoppingBadge", name: "ShoppingBadge",
props: { props: {
item: { type: Object }, item: { type: Object },
override_ignore: { type: Boolean, default: false },
}, },
mixins: [ApiMixin], mixins: [ApiMixin],
data() { data() {
@ -40,13 +39,6 @@ export default {
this.shopping = this.item?.shopping //?? random[Math.floor(Math.random() * random.length)] this.shopping = this.item?.shopping //?? random[Math.floor(Math.random() * random.length)]
}, },
computed: { computed: {
ShowBadge() {
if (this.override_ignore) {
return true
} else {
return !this.item.ignore_shopping
}
},
DeleteConfirmation() { DeleteConfirmation() {
return this.$t("DeleteShoppingConfirm", { food: this.item.name }) return this.$t("DeleteShoppingConfirm", { food: this.item.name })
}, },
@ -54,12 +46,12 @@ export default {
if (this.shopping) { if (this.shopping) {
return "shopping" + this.item.id return "shopping" + this.item.id
} else { } else {
return "NoDialog" return ""
} }
}, },
}, },
watch: { watch: {
"item.shopping": function(newVal, oldVal) { "item.shopping": function (newVal, oldVal) {
this.shopping = newVal this.shopping = newVal
}, },
}, },

View File

@ -1,74 +1,52 @@
<template> <template>
<!-- <b-button variant="link" size="sm" class="text-dark shadow-none"><i class="fas fa-chevron-down"></i></b-button> --> <!-- <b-button variant="link" size="sm" class="text-dark shadow-none"><i class="fas fa-chevron-down"></i></b-button> -->
<span> <span>
<b-dropdown variant="link" toggle-class="text-decoration-none text-dark shadow-none" no-caret <b-dropdown variant="link" toggle-class="text-decoration-none text-dark shadow-none" no-caret style="boundary: window">
style="boundary:window"> <template #button-content>
<template #button-content> <i class="fas fa-chevron-down"></i>
<i class="fas fa-chevron-down"></i> </template>
</template> <b-dropdown-item :href="resolveDjangoUrl('list_food')"> <i class="fas fa-leaf fa-fw"></i> {{ Models["FOOD"].name }} </b-dropdown-item>
<b-dropdown-item :href="resolveDjangoUrl('list_food')">
<i class="fas fa-leaf fa-fw"></i> {{ Models['FOOD'].name }}
</b-dropdown-item>
<b-dropdown-item :href="resolveDjangoUrl('list_keyword')">
<i class="fas fa-tags fa-fw"></i> {{ Models['KEYWORD'].name }}
</b-dropdown-item>
<b-dropdown-item :href="resolveDjangoUrl('list_unit')"> <b-dropdown-item :href="resolveDjangoUrl('list_keyword')"> <i class="fas fa-tags fa-fw"></i> {{ Models["KEYWORD"].name }} </b-dropdown-item>
<i class="fas fa-balance-scale fa-fw"></i> {{ Models['UNIT'].name }}
</b-dropdown-item>
<b-dropdown-item :href="resolveDjangoUrl('list_supermarket')">
<i class="fas fa-store-alt fa-fw"></i> {{ Models['SUPERMARKET'].name }}
</b-dropdown-item>
<b-dropdown-item :href="resolveDjangoUrl('list_supermarket_category')"> <b-dropdown-item :href="resolveDjangoUrl('list_unit')"> <i class="fas fa-balance-scale fa-fw"></i> {{ Models["UNIT"].name }} </b-dropdown-item>
<i class="fas fa-cubes fa-fw"></i> {{ Models['SHOPPING_CATEGORY'].name }}
</b-dropdown-item>
<b-dropdown-item :href="resolveDjangoUrl('list_automation')"> <b-dropdown-item :href="resolveDjangoUrl('list_supermarket')"> <i class="fas fa-store-alt fa-fw"></i> {{ Models["SUPERMARKET"].name }} </b-dropdown-item>
<i class="fas fa-robot fa-fw"></i> {{ Models['AUTOMATION'].name }}
</b-dropdown-item>
<b-dropdown-item :href="resolveDjangoUrl('list_user_file')"> <b-dropdown-item :href="resolveDjangoUrl('list_supermarket_category')"> <i class="fas fa-cubes fa-fw"></i> {{ Models["SHOPPING_CATEGORY"].name }} </b-dropdown-item>
<i class="fas fa-file fa-fw"></i> {{ Models['USERFILE'].name }}
</b-dropdown-item>
<b-dropdown-item :href="resolveDjangoUrl('list_step')"> <b-dropdown-item :href="resolveDjangoUrl('list_automation')"> <i class="fas fa-robot fa-fw"></i> {{ Models["AUTOMATION"].name }} </b-dropdown-item>
<i class="fas fa-puzzle-piece fa-fw"></i>{{ Models['STEP'].name }}
</b-dropdown-item>
<b-dropdown-item :href="resolveDjangoUrl('list_user_file')"> <i class="fas fa-file fa-fw"></i> {{ Models["USERFILE"].name }} </b-dropdown-item>
<b-dropdown-item :href="resolveDjangoUrl('list_step')"> <i class="fas fa-puzzle-piece fa-fw"></i>{{ Models["STEP"].name }} </b-dropdown-item>
</b-dropdown> </b-dropdown>
</span> </span>
</template> </template>
<script> <script>
import Vue from "vue"
import { BootstrapVue } from "bootstrap-vue"
import "bootstrap-vue/dist/bootstrap-vue.css"
import Vue from 'vue' import { Models } from "@/utils/models"
import {BootstrapVue} from 'bootstrap-vue' import { ResolveUrlMixin } from "@/utils/utils"
import 'bootstrap-vue/dist/bootstrap-vue.css'
import {Models} from "@/utils/models";
import {ResolveUrlMixin} from "@/utils/utils";
Vue.use(BootstrapVue) Vue.use(BootstrapVue)
export default { export default {
name: 'ModelMenu', name: "ModelMenu",
mixins: [ResolveUrlMixin], mixins: [ResolveUrlMixin],
data() { data() {
return { return {
Models: Models Models: Models,
} }
}, },
mounted() { mounted() {},
}, methods: {
methods: { gotoURL: function (model) {
gotoURL: function (model) { return
return },
} },
}
} }
</script>
</script>

View File

@ -17,13 +17,14 @@
> >
<b-row no-gutters> <b-row no-gutters>
<b-col no-gutters class="col-sm-3"> <b-col no-gutters class="col-sm-3">
<b-card-img-lazy style="object-fit: cover; height: 6em;" :src="item_image" v-bind:alt="$t('Recipe_Image')"></b-card-img-lazy> <b-card-img-lazy style="object-fit: cover; height: 6em" :src="item_image" v-bind:alt="$t('Recipe_Image')"></b-card-img-lazy>
</b-col> </b-col>
<b-col no-gutters class="col-sm-9"> <b-col no-gutters class="col-sm-9">
<b-card-body class="m-0 py-0"> <b-card-body class="m-0 py-0">
<b-card-text class=" h-100 my-0 d-flex flex-column" style="text-overflow: ellipsis"> <b-card-text class="h-100 my-0 d-flex flex-column" style="text-overflow: ellipsis">
<h5 class="m-0 mt-1 text-truncate">{{ item[title] }}</h5> <h5 class="m-0 mt-1 text-truncate">{{ item[title] }}</h5>
<div class="m-0 text-truncate">{{ item[subtitle] }}</div> <div class="m-0 text-truncate">{{ item[subtitle] }}</div>
<div class="m-0 text-truncate small text-muted" v-if="getFullname">{{ getFullname }}</div>
<generic-pill v-for="x in itemTags" :key="x.field" :item_list="item[x.field]" :label="x.label" :color="x.color" /> <generic-pill v-for="x in itemTags" :key="x.field" :item_list="item[x.field]" :label="x.label" :color="x.color" />
<generic-ordered-pill <generic-ordered-pill
@ -37,21 +38,11 @@
@finish-action="finishAction" @finish-action="finishAction"
/> />
<div class="mt-auto mb-1" align="right"> <div class="mt-auto mb-1" align="right">
<span <span v-if="item[child_count]" class="mx-2 btn btn-link btn-sm" style="z-index: 800" v-on:click="$emit('item-action', { action: 'get-children', source: item })">
v-if="item[child_count]"
class="mx-2 btn btn-link btn-sm"
style="z-index: 800;"
v-on:click="$emit('item-action', { action: 'get-children', source: item })"
>
<div v-if="!item.show_children">{{ item[child_count] }} {{ itemName }}</div> <div v-if="!item.show_children">{{ item[child_count] }} {{ itemName }}</div>
<div v-else>{{ text.hide_children }}</div> <div v-else>{{ text.hide_children }}</div>
</span> </span>
<span <span v-if="item[recipe_count]" class="mx-2 btn btn-link btn-sm" style="z-index: 800" v-on:click="$emit('item-action', { action: 'get-recipes', source: item })">
v-if="item[recipe_count]"
class="mx-2 btn btn-link btn-sm"
style="z-index: 800;"
v-on:click="$emit('item-action', { action: 'get-recipes', source: item })"
>
<div v-if="!item.show_recipes">{{ item[recipe_count] }} {{ $t("Recipes") }}</div> <div v-if="!item.show_recipes">{{ item[recipe_count] }} {{ $t("Recipes") }}</div>
<div v-else>{{ $t("Hide_Recipes") }}</div> <div v-else>{{ $t("Hide_Recipes") }}</div>
</span> </span>
@ -77,20 +68,19 @@
<!-- recursively add child cards --> <!-- recursively add child cards -->
<div class="row" v-if="item.show_children"> <div class="row" v-if="item.show_children">
<div class="col-md-10 offset-md-2"> <div class="col-md-10 offset-md-2">
<generic-horizontal-card v-for="child in item[children]" v-bind:key="child.id" :item="child" :model="model" @item-action="$emit('item-action', $event)"> <generic-horizontal-card v-for="child in item[children]" v-bind:key="child.id" :item="child" :model="model" @item-action="$emit('item-action', $event)"> </generic-horizontal-card>
</generic-horizontal-card>
</div> </div>
</div> </div>
<!-- conditionally view recipes --> <!-- conditionally view recipes -->
<div class="row" v-if="item.show_recipes"> <div class="row" v-if="item.show_recipes">
<div class="col-md-10 offset-md-2"> <div class="col-md-10 offset-md-2">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));grid-gap: 1rem;"> <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-gap: 1rem">
<recipe-card v-for="r in item[recipes]" v-bind:key="r.id" :recipe="r"> </recipe-card> <recipe-card v-for="r in item[recipes]" v-bind:key="r.id" :recipe="r"> </recipe-card>
</div> </div>
</div> </div>
</div> </div>
<!-- this should be made a generic component, would also require mixin for functions that generate the popup and put in parent container--> <!-- this should be made a generic component, would also require mixin for functions that generate the popup and put in parent container-->
<b-list-group ref="tooltip" variant="light" v-show="show_menu" v-on-clickaway="closeMenu" style="z-index:9999; cursor:pointer"> <b-list-group ref="tooltip" variant="light" v-show="show_menu" v-on-clickaway="closeMenu" style="z-index: 9999; cursor: pointer">
<b-list-group-item <b-list-group-item
v-if="useMove" v-if="useMove"
action action
@ -176,47 +166,53 @@ export default {
this.text.hide_children = this.$t("Hide_" + this.itemName) this.text.hide_children = this.$t("Hide_" + this.itemName)
}, },
computed: { computed: {
itemName: function() { itemName: function () {
return this.model?.name ?? "You Forgot To Set Model Name in model.js" return this.model?.name ?? "You Forgot To Set Model Name in model.js"
}, },
useMove: function() { useMove: function () {
return this.model?.["move"] ?? false ? true : false return this.model?.["move"] ?? false ? true : false
}, },
useMerge: function() { useMerge: function () {
return this.model?.["merge"] ?? false ? true : false return this.model?.["merge"] ?? false ? true : false
}, },
useShopping: function() { useShopping: function () {
return this.model?.["shop"] ?? false ? true : false return this.model?.["shop"] ?? false ? true : false
}, },
useOnhand: function() { useOnhand: function () {
return this.model?.["onhand"] ?? false ? true : false return this.model?.["onhand"] ?? false ? true : false
}, },
useDrag: function() { useDrag: function () {
return this.useMove || this.useMerge return this.useMove || this.useMerge
}, },
itemTags: function() { itemTags: function () {
return this.model?.tags ?? [] return this.model?.tags ?? []
}, },
itemOrderedTags: function() { itemOrderedTags: function () {
return this.model?.ordered_tags ?? [] return this.model?.ordered_tags ?? []
}, },
getFullname: function () {
if (!this.item?.full_name?.includes(">")) {
return undefined
}
return this.item?.full_name
},
}, },
methods: { methods: {
handleDragStart: function(e) { handleDragStart: function (e) {
this.isError = false this.isError = false
e.dataTransfer.setData("source", JSON.stringify(this.item)) e.dataTransfer.setData("source", JSON.stringify(this.item))
}, },
handleDragEnter: function(e) { handleDragEnter: function (e) {
if (!e.currentTarget.contains(e.relatedTarget) && e.relatedTarget != null) { if (!e.currentTarget.contains(e.relatedTarget) && e.relatedTarget != null) {
this.over = true this.over = true
} }
}, },
handleDragLeave: function(e) { handleDragLeave: function (e) {
if (!e.currentTarget.contains(e.relatedTarget)) { if (!e.currentTarget.contains(e.relatedTarget)) {
this.over = false this.over = false
} }
}, },
handleDragDrop: function(e) { handleDragDrop: function (e) {
let source = JSON.parse(e.dataTransfer.getData("source")) let source = JSON.parse(e.dataTransfer.getData("source"))
if (source.id != this.item.id) { if (source.id != this.item.id) {
this.source = source this.source = source
@ -247,7 +243,7 @@ export default {
this.isError = true this.isError = true
} }
}, },
generateLocation: function(x = 0, y = 0) { generateLocation: function (x = 0, y = 0) {
return () => ({ return () => ({
width: 0, width: 0,
height: 0, height: 0,
@ -257,10 +253,10 @@ export default {
left: x, left: x,
}) })
}, },
closeMenu: function() { closeMenu: function () {
this.show_menu = false this.show_menu = false
}, },
finishAction: function(e) { finishAction: function (e) {
this.$emit("finish-action", e) this.$emit("finish-action", e)
}, },
}, },

View File

@ -10,12 +10,7 @@
export default { export default {
name: "GenericPill", name: "GenericPill",
props: { props: {
item_list: { item_list: { type: Object },
type: Array,
default() {
return []
},
},
label: { type: String, default: "name" }, label: { type: String, default: "name" },
color: { type: String, default: "light" }, color: { type: String, default: "light" },
}, },

View File

@ -33,31 +33,20 @@
</div> </div>
</td> </td>
<td v-else-if="show_shopping" class="text-right text-nowrap"> <td v-else-if="show_shopping" class="text-right text-nowrap">
<!-- in shopping mode and ingredient is not ignored --> <b-button
<div v-if="!ingredient.food.ignore_shopping"> class="btn text-decoration-none fas fa-shopping-cart px-2 user-select-none"
<b-button variant="link"
class="btn text-decoration-none fas fa-shopping-cart px-2 user-select-none" v-b-popover.hover.click.blur.html.top="{ title: ShoppingPopover, variant: 'outline-dark' }"
variant="link" :class="{
v-b-popover.hover.click.blur.html.top="{ title: ShoppingPopover, variant: 'outline-dark' }" 'text-success': shopping_status === true,
:class="{ 'text-muted': shopping_status === false,
'text-success': shopping_status === true, 'text-warning': shopping_status === null,
'text-muted': shopping_status === false, }"
'text-warning': shopping_status === null, />
}" <span class="px-2">
/> <input type="checkbox" class="align-middle" v-model="shop" @change="changeShopping" />
<span class="px-2"> </span>
<input type="checkbox" class="align-middle" v-model="shop" @change="changeShopping" /> <on-hand-badge :item="ingredient.food" />
</span>
<on-hand-badge :item="ingredient.food" />
</div>
<div v-else>
<!-- or in shopping mode and food is ignored: Shopping Badge bypasses linking ingredient to Recipe which would get ignored -->
<shopping-badge :item="ingredient.food" :override_ignore="true" class="px-1" />
<span class="px-2">
<input type="checkbox" class="align-middle" disabled v-b-popover.hover.click.blur :title="$t('IgnoredFood', { food: ingredient.food.name })" />
</span>
<on-hand-badge :item="ingredient.food" />
</div>
</td> </td>
</template> </template>
</tr> </tr>
@ -66,11 +55,10 @@
<script> <script>
import { calculateAmount, ResolveUrlMixin, ApiMixin } from "@/utils/utils" import { calculateAmount, ResolveUrlMixin, ApiMixin } from "@/utils/utils"
import OnHandBadge from "@/components/Badges/OnHand" import OnHandBadge from "@/components/Badges/OnHand"
import ShoppingBadge from "@/components/Badges/Shopping"
export default { export default {
name: "IngredientComponent", name: "IngredientComponent",
components: { OnHandBadge, ShoppingBadge }, components: { OnHandBadge },
props: { props: {
ingredient: Object, ingredient: Object,
ingredient_factor: { type: Number, default: 1 }, ingredient_factor: { type: Number, default: 1 },
@ -89,9 +77,9 @@ export default {
data() { data() {
return { return {
checked: false, checked: false,
shopping_status: null, shopping_status: null, // in any shopping list: boolean + null=in shopping list, but not for this recipe
shopping_items: [], shopping_items: [],
shop: false, shop: false, // in shopping list for this recipe: boolean
dirty: undefined, dirty: undefined,
} }
}, },
@ -99,6 +87,13 @@ export default {
ShoppingListAndFilter: { ShoppingListAndFilter: {
immediate: true, immediate: true,
handler(newVal, oldVal) { handler(newVal, oldVal) {
// this whole sections is overly complicated
// trying to infer status of shopping for THIS recipe and THIS ingredient
// without know which recipe it is.
// If refactored:
// ## Needs to handle same recipe (multiple mealplans) being in shopping list multiple times
// ## Needs to handle same recipe being added as ShoppingListRecipe AND ingredients added from recipe as one-off
let filtered_list = this.shopping_list let filtered_list = this.shopping_list
// if a recipe list is provided, filter the shopping list // if a recipe list is provided, filter the shopping list
if (this.recipe_list) { if (this.recipe_list) {
@ -108,34 +103,39 @@ export default {
let count_shopping_recipes = [...new Set(filtered_list.map((x) => x.list_recipe))].length let count_shopping_recipes = [...new Set(filtered_list.map((x) => x.list_recipe))].length
let count_shopping_ingredient = filtered_list.filter((x) => x.ingredient == this.ingredient.id).length let count_shopping_ingredient = filtered_list.filter((x) => x.ingredient == this.ingredient.id).length
if (count_shopping_recipes > 1) { if (count_shopping_recipes >= 1) {
// This recipe is in the shopping list
this.shop = false // don't check any boxes until user selects a shopping list to edit this.shop = false // don't check any boxes until user selects a shopping list to edit
if (count_shopping_ingredient >= 1) { if (count_shopping_ingredient >= 1) {
this.shopping_status = true this.shopping_status = true // ingredient is in the shopping list - probably (but not definitely, this ingredient)
} else if (this.ingredient.food.shopping) { } else if (this.ingredient.food.shopping) {
this.shopping_status = null // food is in the shopping list, just not for this ingredient/recipe this.shopping_status = null // food is in the shopping list, just not for this ingredient/recipe
} else { } else {
this.shopping_status = false // food is not in any shopping list // food is not in any shopping list
this.shopping_status = false
} }
} else { } else {
// there are not recipes in the shopping list
// set default value
this.shop = !this.ingredient?.food?.food_onhand && !this.ingredient?.food?.recipe
this.$emit("add-to-shopping", { item: this.ingredient, add: this.shop })
// mark checked if the food is in the shopping list for this ingredient/recipe // mark checked if the food is in the shopping list for this ingredient/recipe
if (count_shopping_ingredient >= 1) { if (count_shopping_ingredient >= 1) {
// ingredient is in this shopping list // ingredient is in this shopping list (not entirely sure how this could happen?)
this.shop = true
this.shopping_status = true this.shopping_status = true
} else if (count_shopping_ingredient == 0 && this.ingredient.food.shopping) { } else if (count_shopping_ingredient == 0 && this.ingredient.food.shopping) {
// food is in the shopping list, just not for this ingredient/recipe // food is in the shopping list, just not for this ingredient/recipe
this.shop = false
this.shopping_status = null this.shopping_status = null
} else { } else {
// the food is not in any shopping list // the food is not in any shopping list
this.shop = false
this.shopping_status = false this.shopping_status = false
} }
} }
// if we are in add shopping mode start with all checks marked
if (this.add_shopping_mode) { if (this.add_shopping_mode) {
this.shop = !this.ingredient.food.on_hand && !this.ingredient.food.ignore_shopping && !this.ingredient.food.recipe // if we are in add shopping mode (e.g. recipe_shopping_modal) start with all checks marked
// except if on_hand (could be if recipe too?)
this.shop = !this.ingredient?.food?.food_onhand && !this.ingredient?.food?.recipe
} }
}, },
}, },

View File

@ -1,19 +1,18 @@
<template> <template>
<div> <div>
<b-modal :id="'modal_' + id" @hidden="cancelAction"> <b-modal :id="'modal_' + id" @hidden="cancelAction">
<template v-slot:modal-title <template v-slot:modal-title>
><h4>{{ form.title }}</h4></template <h4>{{ form.title }}</h4>
> </template>
<div v-for="(f, i) in form.fields" v-bind:key="i"> <div v-for="(f, i) in form.fields" v-bind:key="i">
<p v-if="f.type == 'instruction'">{{ f.label }}</p> <p v-if="visibleCondition(f, 'instruction')">{{ f.label }}</p>
<!-- this lookup is single selection --> <lookup-input v-if="visibleCondition(f, 'lookup')" :form="f" :model="listModel(f.list)" @change="storeValue" />
<lookup-input v-if="f.type == 'lookup'" :form="f" :model="listModel(f.list)" @change="storeValue" /> <checkbox-input class="mb-3" v-if="visibleCondition(f, 'checkbox')" :label="f.label" :value="f.value" :field="f.field" />
<!-- TODO: add multi-selection input list --> <text-input v-if="visibleCondition(f, 'text')" :label="f.label" :value="f.value" :field="f.field" :placeholder="f.placeholder" />
<checkbox-input v-if="f.type == 'checkbox'" :label="f.label" :value="f.value" :field="f.field" /> <choice-input v-if="visibleCondition(f, 'choice')" :label="f.label" :value="f.value" :field="f.field" :options="f.options" :placeholder="f.placeholder" />
<text-input v-if="f.type == 'text'" :label="f.label" :value="f.value" :field="f.field" :placeholder="f.placeholder" /> <emoji-input v-if="visibleCondition(f, 'emoji')" :label="f.label" :value="f.value" :field="f.field" @change="storeValue" />
<choice-input v-if="f.type == 'choice'" :label="f.label" :value="f.value" :field="f.field" :options="f.options" :placeholder="f.placeholder" /> <file-input v-if="visibleCondition(f, 'file')" :label="f.label" :value="f.value" :field="f.field" @change="storeValue" />
<emoji-input v-if="f.type == 'emoji'" :label="f.label" :value="f.value" :field="f.field" @change="storeValue" /> <small-text v-if="visibleCondition(f, 'smalltext')" :value="f.value" />
<file-input v-if="f.type == 'file'" :label="f.label" :value="f.value" :field="f.field" @change="storeValue" />
</div> </div>
<template v-slot:modal-footer> <template v-slot:modal-footer>
@ -39,14 +38,20 @@ import TextInput from "@/components/Modals/TextInput"
import EmojiInput from "@/components/Modals/EmojiInput" import EmojiInput from "@/components/Modals/EmojiInput"
import ChoiceInput from "@/components/Modals/ChoiceInput" import ChoiceInput from "@/components/Modals/ChoiceInput"
import FileInput from "@/components/Modals/FileInput" import FileInput from "@/components/Modals/FileInput"
import SmallText from "@/components/Modals/SmallText"
export default { export default {
name: "GenericModalForm", name: "GenericModalForm",
components: { FileInput, CheckboxInput, LookupInput, TextInput, EmojiInput, ChoiceInput }, components: { FileInput, CheckboxInput, LookupInput, TextInput, EmojiInput, ChoiceInput, SmallText },
mixins: [ApiMixin, ToastMixin], mixins: [ApiMixin, ToastMixin],
props: { props: {
model: { required: true, type: Object }, model: { required: true, type: Object },
action: { type: Object }, action: {
type: Object,
default() {
return {}
},
},
item1: { item1: {
type: Object, type: Object,
default() { default() {
@ -247,6 +252,21 @@ export default {
apiClient.createAutomation(automation) apiClient.createAutomation(automation)
} }
}, },
visibleCondition(field, field_type) {
let type_match = field?.type == field_type
let checks = true
if (type_match && field?.condition) {
if (field.condition?.condition === "exists") {
if ((this.item1[field.condition.field] != undefined) === field.condition.value) {
checks = true
} else {
checks = false
}
}
}
return type_match && checks
},
}, },
} }
</script> </script>

View File

@ -106,7 +106,7 @@ export default {
...this.steps ...this.steps
.map((x) => x.ingredients) .map((x) => x.ingredients)
.flat() .flat()
.filter((x) => !x?.food?.on_hand && !x?.food?.ignore_shopping) .filter((x) => !x?.food?.food_onhand)
.map((x) => x.id), .map((x) => x.id),
] ]
this.recipe_servings = result.data?.servings this.recipe_servings = result.data?.servings
@ -141,7 +141,7 @@ export default {
.flat() .flat()
.map((x) => x.ingredients) .map((x) => x.ingredients)
.flat() .flat()
.filter((x) => !x.food.on_hand && !x.food.ignore_shopping) .filter((x) => !x.food.override_ignore)
.map((x) => x.id), .map((x) => x.id),
] ]
}) })

View File

@ -0,0 +1,20 @@
<template>
<div class="small text-muted">
{{ value }}
</div>
</template>
<script>
export default {
name: "TextInput",
props: {
value: { type: String, default: "" },
},
data() {
return {}
},
mounted() {},
watch: {},
methods: {},
}
</script>

View File

@ -1,42 +1,34 @@
<template> <template>
<div> <div>
<b-form-group <b-form-group v-bind:label="label" class="mb-3">
v-bind:label="label" <b-form-input v-model="new_value" type="text" :placeholder="placeholder"></b-form-input>
class="mb-3">
<b-form-input
v-model="new_value"
type="string"
:placeholder="placeholder"
></b-form-input>
</b-form-group> </b-form-group>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'TextInput', name: "TextInput",
props: { props: {
field: {type: String, default: 'You Forgot To Set Field Name'}, field: { type: String, default: "You Forgot To Set Field Name" },
label: {type: String, default: 'Text Field'}, label: { type: String, default: "Text Field" },
value: {type: String, default: ''}, value: { type: String, default: "" },
placeholder: {type: String, default: 'You Should Add Placeholder Text'}, placeholder: { type: String, default: "You Should Add Placeholder Text" },
show_merge: {type: Boolean, default: false}, show_merge: { type: Boolean, default: false },
},
data() {
return {
new_value: undefined,
}
},
mounted() {
this.new_value = this.value
},
watch: {
'new_value': function () {
this.$root.$emit('change', this.field, this.new_value)
}, },
}, data() {
methods: { return {
} new_value: undefined,
}
},
mounted() {
this.new_value = this.value
},
watch: {
new_value: function () {
this.$root.$emit("change", this.field, this.new_value)
},
},
methods: {},
} }
</script> </script>

View File

@ -47,7 +47,7 @@
<!-- detail rows --> <!-- detail rows -->
<div class="card no-body" v-if="showDetails"> <div class="card no-body" v-if="showDetails">
<b-container fluid> <b-container fluid>
<div v-for="(e, z) in entries" :key="z"> <div v-for="e in entries" :key="e.id">
<b-row class="ml-2 small"> <b-row class="ml-2 small">
<b-col cols="6" md="4" class="overflow-hidden text-nowrap"> <b-col cols="6" md="4" class="overflow-hidden text-nowrap">
<button <button
@ -63,7 +63,10 @@
</button> </button>
</b-col> </b-col>
<b-col cols="6" md="4" class="col-md-4 text-muted">{{ formatOneMealPlan(e) }}</b-col> <b-col cols="6" md="4" class="col-md-4 text-muted">{{ formatOneMealPlan(e) }}</b-col>
<b-col cols="12" md="4" class="col-md-4 text-muted text-right overflow-hidden text-nowrap">{{ formatOneCreatedBy(e) }}</b-col> <b-col cols="12" md="4" class="col-md-4 text-muted text-right overflow-hidden text-nowrap">
{{ formatOneCreatedBy(e) }}
<div v-if="formatOneCompletedAt(e)">{{ formatOneCompletedAt(e) }}</div>
</b-col>
</b-row> </b-row>
<b-row class="ml-2 light"> <b-row class="ml-2 light">
@ -240,9 +243,6 @@ export default {
formatOneFood: function (item) { formatOneFood: function (item) {
return item.food.name return item.food.name
}, },
formatOneChecked: function (item) {
return item.checked
},
formatOneDelayUntil: function (item) { formatOneDelayUntil: function (item) {
if (!item.delay_until || (item.delay_until && item.checked)) { if (!item.delay_until || (item.delay_until && item.checked)) {
return false return false
@ -273,12 +273,13 @@ export default {
}) })
}, },
updateChecked: function (e, item) { updateChecked: function (e, item) {
let update = undefined
if (!item) { if (!item) {
let update = { entries: this.entries.map((x) => x.id), checked: !this.formatChecked } update = { entries: this.entries.map((x) => x.id), checked: !this.formatChecked }
this.$emit("update-checkbox", update)
} else { } else {
this.$emit("update-checkbox", { id: item.id, checked: !item.checked }) update = { entries: [item], checked: !item.checked }
} }
this.$emit("update-checkbox", update)
}, },
}, },
} }

View File

@ -11,11 +11,7 @@
<small style="margin-left: 4px" class="text-muted" v-if="step.time !== 0"><i class="fas fa-user-clock"></i> {{ step.time }} {{ $t("min") }} </small> <small style="margin-left: 4px" class="text-muted" v-if="step.time !== 0"><i class="fas fa-user-clock"></i> {{ step.time }} {{ $t("min") }} </small>
<small v-if="start_time !== ''" class="d-print-none"> <small v-if="start_time !== ''" class="d-print-none">
<b-link :id="`id_reactive_popover_${step.id}`" @click="openPopover" href="#"> <b-link :id="`id_reactive_popover_${step.id}`" @click="openPopover" href="#">
{{ {{ moment(start_time).add(step.time_offset, "minutes").format("HH:mm") }}
moment(start_time)
.add(step.time_offset, "minutes")
.format("HH:mm")
}}
</b-link> </b-link>
</small> </small>
</h5> </h5>
@ -57,11 +53,7 @@
</h4> </h4>
<span style="margin-left: 4px" class="text-muted" v-if="step.time !== 0"><i class="fa fa-stopwatch"></i> {{ step.time }} {{ $t("min") }}</span> <span style="margin-left: 4px" class="text-muted" v-if="step.time !== 0"><i class="fa fa-stopwatch"></i> {{ step.time }} {{ $t("min") }}</span>
<b-link class="d-print-none" :id="`id_reactive_popover_${step.id}`" @click="openPopover" href="#" v-if="start_time !== ''"> <b-link class="d-print-none" :id="`id_reactive_popover_${step.id}`" @click="openPopover" href="#" v-if="start_time !== ''">
{{ {{ moment(start_time).add(step.time_offset, "minutes").format("HH:mm") }}
moment(start_time)
.add(step.time_offset, "minutes")
.format("HH:mm")
}}
</b-link> </b-link>
</div> </div>
@ -106,14 +98,14 @@
<a :href="resolveDjangoUrl('view_recipe', step.step_recipe_data.id)">{{ step.step_recipe_data.name }}</a> <a :href="resolveDjangoUrl('view_recipe', step.step_recipe_data.id)">{{ step.step_recipe_data.name }}</a>
</h2> </h2>
<div v-for="(sub_step, index) in step.step_recipe_data.steps" v-bind:key="`substep_${sub_step.id}`"> <div v-for="(sub_step, index) in step.step_recipe_data.steps" v-bind:key="`substep_${sub_step.id}`">
<Step <step-component
:recipe="step.step_recipe_data" :recipe="step.step_recipe_data"
:step="sub_step" :step="sub_step"
:ingredient_factor="ingredient_factor" :ingredient_factor="ingredient_factor"
:index="index" :index="index"
:start_time="start_time" :start_time="start_time"
:force_ingredients="true" :force_ingredients="true"
></Step> ></step-component>
</div> </div>
</div> </div>
</b-collapse> </b-collapse>
@ -128,8 +120,8 @@
</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 @click="closePopover" size="sm" variant="secondary" style="margin-right:8px">Cancel</b-button> <b-button @click="closePopover" size="sm" variant="secondary" style="margin-right: 8px">{{ $t("Cancel") }}</b-button>
<b-button @click="updateTime" size="sm" variant="primary">Ok</b-button> <b-button @click="updateTime" size="sm" variant="primary">{{ $t("Ok") }}</b-button>
</div> </div>
</div> </div>
</b-popover> </b-popover>
@ -172,16 +164,14 @@ export default {
} }
}, },
mounted() { mounted() {
this.set_time_input = moment(this.start_time) this.set_time_input = moment(this.start_time).add(this.step.time_offset, "minutes").format("yyyy-MM-DDTHH:mm")
.add(this.step.time_offset, "minutes")
.format("yyyy-MM-DDTHH:mm")
}, },
methods: { methods: {
calculateAmount: function(x) { calculateAmount: function (x) {
// used by the jinja2 template // used by the jinja2 template
return calculateAmount(x, this.ingredient_factor) return calculateAmount(x, this.ingredient_factor)
}, },
updateTime: function() { updateTime: function () {
let new_start_time = moment(this.set_time_input) let new_start_time = moment(this.set_time_input)
.add(this.step.time_offset * -1, "minutes") .add(this.step.time_offset * -1, "minutes")
.format("yyyy-MM-DDTHH:mm") .format("yyyy-MM-DDTHH:mm")
@ -189,10 +179,10 @@ export default {
this.$emit("update-start-time", new_start_time) this.$emit("update-start-time", new_start_time)
this.closePopover() this.closePopover()
}, },
closePopover: function() { closePopover: function () {
this.$refs[`id_reactive_popover_${this.step.id}`].$emit("close") this.$refs[`id_reactive_popover_${this.step.id}`].$emit("close")
}, },
openPopover: function() { openPopover: function () {
this.$refs[`id_reactive_popover_${this.step.id}`].$emit("open") this.$refs[`id_reactive_popover_${this.step.id}`].$emit("open")
}, },
}, },

View File

@ -179,7 +179,7 @@
"AddToShopping": "Add to shopping list", "AddToShopping": "Add to shopping list",
"IngredientInShopping": "This ingredient is in your shopping list.", "IngredientInShopping": "This ingredient is in your shopping list.",
"NotInShopping": "{food} is not in your shopping list.", "NotInShopping": "{food} is not in your shopping list.",
"OnHand": "Have On Hand", "OnHand": "Currently On Hand",
"FoodOnHand": "You have {food} on hand.", "FoodOnHand": "You have {food} on hand.",
"FoodNotOnHand": "You do not have {food} on hand.", "FoodNotOnHand": "You do not have {food} on hand.",
"Undefined": "Undefined", "Undefined": "Undefined",
@ -222,7 +222,7 @@
"Next_Day": "Next Day", "Next_Day": "Next Day",
"Previous_Day": "Previous Day", "Previous_Day": "Previous Day",
"Inherit": "Inherit", "Inherit": "Inherit",
"IgnoreInherit": "Do Not Inherit Fields", "InheritFields": "Inherit Fields Values",
"FoodInherit": "Food Inheritable Fields", "FoodInherit": "Food Inheritable Fields",
"ShowUncategorizedFood": "Show Undefined", "ShowUncategorizedFood": "Show Undefined",
"GroupBy": "Group By", "GroupBy": "Group By",
@ -240,13 +240,13 @@
"shopping_share": "Share Shopping List", "shopping_share": "Share Shopping List",
"shopping_auto_sync": "Autosync", "shopping_auto_sync": "Autosync",
"mealplan_autoadd_shopping": "Auto Add Meal Plan", "mealplan_autoadd_shopping": "Auto Add Meal Plan",
"mealplan_autoexclude_onhand": "Exclude On Hand", "mealplan_autoexclude_onhand": "Exclude Food On Hand",
"mealplan_autoinclude_related": "Add Related Recipes", "mealplan_autoinclude_related": "Add Related Recipes",
"default_delay": "Default Delay Hours", "default_delay": "Default Delay Hours",
"shopping_share_desc": "Users will see all items you add to your shopping list. They must add you to see items on their list.", "shopping_share_desc": "Users will see all items you add to your shopping list. They must add you to see items on their list.",
"shopping_auto_sync_desc": "Setting to 0 will disable auto sync. When viewing a shopping list the list is updated every set seconds to sync changes someone else might have made. Useful when shopping with multiple people but will use mobile data.", "shopping_auto_sync_desc": "Setting to 0 will disable auto sync. When viewing a shopping list the list is updated every set seconds to sync changes someone else might have made. Useful when shopping with multiple people but will use mobile data.",
"mealplan_autoadd_shopping_desc": "Automatically add meal plan ingredients to shopping list.", "mealplan_autoadd_shopping_desc": "Automatically add meal plan ingredients to shopping list.",
"mealplan_autoexclude_onhand_desc": "When adding a meal plan to the shopping list (manually or automatically), exclude ingredients that are on hand.", "mealplan_autoexclude_onhand_desc": "When adding a meal plan to the shopping list (manually or automatically), exclude ingredients that are currently on hand.",
"mealplan_autoinclude_related_desc": "When adding a meal plan to the shopping list (manually or automatically), include all related recipes.", "mealplan_autoinclude_related_desc": "When adding a meal plan to the shopping list (manually or automatically), include all related recipes.",
"default_delay_desc": "Default number of hours to delay a shopping list entry.", "default_delay_desc": "Default number of hours to delay a shopping list entry.",
"filter_to_supermarket": "Filter to Supermarket", "filter_to_supermarket": "Filter to Supermarket",

View File

@ -69,14 +69,14 @@ export class Models {
onhand: true, onhand: true,
badges: { badges: {
linked_recipe: true, linked_recipe: true,
on_hand: true, food_onhand: true,
shopping: true, shopping: true,
}, },
tags: [{ field: "supermarket_category", label: "name", color: "info" }], tags: [{ field: "supermarket_category", label: "name", color: "info" }],
// REQUIRED: unordered array of fields that can be set during create // REQUIRED: unordered array of fields that can be set during create
create: { create: {
// if not defined partialUpdate will use the same parameters, prepending 'id' // 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", "food_onhand", "supermarket_category", "inherit", "inherit_fields"]],
form: { form: {
name: { name: {
@ -103,13 +103,7 @@ export class Models {
shopping: { shopping: {
form_field: true, form_field: true,
type: "checkbox", type: "checkbox",
field: "ignore_shopping", field: "food_onhand",
label: i18n.t("Ignore_Shopping"),
},
onhand: {
form_field: true,
type: "checkbox",
field: "on_hand",
label: i18n.t("OnHand"), label: i18n.t("OnHand"),
}, },
shopping_category: { shopping_category: {
@ -120,19 +114,19 @@ export class Models {
label: i18n.t("Shopping_Category"), label: i18n.t("Shopping_Category"),
allow_create: true, allow_create: true,
}, },
inherit: { inherit_fields: {
form_field: true,
type: "checkbox",
field: "inherit",
label: i18n.t("Inherit"),
},
ignore_inherit: {
form_field: true, form_field: true,
type: "lookup", type: "lookup",
multiple: true, multiple: true,
field: "ignore_inherit", field: "inherit_fields",
list: "FOOD_INHERIT_FIELDS", list: "FOOD_INHERIT_FIELDS",
label: i18n.t("IgnoreInherit"), label: i18n.t("InheritFields"),
condition: { field: "parent", value: true, condition: "exists" },
},
full_name: {
form_field: true,
type: "smalltext",
field: "full_name",
}, },
form_function: "FoodCreateDefault", form_function: "FoodCreateDefault",
}, },
@ -180,6 +174,11 @@ export class Models {
field: "icon", field: "icon",
label: i18n.t("Icon"), label: i18n.t("Icon"),
}, },
full_name: {
form_field: true,
type: "smalltext",
field: "full_name",
},
}, },
}, },
} }
@ -497,6 +496,14 @@ export class Models {
apiName: "User", apiName: "User",
paginated: false, paginated: false,
} }
static STEP = {
name: i18n.t("Step"),
apiName: "Step",
list: {
params: ["recipe", "query", "page", "pageSize", "options"],
},
}
} }
export class Actions { export class Actions {

View File

@ -214,7 +214,7 @@ export interface Food {
* @type {boolean} * @type {boolean}
* @memberof Food * @memberof Food
*/ */
ignore_shopping?: boolean; food_onhand?: boolean;
/** /**
* *
* @type {FoodSupermarketCategory} * @type {FoodSupermarketCategory}
@ -235,47 +235,16 @@ export interface Food {
numchild?: number; numchild?: number;
/** /**
* *
* @type {boolean} * @type {Array<FoodInheritFields>}
* @memberof Food * @memberof Food
*/ */
on_hand?: boolean; inherit_fields?: Array<FoodInheritFields> | null;
/**
*
* @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} * @type {string}
* @memberof FoodIgnoreInherit * @memberof Food
*/ */
name?: string; full_name?: string;
/**
*
* @type {string}
* @memberof FoodIgnoreInherit
*/
field?: string;
} }
/** /**
* *
@ -294,13 +263,38 @@ export interface FoodInheritField {
* @type {string} * @type {string}
* @memberof FoodInheritField * @memberof FoodInheritField
*/ */
name?: string; name?: string | null;
/** /**
* *
* @type {string} * @type {string}
* @memberof FoodInheritField * @memberof FoodInheritField
*/ */
field?: string; field?: string | null;
}
/**
*
* @export
* @interface FoodInheritFields
*/
export interface FoodInheritFields {
/**
*
* @type {number}
* @memberof FoodInheritFields
*/
id?: number;
/**
*
* @type {string}
* @memberof FoodInheritFields
*/
name?: string | null;
/**
*
* @type {string}
* @memberof FoodInheritFields
*/
field?: string | null;
} }
/** /**
* *
@ -513,6 +507,12 @@ export interface ImportLogKeyword {
* @memberof ImportLogKeyword * @memberof ImportLogKeyword
*/ */
updated_at?: string; updated_at?: string;
/**
*
* @type {string}
* @memberof ImportLogKeyword
*/
full_name?: string;
} }
/** /**
* *
@ -610,7 +610,7 @@ export interface IngredientFood {
* @type {boolean} * @type {boolean}
* @memberof IngredientFood * @memberof IngredientFood
*/ */
ignore_shopping?: boolean; food_onhand?: boolean;
/** /**
* *
* @type {FoodSupermarketCategory} * @type {FoodSupermarketCategory}
@ -631,22 +631,16 @@ export interface IngredientFood {
numchild?: number; numchild?: number;
/** /**
* *
* @type {boolean} * @type {Array<FoodInheritFields>}
* @memberof IngredientFood * @memberof IngredientFood
*/ */
on_hand?: boolean; inherit_fields?: Array<FoodInheritFields> | null;
/** /**
* *
* @type {boolean} * @type {string}
* @memberof IngredientFood * @memberof IngredientFood
*/ */
inherit?: boolean; full_name?: string;
/**
*
* @type {Array<FoodIgnoreInherit>}
* @memberof IngredientFood
*/
ignore_inherit?: Array<FoodIgnoreInherit> | null;
} }
/** /**
* *
@ -1018,6 +1012,12 @@ export interface Keyword {
* @memberof Keyword * @memberof Keyword
*/ */
updated_at?: string; updated_at?: string;
/**
*
* @type {string}
* @memberof Keyword
*/
full_name?: string;
} }
/** /**
* *
@ -1691,6 +1691,12 @@ export interface RecipeKeywords {
* @memberof RecipeKeywords * @memberof RecipeKeywords
*/ */
updated_at?: string; updated_at?: string;
/**
*
* @type {string}
* @memberof RecipeKeywords
*/
full_name?: string;
} }
/** /**
* *
@ -2996,10 +3002,10 @@ export interface UserPreference {
mealplan_autoadd_shopping?: boolean; mealplan_autoadd_shopping?: boolean;
/** /**
* *
* @type {string} * @type {Array<FoodInheritFields>}
* @memberof UserPreference * @memberof UserPreference
*/ */
food_ignore_default?: string; food_inherit_default?: Array<FoodInheritFields> | null;
/** /**
* *
* @type {string} * @type {string}
@ -3042,6 +3048,12 @@ export interface UserPreference {
* @memberof UserPreference * @memberof UserPreference
*/ */
csv_prefix?: string; csv_prefix?: string;
/**
*
* @type {boolean}
* @memberof UserPreference
*/
filter_to_supermarket?: boolean;
} }
/** /**

View File

@ -220,11 +220,6 @@ export const ApiMixin = {
return { return {
Models: Models, Models: Models,
Actions: Actions, Actions: Actions,
FoodCreateDefault: function (form) {
form.inherit_ignore = getUserPreference("food_ignore_default")
form.inherit = form.supermarket_category.length > 0
return form
},
} }
}, },
methods: { methods: {
@ -369,6 +364,7 @@ export function getForm(model, action, item1, item2) {
if (f === "partialUpdate" && Object.keys(config).length == 0) { if (f === "partialUpdate" && Object.keys(config).length == 0) {
config = { ...Actions.CREATE?.form, ...model.model_type?.["create"]?.form, ...model?.["create"]?.form } config = { ...Actions.CREATE?.form, ...model.model_type?.["create"]?.form, ...model?.["create"]?.form }
config["title"] = { ...action?.form_title, ...model.model_type?.[f]?.form_title, ...model?.[f]?.form_title } config["title"] = { ...action?.form_title, ...model.model_type?.[f]?.form_title, ...model?.[f]?.form_title }
// form functions should not be inherited
if (config?.["form_function"]?.includes("Create")) { if (config?.["form_function"]?.includes("Create")) {
delete config["form_function"] delete config["form_function"]
} }
@ -542,8 +538,7 @@ const specialCases = {
export const formFunctions = { export const formFunctions = {
FoodCreateDefault: function (form) { FoodCreateDefault: function (form) {
form.fields.filter((x) => x.field === "ignore_inherit")[0].value = getUserPreference("food_ignore_default") form.fields.filter((x) => x.field === "inherit_fields")[0].value = getUserPreference("food_inherit_default")
form.fields.filter((x) => x.field === "inherit")[0].value = getUserPreference("food_ignore_default").length > 0
return form return form
}, },
} }