mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-08 15:48:27 -05:00
Merge branch 'develop' of https://github.com/vabene1111/recipes into develop
This commit is contained in:
@@ -35,4 +35,3 @@ class CookbookConfig(AppConfig):
|
|||||||
# 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
|
||||||
|
|
||||||
|
|||||||
@@ -481,7 +481,7 @@ class ShoppingPreferenceForm(forms.ModelForm):
|
|||||||
|
|
||||||
fields = (
|
fields = (
|
||||||
'shopping_share', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'mealplan_autoexclude_onhand',
|
'shopping_share', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'mealplan_autoexclude_onhand',
|
||||||
'mealplan_autoinclude_related', 'default_delay', 'filter_to_supermarket', 'shopping_recent_days', 'csv_delim', 'csv_prefix'
|
'mealplan_autoinclude_related', 'shopping_add_onhand', 'default_delay', 'filter_to_supermarket', 'shopping_recent_days', 'csv_delim', 'csv_prefix'
|
||||||
)
|
)
|
||||||
|
|
||||||
help_texts = {
|
help_texts = {
|
||||||
@@ -496,6 +496,7 @@ class ShoppingPreferenceForm(forms.ModelForm):
|
|||||||
'default_delay': _('Default number of hours to delay a shopping list entry.'),
|
'default_delay': _('Default number of hours to delay a shopping list entry.'),
|
||||||
'filter_to_supermarket': _('Filter shopping list to only include supermarket categories.'),
|
'filter_to_supermarket': _('Filter shopping list to only include supermarket categories.'),
|
||||||
'shopping_recent_days': _('Days of recent shopping list entries to display.'),
|
'shopping_recent_days': _('Days of recent shopping list entries to display.'),
|
||||||
|
'shopping_add_onhand': _("Mark food 'On Hand' when checked off shopping list."),
|
||||||
'csv_delim': _('Delimiter to use for CSV exports.'),
|
'csv_delim': _('Delimiter to use for CSV exports.'),
|
||||||
'csv_prefix': _('Prefix to add when copying list to the clipboard.'),
|
'csv_prefix': _('Prefix to add when copying list to the clipboard.'),
|
||||||
|
|
||||||
@@ -510,7 +511,8 @@ class ShoppingPreferenceForm(forms.ModelForm):
|
|||||||
'filter_to_supermarket': _('Filter to Supermarket'),
|
'filter_to_supermarket': _('Filter to Supermarket'),
|
||||||
'shopping_recent_days': _('Recent Days'),
|
'shopping_recent_days': _('Recent Days'),
|
||||||
'csv_delim': _('CSV Delimiter'),
|
'csv_delim': _('CSV Delimiter'),
|
||||||
"csv_prefix_label": _("List Prefix")
|
"csv_prefix_label": _("List Prefix"),
|
||||||
|
'shopping_add_onhand': _("Auto On Hand"),
|
||||||
}
|
}
|
||||||
|
|
||||||
widgets = {
|
widgets = {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from rest_framework.authtoken.models import Token
|
|||||||
from rest_framework.exceptions import AuthenticationFailed
|
from rest_framework.exceptions import AuthenticationFailed
|
||||||
|
|
||||||
from cookbook.views import views
|
from cookbook.views import views
|
||||||
|
from recipes import settings
|
||||||
|
|
||||||
|
|
||||||
class ScopeMiddleware:
|
class ScopeMiddleware:
|
||||||
@@ -14,14 +15,15 @@ class ScopeMiddleware:
|
|||||||
def __call__(self, request):
|
def __call__(self, request):
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
|
|
||||||
if request.path.startswith('/admin/'):
|
prefix = settings.JS_REVERSE_SCRIPT_PREFIX or ''
|
||||||
|
if request.path.startswith(prefix + '/admin/'):
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
return self.get_response(request)
|
return self.get_response(request)
|
||||||
|
|
||||||
if request.path.startswith('/signup/') or request.path.startswith('/invite/'):
|
if request.path.startswith(prefix + '/signup/') or request.path.startswith(prefix + '/invite/'):
|
||||||
return self.get_response(request)
|
return self.get_response(request)
|
||||||
|
|
||||||
if request.path.startswith('/accounts/'):
|
if request.path.startswith(prefix + '/accounts/'):
|
||||||
return self.get_response(request)
|
return self.get_response(request)
|
||||||
|
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
@@ -36,7 +38,7 @@ class ScopeMiddleware:
|
|||||||
with scope(space=request.space):
|
with scope(space=request.space):
|
||||||
return self.get_response(request)
|
return self.get_response(request)
|
||||||
else:
|
else:
|
||||||
if request.path.startswith('/api/'):
|
if request.path.startswith(prefix + '/api/'):
|
||||||
try:
|
try:
|
||||||
if auth := TokenAuthentication().authenticate(request):
|
if auth := TokenAuthentication().authenticate(request):
|
||||||
request.space = auth[0].userpreference.space
|
request.space = auth[0].userpreference.space
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ def shopping_helper(qs, request):
|
|||||||
supermarket = request.query_params.get('supermarket', None)
|
supermarket = request.query_params.get('supermarket', None)
|
||||||
checked = request.query_params.get('checked', 'recent')
|
checked = request.query_params.get('checked', 'recent')
|
||||||
user = request.user
|
user = request.user
|
||||||
|
supermarket_order = [F('food__supermarket_category__name').asc(nulls_first=True), 'food__name']
|
||||||
supermarket_order = ['food__supermarket_category__name', 'food__name']
|
|
||||||
|
|
||||||
# TODO created either scheduled task or startup task to delete very old shopping list entries
|
# TODO created either scheduled task or startup task to delete very old shopping list entries
|
||||||
# TODO create user preference to define 'very old'
|
# TODO create user preference to define 'very old'
|
||||||
@@ -82,7 +81,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__food_onhand=True)
|
ingredients = ingredients.exclude(food__onhand_users__id__in=[x.id for x in shared_users])
|
||||||
|
|
||||||
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 +92,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__food_onhand=False, space=space).values_list('id', flat=True)
|
related_step_ing += Ingredient.objects.filter(step__recipe=x, space=space).exclude(food__onhand_users__id__in=[x.id for x in shared_users]).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,7 +100,7 @@ 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__food_onhand=False, space=space)
|
x_ing = Ingredient.objects.filter(step__recipe=x, space=space).exclude(food__onhand_users__id__in=[x.id for x in shared_users])
|
||||||
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]:
|
for i in [x for x in x_ing]:
|
||||||
|
|||||||
41
cookbook/migrations/0163_auto_20220105_0758.py
Normal file
41
cookbook/migrations/0163_auto_20220105_0758.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Generated by Django 3.2.10 on 2022-01-05 13:58
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
from cookbook.models import FoodInheritField
|
||||||
|
|
||||||
|
|
||||||
|
def rename_inherit_field(apps, schema_editor):
|
||||||
|
x = FoodInheritField.objects.filter(name='On Hand', field='food_onhand').first()
|
||||||
|
if x:
|
||||||
|
x.name = "Ignore Shopping"
|
||||||
|
x.field = "ignore_shopping"
|
||||||
|
x.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('cookbook', '0162_userpreference_csv_delim'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='food',
|
||||||
|
name='onhand_users',
|
||||||
|
field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='userpreference',
|
||||||
|
name='shopping_add_onhand',
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='food',
|
||||||
|
old_name='food_onhand',
|
||||||
|
new_name='ignore_shopping',
|
||||||
|
),
|
||||||
|
migrations.RunPython(rename_inherit_field),
|
||||||
|
]
|
||||||
@@ -332,6 +332,7 @@ class UserPreference(models.Model, PermissionModelMixin):
|
|||||||
mealplan_autoadd_shopping = models.BooleanField(default=False)
|
mealplan_autoadd_shopping = models.BooleanField(default=False)
|
||||||
mealplan_autoexclude_onhand = models.BooleanField(default=True)
|
mealplan_autoexclude_onhand = models.BooleanField(default=True)
|
||||||
mealplan_autoinclude_related = models.BooleanField(default=True)
|
mealplan_autoinclude_related = models.BooleanField(default=True)
|
||||||
|
shopping_add_onhand = models.BooleanField(default=True)
|
||||||
filter_to_supermarket = models.BooleanField(default=False)
|
filter_to_supermarket = models.BooleanField(default=False)
|
||||||
default_delay = models.DecimalField(default=4, max_digits=8, decimal_places=4)
|
default_delay = models.DecimalField(default=4, max_digits=8, decimal_places=4)
|
||||||
shopping_recent_days = models.PositiveIntegerField(default=7)
|
shopping_recent_days = models.PositiveIntegerField(default=7)
|
||||||
@@ -489,10 +490,11 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
|
|||||||
node_order_by = ['name']
|
node_order_by = ['name']
|
||||||
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) # inherited field
|
||||||
food_onhand = models.BooleanField(default=False) # inherited field
|
ignore_shopping = models.BooleanField(default=False) # inherited field
|
||||||
|
onhand_users = models.ManyToManyField(User, blank=True)
|
||||||
description = models.TextField(default='', blank=True)
|
description = models.TextField(default='', blank=True)
|
||||||
inherit_fields = models.ManyToManyField(FoodInheritField, blank=True) # inherited field: is this name better as inherit instead of ignore inherit? which is more intuitive?
|
inherit_fields = models.ManyToManyField(FoodInheritField, blank=True)
|
||||||
|
|
||||||
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)
|
||||||
@@ -524,10 +526,10 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
|
|||||||
])
|
])
|
||||||
|
|
||||||
inherit = inherit.values_list('field', flat=True)
|
inherit = inherit.values_list('field', flat=True)
|
||||||
if 'food_onhand' in inherit:
|
if 'ignore_shopping' 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, food_onhand=True)).update(food_onhand=True)
|
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=False)).update(food_onhand=False)
|
Food.include_descendants(queryset=Food.objects.filter(depth=1, numchild__gt=0, space=space, ignore_shopping=False)).update(ignore_shopping=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
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from django.utils import timezone
|
|||||||
from drf_writable_nested import UniqueFieldsMixin, WritableNestedModelSerializer
|
from drf_writable_nested import UniqueFieldsMixin, WritableNestedModelSerializer
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import NotFound, ValidationError
|
from rest_framework.exceptions import NotFound, ValidationError
|
||||||
|
from rest_framework.fields import empty
|
||||||
|
|
||||||
from cookbook.helper.shopping_helper import list_from_recipe
|
from cookbook.helper.shopping_helper import list_from_recipe
|
||||||
from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, Food,
|
from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, Food,
|
||||||
@@ -92,6 +93,18 @@ class CustomDecimalField(serializers.Field):
|
|||||||
raise ValidationError('A valid number is required')
|
raise ValidationError('A valid number is required')
|
||||||
|
|
||||||
|
|
||||||
|
class CustomOnHandField(serializers.Field):
|
||||||
|
def get_attribute(self, instance):
|
||||||
|
return instance
|
||||||
|
|
||||||
|
def to_representation(self, obj):
|
||||||
|
shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [self.context['request'].user.id]
|
||||||
|
return obj.onhand_users.filter(id__in=shared_users).exists()
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
class SpaceFilterSerializer(serializers.ListSerializer):
|
class SpaceFilterSerializer(serializers.ListSerializer):
|
||||||
|
|
||||||
def to_representation(self, data):
|
def to_representation(self, data):
|
||||||
@@ -167,16 +180,13 @@ class UserPreferenceSerializer(serializers.ModelSerializer):
|
|||||||
raise NotFound()
|
raise NotFound()
|
||||||
return super().create(validated_data)
|
return super().create(validated_data)
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
|
||||||
# don't allow writing to FoodInheritField via API
|
|
||||||
return super().update(instance, validated_data)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
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_inherit_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', 'shopping_add_onhand'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -371,6 +381,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
|
|||||||
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')
|
||||||
inherit_fields = FoodInheritFieldSerializer(many=True, allow_null=True, required=False)
|
inherit_fields = FoodInheritFieldSerializer(many=True, allow_null=True, required=False)
|
||||||
|
food_onhand = CustomOnHandField(required=False, allow_null=True)
|
||||||
|
|
||||||
recipe_filter = 'steps__ingredients__food'
|
recipe_filter = 'steps__ingredients__food'
|
||||||
|
|
||||||
@@ -385,12 +396,29 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
|
|||||||
validated_data['supermarket_category'], sc_created = SupermarketCategory.objects.get_or_create(
|
validated_data['supermarket_category'], sc_created = SupermarketCategory.objects.get_or_create(
|
||||||
name=validated_data.pop('supermarket_category')['name'],
|
name=validated_data.pop('supermarket_category')['name'],
|
||||||
space=self.context['request'].space)
|
space=self.context['request'].space)
|
||||||
|
onhand = validated_data.get('food_onhand', None)
|
||||||
|
|
||||||
|
# assuming if on hand for user also onhand for shopping_share users
|
||||||
|
if not onhand is None:
|
||||||
|
shared_users = [user := self.context['request'].user] + list(user.userpreference.shopping_share.all())
|
||||||
|
if onhand:
|
||||||
|
validated_data['onhand_users'] = list(self.instance.onhand_users.all()) + shared_users
|
||||||
|
else:
|
||||||
|
validated_data['onhand_users'] = list(set(self.instance.onhand_users.all()) - set(shared_users))
|
||||||
obj, created = Food.objects.get_or_create(**validated_data)
|
obj, created = Food.objects.get_or_create(**validated_data)
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
if name := validated_data.get('name', None):
|
if name := validated_data.get('name', None):
|
||||||
validated_data['name'] = name.strip()
|
validated_data['name'] = name.strip()
|
||||||
|
# assuming if on hand for user also onhand for shopping_share users
|
||||||
|
onhand = validated_data.get('food_onhand', None)
|
||||||
|
if not onhand is None:
|
||||||
|
shared_users = [user := self.context['request'].user] + list(user.userpreference.shopping_share.all())
|
||||||
|
if onhand:
|
||||||
|
validated_data['onhand_users'] = list(self.instance.onhand_users.all()) + shared_users
|
||||||
|
else:
|
||||||
|
validated_data['onhand_users'] = list(set(self.instance.onhand_users.all()) - set(shared_users))
|
||||||
return super(FoodSerializer, self).update(instance, validated_data)
|
return super(FoodSerializer, self).update(instance, validated_data)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -687,6 +715,7 @@ class ShoppingListEntrySerializer(WritableNestedModelSerializer):
|
|||||||
return fields
|
return fields
|
||||||
|
|
||||||
def run_validation(self, data):
|
def run_validation(self, data):
|
||||||
|
if self.root.instance.__class__.__name__ == 'ShoppingListEntry':
|
||||||
if (
|
if (
|
||||||
data.get('checked', False)
|
data.get('checked', False)
|
||||||
and self.root.instance
|
and self.root.instance
|
||||||
@@ -694,6 +723,7 @@ class ShoppingListEntrySerializer(WritableNestedModelSerializer):
|
|||||||
):
|
):
|
||||||
# if checked flips from false to true set completed datetime
|
# if checked flips from false to true set completed datetime
|
||||||
data['completed_at'] = timezone.now()
|
data['completed_at'] = timezone.now()
|
||||||
|
|
||||||
elif not data.get('checked', False):
|
elif not data.get('checked', False):
|
||||||
# if not checked set completed to None
|
# if not checked set completed to None
|
||||||
data['completed_at'] = None
|
data['completed_at'] = None
|
||||||
@@ -709,6 +739,16 @@ class ShoppingListEntrySerializer(WritableNestedModelSerializer):
|
|||||||
validated_data['created_by'] = self.context['request'].user
|
validated_data['created_by'] = self.context['request'].user
|
||||||
return super().create(validated_data)
|
return super().create(validated_data)
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
user = self.context['request'].user
|
||||||
|
# update the onhand for food if shopping_add_onhand is True
|
||||||
|
if user.userpreference.shopping_add_onhand:
|
||||||
|
if checked := validated_data.get('checked', None):
|
||||||
|
instance.food.onhand_users.add(*user.userpreference.shopping_share.all(), user)
|
||||||
|
elif checked == False:
|
||||||
|
instance.food.onhand_users.remove(*user.userpreference.shopping_share.all(), user)
|
||||||
|
return super().update(instance, validated_data)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ShoppingListEntry
|
model = ShoppingListEntry
|
||||||
fields = (
|
fields = (
|
||||||
@@ -861,7 +901,7 @@ class FoodExportSerializer(FoodSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Food
|
model = Food
|
||||||
fields = ('name', 'food_onhand', 'supermarket_category',)
|
fields = ('name', 'ignore_shopping', 'supermarket_category',)
|
||||||
|
|
||||||
|
|
||||||
class IngredientExportSerializer(WritableNestedModelSerializer):
|
class IngredientExportSerializer(WritableNestedModelSerializer):
|
||||||
|
|||||||
@@ -75,8 +75,8 @@ def update_food_inheritance(sender, instance=None, created=False, **kwargs):
|
|||||||
# apply changes from parent to instance for each inheritted field
|
# apply changes from parent to instance for each inheritted field
|
||||||
if instance.parent and inherit.count() > 0:
|
if instance.parent and inherit.count() > 0:
|
||||||
parent = instance.get_parent()
|
parent = instance.get_parent()
|
||||||
if 'food_onhand' in inherit:
|
if 'ignore_shopping' in inherit:
|
||||||
instance.food_onhand = parent.food_onhand
|
instance.ignore_shopping = parent.ignore_shopping
|
||||||
# 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,8 +89,8 @@ 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_fields__field='food_onhand'):
|
for child in instance.get_children().filter(inherit_fields__field='ignore_shopping'):
|
||||||
child.food_onhand = instance.food_onhand
|
child.ignore_shopping = instance.ignore_shopping
|
||||||
_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:
|
||||||
@@ -121,3 +121,11 @@ def auto_add_shopping(sender, instance=None, created=False, weak=False, **kwargs
|
|||||||
'servings': instance.servings
|
'servings': instance.servings
|
||||||
}
|
}
|
||||||
list_recipe = list_from_recipe(**kwargs)
|
list_recipe = list_from_recipe(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
# user = self.context['request'].user
|
||||||
|
# if user.userpreference.shopping_add_onhand:
|
||||||
|
# if checked := validated_data.get('checked', None):
|
||||||
|
# instance.food.onhand_users.add(*user.userpreference.shopping_share.all(), user)
|
||||||
|
# elif checked == False:
|
||||||
|
# instance.food.onhand_users.remove(*user.userpreference.shopping_share.all(), user)
|
||||||
|
|||||||
@@ -483,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'),
|
||||||
({'food_onhand': True, 'inherit': True}, 'food_onhand', True, 'false'),
|
({'ignore_shopping': True, 'inherit': True}, 'ignore_shopping', True, 'false'),
|
||||||
({'food_onhand': True, 'inherit': False}, 'food_onhand', False, 'false'),
|
({'ignore_shopping': True, 'inherit': False}, 'ignore_shopping', 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):
|
||||||
@@ -509,16 +509,16 @@ def test_inherit(request, obj_tree_1, field, inherit, new_val, u1_s1):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("obj_tree_1", [
|
@pytest.mark.parametrize("obj_tree_1", [
|
||||||
({'has_category': True, 'inherit': False, 'food_onhand': True}),
|
({'has_category': True, 'inherit': False, 'ignore_shopping': 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.inheritable_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.food_onhand = False
|
obj_tree_1.ignore_shopping = False
|
||||||
assert parent.food_onhand == child.food_onhand
|
assert parent.ignore_shopping == child.ignore_shopping
|
||||||
assert parent.food_onhand != obj_tree_1.food_onhand
|
assert parent.ignore_shopping != obj_tree_1.ignore_shopping
|
||||||
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
|
||||||
|
|
||||||
@@ -527,5 +527,26 @@ 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.food_onhand == obj_tree_1.food_onhand == child.food_onhand
|
assert parent.ignore_shopping == obj_tree_1.ignore_shopping == child.ignore_shopping
|
||||||
assert parent.supermarket_category == obj_tree_1.supermarket_category == child.supermarket_category
|
assert parent.supermarket_category == obj_tree_1.supermarket_category == child.supermarket_category
|
||||||
|
|
||||||
|
|
||||||
|
def test_onhand(obj_1, u1_s1, u2_s1):
|
||||||
|
assert json.loads(u1_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] == False
|
||||||
|
assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] == False
|
||||||
|
|
||||||
|
u1_s1.patch(
|
||||||
|
reverse(
|
||||||
|
DETAIL_URL,
|
||||||
|
args={obj_1.id}
|
||||||
|
),
|
||||||
|
{'food_onhand': True},
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
assert json.loads(u1_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] == True
|
||||||
|
assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] == False
|
||||||
|
|
||||||
|
user1 = auth.get_user(u1_s1)
|
||||||
|
user2 = auth.get_user(u2_s1)
|
||||||
|
user1.userpreference.shopping_share.add(user2)
|
||||||
|
assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] == True
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ def test_related_mixed_space(request, recipe, u1_s2):
|
|||||||
reverse(RELATED_URL, args={recipe.id})).content)) == 0
|
reverse(RELATED_URL, args={recipe.id})).content)) == 0
|
||||||
|
|
||||||
|
|
||||||
# TODO add tests for mealplan related when thats added
|
|
||||||
# TODO if/when related recipes includes multiple levels (related recipes of related recipes) add the following tests
|
# TODO if/when related recipes includes multiple levels (related recipes of related recipes) add the following tests
|
||||||
# -- step recipes included in step recipes
|
# -- step recipes included in step recipes
|
||||||
# -- step recipes included in food recipes
|
# -- step recipes included in food recipes
|
||||||
|
|||||||
@@ -217,3 +217,6 @@ def test_recent(sle, u1_s1):
|
|||||||
r = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?recent=1').content)
|
r = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?recent=1').content)
|
||||||
assert len(r) == 10
|
assert len(r) == 10
|
||||||
assert [x['checked'] for x in r].count(False) == 9
|
assert [x['checked'] for x in r].count(False) == 9
|
||||||
|
|
||||||
|
|
||||||
|
# TODO test auto onhand
|
||||||
|
|||||||
@@ -198,11 +198,11 @@ def test_shopping_recipe_userpreference(recipe, sle_count, use_mealplan, user2):
|
|||||||
# setup recipe with 10 ingredients, 1 step recipe with 10 ingredients, 2 food onhand(from recipe and step_recipe)
|
# 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.food_onhand = True
|
food.onhand_users.add(user)
|
||||||
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.food_onhand = True
|
food.onhand_users.add(user)
|
||||||
food.save()
|
food.save()
|
||||||
|
|
||||||
if use_mealplan:
|
if use_mealplan:
|
||||||
@@ -233,7 +233,6 @@ def test_shopping_recipe_mixed_authors(u1_s1, u2_s1):
|
|||||||
assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 0
|
assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 0
|
||||||
|
|
||||||
|
|
||||||
# TODO test adding recipe with ingredients that are not food
|
|
||||||
@pytest.mark.parametrize("recipe", [{'steps__ingredients__header': 1}], indirect=['recipe'])
|
@pytest.mark.parametrize("recipe", [{'steps__ingredients__header': 1}], indirect=['recipe'])
|
||||||
def test_shopping_with_header_ingredient(u1_s1, recipe):
|
def test_shopping_with_header_ingredient(u1_s1, recipe):
|
||||||
# with scope(space=recipe.space):
|
# with scope(space=recipe.space):
|
||||||
|
|||||||
@@ -689,8 +689,11 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
|||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
if obj.get_space() != request.space:
|
if obj.get_space() != request.space:
|
||||||
raise PermissionDenied(detail='You do not have the required permission to perform this action', code=403)
|
raise PermissionDenied(detail='You do not have the required permission to perform this action', code=403)
|
||||||
qs = obj.get_related_recipes(levels=1) # TODO: make levels a user setting, included in request data?, keep solely in the backend?
|
try:
|
||||||
# mealplans= TODO get todays mealplans
|
levels = int(request.query_params.get('levels', 1))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
levels = 1
|
||||||
|
qs = obj.get_related_recipes(levels=levels) # TODO: make levels a user setting, included in request data?, keep solely in the backend?
|
||||||
return Response(self.serializer_class(qs, many=True).data)
|
return Response(self.serializer_class(qs, many=True).data)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -388,6 +388,7 @@ def user_settings(request):
|
|||||||
up.filter_to_supermarket = shopping_form.cleaned_data['filter_to_supermarket']
|
up.filter_to_supermarket = shopping_form.cleaned_data['filter_to_supermarket']
|
||||||
up.default_delay = shopping_form.cleaned_data['default_delay']
|
up.default_delay = shopping_form.cleaned_data['default_delay']
|
||||||
up.shopping_recent_days = shopping_form.cleaned_data['shopping_recent_days']
|
up.shopping_recent_days = shopping_form.cleaned_data['shopping_recent_days']
|
||||||
|
up.shopping_add_onhand = shopping_form.cleaned_data['shopping_add_onhand']
|
||||||
up.csv_delim = shopping_form.cleaned_data['csv_delim']
|
up.csv_delim = shopping_form.cleaned_data['csv_delim']
|
||||||
up.csv_prefix = shopping_form.cleaned_data['csv_prefix']
|
up.csv_prefix = shopping_form.cleaned_data['csv_prefix']
|
||||||
if up.shopping_auto_sync < settings.SHOPPING_MIN_AUTOSYNC_INTERVAL:
|
if up.shopping_auto_sync < settings.SHOPPING_MIN_AUTOSYNC_INTERVAL:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<div class="col-xl-8 col-12">
|
<div class="col-xl-8 col-12">
|
||||||
<div class="container-fluid d-flex flex-column flex-grow-1">
|
<div class="container-fluid d-flex flex-column flex-grow-1">
|
||||||
<!-- dynamically loaded header components -->
|
<!-- dynamically loaded header components -->
|
||||||
<div class="row" v-if="header_component_name !== ''">
|
<div class="row" v-if="header_component_name">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<component :is="headerComponent"></component>
|
<component :is="headerComponent"></component>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app" style="margin-bottom: 4vh">
|
<div id="app" style="margin-bottom: 4vh">
|
||||||
|
<RecipeSwitcher mode="mealplan" />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-xl-8 col-lg-10 offset-xl-2 offset-lg-1">
|
<div class="col-12 col-xl-8 col-lg-10 offset-xl-2 offset-lg-1">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -244,6 +245,7 @@ import RecipeCard from "@/components/RecipeCard"
|
|||||||
import GenericMultiselect from "@/components/GenericMultiselect"
|
import GenericMultiselect from "@/components/GenericMultiselect"
|
||||||
import Treeselect from "@riophae/vue-treeselect"
|
import Treeselect from "@riophae/vue-treeselect"
|
||||||
import "@riophae/vue-treeselect/dist/vue-treeselect.css"
|
import "@riophae/vue-treeselect/dist/vue-treeselect.css"
|
||||||
|
import RecipeSwitcher from "@/components/Buttons/RecipeSwitcher"
|
||||||
|
|
||||||
Vue.use(BootstrapVue)
|
Vue.use(BootstrapVue)
|
||||||
|
|
||||||
@@ -252,7 +254,7 @@ let SETTINGS_COOKIE_NAME = "search_settings"
|
|||||||
export default {
|
export default {
|
||||||
name: "RecipeSearchView",
|
name: "RecipeSearchView",
|
||||||
mixins: [ResolveUrlMixin, ApiMixin],
|
mixins: [ResolveUrlMixin, ApiMixin],
|
||||||
components: { GenericMultiselect, RecipeCard, Treeselect },
|
components: { GenericMultiselect, RecipeCard, Treeselect, RecipeSwitcher },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
// this.Models and this.Actions inherited from ApiMixin
|
// this.Models and this.Actions inherited from ApiMixin
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-if="!loading">
|
<div v-if="!loading">
|
||||||
|
<RecipeSwitcher :recipe="rootrecipe.id" :name="rootrecipe.name" mode="recipe" @switch="quickSwitch($event)" />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12" style="text-align: center">
|
<div class="col-12" style="text-align: center">
|
||||||
<h3>{{ recipe.name }}</h3>
|
<h3>{{ recipe.name }}</h3>
|
||||||
@@ -75,12 +76,12 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="my-auto">
|
<div class="my-auto">
|
||||||
<span class="text-primary"
|
<span class="text-primary">
|
||||||
><b
|
<b>
|
||||||
><template v-if="recipe.servings_text === ''">{{ $t("Servings") }}</template
|
<template v-if="recipe.servings_text === ''">{{ $t("Servings") }}</template>
|
||||||
><template v-else>{{ recipe.servings_text }}</template></b
|
<template v-else>{{ recipe.servings_text }}</template>
|
||||||
></span
|
</b>
|
||||||
>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -172,6 +173,7 @@ import IngredientsCard from "@/components/IngredientsCard"
|
|||||||
import StepComponent from "@/components/StepComponent"
|
import StepComponent from "@/components/StepComponent"
|
||||||
import KeywordsComponent from "@/components/KeywordsComponent"
|
import KeywordsComponent from "@/components/KeywordsComponent"
|
||||||
import NutritionComponent from "@/components/NutritionComponent"
|
import NutritionComponent from "@/components/NutritionComponent"
|
||||||
|
import RecipeSwitcher from "@/components/Buttons/RecipeSwitcher"
|
||||||
|
|
||||||
Vue.prototype.moment = moment
|
Vue.prototype.moment = moment
|
||||||
|
|
||||||
@@ -192,22 +194,32 @@ export default {
|
|||||||
KeywordsComponent,
|
KeywordsComponent,
|
||||||
LoadingSpinner,
|
LoadingSpinner,
|
||||||
AddRecipeToBook,
|
AddRecipeToBook,
|
||||||
|
RecipeSwitcher,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
ingredient_factor: function () {
|
ingredient_factor: function () {
|
||||||
return this.servings / this.recipe.servings
|
return this.servings / this.recipe.servings
|
||||||
},
|
},
|
||||||
|
ingredient_count() {
|
||||||
|
return this.recipe?.steps.map((x) => x.ingredients).flat().length
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loading: true,
|
loading: true,
|
||||||
recipe: undefined,
|
recipe: undefined,
|
||||||
ingredient_count: 0,
|
rootrecipe: undefined,
|
||||||
servings: 1,
|
servings: 1,
|
||||||
|
servings_cache: {},
|
||||||
start_time: "",
|
start_time: "",
|
||||||
share_uid: window.SHARE_UID,
|
share_uid: window.SHARE_UID,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
servings(newVal, oldVal) {
|
||||||
|
this.servings_cache[this.recipe.id] = this.servings
|
||||||
|
},
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.loadRecipe(window.RECIPE_ID)
|
this.loadRecipe(window.RECIPE_ID)
|
||||||
this.$i18n.locale = window.CUSTOM_LOCALE
|
this.$i18n.locale = window.CUSTOM_LOCALE
|
||||||
@@ -218,12 +230,9 @@ export default {
|
|||||||
if (window.USER_SERVINGS !== 0) {
|
if (window.USER_SERVINGS !== 0) {
|
||||||
recipe.servings = window.USER_SERVINGS
|
recipe.servings = window.USER_SERVINGS
|
||||||
}
|
}
|
||||||
this.servings = recipe.servings
|
|
||||||
|
|
||||||
let total_time = 0
|
let total_time = 0
|
||||||
for (let step of recipe.steps) {
|
for (let step of recipe.steps) {
|
||||||
this.ingredient_count += step.ingredients.length
|
|
||||||
|
|
||||||
for (let ingredient of step.ingredients) {
|
for (let ingredient of step.ingredients) {
|
||||||
this.$set(ingredient, "checked", false)
|
this.$set(ingredient, "checked", false)
|
||||||
}
|
}
|
||||||
@@ -237,7 +246,8 @@ export default {
|
|||||||
this.start_time = moment().format("yyyy-MM-DDTHH:mm")
|
this.start_time = moment().format("yyyy-MM-DDTHH:mm")
|
||||||
}
|
}
|
||||||
|
|
||||||
this.recipe = recipe
|
this.recipe = this.rootrecipe = recipe
|
||||||
|
this.servings = this.servings_cache[this.rootrecipe.id] = recipe.servings
|
||||||
this.loading = false
|
this.loading = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -253,6 +263,15 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
quickSwitch: function (e) {
|
||||||
|
if (e === -1) {
|
||||||
|
this.recipe = this.rootrecipe
|
||||||
|
this.servings = this.servings_cache[this.rootrecipe?.id ?? 1]
|
||||||
|
} else {
|
||||||
|
this.recipe = e
|
||||||
|
this.servings = this.servings_cache?.[e.id] ?? e.servings
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -31,15 +31,19 @@
|
|||||||
<div class="col col-md-12">
|
<div class="col col-md-12">
|
||||||
<div role="tablist">
|
<div role="tablist">
|
||||||
<!-- add to shopping form -->
|
<!-- add to shopping form -->
|
||||||
|
|
||||||
<b-row class="row justify-content-md-center" v-if="entrymode">
|
<b-row class="row justify-content-md-center" v-if="entrymode">
|
||||||
<b-col cols="12" sm="4" md="2">
|
<b-col cols="12" sm="4" md="2" v-if="!entry_mode_simple">
|
||||||
<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>
|
||||||
</b-col>
|
</b-col>
|
||||||
<b-col cols="12" sm="8" md="3">
|
<b-col cols="12" sm="8" md="3" v-if="!entry_mode_simple">
|
||||||
<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" :clear="clear" />
|
||||||
</b-col>
|
</b-col>
|
||||||
<b-col cols="12" sm="8" md="4">
|
<b-col cols="12" sm="8" md="4" v-if="!entry_mode_simple">
|
||||||
<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" :clear="clear" />
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="12" sm="8" v-if="entry_mode_simple">
|
||||||
|
<b-form-input type="text" :placeholder="$t('QuickEntry')" v-model="new_item.ingredient" @keyup.enter="addItem"></b-form-input>
|
||||||
</b-col>
|
</b-col>
|
||||||
<b-col cols="12" sm="4" md="1">
|
<b-col cols="12" sm="4" md="1">
|
||||||
<b-button variant="link" class="px-0">
|
<b-button variant="link" class="px-0">
|
||||||
@@ -47,6 +51,12 @@
|
|||||||
</b-button>
|
</b-button>
|
||||||
</b-col>
|
</b-col>
|
||||||
</b-row>
|
</b-row>
|
||||||
|
|
||||||
|
<b-row class="row justify-content-md-end" v-if="entrymode">
|
||||||
|
<b-form-checkbox switch v-model="entry_mode_simple">
|
||||||
|
{{ $t("QuickEntry") }}
|
||||||
|
</b-form-checkbox>
|
||||||
|
</b-row>
|
||||||
<!-- shopping list table -->
|
<!-- 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">
|
||||||
@@ -58,6 +68,17 @@
|
|||||||
|
|
||||||
<div v-for="(s, i) in done" :key="i">
|
<div v-for="(s, i) in done" :key="i">
|
||||||
<h5 v-if="Object.entries(s).length > 0">
|
<h5 v-if="Object.entries(s).length > 0">
|
||||||
|
<div class="dropdown b-dropdown position-static inline-block" data-html2canvas-ignore="true">
|
||||||
|
<button
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-expanded="false"
|
||||||
|
type="button"
|
||||||
|
class="btn dropdown-toggle btn-link text-decoration-none text-dark pr-2 dropdown-toggle-no-caret"
|
||||||
|
@click.stop="openContextMenu($event, s, true)"
|
||||||
|
>
|
||||||
|
<i class="fas fa-ellipsis-v fa-lg"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
<b-button
|
<b-button
|
||||||
class="btn btn-lg text-decoration-none text-dark px-1 py-0 border-0"
|
class="btn btn-lg text-decoration-none text-dark px-1 py-0 border-0"
|
||||||
variant="link"
|
variant="link"
|
||||||
@@ -68,6 +89,7 @@
|
|||||||
<i class="fa fa-chevron-right rotate" />
|
<i class="fa fa-chevron-right rotate" />
|
||||||
{{ i }}
|
{{ i }}
|
||||||
</b-button>
|
</b-button>
|
||||||
|
</div>
|
||||||
</h5>
|
</h5>
|
||||||
|
|
||||||
<div class="collapse" :id="'section-' + sectionID(x, i)" visible role="tabpanel" :class="{ show: x == 'false' }">
|
<div class="collapse" :id="'section-' + sectionID(x, i)" visible role="tabpanel" :class="{ show: x == 'false' }">
|
||||||
@@ -353,6 +375,19 @@
|
|||||||
</em>
|
</em>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-md-6">{{ $t("shopping_add_onhand") }}</div>
|
||||||
|
<div class="col col-md-6 text-right">
|
||||||
|
<input type="checkbox" size="sm" v-model="settings.shopping_add_onhand" @change="saveSettings" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row sm mb-3">
|
||||||
|
<div class="col">
|
||||||
|
<em class="small text-muted">
|
||||||
|
{{ $t("shopping_add_onhand_desc") }}
|
||||||
|
</em>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-6">{{ $t("shopping_recent_days") }}</div>
|
<div class="col col-md-6">{{ $t("shopping_recent_days") }}</div>
|
||||||
<div class="col col-md-6 text-right">
|
<div class="col col-md-6 text-right">
|
||||||
@@ -492,6 +527,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</b-form-group>
|
</b-form-group>
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem
|
||||||
|
@click="
|
||||||
|
$refs.menu.close()
|
||||||
|
updateChecked({ entries: contextData, checked: true })
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<a class="dropdown-item p-2" href="#"><i class="fas fa-check-square"></i> {{ $t("mark_complete") }}</a>
|
||||||
|
</ContextMenuItem>
|
||||||
|
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
@click="
|
@click="
|
||||||
@@ -510,6 +553,7 @@
|
|||||||
import Vue from "vue"
|
import Vue from "vue"
|
||||||
import { BootstrapVue } from "bootstrap-vue"
|
import { BootstrapVue } from "bootstrap-vue"
|
||||||
import "bootstrap-vue/dist/bootstrap-vue.css"
|
import "bootstrap-vue/dist/bootstrap-vue.css"
|
||||||
|
import VueCookies from "vue-cookies"
|
||||||
|
|
||||||
import ContextMenu from "@/components/ContextMenu/ContextMenu"
|
import ContextMenu from "@/components/ContextMenu/ContextMenu"
|
||||||
import ContextMenuItem from "@/components/ContextMenu/ContextMenuItem"
|
import ContextMenuItem from "@/components/ContextMenu/ContextMenuItem"
|
||||||
@@ -522,11 +566,12 @@ import GenericPill from "@/components/GenericPill"
|
|||||||
import LookupInput from "@/components/Modals/LookupInput"
|
import LookupInput from "@/components/Modals/LookupInput"
|
||||||
import draggable from "vuedraggable"
|
import draggable from "vuedraggable"
|
||||||
|
|
||||||
import { ApiMixin, getUserPreference } from "@/utils/utils"
|
import { ApiMixin, getUserPreference, StandardToasts, makeToast } from "@/utils/utils"
|
||||||
import { ApiApiFactory } from "@/utils/openapi/api"
|
import { ApiApiFactory } from "@/utils/openapi/api"
|
||||||
import { StandardToasts, makeToast } from "@/utils/utils"
|
|
||||||
|
|
||||||
Vue.use(BootstrapVue)
|
Vue.use(BootstrapVue)
|
||||||
|
Vue.use(VueCookies)
|
||||||
|
let SETTINGS_COOKIE_NAME = "shopping_settings"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "ShoppingListView",
|
name: "ShoppingListView",
|
||||||
@@ -546,6 +591,8 @@ export default {
|
|||||||
supermarket_categories_only: false,
|
supermarket_categories_only: false,
|
||||||
shopcat: null,
|
shopcat: null,
|
||||||
delay: 0,
|
delay: 0,
|
||||||
|
clear: Math.random(),
|
||||||
|
entry_mode_simple: false,
|
||||||
settings: {
|
settings: {
|
||||||
shopping_auto_sync: 0,
|
shopping_auto_sync: 0,
|
||||||
default_delay: 4,
|
default_delay: 4,
|
||||||
@@ -556,6 +603,7 @@ export default {
|
|||||||
shopping_recent_days: 7,
|
shopping_recent_days: 7,
|
||||||
csv_delim: ",",
|
csv_delim: ",",
|
||||||
csv_prefix: undefined,
|
csv_prefix: undefined,
|
||||||
|
shopping_add_onhand: true,
|
||||||
},
|
},
|
||||||
new_supermarket: { entrymode: false, value: undefined, editmode: undefined },
|
new_supermarket: { entrymode: false, value: undefined, editmode: undefined },
|
||||||
new_category: { entrymode: false, value: undefined },
|
new_category: { entrymode: false, value: undefined },
|
||||||
@@ -567,7 +615,7 @@ export default {
|
|||||||
fields: ["checked", "amount", "category", "unit", "food", "recipe", "details"],
|
fields: ["checked", "amount", "category", "unit", "food", "recipe", "details"],
|
||||||
loading: true,
|
loading: true,
|
||||||
entrymode: false,
|
entrymode: false,
|
||||||
new_item: { amount: 1, unit: undefined, food: undefined },
|
new_item: { amount: 1, unit: undefined, food: undefined, ingredient: undefined },
|
||||||
online: true,
|
online: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -594,6 +642,7 @@ export default {
|
|||||||
// if a supermarket is selected and filtered to only supermarket categories filter out everything else
|
// if a supermarket is selected and filtered to only supermarket categories filter out everything else
|
||||||
if (this.selected_supermarket && this.supermarket_categories_only) {
|
if (this.selected_supermarket && this.supermarket_categories_only) {
|
||||||
let shopping_categories = this.supermarkets // category IDs configured on supermarket
|
let shopping_categories = this.supermarkets // category IDs configured on supermarket
|
||||||
|
.filter((x) => x.id === this.selected_supermarket)
|
||||||
.map((x) => x.category_to_supermarket)
|
.map((x) => x.category_to_supermarket)
|
||||||
.flat()
|
.flat()
|
||||||
.map((x) => x.category.id)
|
.map((x) => x.category.id)
|
||||||
@@ -603,7 +652,7 @@ export default {
|
|||||||
shopping_list = shopping_list.filter((x) => x?.food?.supermarket_category)
|
shopping_list = shopping_list.filter((x) => x?.food?.supermarket_category)
|
||||||
}
|
}
|
||||||
|
|
||||||
let groups = { false: {}, true: {} } // force unchecked to always be first
|
var groups = { false: {}, true: {} } // force unchecked to always be first
|
||||||
if (this.selected_supermarket) {
|
if (this.selected_supermarket) {
|
||||||
let super_cats = this.supermarkets
|
let super_cats = this.supermarkets
|
||||||
.filter((x) => x.id === this.selected_supermarket)
|
.filter((x) => x.id === this.selected_supermarket)
|
||||||
@@ -611,10 +660,13 @@ export default {
|
|||||||
.flat()
|
.flat()
|
||||||
.map((x) => x.category.name)
|
.map((x) => x.category.name)
|
||||||
new Set([...super_cats, ...this.shopping_categories.map((x) => x.name)]).forEach((cat) => {
|
new Set([...super_cats, ...this.shopping_categories.map((x) => x.name)]).forEach((cat) => {
|
||||||
groups["false"][cat.name] = {}
|
groups["false"][cat] = {}
|
||||||
groups["true"][cat.name] = {}
|
groups["true"][cat] = {}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
// TODO: make nulls_first a user setting
|
||||||
|
groups.false[this.$t("Undefined")] = {}
|
||||||
|
groups.true[this.$t("Undefined")] = {}
|
||||||
this.shopping_categories.forEach((cat) => {
|
this.shopping_categories.forEach((cat) => {
|
||||||
groups.false[cat.name] = {}
|
groups.false[cat.name] = {}
|
||||||
groups.true[cat.name] = {}
|
groups.true[cat.name] = {}
|
||||||
@@ -712,6 +764,9 @@ export default {
|
|||||||
"settings.default_delay": function (newVal, oldVal) {
|
"settings.default_delay": function (newVal, oldVal) {
|
||||||
this.delay = Number(newVal)
|
this.delay = Number(newVal)
|
||||||
},
|
},
|
||||||
|
entry_mode_simple(newVal) {
|
||||||
|
this.$cookies.set(SETTINGS_COOKIE_NAME, newVal)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.getShoppingList()
|
this.getShoppingList()
|
||||||
@@ -725,11 +780,29 @@ export default {
|
|||||||
window.addEventListener("online", this.updateOnlineStatus)
|
window.addEventListener("online", this.updateOnlineStatus)
|
||||||
window.addEventListener("offline", this.updateOnlineStatus)
|
window.addEventListener("offline", this.updateOnlineStatus)
|
||||||
}
|
}
|
||||||
|
this.$nextTick(function () {
|
||||||
|
if (this.$cookies.isKey(SETTINGS_COOKIE_NAME)) {
|
||||||
|
this.entry_mode_simple = this.$cookies.get(SETTINGS_COOKIE_NAME)
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// this.genericAPI inherited from ApiMixin
|
// this.genericAPI inherited from ApiMixin
|
||||||
|
addItem: function () {
|
||||||
addItem() {
|
if (this.entry_mode_simple) {
|
||||||
|
this.genericPostAPI("api_ingredient_from_string", { text: this.new_item.ingredient }).then((result) => {
|
||||||
|
this.new_item = {
|
||||||
|
amount: result.data.amount,
|
||||||
|
unit: { name: result.data.unit },
|
||||||
|
food: { name: result.data.food },
|
||||||
|
}
|
||||||
|
this.addEntry()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.addEntry()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addEntry: function (x) {
|
||||||
let api = new ApiApiFactory()
|
let api = new ApiApiFactory()
|
||||||
api.createShoppingListEntry(this.new_item)
|
api.createShoppingListEntry(this.new_item)
|
||||||
.then((results) => {
|
.then((results) => {
|
||||||
@@ -739,7 +812,8 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
console.log("no data returned")
|
console.log("no data returned")
|
||||||
}
|
}
|
||||||
this.new_item = { amount: 1, unit: undefined, food: undefined }
|
this.new_item = { amount: 1, unit: undefined, food: undefined, ingredient: undefined }
|
||||||
|
this.clear += 1
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
@@ -917,8 +991,17 @@ export default {
|
|||||||
|
|
||||||
// TODO make decision - should inheritance always be set manually or give user a choice at front-end or make it a setting?
|
// TODO make decision - should inheritance always be set manually or give user a choice at front-end or make it a setting?
|
||||||
let food = this.items.filter((x) => x.food.id == item?.[0]?.food.id ?? item.food.id)[0].food
|
let food = this.items.filter((x) => x.food.id == item?.[0]?.food.id ?? item.food.id)[0].food
|
||||||
food.supermarket_category = this.shopping_categories.filter((x) => x?.id === this.shopcat)?.[0]
|
let supermarket_category = this.shopping_categories.filter((x) => x?.id === this.shopcat)?.[0]
|
||||||
this.updateFood(food, "supermarket_category")
|
food.supermarket_category = supermarket_category
|
||||||
|
this.updateFood(food, "supermarket_category").then((result) => {
|
||||||
|
this.items = this.items.map((x) => {
|
||||||
|
if (x.food.id === food.id) {
|
||||||
|
return { ...x, food: { ...x.food, supermarket_category: supermarket_category } }
|
||||||
|
} else {
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
this.shopcat = null
|
this.shopcat = null
|
||||||
},
|
},
|
||||||
onHand: function (item) {
|
onHand: function (item) {
|
||||||
@@ -940,8 +1023,13 @@ export default {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
openContextMenu(e, value) {
|
openContextMenu(e, value, section = false) {
|
||||||
|
if (section) {
|
||||||
|
value = Object.values(value).flat()
|
||||||
|
} else {
|
||||||
this.shopcat = value?.food?.supermarket_category?.id ?? value?.[0]?.food?.supermarket_category?.id ?? undefined
|
this.shopcat = value?.food?.supermarket_category?.id ?? value?.[0]?.food?.supermarket_category?.id ?? undefined
|
||||||
|
}
|
||||||
|
|
||||||
this.$refs.menu.open(e, value)
|
this.$refs.menu.open(e, value)
|
||||||
},
|
},
|
||||||
saveSettings: function () {
|
saveSettings: function () {
|
||||||
@@ -1010,24 +1098,15 @@ export default {
|
|||||||
},
|
},
|
||||||
updateFood: function (food, field) {
|
updateFood: function (food, field) {
|
||||||
let api = new ApiApiFactory()
|
let api = new ApiApiFactory()
|
||||||
let ignore_category
|
|
||||||
if (field) {
|
if (field) {
|
||||||
ignore_category = food.inherit_fields
|
// assume if field is changing it should no longer be inheritted
|
||||||
.map((x) => food.inherit_fields.fields)
|
food.inherit_fields = food.inherit_fields.filter((x) => x.field !== field)
|
||||||
.flat()
|
|
||||||
.includes(field)
|
|
||||||
} else {
|
|
||||||
ignore_category = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return api
|
return api
|
||||||
.partialUpdateFood(food.id, food)
|
.partialUpdateFood(food.id, food)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (food.supermarket_category && !ignore_category && food.parent) {
|
|
||||||
makeToast(this.$t("Warning"), this.$t("InheritWarning", { food: food.name }), "warning")
|
|
||||||
} else {
|
|
||||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_UPDATE)
|
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_UPDATE)
|
||||||
}
|
|
||||||
if (food?.numchild > 0) {
|
if (food?.numchild > 0) {
|
||||||
this.getShoppingList() // if food has children, just get the whole list. probably could be more efficient
|
this.getShoppingList() // if food has children, just get the whole list. probably could be more efficient
|
||||||
}
|
}
|
||||||
|
|||||||
185
vue/src/components/Buttons/RecipeSwitcher.vue
Normal file
185
vue/src/components/Buttons/RecipeSwitcher.vue
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="recipes.length > 0">
|
||||||
|
<div id="switcher">
|
||||||
|
<i class="btn btn-outline-dark fas fa-receipt fa-xl shadow-none btn-circle" v-b-toggle.related-recipes />
|
||||||
|
</div>
|
||||||
|
<b-sidebar id="related-recipes" :title="title" backdrop right shadow="sm" style="z-index: 10000">
|
||||||
|
<template #default="{ hide }">
|
||||||
|
<nav class="mb-3">
|
||||||
|
<b-nav vertical>
|
||||||
|
<b-nav-item
|
||||||
|
variant="link"
|
||||||
|
@click="
|
||||||
|
navRecipe(-1)
|
||||||
|
hide()
|
||||||
|
"
|
||||||
|
>{{ name }}</b-nav-item
|
||||||
|
>
|
||||||
|
<div v-for="r in recipes" :key="r.id">
|
||||||
|
<b-nav-item
|
||||||
|
variant="link"
|
||||||
|
@click="
|
||||||
|
navRecipe(r)
|
||||||
|
hide()
|
||||||
|
"
|
||||||
|
>{{ r.name }}</b-nav-item
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</b-nav>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
</b-sidebar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const { ApiApiFactory } = require("@/utils/openapi/api")
|
||||||
|
import { ResolveUrlMixin } from "@/utils/utils"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "RecipeSwitcher",
|
||||||
|
mixins: [ResolveUrlMixin],
|
||||||
|
props: {
|
||||||
|
recipe: { type: Number, default: undefined },
|
||||||
|
name: { type: String, default: undefined },
|
||||||
|
mode: { type: String, default: "recipe" },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
recipes: [],
|
||||||
|
recipe_list: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
title() {
|
||||||
|
let title = ""
|
||||||
|
switch (this.mode) {
|
||||||
|
case "recipe":
|
||||||
|
title = this.$t("related_recipes")
|
||||||
|
break
|
||||||
|
case "mealplan":
|
||||||
|
title = this.$t("today_recipes")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return title
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.recipes = []
|
||||||
|
switch (this.mode) {
|
||||||
|
case "recipe":
|
||||||
|
this.loadRecipes()
|
||||||
|
break
|
||||||
|
case "mealplan":
|
||||||
|
this.loadMealPlans()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
navRecipe: function (recipe) {
|
||||||
|
switch (this.mode) {
|
||||||
|
case "recipe":
|
||||||
|
this.$emit("switch", recipe)
|
||||||
|
break
|
||||||
|
case "mealplan":
|
||||||
|
window.location.href = this.resolveDjangoUrl("view_recipe", recipe.id)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
console.log(this.mode, " isn't defined.")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loadRecipes: function () {
|
||||||
|
let apiClient = new ApiApiFactory()
|
||||||
|
|
||||||
|
apiClient
|
||||||
|
.relatedRecipe(this.recipe, { query: { levels: 2 } })
|
||||||
|
// get related recipes and save them for later
|
||||||
|
.then((result) => {
|
||||||
|
this.recipe_list = result.data
|
||||||
|
})
|
||||||
|
// get all recipes for today
|
||||||
|
.then(() => {
|
||||||
|
this.loadMealPlans()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
loadMealPlans: function () {
|
||||||
|
let apiClient = new ApiApiFactory()
|
||||||
|
// TODO move to utility function moment is in maintenance mode https://momentjs.com/docs/
|
||||||
|
var tzoffset = new Date().getTimezoneOffset() * 60000 //offset in milliseconds
|
||||||
|
let today = new Date(Date.now() - tzoffset).toISOString().split("T")[0]
|
||||||
|
apiClient
|
||||||
|
.listMealPlans({
|
||||||
|
query: {
|
||||||
|
from_date: today,
|
||||||
|
to_date: today,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
let promises = []
|
||||||
|
result.data.forEach((mealplan) => {
|
||||||
|
this.recipe_list.push({ ...mealplan?.recipe, servings: mealplan?.servings })
|
||||||
|
const serving_factor = (mealplan?.servings ?? mealplan?.recipe?.servings ?? 1) / (mealplan?.recipe?.servings ?? 1)
|
||||||
|
promises.push(
|
||||||
|
apiClient.relatedRecipe(mealplan?.recipe?.id, { query: { levels: 2 } }).then((r) => {
|
||||||
|
// scale all recipes to mealplan servings
|
||||||
|
r.data = r.data.map((x) => {
|
||||||
|
return { ...x, factor: serving_factor }
|
||||||
|
})
|
||||||
|
this.recipe_list = [...this.recipe_list, ...r.data]
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return Promise.all(promises).then(() => {
|
||||||
|
let promises = []
|
||||||
|
let dedup = []
|
||||||
|
this.recipe_list.forEach((recipe) => {
|
||||||
|
if (!dedup.includes(recipe.id)) {
|
||||||
|
dedup.push(recipe.id)
|
||||||
|
promises.push(
|
||||||
|
apiClient.retrieveRecipe(recipe.id).then((result) => {
|
||||||
|
// scale all recipes to mealplan servings
|
||||||
|
result.data.servings = recipe?.servings ?? result.data.servings * (recipe?.factor ?? 1)
|
||||||
|
this.recipes.push(result.data)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return Promise.all(promises)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.btn-circle {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 35px;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.33;
|
||||||
|
z-index: 9000;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
#switcher .btn-circle {
|
||||||
|
position: fixed;
|
||||||
|
top: 9px;
|
||||||
|
left: 80px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 2000px) {
|
||||||
|
#switcher .btn-circle {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 10px;
|
||||||
|
right: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
<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>
|
<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="itemList(x)" :label="x.label" :color="x.color" />
|
||||||
<generic-ordered-pill
|
<generic-ordered-pill
|
||||||
v-for="x in itemOrderedTags"
|
v-for="x in itemOrderedTags"
|
||||||
:key="x.field"
|
:key="x.field"
|
||||||
@@ -259,6 +259,14 @@ export default {
|
|||||||
finishAction: function (e) {
|
finishAction: function (e) {
|
||||||
this.$emit("finish-action", e)
|
this.$emit("finish-action", e)
|
||||||
},
|
},
|
||||||
|
itemList: function (tag) {
|
||||||
|
let itemlist = this.item?.[tag?.field] ?? []
|
||||||
|
if (Array.isArray(itemlist)) {
|
||||||
|
return itemlist
|
||||||
|
} else {
|
||||||
|
return [itemlist]
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -62,12 +62,16 @@ export default {
|
|||||||
multiple: { type: Boolean, default: true },
|
multiple: { type: Boolean, default: true },
|
||||||
allow_create: { type: Boolean, default: false },
|
allow_create: { type: Boolean, default: false },
|
||||||
create_placeholder: { type: String, default: "You Forgot to Add a Tag Placeholder" },
|
create_placeholder: { type: String, default: "You Forgot to Add a Tag Placeholder" },
|
||||||
|
clear: { type: Number },
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
initial_selection: function (newVal, oldVal) {
|
initial_selection: function (newVal, oldVal) {
|
||||||
// watch it
|
// watch it
|
||||||
this.selected_objects = newVal
|
this.selected_objects = newVal
|
||||||
},
|
},
|
||||||
|
clear: function (newVal, oldVal) {
|
||||||
|
this.selected_objects = []
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.search("")
|
this.search("")
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
export default {
|
export default {
|
||||||
name: "GenericPill",
|
name: "GenericPill",
|
||||||
props: {
|
props: {
|
||||||
item_list: { type: Object },
|
item_list: { type: Array },
|
||||||
label: { type: String, default: "name" },
|
label: { type: String, default: "name" },
|
||||||
color: { type: String, default: "light" },
|
color: { type: String, default: "light" },
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ export default {
|
|||||||
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 // ingredient is in the shopping list - probably (but not definitely, this ingredient)
|
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 {
|
||||||
// food is not in any shopping list
|
// food is not in any shopping list
|
||||||
@@ -123,7 +123,7 @@ export default {
|
|||||||
if (count_shopping_ingredient >= 1) {
|
if (count_shopping_ingredient >= 1) {
|
||||||
// ingredient is in this shopping list (not entirely sure how this could happen?)
|
// ingredient is in this shopping list (not entirely sure how this could happen?)
|
||||||
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.shopping_status = null
|
this.shopping_status = null
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
:sticky_options="sticky_options"
|
:sticky_options="sticky_options"
|
||||||
:allow_create="form.allow_create"
|
:allow_create="form.allow_create"
|
||||||
:create_placeholder="createPlaceholder"
|
:create_placeholder="createPlaceholder"
|
||||||
|
:clear="clear"
|
||||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||||
:placeholder="modelName"
|
:placeholder="modelName"
|
||||||
@new="addNew"
|
@new="addNew"
|
||||||
@@ -44,6 +45,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
show_label: { type: Boolean, default: true },
|
show_label: { type: Boolean, default: true },
|
||||||
|
clear: { type: Number },
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -276,5 +276,11 @@
|
|||||||
"csv_prefix_label": "List Prefix",
|
"csv_prefix_label": "List Prefix",
|
||||||
"copy_markdown_table": "Copy as Markdown Table",
|
"copy_markdown_table": "Copy as Markdown Table",
|
||||||
"in_shopping": "In Shopping List",
|
"in_shopping": "In Shopping List",
|
||||||
"DelayUntil": "Delay Until"
|
"DelayUntil": "Delay Until",
|
||||||
|
"mark_complete": "Mark Complete",
|
||||||
|
"QuickEntry": "Quick Entry",
|
||||||
|
"shopping_add_onhand_desc": "Mark food 'On Hand' when checked off shopping list.",
|
||||||
|
"shopping_add_onhand": "Auto On Hand",
|
||||||
|
"related_recipes": "Related Recipes",
|
||||||
|
"today_recipes": "Today's Recipes"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -234,7 +234,14 @@ export const ApiMixin = {
|
|||||||
return apiClient[func](...parameters)
|
return apiClient[func](...parameters)
|
||||||
},
|
},
|
||||||
genericGetAPI: function (url, options) {
|
genericGetAPI: function (url, options) {
|
||||||
return axios.get(this.resolveDjangoUrl(url), { params: options, emulateJSON: true })
|
return axios.get(resolveDjangoUrl(url), { params: options, emulateJSON: true })
|
||||||
|
},
|
||||||
|
genericPostAPI: function (url, form) {
|
||||||
|
let data = new FormData()
|
||||||
|
Object.keys(form).forEach((field) => {
|
||||||
|
data.append(field, form[field])
|
||||||
|
})
|
||||||
|
return axios.post(resolveDjangoUrl(url), data)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user