Squashed commit of shoppinglist_v2
This commit is contained in:
@ -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.
|
||||||
|
@ -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'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
84
cookbook/integration/copymethat.py
Normal file
84
cookbook/integration/copymethat.py
Normal 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"})
|
@ -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)
|
||||||
|
@ -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),
|
||||||
]
|
]
|
||||||
|
@ -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')
|
||||||
|
@ -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 = [
|
||||||
|
@ -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):
|
||||||
|
@ -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
@ -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 %}
|
||||||
|
@ -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,
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
|
||||||
|
@ -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):
|
||||||
|
@ -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')
|
||||||
|
@ -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:
|
||||||
|
@ -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():
|
||||||
|
@ -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.
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
},
|
},
|
||||||
|
@ -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) => {
|
||||||
|
@ -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
|
||||||
) {
|
) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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)
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
})
|
})
|
||||||
|
@ -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
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -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>
|
|
||||||
|
@ -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)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -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" },
|
||||||
},
|
},
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -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>
|
||||||
|
@ -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),
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
20
vue/src/components/Modals/SmallText.vue
Normal file
20
vue/src/components/Modals/SmallText.vue
Normal 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>
|
@ -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>
|
||||||
|
@ -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)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -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",
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user